diff --git a/TMessagesProj/build.gradle b/TMessagesProj/build.gradle index e1837d117..a329a7f57 100644 --- a/TMessagesProj/build.gradle +++ b/TMessagesProj/build.gradle @@ -9,15 +9,17 @@ configurations { } dependencies { - compile 'com.google.android.gms:play-services-gcm:10.2.0' - compile 'com.google.android.gms:play-services-maps:10.2.0' - compile 'com.google.android.gms:play-services-vision:10.2.0' - compile 'com.android.support:support-core-ui:25.3.0' - compile 'com.android.support:support-compat:25.3.0' - compile 'com.android.support:support-core-utils:25.3.0' - compile 'com.android.support:support-v13:25.3.0' - compile 'com.android.support:palette-v7:25.3.0' - compile 'net.hockeyapp.android:HockeySDK:4.1.2' + compile 'com.google.android.gms:play-services-gcm:11.0.1' + compile 'com.google.android.gms:play-services-maps:11.0.1' + compile 'com.google.android.gms:play-services-vision:11.0.1' + compile 'com.google.android.gms:play-services-wallet:11.0.1' + compile 'com.google.android.gms:play-services-wearable:11.0.1' + compile 'com.android.support:support-core-ui:25.3.1' + compile 'com.android.support:support-compat:25.3.1' + compile 'com.android.support:support-core-utils:25.3.1' + compile 'com.android.support:support-v13:25.3.1' + compile 'com.android.support:palette-v7:25.3.1' + compile 'net.hockeyapp.android:HockeySDK:4.1.3' compile 'com.googlecode.mp4parser:isoparser:1.0.6' compile 'com.stripe:stripe-android:2.0.2' } @@ -88,7 +90,7 @@ android { } } - defaultConfig.versionCode = 957 + defaultConfig.versionCode = 1030 sourceSets.debug { manifest.srcFile 'config/debug/AndroidManifest.xml' @@ -160,7 +162,7 @@ android { defaultConfig { minSdkVersion 14 targetSdkVersion 25 - versionName "3.18.0" + versionName "4.1.1" externalNativeBuild { ndkBuild { diff --git a/TMessagesProj/config/debug/AndroidManifest.xml b/TMessagesProj/config/debug/AndroidManifest.xml index 176edd31f..6117e393f 100644 --- a/TMessagesProj/config/debug/AndroidManifest.xml +++ b/TMessagesProj/config/debug/AndroidManifest.xml @@ -22,6 +22,7 @@ request = (NativeByteBuffer *) object; if (onComplete != nullptr) { @@ -170,6 +178,9 @@ void sendRequest(JNIEnv *env, jclass c, jint object, jobject onComplete, jobject if (onQuickAck != nullptr) { onQuickAck = env->NewGlobalRef(onQuickAck); } + if (onWriteToSocket != nullptr) { + onWriteToSocket = env->NewGlobalRef(onWriteToSocket); + } ConnectionsManager::getInstance().sendRequest(request, ([onComplete](TLObject *response, TL_error *error, int32_t networkType) { TL_api_response *resp = (TL_api_response *) response; jint ptr = 0; @@ -191,7 +202,11 @@ void sendRequest(JNIEnv *env, jclass c, jint object, jobject onComplete, jobject if (onQuickAck != nullptr) { jniEnv->CallVoidMethod(onQuickAck, jclass_QuickAckDelegate_run); } - }), flags, datacenterId, (ConnectionType) connetionType, immediate, token, onComplete, onQuickAck); + }), ([onWriteToSocket] { + if (onWriteToSocket != nullptr) { + jniEnv->CallVoidMethod(onWriteToSocket, jclass_WriteToSocketDelegate_run); + } + }), flags, datacenterId, (ConnectionType) connetionType, immediate, token, onComplete, onQuickAck, onWriteToSocket); } void cancelRequest(JNIEnv *env, jclass c, jint token, jboolean notifyServer) { @@ -220,6 +235,24 @@ void applyDatacenterAddress(JNIEnv *env, jclass c, jint datacenterId, jstring ip } } +void setProxySettings(JNIEnv *env, jclass c, jstring address, jint port, jstring username, jstring password) { + const char *addressStr = env->GetStringUTFChars(address, 0); + const char *usernameStr = env->GetStringUTFChars(username, 0); + const char *passwordStr = env->GetStringUTFChars(password, 0); + + ConnectionsManager::getInstance().setProxySettings(addressStr, (uint16_t) port, usernameStr, passwordStr); + + if (addressStr != 0) { + env->ReleaseStringUTFChars(address, addressStr); + } + if (usernameStr != 0) { + env->ReleaseStringUTFChars(username, usernameStr); + } + if (passwordStr != 0) { + env->ReleaseStringUTFChars(password, passwordStr); + } +} + jint getConnectionState(JNIEnv *env, jclass c) { return ConnectionsManager::getInstance().getConnectionState(); } @@ -241,7 +274,7 @@ void resumeNetwork(JNIEnv *env, jclass c, jboolean partial) { } void updateDcSettings(JNIEnv *env, jclass c) { - ConnectionsManager::getInstance().updateDcSettings(0); + ConnectionsManager::getInstance().updateDcSettings(0, false); } void setUseIpv6(JNIEnv *env, jclass c, bool value) { @@ -256,6 +289,10 @@ void setPushConnectionEnabled(JNIEnv *env, jclass c, jboolean value) { ConnectionsManager::getInstance().setPushConnectionEnabled(value); } +void applyDnsConfig(JNIEnv *env, jclass c, jint address) { + ConnectionsManager::getInstance().applyDnsConfig((NativeByteBuffer *) address); +} + class Delegate : public ConnectiosManagerDelegate { void onUpdate() { @@ -299,17 +336,32 @@ class Delegate : public ConnectiosManagerDelegate { void onBytesSent(int32_t amount, int32_t networkType) { jniEnv->CallStaticVoidMethod(jclass_ConnectionsManager, jclass_ConnectionsManager_onBytesSent, amount, networkType); } + + void onRequestNewServerIpAndPort(int32_t second) { + jniEnv->CallStaticVoidMethod(jclass_ConnectionsManager, jclass_ConnectionsManager_onRequestNewServerIpAndPort, second); + } }; -void init(JNIEnv *env, jclass c, jint version, jint layer, jint apiId, jstring deviceModel, jstring systemVersion, jstring appVersion, jstring langCode, jstring configPath, jstring logPath, jint userId, jboolean enablePushConnection, jboolean hasNetwork, jint networkType) { +void setLangCode(JNIEnv *env, jclass c, jstring langCode) { + const char *langCodeStr = env->GetStringUTFChars(langCode, 0); + + ConnectionsManager::getInstance().setLangCode(std::string(langCodeStr)); + + if (langCodeStr != 0) { + env->ReleaseStringUTFChars(langCode, langCodeStr); + } +} + +void init(JNIEnv *env, jclass c, jint version, jint layer, jint apiId, jstring deviceModel, jstring systemVersion, jstring appVersion, jstring langCode, jstring systemLangCode, jstring configPath, jstring logPath, 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); const char *langCodeStr = env->GetStringUTFChars(langCode, 0); + const char *systemLangCodeStr = env->GetStringUTFChars(systemLangCode, 0); const char *configPathStr = env->GetStringUTFChars(configPath, 0); const char *logPathStr = env->GetStringUTFChars(logPath, 0); - ConnectionsManager::getInstance().init(version, layer, apiId, std::string(deviceModelStr), std::string(systemVersionStr), std::string(appVersionStr), std::string(langCodeStr), std::string(configPathStr), std::string(logPathStr), userId, true, enablePushConnection, hasNetwork, networkType); + ConnectionsManager::getInstance().init(version, layer, apiId, std::string(deviceModelStr), std::string(systemVersionStr), std::string(appVersionStr), std::string(langCodeStr), std::string(systemLangCodeStr), std::string(configPathStr), std::string(logPathStr), userId, true, enablePushConnection, hasNetwork, networkType); if (deviceModelStr != 0) { env->ReleaseStringUTFChars(deviceModel, deviceModelStr); @@ -323,6 +375,9 @@ void init(JNIEnv *env, jclass c, jint version, jint layer, jint apiId, jstring d if (langCodeStr != 0) { env->ReleaseStringUTFChars(langCode, langCodeStr); } + if (systemLangCodeStr != 0) { + env->ReleaseStringUTFChars(systemLangCode, systemLangCodeStr); + } if (configPathStr != 0) { env->ReleaseStringUTFChars(configPath, configPathStr); } @@ -340,16 +395,19 @@ static const char *ConnectionsManagerClassPathName = "org/telegram/tgnet/Connect static JNINativeMethod ConnectionsManagerMethods[] = { {"native_getCurrentTimeMillis", "()J", (void *) getCurrentTimeMillis}, {"native_getCurrentTime", "()I", (void *) getCurrentTime}, + {"native_isTestBackend", "()I", (void *) isTestBackend}, {"native_getTimeDifference", "()I", (void *) getTimeDifference}, - {"native_sendRequest", "(ILorg/telegram/tgnet/RequestDelegateInternal;Lorg/telegram/tgnet/QuickAckDelegate;IIIZI)V", (void *) sendRequest}, + {"native_sendRequest", "(ILorg/telegram/tgnet/RequestDelegateInternal;Lorg/telegram/tgnet/QuickAckDelegate;Lorg/telegram/tgnet/WriteToSocketDelegate;IIIZI)V", (void *) sendRequest}, {"native_cancelRequest", "(IZ)V", (void *) cancelRequest}, {"native_cleanUp", "()V", (void *) cleanUp}, {"native_cancelRequestsForGuid", "(I)V", (void *) cancelRequestsForGuid}, {"native_bindRequestToGuid", "(II)V", (void *) bindRequestToGuid}, {"native_applyDatacenterAddress", "(ILjava/lang/String;I)V", (void *) applyDatacenterAddress}, + {"native_setProxySettings", "(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V", (void *) setProxySettings}, {"native_getConnectionState", "()I", (void *) getConnectionState}, {"native_setUserId", "(I)V", (void *) setUserId}, - {"native_init", "(IIILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZZI)V", (void *) init}, + {"native_init", "(IIILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZZI)V", (void *) init}, + {"native_setLangCode", "(Ljava/lang/String;)V", (void *) setLangCode}, {"native_switchBackend", "()V", (void *) switchBackend}, {"native_pauseNetwork", "()V", (void *) pauseNetwork}, {"native_resumeNetwork", "(Z)V", (void *) resumeNetwork}, @@ -357,7 +415,8 @@ static JNINativeMethod ConnectionsManagerMethods[] = { {"native_setUseIpv6", "(Z)V", (void *) setUseIpv6}, {"native_setNetworkAvailable", "(ZI)V", (void *) setNetworkAvailable}, {"native_setPushConnectionEnabled", "(Z)V", (void *) setPushConnectionEnabled}, - {"native_setJava", "(Z)V", (void *) setJava} + {"native_setJava", "(Z)V", (void *) setJava}, + {"native_applyDnsConfig", "(I)V", (void *) applyDnsConfig} }; inline int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int methodsCount) { @@ -405,6 +464,15 @@ extern "C" int registerNativeTgNetFunctions(JavaVM *vm, JNIEnv *env) { return JNI_FALSE; } + jclass_WriteToSocketDelegate = (jclass) env->NewGlobalRef(env->FindClass("org/telegram/tgnet/WriteToSocketDelegate")); + if (jclass_WriteToSocketDelegate == 0) { + return JNI_FALSE; + } + jclass_WriteToSocketDelegate_run = env->GetMethodID(jclass_WriteToSocketDelegate, "run", "()V"); + if (jclass_WriteToSocketDelegate_run == 0) { + return JNI_FALSE; + } + jclass_FileLoadOperationDelegate = (jclass) env->NewGlobalRef(env->FindClass("org/telegram/tgnet/FileLoadOperationDelegate")); if (jclass_FileLoadOperationDelegate == 0) { return JNI_FALSE; @@ -465,6 +533,10 @@ extern "C" int registerNativeTgNetFunctions(JavaVM *vm, JNIEnv *env) { if (jclass_ConnectionsManager_onBytesReceived == 0) { return JNI_FALSE; } + jclass_ConnectionsManager_onRequestNewServerIpAndPort = env->GetStaticMethodID(jclass_ConnectionsManager, "onRequestNewServerIpAndPort", "(I)V"); + if (jclass_ConnectionsManager_onRequestNewServerIpAndPort == 0) { + return JNI_FALSE; + } ConnectionsManager::getInstance().setDelegate(new Delegate()); return JNI_TRUE; diff --git a/TMessagesProj/jni/gifvideo.cpp b/TMessagesProj/jni/gifvideo.cpp index 06532917c..2de90d5cc 100644 --- a/TMessagesProj/jni/gifvideo.cpp +++ b/TMessagesProj/jni/gifvideo.cpp @@ -252,16 +252,27 @@ jint Java_org_telegram_ui_Components_AnimatedFileDrawable_getVideoFrame(JNIEnv * //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); + int wantedWidth; + int wantedHeight; if (dataArr != nullptr) { + wantedWidth = dataArr[0]; + wantedHeight = dataArr[1]; dataArr[3] = (int) (1000 * info->frame->pkt_pts * 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 (info->frame->format == AV_PIX_FMT_YUV420P || info->frame->format == AV_PIX_FMT_YUVJ420P) { //LOGD("y %d, u %d, v %d, width %d, height %d", info->frame->linesize[0], info->frame->linesize[2], info->frame->linesize[1], info->frame->width, info->frame->height); - 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); + if (wantedWidth == info->frame->width && wantedHeight == info->frame->height || wantedWidth == info->frame->height && wantedHeight == info->frame->width) { + 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); } diff --git a/TMessagesProj/jni/jni.c b/TMessagesProj/jni/jni.c index 1589a61e9..4bf8971fe 100644 --- a/TMessagesProj/jni/jni.c +++ b/TMessagesProj/jni/jni.c @@ -56,6 +56,22 @@ JNIEXPORT void Java_org_telegram_messenger_Utilities_aesIgeEncryption(JNIEnv *en (*env)->ReleaseByteArrayElements(env, iv, ivBuff, 0); } +JNIEXPORT jint Java_org_telegram_messenger_Utilities_aesCtrDecryption(JNIEnv *env, jclass class, jobject buffer, jbyteArray key, jbyteArray iv, int offset, int length) { + jbyte *what = (*env)->GetDirectBufferAddress(env, buffer) + offset; + unsigned char *keyBuff = (unsigned char *)(*env)->GetByteArrayElements(env, key, NULL); + unsigned char *ivBuff = (unsigned char *)(*env)->GetByteArrayElements(env, iv, NULL); + + AES_KEY akey; + unsigned int num = 0; + uint8_t count[16]; + memset(count, 0, 16); + AES_set_encrypt_key(keyBuff, 32 * 8, &akey); + AES_ctr128_encrypt(what, what, length, &akey, ivBuff, count, &num); + (*env)->ReleaseByteArrayElements(env, key, keyBuff, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, iv, ivBuff, JNI_ABORT); + return num; +} + JNIEXPORT jstring Java_org_telegram_messenger_Utilities_readlink(JNIEnv *env, jclass class, jstring path) { static char buf[1000]; char *fileName = (*env)->GetStringUTFChars(env, path, NULL); diff --git a/TMessagesProj/jni/tgnet/ApiScheme.cpp b/TMessagesProj/jni/tgnet/ApiScheme.cpp index 0e2b47c72..0a6cb163b 100644 --- a/TMessagesProj/jni/tgnet/ApiScheme.cpp +++ b/TMessagesProj/jni/tgnet/ApiScheme.cpp @@ -50,6 +50,11 @@ TL_dcOption *TL_dcOption::TLdeserialize(NativeByteBuffer *stream, uint32_t const void TL_dcOption::readParams(NativeByteBuffer *stream, bool &error) { flags = stream->readInt32(&error); + ipv6 = (flags & 1) != 0; + media_only = (flags & 2) != 0; + tcpo_only = (flags & 4) != 0; + cdn = (flags & 8) != 0; + isStatic = (flags & 16) != 0; id = stream->readInt32(&error); ip_address = stream->readString(&error); port = stream->readInt32(&error); @@ -57,12 +62,89 @@ void TL_dcOption::readParams(NativeByteBuffer *stream, bool &error) { void TL_dcOption::serializeToStream(NativeByteBuffer *stream) { stream->writeInt32(constructor); + flags = ipv6 ? (flags | 1) : (flags &~ 1); + flags = media_only ? (flags | 2) : (flags &~ 2); + flags = tcpo_only ? (flags | 4) : (flags &~ 4); + flags = cdn ? (flags | 8) : (flags &~ 8); + flags = isStatic ? (flags | 16) : (flags &~ 16); stream->writeInt32(flags); stream->writeInt32(id); stream->writeString(ip_address); stream->writeInt32(port); } +TL_cdnPublicKey *TL_cdnPublicKey::TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool &error) { + if (TL_cdnPublicKey::constructor != constructor) { + error = true; + DEBUG_E("can't parse magic %x in TL_cdnPublicKey", constructor); + return nullptr; + } + TL_cdnPublicKey *result = new TL_cdnPublicKey(); + result->readParams(stream, error); + return result; +} + +void TL_cdnPublicKey::readParams(NativeByteBuffer *stream, bool &error) { + dc_id = stream->readInt32(&error); + public_key = stream->readString(&error); +} + +void TL_cdnPublicKey::serializeToStream(NativeByteBuffer *stream) { + stream->writeInt32(constructor); + stream->writeInt32(dc_id); + stream->writeString(public_key); +} + +TL_cdnConfig *TL_cdnConfig::TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool &error) { + if (TL_cdnConfig::constructor != constructor) { + error = true; + DEBUG_E("can't parse magic %x in TL_cdnConfig", constructor); + return nullptr; + } + TL_cdnConfig *result = new TL_cdnConfig(); + result->readParams(stream, error); + return result; +} + +void TL_cdnConfig::readParams(NativeByteBuffer *stream, bool &error) { + int magic = stream->readInt32(&error); + if (magic != 0x1cb5c415) { + error = true; + DEBUG_E("wrong Vector magic, got %x", magic); + return; + } + int count = stream->readInt32(&error); + for (int a = 0; a < count; a++) { + TL_cdnPublicKey *object = TL_cdnPublicKey::TLdeserialize(stream, stream->readUint32(&error), error); + if (object == nullptr) { + return; + } + public_keys.push_back(std::unique_ptr(object)); + } +} + +void TL_cdnConfig::serializeToStream(NativeByteBuffer *stream) { + stream->writeInt32(constructor); + stream->writeInt32(0x1cb5c415); + int count = public_keys.size(); + stream->writeInt32(count); + for (int a = 0; a < count; a++) { + public_keys[a]->serializeToStream(stream); + } +} + +bool TL_help_getCdnConfig::isNeedLayer() { + return true; +} + +TLObject *TL_help_getCdnConfig::deserializeResponse(NativeByteBuffer *stream, uint32_t constructor, bool &error) { + return TL_cdnConfig::TLdeserialize(stream, constructor, error); +} + +void TL_help_getCdnConfig::serializeToStream(NativeByteBuffer *stream) { + stream->writeInt32(constructor); +} + TL_disabledFeature *TL_disabledFeature::TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool &error) { if (TL_disabledFeature::constructor != constructor) { error = true; @@ -141,6 +223,12 @@ void TL_config::readParams(NativeByteBuffer *stream, bool &error) { call_connect_timeout_ms = stream->readInt32(&error); call_packet_timeout_ms = stream->readInt32(&error); me_url_prefix = stream->readString(&error); + if ((flags & 4) != 0) { + suggested_lang_code = stream->readString(&error); + } + if ((flags & 4) != 0) { + lang_pack_version = stream->readInt32(&error); + } magic = stream->readUint32(&error); if (magic != 0x1cb5c415) { error = true; @@ -195,6 +283,12 @@ void TL_config::serializeToStream(NativeByteBuffer *stream) { stream->writeInt32(call_connect_timeout_ms); stream->writeInt32(call_packet_timeout_ms); stream->writeString(me_url_prefix); + if ((flags & 4) != 0) { + stream->writeString(suggested_lang_code); + } + if ((flags & 4) != 0) { + stream->writeInt32(lang_pack_version); + } stream->writeInt32(0x1cb5c415); count = (uint32_t) disabled_features.size(); stream->writeInt32(count); @@ -235,7 +329,7 @@ User *User::TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool & case 0x200250ba: result = new TL_userEmpty(); break; - case 0xd10d979a: + case 0x2e13f4c3: result = new TL_user(); break; default: @@ -289,6 +383,9 @@ void TL_user::readParams(NativeByteBuffer *stream, bool &error) { if ((flags & 524288) != 0) { bot_inline_placeholder = stream->readString(&error); } + if ((flags & 4194304) != 0) { + lang_code = stream->readString(&error); + } } void TL_user::serializeToStream(NativeByteBuffer *stream) { @@ -325,6 +422,9 @@ void TL_user::serializeToStream(NativeByteBuffer *stream) { if ((flags & 524288) != 0) { stream->writeString(bot_inline_placeholder); } + if ((flags & 4194304) != 0) { + stream->writeString(lang_code); + } } TL_auth_authorization *TL_auth_authorization::TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool &error) { @@ -640,7 +740,7 @@ TL_upload_file::~TL_upload_file() { } void TL_upload_file::readParams(NativeByteBuffer *stream, bool &error) { - type = std::unique_ptr(storage_FileType::TLdeserialize(stream, stream->readInt32(&error), error)); + type = std::unique_ptr(storage_FileType::TLdeserialize(stream, stream->readUint32(&error), error)); mtime = stream->readInt32(&error); bytes = stream->readByteBuffer(true, &error); } diff --git a/TMessagesProj/jni/tgnet/ApiScheme.h b/TMessagesProj/jni/tgnet/ApiScheme.h index 66c82baf1..29d529a2c 100644 --- a/TMessagesProj/jni/tgnet/ApiScheme.h +++ b/TMessagesProj/jni/tgnet/ApiScheme.h @@ -45,6 +45,11 @@ public: static const uint32_t constructor = 0x5d8c6cc; int32_t flags; + bool ipv6; + bool media_only; + bool tcpo_only; + bool cdn; + bool isStatic; int32_t id; std::string ip_address; int32_t port; @@ -54,6 +59,41 @@ public: void serializeToStream(NativeByteBuffer *stream); }; +class TL_cdnPublicKey : public TLObject { + +public: + static const uint32_t constructor = 0xc982eaba; + + int32_t dc_id; + std::string public_key; + + static TL_cdnPublicKey *TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool &error); + void readParams(NativeByteBuffer *stream, bool &error); + void serializeToStream(NativeByteBuffer *stream); +}; + +class TL_cdnConfig : public TLObject { + +public: + static const uint32_t constructor = 0x5725e40a; + + std::vector> public_keys; + + static TL_cdnConfig *TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool &error); + void readParams(NativeByteBuffer *stream, bool &error); + void serializeToStream(NativeByteBuffer *stream); +}; + +class TL_help_getCdnConfig : public TLObject { + +public: + static const uint32_t constructor = 0x52029342; + + bool isNeedLayer(); + TLObject *deserializeResponse(NativeByteBuffer *stream, uint32_t constructor, bool &error); + void serializeToStream(NativeByteBuffer *stream); +}; + class TL_disabledFeature : public TLObject { public: @@ -70,7 +110,7 @@ public: class TL_config : public TLObject { public: - static const uint32_t constructor = 0xcb601684; + static const uint32_t constructor = 0x7feec888; int32_t flags; int32_t date; @@ -101,6 +141,8 @@ public: int32_t call_connect_timeout_ms; int32_t call_packet_timeout_ms; std::string me_url_prefix; + std::string suggested_lang_code; + int32_t lang_pack_version; std::vector> disabled_features; static TL_config *TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool &error); @@ -271,6 +313,7 @@ public: int32_t bot_info_version; std::string restriction_reason; std::string bot_inline_placeholder; + std::string lang_code; static User *TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool &error); }; @@ -287,7 +330,7 @@ public: class TL_user : public User { public: - static const uint32_t constructor = 0xd10d979a; + static const uint32_t constructor = 0x2e13f4c3; void readParams(NativeByteBuffer *stream, bool &error); void serializeToStream(NativeByteBuffer *stream); diff --git a/TMessagesProj/jni/tgnet/ByteStream.h b/TMessagesProj/jni/tgnet/ByteStream.h index 56c3e4401..e417c6395 100644 --- a/TMessagesProj/jni/tgnet/ByteStream.h +++ b/TMessagesProj/jni/tgnet/ByteStream.h @@ -20,6 +20,7 @@ public: ByteStream(); ~ByteStream(); void append(NativeByteBuffer *buffer); + void append(uint8_t *buffer, uint32_t size); bool hasData(); void get(NativeByteBuffer *dst); void discard(uint32_t count); diff --git a/TMessagesProj/jni/tgnet/Connection.cpp b/TMessagesProj/jni/tgnet/Connection.cpp index 3848d29a3..743f98870 100644 --- a/TMessagesProj/jni/tgnet/Connection.cpp +++ b/TMessagesProj/jni/tgnet/Connection.cpp @@ -45,7 +45,7 @@ void Connection::suspendConnection() { DEBUG_D("connection(%p, dc%u, type %d) suspend", this, currentDatacenter->getDatacenterId(), connectionType); connectionState = TcpConnectionStageSuspended; dropConnection(); - ConnectionsManager::getInstance().onConnectionClosed(this); + ConnectionsManager::getInstance().onConnectionClosed(this, 0); firstPacketSent = false; if (restOfTheData != nullptr) { restOfTheData->reuse(); @@ -216,33 +216,36 @@ void Connection::onReceivedData(NativeByteBuffer *buffer) { void Connection::connect() { if (!ConnectionsManager::getInstance().isNetworkAvailable()) { - ConnectionsManager::getInstance().onConnectionClosed(this); + ConnectionsManager::getInstance().onConnectionClosed(this, 0); return; } if ((connectionState == TcpConnectionStageConnected || connectionState == TcpConnectionStageConnecting)) { return; } connectionState = TcpConnectionStageConnecting; - bool ipv6 = ConnectionsManager::getInstance().isIpv6Enabled(); + uint32_t ipv6 = ConnectionsManager::getInstance().isIpv6Enabled() ? TcpAddressFlagIpv6 : 0; + uint32_t isStatic = !ConnectionsManager::getInstance().proxyAddress.empty() ? TcpAddressFlagStatic : 0; if (connectionType == ConnectionTypeDownload) { - currentAddressFlags = 2; - hostAddress = currentDatacenter->getCurrentAddress(currentAddressFlags | (ipv6 ? 1 : 0)); + currentAddressFlags = TcpAddressFlagDownload | isStatic; + hostAddress = currentDatacenter->getCurrentAddress(currentAddressFlags | ipv6); if (hostAddress.empty()) { - currentAddressFlags = 0; - hostAddress = currentDatacenter->getCurrentAddress(currentAddressFlags | (ipv6 ? 1 : 0)); + currentAddressFlags = isStatic; + hostAddress = currentDatacenter->getCurrentAddress(currentAddressFlags | ipv6); } if (hostAddress.empty() && ipv6) { - currentAddressFlags = 2; + ipv6 = 0; + currentAddressFlags = TcpAddressFlagDownload | isStatic; hostAddress = currentDatacenter->getCurrentAddress(currentAddressFlags); if (hostAddress.empty()) { - currentAddressFlags = 0; + currentAddressFlags = isStatic; hostAddress = currentDatacenter->getCurrentAddress(currentAddressFlags); } } } else { - currentAddressFlags = 0; - hostAddress = currentDatacenter->getCurrentAddress(currentAddressFlags | (ipv6 ? 1 : 0)); - if (ipv6 && hostAddress.empty()) { + currentAddressFlags = isStatic; + hostAddress = currentDatacenter->getCurrentAddress(currentAddressFlags | ipv6); + if (hostAddress.empty() && ipv6) { + ipv6 = 0; hostAddress = currentDatacenter->getCurrentAddress(currentAddressFlags); } } @@ -259,7 +262,7 @@ void Connection::connect() { lastPacketLength = 0; wasConnected = false; hasSomeDataSinceLastConnect = false; - openConnection(hostAddress, hostPort, ipv6, ConnectionsManager::getInstance().currentNetworkType); + openConnection(hostAddress, hostPort, ipv6 != 0, ConnectionsManager::getInstance().currentNetworkType); if (connectionType == ConnectionTypePush) { if (isTryingNextPort) { setTimeout(20); @@ -273,7 +276,7 @@ void Connection::connect() { if (connectionType == ConnectionTypeUpload) { setTimeout(25); } else { - setTimeout(15); + setTimeout(12); } } } @@ -285,6 +288,14 @@ void Connection::reconnect() { connect(); } +bool Connection::hasUsefullData() { + return usefullData; +} + +void Connection::setHasUsefullData() { + usefullData = true; +} + void Connection::sendData(NativeByteBuffer *buff, bool reportAck) { if (buff == nullptr) { return; @@ -391,7 +402,8 @@ void Connection::onDisconnected(int reason) { if (connectionState != TcpConnectionStageSuspended && connectionState != TcpConnectionStageIdle) { connectionState = TcpConnectionStageIdle; } - ConnectionsManager::getInstance().onConnectionClosed(this); + ConnectionsManager::getInstance().onConnectionClosed(this, reason); + usefullData = false; uint32_t datacenterId = currentDatacenter->getDatacenterId(); if (connectionState == TcpConnectionStageIdle && connectionType == ConnectionTypeGeneric && (currentDatacenter->isHandshaking() || datacenterId == ConnectionsManager::getInstance().currentDatacenterId || datacenterId == ConnectionsManager::getInstance().movingToDatacenterId)) { diff --git a/TMessagesProj/jni/tgnet/Connection.h b/TMessagesProj/jni/tgnet/Connection.h index 76da54f4f..280d9295f 100644 --- a/TMessagesProj/jni/tgnet/Connection.h +++ b/TMessagesProj/jni/tgnet/Connection.h @@ -31,6 +31,8 @@ public: void connect(); void suspendConnection(); void sendData(NativeByteBuffer *buffer, bool reportAck); + bool hasUsefullData(); + void setHasUsefullData(); uint32_t getConnectionToken(); ConnectionType getConnectionType(); Datacenter *getDatacenter(); @@ -68,6 +70,7 @@ private: bool wasConnected = false; uint32_t willRetryConnectCount = 5; Timer *reconnectTimer; + bool usefullData = false; AES_KEY encryptKey; uint8_t encryptIv[16]; diff --git a/TMessagesProj/jni/tgnet/ConnectionSocket.cpp b/TMessagesProj/jni/tgnet/ConnectionSocket.cpp index 227ab5e10..1c60c264a 100644 --- a/TMessagesProj/jni/tgnet/ConnectionSocket.cpp +++ b/TMessagesProj/jni/tgnet/ConnectionSocket.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "ByteStream.h" #include "ConnectionSocket.h" #include "FileLog.h" @@ -21,6 +22,7 @@ #include "EventObject.h" #include "Timer.h" #include "NativeByteBuffer.h" +#include "BuffersStorage.h" #ifndef EPOLLRDHUP #define EPOLLRDHUP 0x2000 @@ -45,34 +47,81 @@ ConnectionSocket::~ConnectionSocket() { void ConnectionSocket::openConnection(std::string address, uint16_t port, bool ipv6, int32_t networkType) { currentNetworkType = networkType; + isIpv6 = ipv6; + currentAddress = address; + currentPort = port; int epolFd = ConnectionsManager::getInstance().epolFd; ConnectionsManager::getInstance().attachConnection(this); - if ((socketFd = socket(ipv6 ? AF_INET6 : AF_INET, SOCK_STREAM, 0)) < 0) { - DEBUG_E("connection(%p) can't create socket", this); - closeSocket(1); - return; - } - memset(&socketAddress, 0, sizeof(sockaddr_in)); memset(&socketAddress6, 0, sizeof(sockaddr_in6)); - if (ipv6) { - socketAddress6.sin6_family = AF_INET6; - socketAddress6.sin6_port = htons(port); - if (inet_pton(AF_INET6, address.c_str(), &socketAddress6.sin6_addr.s6_addr) != 1) { - DEBUG_E("connection(%p) bad ipv6 %s", this, address.c_str()); + if (!ConnectionsManager::getInstance().proxyAddress.empty()) { + std::string &proxyAddress = ConnectionsManager::getInstance().proxyAddress; + if ((socketFd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { + DEBUG_E("connection(%p) can't create proxy socket", this); closeSocket(1); return; } - } else { + proxyAuthState = 1; socketAddress.sin_family = AF_INET; - socketAddress.sin_port = htons(port); - if (inet_pton(AF_INET, address.c_str(), &socketAddress.sin_addr.s_addr) != 1) { - DEBUG_E("connection(%p) bad ipv4 %s", this, address.c_str()); + socketAddress.sin_port = htons(ConnectionsManager::getInstance().proxyPort); + bool continueCheckAddress; + if (inet_pton(AF_INET, proxyAddress.c_str(), &socketAddress.sin_addr.s_addr) != 1) { + continueCheckAddress = true; + DEBUG_D("connection(%p) not ipv4 address %s", this, proxyAddress.c_str()); + } else { + continueCheckAddress = false; + } + if (continueCheckAddress) { + if (inet_pton(AF_INET6, proxyAddress.c_str(), &socketAddress.sin_addr.s_addr) != 1) { + continueCheckAddress = true; + DEBUG_D("connection(%p) not ipv6 address %s", this, proxyAddress.c_str()); + } else { + continueCheckAddress = false; + } + if (continueCheckAddress) { + struct hostent *he; + if ((he = gethostbyname(proxyAddress.c_str())) == nullptr) { + DEBUG_E("connection(%p) can't resolve host %s address", this, proxyAddress.c_str()); + closeSocket(1); + return; + } + struct in_addr **addr_list = (struct in_addr **) he->h_addr_list; + if (addr_list[0] != nullptr) { + socketAddress.sin_addr.s_addr = addr_list[0]->s_addr; + DEBUG_D("connection(%p) resolved host %s address %x", this, proxyAddress.c_str(), addr_list[0]->s_addr); + } else { + DEBUG_E("connection(%p) can't resolve host %s address", this, proxyAddress.c_str()); + closeSocket(1); + return; + } + } + } + } else { + proxyAuthState = 0; + if ((socketFd = socket(ipv6 ? AF_INET6 : AF_INET, SOCK_STREAM, 0)) < 0) { + DEBUG_E("connection(%p) can't create socket", this); closeSocket(1); return; } + if (ipv6) { + socketAddress6.sin6_family = AF_INET6; + socketAddress6.sin6_port = htons(port); + if (inet_pton(AF_INET6, address.c_str(), &socketAddress6.sin6_addr.s6_addr) != 1) { + DEBUG_E("connection(%p) bad ipv6 %s", this, address.c_str()); + closeSocket(1); + return; + } + } else { + socketAddress.sin_family = AF_INET; + socketAddress.sin_port = htons(port); + if (inet_pton(AF_INET, address.c_str(), &socketAddress.sin_addr.s_addr) != 1) { + DEBUG_E("connection(%p) bad ipv4 %s", this, address.c_str()); + closeSocket(1); + return; + } + } } int yes = 1; @@ -119,6 +168,7 @@ void ConnectionSocket::closeSocket(int reason) { } socketFd = -1; } + proxyAuthState = 0; onConnectedSent = false; outgoingByteStream->clean(); onDisconnected(reason); @@ -143,10 +193,58 @@ void ConnectionSocket::onEvent(uint32_t events) { if (readCount > 0) { buffer->limit((uint32_t) readCount); lastEventTime = ConnectionsManager::getInstance().getCurrentTimeMonotonicMillis(); - if (ConnectionsManager::getInstance().delegate != nullptr) { - ConnectionsManager::getInstance().delegate->onBytesReceived(readCount, currentNetworkType); + if (proxyAuthState == 2) { + if (readCount == 2) { + uint8_t auth_method = buffer->bytes()[1]; + if (auth_method == 0xff) { + closeSocket(1); + DEBUG_E("connection(%p) unsupported proxy auth method", this); + } else if (auth_method == 0x02) { + DEBUG_D("connection(%p) proxy auth required", this); + proxyAuthState = 3; + } else if (auth_method == 0x00) { + proxyAuthState = 5; + } + adjustWriteOp(); + } else { + closeSocket(1); + DEBUG_E("connection(%p) invalid proxy response on state 2", this); + } + } else if (proxyAuthState == 4) { + if (readCount == 2) { + uint8_t auth_method = buffer->bytes()[1]; + if (auth_method != 0x00) { + closeSocket(1); + DEBUG_E("connection(%p) auth invalid", this); + } else { + proxyAuthState = 5; + } + adjustWriteOp(); + } else { + closeSocket(1); + DEBUG_E("connection(%p) invalid proxy response on state 4", this); + } + } else if (proxyAuthState == 6) { + if (readCount > 2) { + uint8_t status = buffer->bytes()[1]; + if (status == 0x00) { + DEBUG_D("connection(%p) connected via proxy", this); + proxyAuthState = 0; + adjustWriteOp(); + } else { + closeSocket(1); + DEBUG_E("connection(%p) invalid proxy status on state 6, 0x%x", this, status); + } + } else { + closeSocket(1); + DEBUG_E("connection(%p) invalid proxy response on state 6", this); + } + } else if (proxyAuthState == 0) { + if (ConnectionsManager::getInstance().delegate != nullptr) { + ConnectionsManager::getInstance().delegate->onBytesReceived(readCount, currentNetworkType); + } + onReceivedData(buffer); } - onReceivedData(buffer); } if (readCount != READ_BUFFER_SIZE) { break; @@ -159,29 +257,77 @@ void ConnectionSocket::onEvent(uint32_t events) { closeSocket(1); return; } else { - if (!onConnectedSent) { - lastEventTime = ConnectionsManager::getInstance().getCurrentTimeMonotonicMillis(); - onConnected(); - onConnectedSent = true; - } - NativeByteBuffer *buffer = ConnectionsManager::getInstance().networkBuffer; - buffer->clear(); - outgoingByteStream->get(buffer); - buffer->flip(); - - uint32_t remaining = buffer->remaining(); - if (remaining) { - ssize_t sentLength; - if ((sentLength = send(socketFd, buffer->bytes(), remaining, 0)) < 0) { - DEBUG_E("connection(%p) send failed", this); - closeSocket(1); - return; - } else { - if (ConnectionsManager::getInstance().delegate != nullptr) { - ConnectionsManager::getInstance().delegate->onBytesSent(sentLength, currentNetworkType); + if (proxyAuthState != 0) { + static uint8_t buffer[1024]; + if (proxyAuthState == 1) { + lastEventTime = ConnectionsManager::getInstance().getCurrentTimeMonotonicMillis(); + proxyAuthState = 2; + buffer[0] = 0x05; + buffer[1] = 0x02; + buffer[2] = 0x00; + buffer[3] = 0x02; + if (send(socketFd, buffer, 4, 0) < 0) { + DEBUG_E("connection(%p) send failed", this); + closeSocket(1); + return; } - outgoingByteStream->discard((uint32_t) sentLength); adjustWriteOp(); + } else if (proxyAuthState == 3) { + buffer[0] = 0x01; + uint8_t len1 = (uint8_t) ConnectionsManager::getInstance().proxyUser.length(); + uint8_t len2 = (uint8_t) ConnectionsManager::getInstance().proxyPassword.length(); + buffer[1] = len1; + memcpy(&buffer[2], ConnectionsManager::getInstance().proxyUser.c_str(), len1); + buffer[2 + len1] = len2; + memcpy(&buffer[3 + len1], ConnectionsManager::getInstance().proxyPassword.c_str(), len2); + proxyAuthState = 4; + if (send(socketFd, buffer, 3 + len1 + len2, 0) < 0) { + DEBUG_E("connection(%p) send failed", this); + closeSocket(1); + return; + } + adjustWriteOp(); + } else if (proxyAuthState == 5) { + buffer[0] = 0x05; + buffer[1] = 0x01; + buffer[2] = 0x00; + buffer[3] = (uint8_t) (isIpv6 ? 0x04 : 0x01); + uint16_t networkPort = ntohs(currentPort); + inet_pton(isIpv6 ? AF_INET6 : AF_INET, currentAddress.c_str(), &buffer[4]); + memcpy(&buffer[4 + (isIpv6 ? 16 : 4)], &networkPort, sizeof(uint16_t)); + proxyAuthState = 6; + if (send(socketFd, buffer, 4 + (isIpv6 ? 16 : 4) + 2, 0) < 0) { + DEBUG_E("connection(%p) send failed", this); + closeSocket(1); + return; + } + adjustWriteOp(); + } + } else { + if (!onConnectedSent) { + lastEventTime = ConnectionsManager::getInstance().getCurrentTimeMonotonicMillis(); + onConnected(); + onConnectedSent = true; + } + NativeByteBuffer *buffer = ConnectionsManager::getInstance().networkBuffer; + buffer->clear(); + outgoingByteStream->get(buffer); + buffer->flip(); + + uint32_t remaining = buffer->remaining(); + if (remaining) { + ssize_t sentLength; + if ((sentLength = send(socketFd, buffer->bytes(), remaining, 0)) < 0) { + DEBUG_E("connection(%p) send failed", this); + closeSocket(1); + return; + } else { + if (ConnectionsManager::getInstance().delegate != nullptr) { + ConnectionsManager::getInstance().delegate->onBytesSent(sentLength, currentNetworkType); + } + outgoingByteStream->discard((uint32_t) sentLength); + adjustWriteOp(); + } } } } @@ -196,6 +342,13 @@ void ConnectionSocket::onEvent(uint32_t events) { } } +void ConnectionSocket::writeBuffer(uint8_t *data, uint32_t size) { + NativeByteBuffer *buffer = BuffersStorage::getInstance().getFreeBuffer(size); + buffer->writeBytes(data, size); + outgoingByteStream->append(buffer); + adjustWriteOp(); +} + void ConnectionSocket::writeBuffer(NativeByteBuffer *buffer) { outgoingByteStream->append(buffer); adjustWriteOp(); @@ -203,7 +356,7 @@ void ConnectionSocket::writeBuffer(NativeByteBuffer *buffer) { void ConnectionSocket::adjustWriteOp() { eventMask.events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLET; - if (outgoingByteStream->hasData()) { + if (proxyAuthState == 0 && (outgoingByteStream->hasData() || !onConnectedSent) || proxyAuthState == 1 || proxyAuthState == 3 || proxyAuthState == 5) { eventMask.events |= EPOLLOUT; } eventMask.data.ptr = eventObject; @@ -218,6 +371,10 @@ void ConnectionSocket::setTimeout(time_t time) { lastEventTime = ConnectionsManager::getInstance().getCurrentTimeMonotonicMillis(); } +time_t ConnectionSocket::getTimeout() { + return timeout; +} + void ConnectionSocket::checkTimeout(int64_t now) { if (timeout != 0 && (now - lastEventTime) > (int64_t) timeout * 1000) { closeSocket(2); diff --git a/TMessagesProj/jni/tgnet/ConnectionSocket.h b/TMessagesProj/jni/tgnet/ConnectionSocket.h index 6b44560af..7656dee5a 100644 --- a/TMessagesProj/jni/tgnet/ConnectionSocket.h +++ b/TMessagesProj/jni/tgnet/ConnectionSocket.h @@ -24,9 +24,11 @@ public: ConnectionSocket(); virtual ~ConnectionSocket(); + void writeBuffer(uint8_t *data, uint32_t size); void writeBuffer(NativeByteBuffer *buffer); void openConnection(std::string address, uint16_t port, bool ipv6, int32_t networkType); void setTimeout(time_t timeout); + time_t getTimeout(); bool isDisconnected(); void dropConnection(); @@ -43,11 +45,16 @@ private: struct sockaddr_in socketAddress; struct sockaddr_in6 socketAddress6; int socketFd = -1; - time_t timeout = 15; + time_t timeout = 12; bool onConnectedSent = false; int64_t lastEventTime = 0; EventObject *eventObject; int32_t currentNetworkType; + bool isIpv6; + std::string currentAddress; + uint16_t currentPort; + + uint8_t proxyAuthState; bool checkSocketError(); void closeSocket(int reason); diff --git a/TMessagesProj/jni/tgnet/ConnectionsManager.cpp b/TMessagesProj/jni/tgnet/ConnectionsManager.cpp index 2f4ebf088..8c626b49f 100644 --- a/TMessagesProj/jni/tgnet/ConnectionsManager.cpp +++ b/TMessagesProj/jni/tgnet/ConnectionsManager.cpp @@ -144,7 +144,7 @@ int ConnectionsManager::callEvents(int64_t now) { if (!networkPaused) { return 1000; } - int32_t timeToPushPing = (sendingPushPing ? 30000 : 60000 * 3) - abs(now - lastPushPingTime); + int32_t timeToPushPing = (sendingPushPing ? 30000 : 60000 * 3) - llabs(now - lastPushPingTime); if (timeToPushPing <= 0) { return 1000; } @@ -253,7 +253,7 @@ void ConnectionsManager::select() { sendPing(datacenter, false); } if (abs((int32_t) (now / 1000) - lastDcUpdateTime) >= DC_UPDATE_TIME) { - updateDcSettings(0); + updateDcSettings(0, false); } processRequestQueue(0, 0); } else if (!datacenter->isHandshaking()) { @@ -384,11 +384,7 @@ void ConnectionsManager::loadConfig() { movingToDatacenterId = DEFAULT_DATACENTER_ID; } -void ConnectionsManager::saveConfig() { - if (config == nullptr) { - config = new Config("tgnet.dat"); - } - NativeByteBuffer *buffer = BuffersStorage::getInstance().getFreeBuffer(32 * 1024); +void ConnectionsManager::saveConfigInternal(NativeByteBuffer *buffer) { buffer->writeInt32(configVersion); buffer->writeBool(testBackend); Datacenter *currentDatacenter = getDatacenterWithId(currentDatacenterId); @@ -414,6 +410,17 @@ void ConnectionsManager::saveConfig() { iter->second->serializeToStream(buffer); } } +} + +void ConnectionsManager::saveConfig() { + if (config == nullptr) { + config = new Config("tgnet.dat"); + } + static NativeByteBuffer *sizeCalculator = new NativeByteBuffer(true); + sizeCalculator->clearCapacity(); + saveConfigInternal(sizeCalculator); + NativeByteBuffer *buffer = BuffersStorage::getInstance().getFreeBuffer(sizeCalculator->capacity()); + saveConfigInternal(buffer); config->writeConfig(buffer); buffer->reuse(); } @@ -507,6 +514,10 @@ int32_t ConnectionsManager::getCurrentTime() { return (int32_t) (getCurrentTimeMillis() / 1000) + timeDifference; } +bool ConnectionsManager::isTestBackend() { + return testBackend; +} + int32_t ConnectionsManager::getTimeDifference() { return timeDifference; } @@ -572,18 +583,41 @@ void ConnectionsManager::cleanUp() { }); } -void ConnectionsManager::onConnectionClosed(Connection *connection) { +void ConnectionsManager::onConnectionClosed(Connection *connection, int reason) { Datacenter *datacenter = connection->getDatacenter(); if (connection->getConnectionType() == ConnectionTypeGeneric) { + if (proxyAddress.empty()) { + if (reason == 2) { + disconnectTimeoutAmount += connection->getTimeout(); + } else { + disconnectTimeoutAmount += 4; + } + if (disconnectTimeoutAmount >= 20) { + if (!connection->hasUsefullData()) { + requestingSecondAddress = 0; + delegate->onRequestNewServerIpAndPort(requestingSecondAddress); + } + disconnectTimeoutAmount = 0; + } + } if (datacenter->isHandshaking()) { datacenter->onHandshakeConnectionClosed(connection); } if (datacenter->getDatacenterId() == currentDatacenterId) { if (networkAvailable) { - if (connectionState != ConnectionStateConnecting) { - connectionState = ConnectionStateConnecting; - if (delegate != nullptr) { - delegate->onConnectionStateChanged(connectionState); + if (proxyAddress.empty()) { + if (connectionState != ConnectionStateConnecting) { + connectionState = ConnectionStateConnecting; + if (delegate != nullptr) { + delegate->onConnectionStateChanged(connectionState); + } + } + } else { + if (connectionState != ConnectionStateConnectingViaProxy) { + connectionState = ConnectionStateConnectingViaProxy; + if (delegate != nullptr) { + delegate->onConnectionStateChanged(connectionState); + } } } } else { @@ -643,7 +677,13 @@ void ConnectionsManager::onConnectionDataReceived(Connection *connection, Native bool error = false; if (length == 4) { int32_t code = data->readInt32(&error); - DEBUG_E("mtproto error = %d", code); + Datacenter *datacenter = connection->getDatacenter(); + if (code == -404 && datacenter->isCdnDatacenter) { + datacenter->clear(); + DEBUG_D("connection(%p, dc%u, type %d) reset auth key duo to -404 error", connection, datacenter->getDatacenterId(), connection->getConnectionType()); + } else { + DEBUG_E("mtproto error = %d", code); + } connection->reconnect(); return; } @@ -703,6 +743,10 @@ void ConnectionsManager::onConnectionDataReceived(Connection *connection, Native processServerResponse(object, messageId, 0, 0, connection, 0, 0); connection->addProcessedMessageId(messageId); } + connection->setHasUsefullData(); + if (connection->getConnectionType() == ConnectionTypeGeneric) { + disconnectTimeoutAmount = 0; + } delete object; } } else { @@ -740,6 +784,10 @@ void ConnectionsManager::onConnectionDataReceived(Connection *connection, Native if (!doNotProcess) { TLObject *object = TLdeserialize(nullptr, messageLength, data); if (object != nullptr) { + connection->setHasUsefullData(); + if (connection->getConnectionType() == ConnectionTypeGeneric) { + disconnectTimeoutAmount = 0; + } DEBUG_D("connection(%p, dc%u, type %d) received object %s", connection, datacenter->getDatacenterId(), connection->getConnectionType(), typeid(*object).name()); processServerResponse(object, messageId, messageSeqNo, messageServerSalt, connection, 0, 0); connection->addProcessedMessageId(messageId); @@ -980,9 +1028,11 @@ void ConnectionsManager::processServerResponse(TLObject *message, int64_t messag TL_error *error2 = hasResult ? dynamic_cast(response->result.get()) : nullptr; if (error != nullptr) { allowInitConnection = false; + static std::string authRestart = "AUTH_RESTART"; + bool processEvenFailed = error->error_code == 500 && error->error_message.find(authRestart) != std::string::npos; DEBUG_E("request %p rpc error %d: %s", request, error->error_code, error->error_message.c_str()); - if ((request->requestFlags & RequestFlagFailOnServerErrors) == 0) { + if ((request->requestFlags & RequestFlagFailOnServerErrors) == 0 || processEvenFailed) { if (error->error_code == 500 || error->error_code < 0) { discardResponse = true; request->minStartTime = request->startTime + (request->serverFailureCount > 10 ? 10 : request->serverFailureCount); @@ -1378,7 +1428,7 @@ int32_t ConnectionsManager::sendRequestInternal(TLObject *object, onCompleteFunc delete object; return 0; } - Request *request = new Request(lastRequestToken++, connetionType, flags, datacenterId, onComplete, onQuickAck); + Request *request = new Request(lastRequestToken++, connetionType, flags, datacenterId, onComplete, onQuickAck, nullptr); request->rawRequest = object; request->rpcRequest = wrapInLayer(object, getDatacenterWithId(datacenterId), request); requestsQueue.push_back(std::unique_ptr(request)); @@ -1401,7 +1451,7 @@ void ConnectionsManager::sendRequest(TLObject *object, onCompleteFunc onComplete return; } scheduleTask([&, requestToken, object, onComplete, onQuickAck, flags, datacenterId, connetionType, immediate] { - Request *request = new Request(requestToken, connetionType, flags, datacenterId, onComplete, onQuickAck); + Request *request = new Request(requestToken, connetionType, flags, datacenterId, onComplete, onQuickAck, nullptr); request->rawRequest = object; request->rpcRequest = wrapInLayer(object, getDatacenterWithId(datacenterId), request); requestsQueue.push_back(std::unique_ptr(request)); @@ -1412,7 +1462,7 @@ void ConnectionsManager::sendRequest(TLObject *object, onCompleteFunc onComplete } #ifdef ANDROID -void ConnectionsManager::sendRequest(TLObject *object, onCompleteFunc onComplete, onQuickAckFunc onQuickAck, uint32_t flags, uint32_t datacenterId, ConnectionType connetionType, bool immediate, int32_t requestToken, jobject ptr1, jobject ptr2) { +void ConnectionsManager::sendRequest(TLObject *object, onCompleteFunc onComplete, onQuickAckFunc onQuickAck, onWriteToSocketFunc onWriteToSocket, uint32_t flags, uint32_t datacenterId, ConnectionType connetionType, bool immediate, int32_t requestToken, jobject ptr1, jobject ptr2, jobject ptr3) { if (!currentUserId && !(flags & RequestFlagWithoutLogin)) { DEBUG_D("can't do request without login %s", typeid(*object).name()); delete object; @@ -1429,14 +1479,19 @@ void ConnectionsManager::sendRequest(TLObject *object, onCompleteFunc onComplete env->DeleteGlobalRef(ptr2); ptr2 = nullptr; } + if (ptr3 != nullptr) { + env->DeleteGlobalRef(ptr3); + ptr3 = nullptr; + } return; } - scheduleTask([&, requestToken, object, onComplete, onQuickAck, flags, datacenterId, connetionType, immediate, ptr1, ptr2] { + scheduleTask([&, requestToken, object, onComplete, onQuickAck, onWriteToSocket, flags, datacenterId, connetionType, immediate, ptr1, ptr2, ptr3] { DEBUG_D("send request %p - %s", object, typeid(*object).name()); - Request *request = new Request(requestToken, connetionType, flags, datacenterId, onComplete, onQuickAck); + Request *request = new Request(requestToken, connetionType, flags, datacenterId, onComplete, onQuickAck, onWriteToSocket); request->rawRequest = object; request->ptr1 = ptr1; request->ptr2 = ptr2; + request->ptr3 = ptr3; request->rpcRequest = wrapInLayer(object, getDatacenterWithId(datacenterId), request); DEBUG_D("send request wrapped %p - %s", request->rpcRequest.get(), typeid(*(request->rpcRequest.get())).name()); requestsQueue.push_back(std::unique_ptr(request)); @@ -1487,7 +1542,7 @@ void ConnectionsManager::setUserId(int32_t userId) { registerForInternalPushUpdates(); } if (currentUserId != userId && userId != 0) { - updateDcSettings(0); + updateDcSettings(0, false); } if (currentUserId != 0 && pushConnectionEnabled) { Datacenter *datacenter = getDatacenterWithId(currentDatacenterId); @@ -1501,6 +1556,7 @@ void ConnectionsManager::setUserId(int32_t userId) { void ConnectionsManager::switchBackend() { scheduleTask([&] { + currentDatacenterId = 1; testBackend = !testBackend; datacenters.clear(); initDatacenters(); @@ -1699,10 +1755,10 @@ void ConnectionsManager::registerForInternalPushUpdates() { } -inline void addMessageToDatacenter(uint32_t datacenterId, NetworkMessage *networkMessage, std::map>> &genericMessagesToDatacenters) { - std::map>>::iterator iter = genericMessagesToDatacenters.find(datacenterId); - if (iter == genericMessagesToDatacenters.end()) { - std::vector> &array = genericMessagesToDatacenters[datacenterId] = std::vector>(); +inline void addMessageToDatacenter(uint32_t datacenterId, NetworkMessage *networkMessage, std::map>> &messagesToDatacenters) { + std::map>>::iterator iter = messagesToDatacenters.find(datacenterId); + if (iter == messagesToDatacenters.end()) { + std::vector> &array = messagesToDatacenters[datacenterId] = std::vector>(); array.push_back(std::unique_ptr(networkMessage)); } else { iter->second.push_back(std::unique_ptr(networkMessage)); @@ -1711,11 +1767,13 @@ inline void addMessageToDatacenter(uint32_t datacenterId, NetworkMessage *networ void ConnectionsManager::processRequestQueue(uint32_t connectionTypes, uint32_t dc) { static std::map>> genericMessagesToDatacenters; + static std::map>> tempMessagesToDatacenters; static std::vector unknownDatacenterIds; static std::vector neededDatacenters; static std::vector unauthorizedDatacenters; genericMessagesToDatacenters.clear(); + tempMessagesToDatacenters.clear(); unknownDatacenterIds.clear(); neededDatacenters.clear(); unauthorizedDatacenters.clear(); @@ -1775,30 +1833,35 @@ void ConnectionsManager::processRequestQueue(uint32_t connectionTypes, uint32_t } Datacenter *requestDatacenter = getDatacenterWithId(datacenterId); - if (requestDatacenter->lastInitVersion != currentVersion && !request->isInitRequest) { - request->rpcRequest.release(); - request->rpcRequest = wrapInLayer(request->rawRequest, requestDatacenter, request); - request->serializedLength = request->getRpcRequest()->getObjectSize(); - } - if (requestDatacenter == nullptr) { if (std::find(unknownDatacenterIds.begin(), unknownDatacenterIds.end(), datacenterId) == unknownDatacenterIds.end()) { unknownDatacenterIds.push_back(datacenterId); } iter++; continue; - } else if (!requestDatacenter->hasAuthKey()) { - if (std::find(neededDatacenters.begin(), neededDatacenters.end(), requestDatacenter) == neededDatacenters.end()) { - neededDatacenters.push_back(requestDatacenter); + } else { + if (requestDatacenter->isCdnDatacenter) { + request->requestFlags |= RequestFlagEnableUnauthorized; } - iter++; - continue; - } else if (!(request->requestFlags & RequestFlagEnableUnauthorized) && !requestDatacenter->authorized && request->datacenterId != DEFAULT_DATACENTER_ID && request->datacenterId != currentDatacenterId) { - if (std::find(unauthorizedDatacenters.begin(), unauthorizedDatacenters.end(), requestDatacenter) == unauthorizedDatacenters.end()) { - unauthorizedDatacenters.push_back(requestDatacenter); + if (requestDatacenter->lastInitVersion != currentVersion && !request->isInitRequest) { + request->rpcRequest.release(); + request->rpcRequest = wrapInLayer(request->rawRequest, requestDatacenter, request); + request->serializedLength = request->getRpcRequest()->getObjectSize(); + } + + if (!requestDatacenter->hasAuthKey()) { + if (std::find(neededDatacenters.begin(), neededDatacenters.end(), requestDatacenter) == neededDatacenters.end()) { + neededDatacenters.push_back(requestDatacenter); + } + iter++; + continue; + } else if (!(request->requestFlags & RequestFlagEnableUnauthorized) && !requestDatacenter->authorized && request->datacenterId != DEFAULT_DATACENTER_ID && request->datacenterId != currentDatacenterId) { + if (std::find(unauthorizedDatacenters.begin(), unauthorizedDatacenters.end(), requestDatacenter) == unauthorizedDatacenters.end()) { + unauthorizedDatacenters.push_back(requestDatacenter); + } + iter++; + continue; } - iter++; - continue; } Connection *connection = requestDatacenter->getConnectionByType(request->connectionType, true); @@ -1827,7 +1890,7 @@ void ConnectionsManager::processRequestQueue(uint32_t connectionTypes, uint32_t ) ) { if (!forceThisRequest && request->connectionToken > 0) { - if (request->connectionType & ConnectionTypeGeneric && request->connectionToken == connection->getConnectionToken()) { + if ((request->connectionType & ConnectionTypeGeneric || request->connectionType & ConnectionTypeTemp) && request->connectionToken == connection->getConnectionToken()) { DEBUG_D("request token is valid, not retrying %s", typeInfo.name()); iter++; continue; @@ -1892,11 +1955,15 @@ void ConnectionsManager::processRequestQueue(uint32_t connectionTypes, uint32_t case ConnectionTypeGeneric: addMessageToDatacenter(requestDatacenter->getDatacenterId(), networkMessage, genericMessagesToDatacenters); break; + case ConnectionTypeTemp: + addMessageToDatacenter(requestDatacenter->getDatacenterId(), networkMessage, tempMessagesToDatacenters); + break; case ConnectionTypeDownload: case ConnectionTypeUpload: { std::vector> array; array.push_back(std::unique_ptr(networkMessage)); sendMessagesToConnectionWithConfirmation(array, connection, false); + request->onWriteToSocket(); break; } default: @@ -1954,7 +2021,7 @@ void ConnectionsManager::processRequestQueue(uint32_t connectionTypes, uint32_t if (requestStartTime != 0 && abs(currentTime - requestStartTime) >= timeout) { std::vector allDc; for (std::map::iterator iter2 = datacenters.begin(); iter2 != datacenters.end(); iter2++) { - if (iter2->first == datacenterId) { + if (iter2->first == datacenterId || iter2->second->isCdnDatacenter) { continue; } allDc.push_back(iter2->first); @@ -1972,29 +2039,31 @@ void ConnectionsManager::processRequestQueue(uint32_t connectionTypes, uint32_t } Datacenter *requestDatacenter = getDatacenterWithId(datacenterId); - if (requestDatacenter->lastInitVersion != currentVersion && !request->isInitRequest) { - request->rpcRequest.release(); - request->rpcRequest = wrapInLayer(request->rawRequest, requestDatacenter, request); - } - if (requestDatacenter == nullptr) { if (std::find(unknownDatacenterIds.begin(), unknownDatacenterIds.end(), datacenterId) == unknownDatacenterIds.end()) { unknownDatacenterIds.push_back(datacenterId); } iter++; continue; - } else if (!requestDatacenter->hasAuthKey()) { - if (std::find(neededDatacenters.begin(), neededDatacenters.end(), requestDatacenter) == neededDatacenters.end()) { - neededDatacenters.push_back(requestDatacenter); + } else { + if (requestDatacenter->lastInitVersion != currentVersion && !request->isInitRequest) { + request->rpcRequest.release(); + request->rpcRequest = wrapInLayer(request->rawRequest, requestDatacenter, request); } - iter++; - continue; - } else if (!(request->requestFlags & RequestFlagEnableUnauthorized) && !requestDatacenter->authorized && request->datacenterId != DEFAULT_DATACENTER_ID && request->datacenterId != currentDatacenterId) { - if (std::find(unauthorizedDatacenters.begin(), unauthorizedDatacenters.end(), requestDatacenter) == unauthorizedDatacenters.end()) { - unauthorizedDatacenters.push_back(requestDatacenter); + + if (!requestDatacenter->hasAuthKey()) { + if (std::find(neededDatacenters.begin(), neededDatacenters.end(), requestDatacenter) == neededDatacenters.end()) { + neededDatacenters.push_back(requestDatacenter); + } + iter++; + continue; + } else if (!(request->requestFlags & RequestFlagEnableUnauthorized) && !requestDatacenter->authorized && request->datacenterId != DEFAULT_DATACENTER_ID && request->datacenterId != currentDatacenterId) { + if (std::find(unauthorizedDatacenters.begin(), unauthorizedDatacenters.end(), requestDatacenter) == unauthorizedDatacenters.end()) { + unauthorizedDatacenters.push_back(requestDatacenter); + } + iter++; + continue; } - iter++; - continue; } Connection *connection = requestDatacenter->getConnectionByType(request->connectionType, true); @@ -2067,6 +2136,9 @@ void ConnectionsManager::processRequestQueue(uint32_t connectionTypes, uint32_t case ConnectionTypeGeneric: addMessageToDatacenter(requestDatacenter->getDatacenterId(), networkMessage, genericMessagesToDatacenters); break; + case ConnectionTypeTemp: + addMessageToDatacenter(requestDatacenter->getDatacenterId(), networkMessage, tempMessagesToDatacenters); + break; case ConnectionTypeDownload: case ConnectionTypeUpload: { std::vector> array; @@ -2090,6 +2162,14 @@ void ConnectionsManager::processRequestQueue(uint32_t connectionTypes, uint32_t genericMessagesToDatacenters[datacenter->getDatacenterId()] = std::vector>(); } } + + iter2 = tempMessagesToDatacenters.find(datacenter->getDatacenterId()); + if (iter2 == tempMessagesToDatacenters.end()) { + Connection *connection = datacenter->getTempConnection(false); + if (connection != nullptr && connection->getConnectionToken() != 0 && connection->hasMessagesToConfirm()) { + tempMessagesToDatacenters[datacenter->getDatacenterId()] = std::vector>(); + } + } } for (std::map>>::iterator iter = genericMessagesToDatacenters.begin(); iter != genericMessagesToDatacenters.end(); iter++) { @@ -2156,6 +2236,14 @@ void ConnectionsManager::processRequestQueue(uint32_t connectionTypes, uint32_t } } + for (std::map>>::iterator iter = tempMessagesToDatacenters.begin(); iter != tempMessagesToDatacenters.end(); iter++) { + Datacenter *datacenter = getDatacenterWithId(iter->first); + if (datacenter != nullptr) { + std::vector> &array = iter->second; + sendMessagesToConnectionWithConfirmation(array, datacenter->getTempConnection(true), false); + } + } + if (connectionTypes == ConnectionTypeGeneric && dc == currentDatacenterId) { std::map>>::iterator iter2 = genericMessagesToDatacenters.find(currentDatacenterId); if (iter2 == genericMessagesToDatacenters.end()) { @@ -2164,7 +2252,7 @@ void ConnectionsManager::processRequestQueue(uint32_t connectionTypes, uint32_t } if (!unknownDatacenterIds.empty()) { - updateDcSettings(0); + updateDcSettings(0, false); } size_t count = neededDatacenters.size(); @@ -2198,7 +2286,7 @@ Datacenter *ConnectionsManager::getDatacenterWithId(uint32_t datacenterId) { std::unique_ptr ConnectionsManager::wrapInLayer(TLObject *object, Datacenter *datacenter, Request *baseRequest) { if (object->isNeedLayer()) { if (datacenter == nullptr || datacenter->lastInitVersion != currentVersion) { - if (datacenter->getDatacenterId() == currentDatacenterId) { + if (datacenter != nullptr && datacenter->getDatacenterId() == currentDatacenterId) { registerForInternalPushUpdates(); } baseRequest->isInitRequest = true; @@ -2206,20 +2294,29 @@ std::unique_ptr ConnectionsManager::wrapInLayer(TLObject *object, Data request->query = std::unique_ptr(object); request->api_id = currentApiId; request->app_version = currentAppVersion; - request->device_model = currentDeviceModel; request->lang_code = currentLangCode; - request->system_version = currentSystemVersion; + request->system_lang_code = currentLangCode; + request->lang_pack = "android"; + request->system_lang_code = currentSystemLangCode; + + if (datacenter == nullptr || datacenter->isCdnDatacenter) { + request->device_model = "n/a"; + request->system_version = "n/a"; + } else { + request->device_model = currentDeviceModel; + request->system_version = currentSystemVersion; + } if (request->lang_code.empty()) { request->lang_code = "en"; } if (request->device_model.empty()) { - request->device_model = "device model unknown"; + request->device_model = "n/a"; } if (request->app_version.empty()) { - request->app_version = "app version unknown"; + request->app_version = "n/a"; } if (request->system_version.empty()) { - request->system_version = "system version unknown"; + request->system_version = "n/a"; } invokeWithLayer *request2 = new invokeWithLayer(); request2->layer = currentLayer; @@ -2231,54 +2328,68 @@ std::unique_ptr ConnectionsManager::wrapInLayer(TLObject *object, Data return std::unique_ptr(object); } -void ConnectionsManager::updateDcSettings(uint32_t dcNum) { - if (updatingDcSettings) { - return; +void ConnectionsManager::updateDcSettings(uint32_t dcNum, bool workaround) { + if (workaround) { + if (updatingDcSettingsWorkaround) { + return; + } + updatingDcSettingsWorkaround = true; + } else { + if (updatingDcSettings) { + return; + } + updatingDcSettings = true; + updatingDcStartTime = (int32_t) (getCurrentTimeMonotonicMillis() / 1000); } - updatingDcStartTime = (int32_t) (getCurrentTimeMonotonicMillis() / 1000); - updatingDcSettings = true; - TL_help_getConfig *request = new TL_help_getConfig(); - sendRequest(request, [&](TLObject *response, TL_error *error, int32_t networkType) { - if (!updatingDcSettings) { + TL_help_getConfig *request = new TL_help_getConfig(); + sendRequest(request, [&, workaround](TLObject *response, TL_error *error, int32_t networkType) { + if (!workaround && !updatingDcSettings || workaround && !updatingDcSettingsWorkaround) { return; } if (response != nullptr) { TL_config *config = (TL_config *) response; - int32_t updateIn = config->expires - getCurrentTime(); - if (updateIn <= 0) { - updateIn = 120; + if (!workaround) { + int32_t updateIn = config->expires - getCurrentTime(); + if (updateIn <= 0) { + updateIn = 120; + } + lastDcUpdateTime = (int32_t) (getCurrentTimeMonotonicMillis() / 1000) - DC_UPDATE_TIME + updateIn; } - lastDcUpdateTime = (int32_t) (getCurrentTimeMonotonicMillis() / 1000) - DC_UPDATE_TIME + updateIn; struct DatacenterInfo { - std::vector addressesIpv4; - std::vector addressesIpv6; - std::vector addressesIpv4Download; - std::vector addressesIpv6Download; - std::map ports; + std::vector addressesIpv4; + std::vector addressesIpv6; + std::vector addressesIpv4Download; + std::vector addressesIpv6Download; + bool isCdn = false; - void addAddressAndPort(std::string address, uint32_t port, uint32_t flags) { - std::vector *addresses; - if ((flags & 2) != 0) { - if ((flags & 1) != 0) { + void addAddressAndPort(TL_dcOption *dcOption) { + std::vector *addresses; + if (!isCdn) { + isCdn = dcOption->cdn; + } + if (dcOption->media_only) { + if (dcOption->ipv6) { addresses = &addressesIpv6Download; } else { addresses = &addressesIpv4Download; } } else { - if ((flags & 1) != 0) { + if (dcOption->ipv6) { addresses = &addressesIpv6; } else { addresses = &addressesIpv4; } } - if (std::find(addresses->begin(), addresses->end(), address) != addresses->end()) { - return; + for (std::vector::iterator iter = addresses->begin(); iter != addresses->end(); iter++) { + if (iter->address == dcOption->ip_address && iter->port == dcOption->port) { + return; + } } - addresses->push_back(address); - ports[address] = port; + DEBUG_D("getConfig add %s:%d to dc%d", dcOption->ip_address.c_str(), dcOption->port, dcOption->id); + addresses->push_back(TcpAddress(dcOption->ip_address, dcOption->port, dcOption->flags)); } }; @@ -2293,7 +2404,7 @@ void ConnectionsManager::updateDcSettings(uint32_t dcNum) { } else { info = iter->second.get(); } - info->addAddressAndPort(dcOption->ip_address, (uint32_t) dcOption->port, (uint32_t) dcOption->flags); + info->addAddressAndPort(dcOption); } if (!map.empty()) { @@ -2304,10 +2415,10 @@ void ConnectionsManager::updateDcSettings(uint32_t dcNum) { datacenter = new Datacenter(iter->first); datacenters[iter->first] = datacenter; } - datacenter->replaceAddressesAndPorts(info->addressesIpv4, info->ports, 0); - datacenter->replaceAddressesAndPorts(info->addressesIpv6, info->ports, 1); - datacenter->replaceAddressesAndPorts(info->addressesIpv4Download, info->ports, 2); - datacenter->replaceAddressesAndPorts(info->addressesIpv6Download, info->ports, 3); + datacenter->replaceAddresses(info->addressesIpv4, info->isCdn ? 8 : 0); + datacenter->replaceAddresses(info->addressesIpv6, info->isCdn ? 9 : 1); + datacenter->replaceAddresses(info->addressesIpv4Download, info->isCdn ? 10 : 2); + datacenter->replaceAddresses(info->addressesIpv6Download, info->isCdn ? 11 : 3); if (iter->first == movingToDatacenterId) { movingToDatacenterId = DEFAULT_DATACENTER_ID; moveToDatacenter(iter->first); @@ -2320,8 +2431,12 @@ void ConnectionsManager::updateDcSettings(uint32_t dcNum) { delegate->onUpdateConfig(config); } } - updatingDcSettings = false; - }, nullptr, RequestFlagEnableUnauthorized | RequestFlagWithoutLogin | RequestFlagTryDifferentDc, dcNum == 0 ? currentDatacenterId : dcNum, ConnectionTypeGeneric, true); + if (workaround) { + updatingDcSettingsWorkaround = false; + } else { + updatingDcSettings = false; + } + }, nullptr, RequestFlagEnableUnauthorized | RequestFlagWithoutLogin | (workaround ? 0 : RequestFlagTryDifferentDc), dcNum == 0 ? currentDatacenterId : dcNum, workaround ? ConnectionTypeTemp : ConnectionTypeGeneric, true); } void ConnectionsManager::moveToDatacenter(uint32_t datacenterId) { @@ -2352,7 +2467,7 @@ void ConnectionsManager::moveToDatacenter(uint32_t datacenterId) { void ConnectionsManager::authorizeOnMovingDatacenter() { Datacenter *datacenter = getDatacenterWithId(movingToDatacenterId); if (datacenter == nullptr) { - updateDcSettings(0); + updateDcSettings(0, false); return; } datacenter->recreateSessions(); @@ -2391,15 +2506,16 @@ void ConnectionsManager::applyDatacenterAddress(uint32_t datacenterId, std::stri scheduleTask([&, datacenterId, ipAddress, port] { Datacenter *datacenter = getDatacenterWithId(datacenterId); if (datacenter != nullptr) { - std::vector addresses; - std::map ports; - addresses.push_back(ipAddress); - ports[ipAddress] = port; + std::vector addresses; + addresses.push_back(TcpAddress(ipAddress, port, 0)); datacenter->suspendConnections(); - datacenter->replaceAddressesAndPorts(addresses, ports, 0); + datacenter->replaceAddresses(addresses, 0); datacenter->resetAddressAndPortNum(); saveConfig(); - updateDcSettings(datacenterId); + if (datacenter->isHandshaking()) { + datacenter->beginHandshake(true); + } + updateDcSettings(datacenterId, false); } }); } @@ -2428,7 +2544,41 @@ void ConnectionsManager::setPushConnectionEnabled(bool value) { } } -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 configPath, std::string logPath, int32_t userId, bool isPaused, bool enablePushConnection, bool hasNetwork, int32_t networkType) { +void ConnectionsManager::applyDnsConfig(NativeByteBuffer *buffer) { + scheduleTask([&, buffer] { + TL_help_configSimple *config = Datacenter::decodeSimpleConfig(buffer); + int currentDate = getCurrentTime(); + if (config != nullptr && config->date <= currentDate && currentDate <= config->expires) { + Datacenter *datacenter = getDatacenterWithId(config->dc_id); + if (datacenter != nullptr) { + std::vector addresses; + for (std::vector>::iterator iter = config->ip_port_list.begin(); iter != config->ip_port_list.end(); iter++) { + TL_ipPort *ipPort = iter->get(); + addresses.push_back(TcpAddress(ipPort->ipv4, ipPort->port, 0)); + DEBUG_D("got address %s and port %d for dc%d", ipPort->ipv4.c_str(), ipPort->port, config->dc_id); + } + if (!addresses.empty()) { + datacenter->replaceAddresses(addresses, TcpAddressFlagTemp); + Connection *connection = datacenter->getTempConnection(false); + if (connection != nullptr) { + connection->suspendConnection(); + } + if (datacenter->isHandshaking()) { + datacenter->beginHandshake(true); + } + updateDcSettings(config->dc_id, true); + } + } + delete config; + } else if (requestingSecondAddress == 0) { + requestingSecondAddress = 1; + delegate->onRequestNewServerIpAndPort(requestingSecondAddress); + } + buffer->reuse(); + }); +} + +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, int32_t userId, bool isPaused, bool enablePushConnection, bool hasNetwork, int32_t networkType) { currentVersion = version; currentLayer = layer; currentApiId = apiId; @@ -2437,6 +2587,7 @@ void ConnectionsManager::init(uint32_t version, int32_t layer, int32_t apiId, st currentSystemVersion = systemVersion; currentAppVersion = appVersion; currentLangCode = langCode; + currentSystemLangCode = systemLangCode; currentUserId = userId; currentLogPath = logPath; pushConnectionEnabled = enablePushConnection; @@ -2459,6 +2610,32 @@ void ConnectionsManager::init(uint32_t version, int32_t layer, int32_t apiId, st pthread_create(&networkThread, NULL, (ConnectionsManager::ThreadProc), this); } +void ConnectionsManager::setProxySettings(std::string address, uint16_t port, std::string username, std::string password) { + scheduleTask([&, address, port, username, password] { + bool reconnect = proxyAddress != address; + proxyAddress = address; + proxyPort = port; + proxyUser = username; + proxyPassword = password; + if (reconnect) { + for (std::map::iterator iter = datacenters.begin(); iter != datacenters.end(); iter++) { + iter->second->suspendConnections(); + } + processRequestQueue(0, 0); + } + }); +} + +void ConnectionsManager::setLangCode(std::string langCode) { + scheduleTask([&, langCode] { + currentLangCode = langCode; + for (std::map::iterator iter = datacenters.begin(); iter != datacenters.end(); iter++) { + iter->second->lastInitVersion = 0; + } + saveConfig(); + }); +} + void ConnectionsManager::resumeNetwork(bool partial) { scheduleTask([&, partial] { if (partial) { diff --git a/TMessagesProj/jni/tgnet/ConnectionsManager.h b/TMessagesProj/jni/tgnet/ConnectionsManager.h index dacfdb15d..6c1db3e43 100644 --- a/TMessagesProj/jni/tgnet/ConnectionsManager.h +++ b/TMessagesProj/jni/tgnet/ConnectionsManager.h @@ -45,6 +45,7 @@ public: int64_t getCurrentTimeMillis(); int64_t getCurrentTimeMonotonicMillis(); int32_t getCurrentTime(); + bool isTestBackend(); int32_t getTimeDifference(); int32_t sendRequest(TLObject *object, onCompleteFunc onComplete, onQuickAckFunc onQuickAck, uint32_t flags, uint32_t datacenterId, ConnectionType connetionType, bool immediate); void sendRequest(TLObject *object, onCompleteFunc onComplete, onQuickAckFunc onQuickAck, uint32_t flags, uint32_t datacenterId, ConnectionType connetionType, bool immediate, int32_t requestToken); @@ -61,14 +62,17 @@ public: void pauseNetwork(); void setNetworkAvailable(bool value, int32_t type); 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 configPath, std::string logPath, int32_t userId, bool isPaused, bool enablePushConnection, bool hasNetwork, int32_t networkType); - void updateDcSettings(uint32_t datacenterId); + 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, 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); + void setLangCode(std::string langCode); + void updateDcSettings(uint32_t datacenterId, bool workaround); void setPushConnectionEnabled(bool value); + void applyDnsConfig(NativeByteBuffer *buffer); void setMtProtoVersion(int version); int32_t getMtProtoVersion(); #ifdef ANDROID - void sendRequest(TLObject *object, onCompleteFunc onComplete, onQuickAckFunc onQuickAck, uint32_t flags, uint32_t datacenterId, ConnectionType connetionType, bool immediate, int32_t requestToken, jobject ptr1, jobject ptr2); + void sendRequest(TLObject *object, onCompleteFunc onComplete, onQuickAckFunc onQuickAck, onWriteToSocketFunc onWriteToSocket, uint32_t flags, uint32_t datacenterId, ConnectionType connetionType, bool immediate, int32_t requestToken, jobject ptr1, jobject ptr2, jobject ptr3); static void useJavaVM(JavaVM *vm, bool useJavaByteBuffers); #endif @@ -78,6 +82,7 @@ private: void initDatacenters(); void loadConfig(); void saveConfig(); + void saveConfigInternal(NativeByteBuffer *buffer); void select(); void wakeup(); void processServerResponse(TLObject *message, int64_t messageId, int32_t messageSeqNo, int64_t messageSalt, Connection *connection, int64_t innerMsgId, int64_t containerMessageId); @@ -102,7 +107,7 @@ private: void scheduleTask(std::function task); void scheduleEvent(EventObject *eventObject, uint32_t time); void removeEvent(EventObject *eventObject); - void onConnectionClosed(Connection *connection); + void onConnectionClosed(Connection *connection, int reason); void onConnectionConnected(Connection *connection); void onConnectionQuickAckReceived(Connection *connection, int32_t ack); void onConnectionDataReceived(Connection *connection, NativeByteBuffer *data, uint32_t length); @@ -134,6 +139,9 @@ private: int64_t lastPushPingTime = 0; bool sendingPushPing = false; bool updatingDcSettings = false; + bool updatingDcSettingsWorkaround = false; + int32_t disconnectTimeoutAmount = 0; + int32_t requestingSecondAddress = 0; int32_t updatingDcStartTime = 0; int32_t lastDcUpdateTime = 0; int64_t lastPingTime = getCurrentTimeMonotonicMillis(); @@ -147,6 +155,11 @@ private: std::map> requestsByGuids; std::map guidsByRequests; + std::string proxyUser = ""; + std::string proxyPassword = ""; + std::string proxyAddress = ""; + std::uint16_t proxyPort = 1080; + pthread_t networkThread; pthread_mutex_t mutex; std::queue> pendingTasks; @@ -176,6 +189,7 @@ private: std::string currentSystemVersion; std::string currentAppVersion; std::string currentLangCode; + std::string currentSystemLangCode; std::string currentConfigPath; std::string currentLogPath; int32_t currentUserId = 0; diff --git a/TMessagesProj/jni/tgnet/Datacenter.cpp b/TMessagesProj/jni/tgnet/Datacenter.cpp index 0cb457ed2..f6dda9a1e 100644 --- a/TMessagesProj/jni/tgnet/Datacenter.cpp +++ b/TMessagesProj/jni/tgnet/Datacenter.cpp @@ -27,28 +27,33 @@ static std::vector serverPublicKeys; static std::vector serverPublicKeysFingerprints; -static BN_CTX *bnContext; +static std::map cdnPublicKeys; +static std::map cdnPublicKeysFingerprints; +static std::vector cdnWaitingDatacenters; +static bool loadingCdnKeys = false; +static BN_CTX *bnContext = nullptr; static SHA256_CTX sha256Ctx; +static Config *cdnConfig = nullptr; Datacenter::Datacenter(uint32_t id) { datacenterId = id; - for (uint32_t a = 0; a < DOWNLOAD_CONNECTIONS_COUNT; a++) { + for (uint32_t a = 0; a < UPLOAD_CONNECTIONS_COUNT; a++) { uploadConnection[a] = nullptr; } - for (uint32_t a = 0; a < UPLOAD_CONNECTIONS_COUNT; a++) { - downloadConnections[a] = nullptr; + for (uint32_t a = 0; a < DOWNLOAD_CONNECTIONS_COUNT; a++) { + downloadConnection[a] = nullptr; } } Datacenter::Datacenter(NativeByteBuffer *data) { - for (uint32_t a = 0; a < DOWNLOAD_CONNECTIONS_COUNT; a++) { + for (uint32_t a = 0; a < UPLOAD_CONNECTIONS_COUNT; a++) { uploadConnection[a] = nullptr; } - for (uint32_t a = 0; a < UPLOAD_CONNECTIONS_COUNT; a++) { - downloadConnections[a] = nullptr; + for (uint32_t a = 0; a < DOWNLOAD_CONNECTIONS_COUNT; a++) { + downloadConnection[a] = nullptr; } uint32_t currentVersion = data->readUint32(nullptr); - if (currentVersion >= 2 && currentVersion <= 5) { + if (currentVersion >= 2 && currentVersion <= 7) { datacenterId = data->readUint32(nullptr); if (currentVersion >= 3) { lastInitVersion = data->readUint32(nullptr); @@ -56,29 +61,56 @@ Datacenter::Datacenter(NativeByteBuffer *data) { uint32_t len = data->readUint32(nullptr); for (uint32_t a = 0; a < len; a++) { std::string address = data->readString(nullptr); - addressesIpv4.push_back(address); - ports[address] = data->readUint32(nullptr); + uint32_t port = data->readUint32(nullptr); + int32_t flags; + if (currentVersion >= 7) { + flags = data->readInt32(nullptr); + } else { + flags = 0; + } + addressesIpv4.push_back(TcpAddress(address, port, flags)); } if (currentVersion >= 5) { len = data->readUint32(nullptr); for (uint32_t a = 0; a < len; a++) { std::string address = data->readString(nullptr); - addressesIpv6.push_back(address); - ports[address] = data->readUint32(nullptr); + uint32_t port = data->readUint32(nullptr); + int32_t flags; + if (currentVersion >= 7) { + flags = data->readInt32(nullptr); + } else { + flags = 0; + } + addressesIpv6.push_back(TcpAddress(address, port, flags)); } len = data->readUint32(nullptr); for (uint32_t a = 0; a < len; a++) { std::string address = data->readString(nullptr); - addressesIpv4Download.push_back(address); - ports[address] = data->readUint32(nullptr); + uint32_t port = data->readUint32(nullptr); + int32_t flags; + if (currentVersion >= 7) { + flags = data->readInt32(nullptr); + } else { + flags = 0; + } + addressesIpv4Download.push_back(TcpAddress(address, port, flags)); } len = data->readUint32(nullptr); for (uint32_t a = 0; a < len; a++) { std::string address = data->readString(nullptr); - addressesIpv6Download.push_back(address); - ports[address] = data->readUint32(nullptr); + uint32_t port = data->readUint32(nullptr); + int32_t flags; + if (currentVersion >= 7) { + flags = data->readInt32(nullptr); + } else { + flags = 0; + } + addressesIpv6Download.push_back(TcpAddress(address, port, flags)); } } + if (currentVersion >= 6) { + isCdnDatacenter = data->readBool(nullptr); + } len = data->readUint32(nullptr); if (len != 0) { authKey = data->readBytes(len, nullptr); @@ -108,7 +140,7 @@ Datacenter::Datacenter(NativeByteBuffer *data) { NativeByteBuffer *buffer = config->readConfig(); if (buffer != nullptr) { uint32_t version = buffer->readUint32(nullptr); - if (version <= paramsConfigVersion) { + if (version >= 1) { currentPortNumIpv4 = buffer->readUint32(nullptr); currentAddressNumIpv4 = buffer->readUint32(nullptr); currentPortNumIpv6 = buffer->readUint32(nullptr); @@ -133,28 +165,28 @@ Datacenter::Datacenter(NativeByteBuffer *data) { void Datacenter::switchTo443Port() { for (uint32_t a = 0; a < addressesIpv4.size(); a++) { - if (ports[addressesIpv4[a]] == 443) { + if (addressesIpv4[a].port == 443) { currentAddressNumIpv4 = a; currentPortNumIpv4 = 0; break; } } for (uint32_t a = 0; a < addressesIpv6.size(); a++) { - if (ports[addressesIpv6[a]] == 443) { + if (addressesIpv6[a].port == 443) { currentAddressNumIpv6 = a; currentPortNumIpv6 = 0; break; } } for (uint32_t a = 0; a < addressesIpv4Download.size(); a++) { - if (ports[addressesIpv4Download[a]] == 443) { + if (addressesIpv4Download[a].port == 443) { currentAddressNumIpv4Download = a; currentPortNumIpv4Download = 0; break; } } for (uint32_t a = 0; a < addressesIpv6Download.size(); a++) { - if (ports[addressesIpv6Download[a]] == 443) { + if (addressesIpv6Download[a].port == 443) { currentAddressNumIpv6Download = a; currentPortNumIpv6Download = 0; break; @@ -164,9 +196,15 @@ void Datacenter::switchTo443Port() { std::string Datacenter::getCurrentAddress(uint32_t flags) { uint32_t currentAddressNum; - std::vector *addresses; - if ((flags & 2) != 0) { - if ((flags & 1) != 0) { + std::vector *addresses; + if (flags == 0 && !hasAuthKey() && !addressesIpv4Temp.empty()) { + flags = TcpAddressFlagTemp; + } + if ((flags & TcpAddressFlagTemp) != 0) { + currentAddressNum = currentAddressNumIpv4Temp; + addresses = &addressesIpv4Temp; + } else if ((flags & TcpAddressFlagDownload) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { currentAddressNum = currentAddressNumIpv6Download; addresses = &addressesIpv6Download; } else { @@ -174,7 +212,7 @@ std::string Datacenter::getCurrentAddress(uint32_t flags) { addresses = &addressesIpv4Download; } } else { - if ((flags & 1) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { currentAddressNum = currentAddressNumIpv6; addresses = &addressesIpv6; } else { @@ -185,57 +223,111 @@ std::string Datacenter::getCurrentAddress(uint32_t flags) { if (addresses->empty()) { return std::string(""); } + if ((flags & TcpAddressFlagStatic) != 0) { + for (std::vector::iterator iter = addresses->begin(); iter != addresses->end(); iter++) { + if ((iter->flags & TcpAddressFlagStatic) != 0) { + return iter->address; + } + } + } if (currentAddressNum >= addresses->size()) { currentAddressNum = 0; - if ((flags & 2) != 0) { - if ((flags & 1) != 0) { + if ((flags & TcpAddressFlagTemp) != 0) { + currentAddressNumIpv4Temp = currentAddressNum; + } else if ((flags & TcpAddressFlagDownload) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { currentAddressNumIpv6Download = currentAddressNum; } else { currentAddressNumIpv4Download = currentAddressNum; } } else { - if ((flags & 1) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { currentAddressNumIpv6 = currentAddressNum; } else { currentAddressNumIpv4 = currentAddressNum; } } } - return (*addresses)[currentAddressNum]; + return (*addresses)[currentAddressNum].address; } int32_t Datacenter::getCurrentPort(uint32_t flags) { - if (ports.empty()) { - return overridePort == -1 ? 443 : overridePort; - } - - const int32_t *portsArray = overridePort == 8888 ? defaultPorts8888 : defaultPorts; - + uint32_t currentAddressNum; uint32_t currentPortNum; - if ((flags & 2) != 0) { - if ((flags & 1) != 0) { + std::vector *addresses; + if (flags == 0 && !hasAuthKey() && !addressesIpv4Temp.empty()) { + flags = TcpAddressFlagTemp; + } + if ((flags & TcpAddressFlagTemp) != 0) { + currentAddressNum = currentAddressNumIpv4Temp; + currentPortNum = currentPortNumIpv4Temp; + addresses = &addressesIpv4Temp; + } else if ((flags & TcpAddressFlagDownload) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { + currentAddressNum = currentAddressNumIpv6Download; currentPortNum = currentPortNumIpv6Download; + addresses = &addressesIpv6Download; } else { + currentAddressNum = currentAddressNumIpv4Download; currentPortNum = currentPortNumIpv4Download; + addresses = &addressesIpv4Download; } } else { - if ((flags & 1) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { + currentAddressNum = currentAddressNumIpv6; currentPortNum = currentPortNumIpv6; + addresses = &addressesIpv6; } else { + currentAddressNum = currentAddressNumIpv4; currentPortNum = currentPortNumIpv4; + addresses = &addressesIpv4; } } + if (addresses->empty()) { + return overridePort == -1 ? 443 : overridePort; + } + const int32_t *portsArray = overridePort == 8888 ? defaultPorts8888 : defaultPorts; + if ((flags & TcpAddressFlagStatic) != 0) { + uint32_t num = 0; + for (std::vector::iterator iter = addresses->begin(); iter != addresses->end(); iter++) { + if ((iter->flags & TcpAddressFlagStatic) != 0) { + currentAddressNum = num; + break; + } + num++; + } + } + if (currentAddressNum >= addresses->size()) { + currentAddressNum = 0; + if ((flags & TcpAddressFlagTemp) != 0) { + currentAddressNumIpv4Temp = currentAddressNum; + } else if ((flags & TcpAddressFlagDownload) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { + currentAddressNumIpv6Download = currentAddressNum; + } else { + currentAddressNumIpv4Download = currentAddressNum; + } + } else { + if ((flags & TcpAddressFlagIpv6) != 0) { + currentAddressNumIpv6 = currentAddressNum; + } else { + currentAddressNumIpv4 = currentAddressNum; + } + } + } if (currentPortNum >= 11) { currentPortNum = 0; - if ((flags & 2) != 0) { - if ((flags & 1) != 0) { + if ((flags & TcpAddressFlagTemp) != 0) { + currentPortNumIpv4Temp = currentAddressNum; + } else if ((flags & TcpAddressFlagDownload) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { currentPortNumIpv6Download = currentPortNum; } else { currentPortNumIpv4Download = currentPortNum; } } else { - if ((flags & 1) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { currentPortNumIpv6 = currentPortNum; } else { currentPortNumIpv4 = currentPortNum; @@ -247,40 +339,49 @@ int32_t Datacenter::getCurrentPort(uint32_t flags) { if (overridePort != -1) { return overridePort; } - std::string address = getCurrentAddress(flags); - return ports[address]; + return ((*addresses) [currentAddressNum]).port; } return port; } void Datacenter::addAddressAndPort(std::string address, uint32_t port, uint32_t flags) { - std::vector *addresses; - if ((flags & 2) != 0) { - if ((flags & 1) != 0) { + std::vector *addresses; + if ((flags & TcpAddressFlagTemp) != 0) { + addresses = &addressesIpv4Temp; + } else if ((flags & TcpAddressFlagDownload) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { addresses = &addressesIpv6Download; } else { addresses = &addressesIpv4Download; } } else { - if ((flags & 1) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { addresses = &addressesIpv6; } else { addresses = &addressesIpv4; } } - if (std::find(addresses->begin(), addresses->end(), address) != addresses->end()) { - return; + for (std::vector::iterator iter = addresses->begin(); iter != addresses->end(); iter++) { + if (iter->address == address && iter->port == port) { + return; + } } - addresses->push_back(address); - ports[address] = port; + addresses->push_back(TcpAddress(address, port, flags)); } void Datacenter::nextAddressOrPort(uint32_t flags) { uint32_t currentPortNum; uint32_t currentAddressNum; - std::vector *addresses; - if ((flags & 2) != 0) { - if ((flags & 1) != 0) { + std::vector *addresses; + if (flags == 0 && !hasAuthKey() && !addressesIpv4Temp.empty()) { + flags = TcpAddressFlagTemp; + } + if ((flags & TcpAddressFlagTemp) != 0) { + currentPortNum = currentPortNumIpv4Temp; + currentAddressNum = currentAddressNumIpv4Temp; + addresses = &addressesIpv4Temp; + } else if ((flags & TcpAddressFlagDownload) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { currentPortNum = currentPortNumIpv6Download; currentAddressNum = currentAddressNumIpv6Download; addresses = &addressesIpv6Download; @@ -290,7 +391,7 @@ void Datacenter::nextAddressOrPort(uint32_t flags) { addresses = &addressesIpv4Download; } } else { - if ((flags & 1) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { currentPortNum = currentPortNumIpv6; currentAddressNum = currentAddressNumIpv6; addresses = &addressesIpv6; @@ -310,8 +411,11 @@ void Datacenter::nextAddressOrPort(uint32_t flags) { } currentPortNum = 0; } - if ((flags & 2) != 0) { - if ((flags & 1) != 0) { + if ((flags & TcpAddressFlagTemp) != 0) { + currentPortNumIpv4Temp = currentPortNum; + currentAddressNumIpv4Temp = currentAddressNum; + } else if ((flags & TcpAddressFlagDownload) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { currentPortNumIpv6Download = currentPortNum; currentAddressNumIpv6Download = currentAddressNum; } else { @@ -319,7 +423,7 @@ void Datacenter::nextAddressOrPort(uint32_t flags) { currentAddressNumIpv4Download = currentAddressNum; } } else { - if ((flags & 1) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { currentPortNumIpv6 = currentPortNum; currentAddressNumIpv6 = currentAddressNum; } else { @@ -359,42 +463,21 @@ void Datacenter::resetAddressAndPortNum() { storeCurrentAddressAndPortNum(); } -void Datacenter::replaceAddressesAndPorts(std::vector &newAddresses, std::map &newPorts, uint32_t flags) { - std::vector *addresses; - if ((flags & 2) != 0) { - if ((flags & 1) != 0) { - addresses = &addressesIpv6Download; - } else { - addresses = &addressesIpv4Download; - } - } else { - if ((flags & 1) != 0) { - addresses = &addressesIpv6; - } else { - addresses = &addressesIpv4; - } - } - size_t size = addresses->size(); - for (uint32_t a = 0; a < size; a++) { - std::map::iterator iter = ports.find((*addresses)[a]); - if (iter != ports.end()) { - ports.erase(iter); - } - } - if ((flags & 2) != 0) { - if ((flags & 1) != 0) { +void Datacenter::replaceAddresses(std::vector &newAddresses, uint32_t flags) { + isCdnDatacenter = (flags & 8) != 0; + if ((flags & TcpAddressFlagDownload) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { addressesIpv6Download = newAddresses; } else { addressesIpv4Download = newAddresses; } } else { - if ((flags & 1) != 0) { + if ((flags & TcpAddressFlagIpv6) != 0) { addressesIpv6 = newAddresses; } else { addressesIpv4 = newAddresses; } } - ports.insert(newPorts.begin(), newPorts.end()); } void Datacenter::serializeToStream(NativeByteBuffer *stream) { @@ -404,24 +487,29 @@ void Datacenter::serializeToStream(NativeByteBuffer *stream) { size_t size; stream->writeInt32((int32_t) (size = addressesIpv4.size())); for (uint32_t a = 0; a < size; a++) { - stream->writeString(addressesIpv4[a]); - stream->writeInt32(ports[addressesIpv4[a]]); + stream->writeString(addressesIpv4[a].address); + stream->writeInt32(addressesIpv4[a].port); + stream->writeInt32(addressesIpv4[a].flags); } stream->writeInt32((int32_t) (size = addressesIpv6.size())); for (uint32_t a = 0; a < size; a++) { - stream->writeString(addressesIpv6[a]); - stream->writeInt32(ports[addressesIpv6[a]]); + stream->writeString(addressesIpv6[a].address); + stream->writeInt32(addressesIpv6[a].port); + stream->writeInt32(addressesIpv6[a].flags); } stream->writeInt32((int32_t) (size = addressesIpv4Download.size())); for (uint32_t a = 0; a < size; a++) { - stream->writeString(addressesIpv4Download[a]); - stream->writeInt32(ports[addressesIpv4Download[a]]); + stream->writeString(addressesIpv4Download[a].address); + stream->writeInt32(addressesIpv4Download[a].port); + stream->writeInt32(addressesIpv4Download[a].flags); } stream->writeInt32((int32_t) (size = addressesIpv6Download.size())); for (uint32_t a = 0; a < size; a++) { - stream->writeString(addressesIpv6Download[a]); - stream->writeInt32(ports[addressesIpv6Download[a]]); + stream->writeString(addressesIpv6Download[a].address); + stream->writeInt32(addressesIpv6Download[a].port); + stream->writeInt32(addressesIpv6Download[a].flags); } + stream->writeBool(isCdnDatacenter); if (authKey != nullptr) { stream->writeInt32(authKey->length); stream->writeBytes(authKey); @@ -439,7 +527,10 @@ void Datacenter::serializeToStream(NativeByteBuffer *stream) { } void Datacenter::clear() { - authKey = nullptr; + if (authKey != nullptr) { + delete authKey; + authKey = nullptr; + } authKeyId = 0; authorized = false; serverSalts.clear(); @@ -537,14 +628,17 @@ void Datacenter::suspendConnections() { if (genericConnection != nullptr) { genericConnection->suspendConnection(); } + if (tempConnection != nullptr) { + tempConnection->suspendConnection(); + } for (uint32_t a = 0; a < UPLOAD_CONNECTIONS_COUNT; a++) { if (uploadConnection[a] != nullptr) { uploadConnection[a]->suspendConnection(); } } for (uint32_t a = 0; a < DOWNLOAD_CONNECTIONS_COUNT; a++) { - if (downloadConnections[a] != nullptr) { - downloadConnections[a]->suspendConnection(); + if (downloadConnection[a] != nullptr) { + downloadConnection[a]->suspendConnection(); } } } @@ -553,14 +647,17 @@ void Datacenter::getSessions(std::vector &sessions) { if (genericConnection != nullptr) { sessions.push_back(genericConnection->getSissionId()); } + if (tempConnection != nullptr) { + sessions.push_back(tempConnection->getSissionId()); + } for (uint32_t a = 0; a < UPLOAD_CONNECTIONS_COUNT; a++) { if (uploadConnection[a] != nullptr) { sessions.push_back(uploadConnection[a]->getSissionId()); } } for (uint32_t a = 0; a < DOWNLOAD_CONNECTIONS_COUNT; a++) { - if (downloadConnections[a] != nullptr) { - sessions.push_back(downloadConnections[a]->getSissionId()); + if (downloadConnection[a] != nullptr) { + sessions.push_back(downloadConnection[a]->getSissionId()); } } } @@ -569,23 +666,26 @@ void Datacenter::recreateSessions() { if (genericConnection != nullptr) { genericConnection->recreateSession(); } + if (tempConnection != nullptr) { + tempConnection->recreateSession(); + } for (uint32_t a = 0; a < UPLOAD_CONNECTIONS_COUNT; a++) { if (uploadConnection[a] != nullptr) { uploadConnection[a]->recreateSession(); } } for (uint32_t a = 0; a < DOWNLOAD_CONNECTIONS_COUNT; a++) { - if (downloadConnections[a] != nullptr) { - downloadConnections[a]->recreateSession(); + if (downloadConnection[a] != nullptr) { + downloadConnection[a]->recreateSession(); } } } Connection *Datacenter::createDownloadConnection(uint8_t num) { - if (downloadConnections[num] == nullptr) { - downloadConnections[num] = new Connection(this, ConnectionTypeDownload); + if (downloadConnection[num] == nullptr) { + downloadConnection[num] = new Connection(this, ConnectionTypeDownload); } - return downloadConnections[num]; + return downloadConnection[num]; } Connection *Datacenter::createUploadConnection(uint8_t num) { @@ -609,6 +709,13 @@ Connection *Datacenter::createPushConnection() { return pushConnection; } +Connection *Datacenter::createTempConnection() { + if (tempConnection == nullptr) { + tempConnection = new Connection(this, ConnectionTypeTemp); + } + return tempConnection; +} + uint32_t Datacenter::getDatacenterId() { return datacenterId; } @@ -908,61 +1015,6 @@ void Datacenter::aesIgeEncryption(uint8_t *buffer, uint8_t *key, uint8_t *iv, bo } } -int32_t Datacenter::selectPublicKey(std::vector &fingerprints) { - if (serverPublicKeys.empty()) { - serverPublicKeys.push_back("-----BEGIN RSA PUBLIC KEY-----\n" - "MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n" - "lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n" - "an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n" - "Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n" - "8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n" - "Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n" - "-----END RSA PUBLIC KEY-----"); - serverPublicKeysFingerprints.push_back(0xc3b42b026ce86b21LL); - - serverPublicKeys.push_back("-----BEGIN RSA PUBLIC KEY-----\n" - "MIIBCgKCAQEAxq7aeLAqJR20tkQQMfRn+ocfrtMlJsQ2Uksfs7Xcoo77jAid0bRt\n" - "ksiVmT2HEIJUlRxfABoPBV8wY9zRTUMaMA654pUX41mhyVN+XoerGxFvrs9dF1Ru\n" - "vCHbI02dM2ppPvyytvvMoefRoL5BTcpAihFgm5xCaakgsJ/tH5oVl74CdhQw8J5L\n" - "xI/K++KJBUyZ26Uba1632cOiq05JBUW0Z2vWIOk4BLysk7+U9z+SxynKiZR3/xdi\n" - "XvFKk01R3BHV+GUKM2RYazpS/P8v7eyKhAbKxOdRcFpHLlVwfjyM1VlDQrEZxsMp\n" - "NTLYXb6Sce1Uov0YtNx5wEowlREH1WOTlwIDAQAB\n" - "-----END RSA PUBLIC KEY-----"); - serverPublicKeysFingerprints.push_back(0x9a996a1db11c729bLL); - - serverPublicKeys.push_back("-----BEGIN RSA PUBLIC KEY-----\n" - "MIIBCgKCAQEAsQZnSWVZNfClk29RcDTJQ76n8zZaiTGuUsi8sUhW8AS4PSbPKDm+\n" - "DyJgdHDWdIF3HBzl7DHeFrILuqTs0vfS7Pa2NW8nUBwiaYQmPtwEa4n7bTmBVGsB\n" - "1700/tz8wQWOLUlL2nMv+BPlDhxq4kmJCyJfgrIrHlX8sGPcPA4Y6Rwo0MSqYn3s\n" - "g1Pu5gOKlaT9HKmE6wn5Sut6IiBjWozrRQ6n5h2RXNtO7O2qCDqjgB2vBxhV7B+z\n" - "hRbLbCmW0tYMDsvPpX5M8fsO05svN+lKtCAuz1leFns8piZpptpSCFn7bWxiA9/f\n" - "x5x17D7pfah3Sy2pA+NDXyzSlGcKdaUmwQIDAQAB\n" - "-----END RSA PUBLIC KEY-----"); - serverPublicKeysFingerprints.push_back(0xb05b2a6f70cdea78LL); - - serverPublicKeys.push_back("-----BEGIN RSA PUBLIC KEY-----\n" - "MIIBCgKCAQEAwqjFW0pi4reKGbkc9pK83Eunwj/k0G8ZTioMMPbZmW99GivMibwa\n" - "xDM9RDWabEMyUtGoQC2ZcDeLWRK3W8jMP6dnEKAlvLkDLfC4fXYHzFO5KHEqF06i\n" - "qAqBdmI1iBGdQv/OQCBcbXIWCGDY2AsiqLhlGQfPOI7/vvKc188rTriocgUtoTUc\n" - "/n/sIUzkgwTqRyvWYynWARWzQg0I9olLBBC2q5RQJJlnYXZwyTL3y9tdb7zOHkks\n" - "WV9IMQmZmyZh/N7sMbGWQpt4NMchGpPGeJ2e5gHBjDnlIf2p1yZOYeUYrdbwcS0t\n" - "UiggS4UeE8TzIuXFQxw7fzEIlmhIaq3FnwIDAQAB\n" - "-----END RSA PUBLIC KEY-----"); - serverPublicKeysFingerprints.push_back(0x71e025b6c76033e3LL); - } - - size_t count1 = fingerprints.size(); - size_t count2 = serverPublicKeysFingerprints.size(); - for (uint32_t a = 0; a < count1; a++) { - for (uint32_t b = 0; b < count2; b++) { - if ((uint64_t) fingerprints[a] == serverPublicKeysFingerprints[b]) { - return b; - } - } - } - return -1; -} - void Datacenter::processHandshakeResponse(TLObject *message, int64_t messageId) { if (handshakeState == 0) { return; @@ -977,10 +1029,86 @@ void Datacenter::processHandshakeResponse(TLObject *message, int64_t messageId) handshakeState = 2; TL_resPQ *result = (TL_resPQ *) message; if (authNonce->isEqualTo(result->nonce.get())) { - int32_t keyIndex = selectPublicKey(result->server_public_key_fingerprints); - if (keyIndex < 0) { - DEBUG_E("dc%u handshake: can't find valid server public key", datacenterId); - beginHandshake(false); + std::string key; + int64_t keyFingerprint = 0; + + size_t count1 = result->server_public_key_fingerprints.size(); + if (isCdnDatacenter) { + std::map::iterator iter = cdnPublicKeysFingerprints.find(datacenterId); + if (iter != cdnPublicKeysFingerprints.end()) { + for (uint32_t a = 0; a < count1; a++) { + if ((uint64_t) result->server_public_key_fingerprints[a] == iter->second) { + keyFingerprint = iter->second; + key = cdnPublicKeys[datacenterId]; + } + } + } + } else { + if (serverPublicKeys.empty()) { + serverPublicKeys.push_back("-----BEGIN RSA PUBLIC KEY-----\n" + "MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n" + "lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n" + "an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n" + "Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n" + "8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n" + "Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n" + "-----END RSA PUBLIC KEY-----"); + serverPublicKeysFingerprints.push_back(0xc3b42b026ce86b21LL); + + serverPublicKeys.push_back("-----BEGIN RSA PUBLIC KEY-----\n" + "MIIBCgKCAQEAxq7aeLAqJR20tkQQMfRn+ocfrtMlJsQ2Uksfs7Xcoo77jAid0bRt\n" + "ksiVmT2HEIJUlRxfABoPBV8wY9zRTUMaMA654pUX41mhyVN+XoerGxFvrs9dF1Ru\n" + "vCHbI02dM2ppPvyytvvMoefRoL5BTcpAihFgm5xCaakgsJ/tH5oVl74CdhQw8J5L\n" + "xI/K++KJBUyZ26Uba1632cOiq05JBUW0Z2vWIOk4BLysk7+U9z+SxynKiZR3/xdi\n" + "XvFKk01R3BHV+GUKM2RYazpS/P8v7eyKhAbKxOdRcFpHLlVwfjyM1VlDQrEZxsMp\n" + "NTLYXb6Sce1Uov0YtNx5wEowlREH1WOTlwIDAQAB\n" + "-----END RSA PUBLIC KEY-----"); + serverPublicKeysFingerprints.push_back(0x9a996a1db11c729bLL); + + serverPublicKeys.push_back("-----BEGIN RSA PUBLIC KEY-----\n" + "MIIBCgKCAQEAsQZnSWVZNfClk29RcDTJQ76n8zZaiTGuUsi8sUhW8AS4PSbPKDm+\n" + "DyJgdHDWdIF3HBzl7DHeFrILuqTs0vfS7Pa2NW8nUBwiaYQmPtwEa4n7bTmBVGsB\n" + "1700/tz8wQWOLUlL2nMv+BPlDhxq4kmJCyJfgrIrHlX8sGPcPA4Y6Rwo0MSqYn3s\n" + "g1Pu5gOKlaT9HKmE6wn5Sut6IiBjWozrRQ6n5h2RXNtO7O2qCDqjgB2vBxhV7B+z\n" + "hRbLbCmW0tYMDsvPpX5M8fsO05svN+lKtCAuz1leFns8piZpptpSCFn7bWxiA9/f\n" + "x5x17D7pfah3Sy2pA+NDXyzSlGcKdaUmwQIDAQAB\n" + "-----END RSA PUBLIC KEY-----"); + serverPublicKeysFingerprints.push_back(0xb05b2a6f70cdea78LL); + + serverPublicKeys.push_back("-----BEGIN RSA PUBLIC KEY-----\n" + "MIIBCgKCAQEAwqjFW0pi4reKGbkc9pK83Eunwj/k0G8ZTioMMPbZmW99GivMibwa\n" + "xDM9RDWabEMyUtGoQC2ZcDeLWRK3W8jMP6dnEKAlvLkDLfC4fXYHzFO5KHEqF06i\n" + "qAqBdmI1iBGdQv/OQCBcbXIWCGDY2AsiqLhlGQfPOI7/vvKc188rTriocgUtoTUc\n" + "/n/sIUzkgwTqRyvWYynWARWzQg0I9olLBBC2q5RQJJlnYXZwyTL3y9tdb7zOHkks\n" + "WV9IMQmZmyZh/N7sMbGWQpt4NMchGpPGeJ2e5gHBjDnlIf2p1yZOYeUYrdbwcS0t\n" + "UiggS4UeE8TzIuXFQxw7fzEIlmhIaq3FnwIDAQAB\n" + "-----END RSA PUBLIC KEY-----"); + serverPublicKeysFingerprints.push_back(0x71e025b6c76033e3LL); + } + + size_t count2 = serverPublicKeysFingerprints.size(); + for (uint32_t a = 0; a < count1; a++) { + for (uint32_t b = 0; b < count2; b++) { + if ((uint64_t) result->server_public_key_fingerprints[a] == serverPublicKeysFingerprints[b]) { + keyFingerprint = result->server_public_key_fingerprints[a]; + key = serverPublicKeys[a]; + break; + } + } + if (keyFingerprint != 0) { + break; + } + } + } + + if (keyFingerprint == 0) { + if (isCdnDatacenter) { + DEBUG_D("dc%u handshake: can't find valid cdn server public key", datacenterId); + loadCdnConfig(this); + } else { + DEBUG_E("dc%u handshake: can't find valid server public key", datacenterId); + beginHandshake(false); + } return; } @@ -1014,7 +1142,7 @@ void Datacenter::processHandshakeResponse(TLObject *message, int64_t messageId) request->q->bytes[2] = (uint8_t) (q >> 8); request->q->bytes[1] = (uint8_t) (q >> 16); request->q->bytes[0] = (uint8_t) (q >> 24); - request->public_key_fingerprint = (int64_t) serverPublicKeysFingerprints[keyIndex]; + request->public_key_fingerprint = keyFingerprint; TL_p_q_inner_data *innerData = new TL_p_q_inner_data(); innerData->nonce = std::unique_ptr(new ByteArray(authNonce)); @@ -1038,8 +1166,6 @@ void Datacenter::processHandshakeResponse(TLObject *message, int64_t messageId) RAND_bytes(innerDataBuffer->bytes() + SHA_DIGEST_LENGTH + innerDataSize, additionalSize); } - std::string &key = serverPublicKeys[keyIndex]; - BIO *keyBio = BIO_new(BIO_s_mem()); BIO_write(keyBio, key.c_str(), (int) key.length()); RSA *rsaKey = PEM_read_bio_RSAPublicKey(keyBio, NULL, NULL, NULL); @@ -1572,6 +1698,8 @@ Connection *Datacenter::createConnectionByType(uint32_t connectionType) { return createUploadConnection(connectionNum); case ConnectionTypePush: return createPushConnection(); + case ConnectionTypeTemp: + return createTempConnection(); default: return nullptr; } @@ -1584,7 +1712,7 @@ Connection *Datacenter::getDownloadConnection(uint8_t num, bool create) { if (create) { createDownloadConnection(num)->connect(); } - return downloadConnections[num]; + return downloadConnection[num]; } Connection *Datacenter::getUploadConnection(uint8_t num, bool create) { @@ -1617,6 +1745,16 @@ Connection *Datacenter::getPushConnection(bool create) { return pushConnection; } +Connection *Datacenter::getTempConnection(bool create) { + if (authKey == nullptr) { + return nullptr; + } + if (create) { + createTempConnection()->connect(); + } + return tempConnection; +} + Connection *Datacenter::getConnectionByType(uint32_t connectionType, bool create) { uint8_t connectionNum = (uint8_t) (connectionType >> 16); connectionType = connectionType & 0x0000ffff; @@ -1629,13 +1767,15 @@ Connection *Datacenter::getConnectionByType(uint32_t connectionType, bool create return getUploadConnection(connectionNum, create); case ConnectionTypePush: return getPushConnection(create); + case ConnectionTypeTemp: + return getTempConnection(create); default: return nullptr; } } void Datacenter::exportAuthorization() { - if (exportingAuthorization) { + if (exportingAuthorization || isCdnDatacenter) { return; } exportingAuthorization = true; @@ -1668,3 +1808,201 @@ void Datacenter::exportAuthorization() { bool Datacenter::isExportingAuthorization() { return exportingAuthorization; } + +void Datacenter::saveCdnConfigInternal(NativeByteBuffer *buffer) { + buffer->writeInt32(1); + buffer->writeInt32(cdnPublicKeys.size()); + for (std::map::iterator iter = cdnPublicKeys.begin(); iter != cdnPublicKeys.end(); iter++) { + buffer->writeInt32(iter->first); + buffer->writeString(iter->second); + buffer->writeInt64(cdnPublicKeysFingerprints[iter->first]); + } +} + +void Datacenter::saveCdnConfig() { + if (cdnConfig == nullptr) { + cdnConfig = new Config("cdnkeys.dat"); + } + static NativeByteBuffer *sizeCalculator = new NativeByteBuffer(true); + sizeCalculator->clearCapacity(); + saveCdnConfigInternal(sizeCalculator); + NativeByteBuffer *buffer = BuffersStorage::getInstance().getFreeBuffer(sizeCalculator->capacity()); + saveCdnConfigInternal(buffer); + cdnConfig->writeConfig(buffer); + buffer->reuse(); +} + +void Datacenter::loadCdnConfig(Datacenter *datacenter) { + if (std::find(cdnWaitingDatacenters.begin(), cdnWaitingDatacenters.end(), datacenter) != cdnWaitingDatacenters.end()) { + return; + } + cdnWaitingDatacenters.push_back(datacenter); + if (loadingCdnKeys) { + return; + } + if (cdnPublicKeysFingerprints.empty()) { + if (cdnConfig == nullptr) { + cdnConfig = new Config("cdnkeys.dat"); + } + NativeByteBuffer *buffer = cdnConfig->readConfig(); + if (buffer != nullptr) { + uint32_t version = buffer->readUint32(nullptr); + if (version >= 1) { + size_t count = buffer->readUint32(nullptr); + for (uint32_t a = 0; a < count; a++) { + int dcId = buffer->readInt32(nullptr); + cdnPublicKeys[dcId] = buffer->readString(nullptr); + cdnPublicKeysFingerprints[dcId] = buffer->readUint64(nullptr); + } + } + buffer->reuse(); + if (!cdnPublicKeysFingerprints.empty()) { + size_t count = cdnWaitingDatacenters.size(); + for (uint32_t a = 0; a < count; a++) { + cdnWaitingDatacenters[a]->beginHandshake(false); + } + cdnWaitingDatacenters.clear(); + return; + } + } + } + loadingCdnKeys = true; + TL_help_getCdnConfig *request = new TL_help_getCdnConfig(); + + ConnectionsManager::getInstance().sendRequest(request, [&](TLObject *response, TL_error *error, int32_t networkType) { + if (response != nullptr) { + TL_cdnConfig *config = (TL_cdnConfig *) response; + size_t count = config->public_keys.size(); + BIO *keyBio = BIO_new(BIO_s_mem()); + NativeByteBuffer *buffer = BuffersStorage::getInstance().getFreeBuffer(1024); + static uint8_t sha1Buffer[20]; + for (uint32_t a = 0; a < count; a++) { + TL_cdnPublicKey *publicKey = config->public_keys[a].get(); + cdnPublicKeys[publicKey->dc_id] = publicKey->public_key; + + BIO_write(keyBio, publicKey->public_key.c_str(), (int) publicKey->public_key.length()); + RSA *rsaKey = PEM_read_bio_RSAPublicKey(keyBio, NULL, NULL, NULL); + + int nBytes = BN_num_bytes(rsaKey->n); + int eBytes = BN_num_bytes(rsaKey->e); + std::string nStr(nBytes, 0), eStr(eBytes, 0); + BN_bn2bin(rsaKey->n, (uint8_t *)&nStr[0]); + BN_bn2bin(rsaKey->e, (uint8_t *)&eStr[0]); + buffer->writeString(nStr); + buffer->writeString(eStr); + SHA1(buffer->bytes(), buffer->position(), sha1Buffer); + cdnPublicKeysFingerprints[publicKey->dc_id] = ((uint64_t) sha1Buffer[19]) << 56 | + ((uint64_t) sha1Buffer[18]) << 48 | + ((uint64_t) sha1Buffer[17]) << 40 | + ((uint64_t) sha1Buffer[16]) << 32 | + ((uint64_t) sha1Buffer[15]) << 24 | + ((uint64_t) sha1Buffer[14]) << 16 | + ((uint64_t) sha1Buffer[13]) << 8 | + ((uint64_t) sha1Buffer[12]); + RSA_free(rsaKey); + if (a != count - 1) { + buffer->position(0); + BIO_reset(keyBio); + } + } + buffer->reuse(); + BIO_free(keyBio); + count = cdnWaitingDatacenters.size(); + for (uint32_t a = 0; a < count; a++) { + cdnWaitingDatacenters[a]->beginHandshake(false); + } + cdnWaitingDatacenters.clear(); + saveCdnConfig(); + } + loadingCdnKeys = false; + }, nullptr, RequestFlagEnableUnauthorized | RequestFlagWithoutLogin, DEFAULT_DATACENTER_ID, ConnectionTypeGeneric, true); +} + +TL_help_configSimple *Datacenter::decodeSimpleConfig(NativeByteBuffer *buffer) { + TL_help_configSimple *result = nullptr; + + static std::string public_key = + "-----BEGIN RSA PUBLIC KEY-----\n" + "MIIBCgKCAQEAyr+18Rex2ohtVy8sroGPBwXD3DOoKCSpjDqYoXgCqB7ioln4eDCF\n" + "fOBUlfXUEvM/fnKCpF46VkAftlb4VuPDeQSS/ZxZYEGqHaywlroVnXHIjgqoxiAd\n" + "192xRGreuXIaUKmkwlM9JID9WS2jUsTpzQ91L8MEPLJ/4zrBwZua8W5fECwCCh2c\n" + "9G5IzzBm+otMS/YKwmR1olzRCyEkyAEjXWqBI9Ftv5eG8m0VkBzOG655WIYdyV0H\n" + "fDK/NWcvGqa0w/nriMD6mDjKOryamw0OP9QuYgMN0C9xMW9y8SmP4h92OAWodTYg\n" + "Y1hZCxdv6cs5UnW9+PWvS+WIbkh+GaWYxwIDAQAB\n" + "-----END RSA PUBLIC KEY-----"; + + BIO *keyBio = BIO_new(BIO_s_mem()); + BIO_write(keyBio, public_key.c_str(), (int) public_key.length()); + + RSA *rsaKey = PEM_read_bio_RSAPublicKey(keyBio, NULL, NULL, NULL); + if (rsaKey == nullptr) { + if (rsaKey == nullptr) { + DEBUG_E("Invalid rsa public key"); + return nullptr; + } + } + + BIGNUM x, y; + uint8_t *bytes = buffer->bytes(); + if (bnContext == nullptr) { + bnContext = BN_CTX_new(); + } + BN_init(&x); + BN_init(&y); + BN_bin2bn(bytes, 256, &x); + + if (BN_mod_exp(&y, &x, rsaKey->e, rsaKey->n, bnContext) == 1) { + uint8_t temp[256]; + /*BN_bn2bin(&y, temp); + std::string res = ""; + for (int a = 0; a < 256; a++) { + char buf[20]; + sprintf(buf, "%x", temp[a]); + res += buf; + } + DEBUG_D("hex = %s", res.c_str());*/ + unsigned l = 256 - BN_num_bytes(&y); + memset(bytes, 0, l); + if (BN_bn2bin(&y, bytes + l) == 256 - l) { + AES_KEY aeskey; + unsigned char iv[16]; + memcpy(iv, bytes + 16, 16); + AES_set_decrypt_key(bytes, 256, &aeskey); + AES_cbc_encrypt(bytes + 32, bytes + 32, 256 - 32, &aeskey, iv, AES_DECRYPT); + + EVP_MD_CTX ctx; + unsigned char sha256_out[32]; + unsigned olen = 0; + EVP_MD_CTX_init(&ctx); + EVP_DigestInit_ex(&ctx, EVP_sha256(), NULL); + EVP_DigestUpdate(&ctx, bytes + 32, 256 - 32 - 16); + EVP_DigestFinal_ex(&ctx, sha256_out, &olen); + EVP_MD_CTX_cleanup(&ctx); + if (olen == 32) { + if (memcmp(bytes + 256 - 16, sha256_out, 16) == 0) { + unsigned data_len = *(unsigned *) (bytes + 32); + if (data_len && data_len <= 256 - 32 - 16 && !(data_len & 3)) { + buffer->position(32 + 4); + bool error = false; + result = TL_help_configSimple::TLdeserialize(buffer, buffer->readUint32(&error), error); + if (error) { + if (result != nullptr) { + delete result; + result = nullptr; + } + } + } else { + DEBUG_E("TL data length field invalid - %d", data_len); + } + } else { + DEBUG_E("RSA signature check FAILED (SHA256 mismatch)"); + } + } + } + } + BN_free(&x); + BN_free(&y); + RSA_free(rsaKey); + BIO_free(keyBio); + return result; +} diff --git a/TMessagesProj/jni/tgnet/Datacenter.h b/TMessagesProj/jni/tgnet/Datacenter.h index 13338ad93..db72a5748 100644 --- a/TMessagesProj/jni/tgnet/Datacenter.h +++ b/TMessagesProj/jni/tgnet/Datacenter.h @@ -19,6 +19,7 @@ class TL_future_salt; class Connection; class NativeByteBuffer; class TL_future_salt; +class TL_help_configSimple; class ByteArray; class TLObject; class Config; @@ -35,7 +36,7 @@ public: void addAddressAndPort(std::string address, uint32_t port, uint32_t flags); void nextAddressOrPort(uint32_t flags); void storeCurrentAddressAndPortNum(); - void replaceAddressesAndPorts(std::vector &newAddresses, std::map &newPorts, uint32_t flags); + void replaceAddresses(std::vector &newAddresses, uint32_t flags); void serializeToStream(NativeByteBuffer *stream); void clear(); void clearServerSalts(); @@ -55,6 +56,7 @@ public: Connection *getUploadConnection(uint8_t num, bool create); Connection *getGenericConnection(bool create); Connection *getPushConnection(bool create); + Connection *getTempConnection(bool create); Connection *getConnectionByType(uint32_t connectionType, bool create); static void aesIgeEncryption(uint8_t *buffer, uint8_t *key, uint8_t *iv, bool encrypt, bool changeIv, uint32_t length); @@ -72,21 +74,24 @@ private: uint32_t datacenterId; Connection *genericConnection = nullptr; - Connection *downloadConnections[DOWNLOAD_CONNECTIONS_COUNT]; + Connection *tempConnection = nullptr; + Connection *downloadConnection[DOWNLOAD_CONNECTIONS_COUNT]; Connection *uploadConnection[UPLOAD_CONNECTIONS_COUNT]; Connection *pushConnection = nullptr; uint32_t lastInitVersion = 0; bool authorized = false; - std::vector addressesIpv4; - std::vector addressesIpv6; - std::vector addressesIpv4Download; - std::vector addressesIpv6Download; - std::map ports; + std::vector addressesIpv4; + std::vector addressesIpv6; + std::vector addressesIpv4Download; + std::vector addressesIpv6Download; + std::vector addressesIpv4Temp; std::vector> serverSalts; uint32_t currentPortNumIpv4 = 0; uint32_t currentAddressNumIpv4 = 0; + uint32_t currentPortNumIpv4Temp = 0; + uint32_t currentAddressNumIpv4Temp = 0; uint32_t currentPortNumIpv6 = 0; uint32_t currentAddressNumIpv6 = 0; uint32_t currentPortNumIpv4Download = 0; @@ -97,13 +102,15 @@ private: int64_t authKeyId = 0; int32_t overridePort = -1; Config *config = nullptr; + bool isCdnDatacenter = false; - const uint32_t configVersion = 5; + const uint32_t configVersion = 7; const uint32_t paramsConfigVersion = 1; Connection *createDownloadConnection(uint8_t num); Connection *createUploadConnection(uint8_t num); Connection *createGenericConnection(); + Connection *createTempConnection(); Connection *createPushConnection(); Connection *createConnectionByType(uint32_t connectionType); @@ -120,11 +127,16 @@ private: void sendRequestData(TLObject *object, bool important); void cleanupHandshake(); void sendAckRequest(int64_t messageId); - int32_t selectPublicKey(std::vector &fingerprints); bool exportingAuthorization = false; void exportAuthorization(); + static void saveCdnConfig(); + static void saveCdnConfigInternal(NativeByteBuffer *buffer); + static void loadCdnConfig(Datacenter *datacenter); + + static TL_help_configSimple *decodeSimpleConfig(NativeByteBuffer *buffer); + friend class ConnectionsManager; }; diff --git a/TMessagesProj/jni/tgnet/Defines.h b/TMessagesProj/jni/tgnet/Defines.h index a33177315..2a331e0d7 100644 --- a/TMessagesProj/jni/tgnet/Defines.h +++ b/TMessagesProj/jni/tgnet/Defines.h @@ -21,7 +21,7 @@ #define DEFAULT_DATACENTER_ID INT_MAX #define DC_UPDATE_TIME 60 * 60 #define DOWNLOAD_CONNECTIONS_COUNT 2 -#define UPLOAD_CONNECTIONS_COUNT 2 +#define UPLOAD_CONNECTIONS_COUNT 4 #define CONNECTION_BACKGROUND_KEEP_TIME 10000 #define DOWNLOAD_CHUNK_SIZE 1024 * 32 @@ -44,6 +44,7 @@ class FileLoadOperation; typedef std::function onCompleteFunc; typedef std::function onQuickAckFunc; +typedef std::function onWriteToSocketFunc; typedef std::list> requestsList; typedef requestsList::iterator requestsIter; @@ -59,12 +60,23 @@ enum ConnectionType { ConnectionTypeDownload = 2, ConnectionTypeUpload = 4, ConnectionTypePush = 8, + ConnectionTypeTemp = 16 +}; + +enum TcpAddressFlag { + TcpAddressFlagIpv6 = 1, + TcpAddressFlagDownload = 2, + TcpAddressFlagO = 4, + TcpAddressFlagCdn = 8, + TcpAddressFlagStatic = 16, + TcpAddressFlagTemp = 2048 }; enum ConnectionState { ConnectionStateConnecting = 1, ConnectionStateWaitingForNetwork = 2, - ConnectionStateConnected = 3 + ConnectionStateConnected = 3, + ConnectionStateConnectingViaProxy = 4 }; enum EventObjectType { @@ -87,6 +99,20 @@ enum FileLoadFailReason { FileLoadFailReasonRetryLimit }; +class TcpAddress { + +public: + std::string address; + int32_t flags; + int32_t port; + + TcpAddress(std::string addr, int32_t p, int32_t f) { + address = addr; + port = p; + flags = f; + } +}; + typedef std::function onFinishedFunc; typedef std::function onFailedFunc; typedef std::function onProgressChangedFunc; @@ -101,6 +127,7 @@ typedef struct ConnectiosManagerDelegate { virtual void onInternalPushReceived() = 0; virtual void onBytesSent(int32_t amount, int32_t networkType) = 0; virtual void onBytesReceived(int32_t amount, int32_t networkType) = 0; + virtual void onRequestNewServerIpAndPort(int32_t second) = 0; } ConnectiosManagerDelegate; #define AllConnectionTypes ConnectionTypeGeneric | ConnectionTypeDownload | ConnectionTypeUpload diff --git a/TMessagesProj/jni/tgnet/MTProtoScheme.cpp b/TMessagesProj/jni/tgnet/MTProtoScheme.cpp index cf06ce96e..b5a9b664a 100644 --- a/TMessagesProj/jni/tgnet/MTProtoScheme.cpp +++ b/TMessagesProj/jni/tgnet/MTProtoScheme.cpp @@ -7,6 +7,7 @@ */ #include +#include #include "MTProtoScheme.h" #include "ApiScheme.h" #include "FileLog.h" @@ -852,6 +853,47 @@ void initConnection::serializeToStream(NativeByteBuffer *stream) { stream->writeString(device_model); stream->writeString(system_version); stream->writeString(app_version); + stream->writeString(system_lang_code); + stream->writeString(lang_pack); stream->writeString(lang_code); query->serializeToStream(stream); } + +void TL_ipPort::readParams(NativeByteBuffer *stream, bool &error) { + struct in_addr ip_addr; + ip_addr.s_addr = htonl(stream->readUint32(&error)); + ipv4 = inet_ntoa(ip_addr); + port = stream->readUint32(&error); +} + +TL_help_configSimple *TL_help_configSimple::TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool &error) { + if (TL_help_configSimple::constructor != constructor) { + error = true; + DEBUG_E("can't parse magic %x in TL_help_configSimple", constructor); + return nullptr; + } + TL_help_configSimple *result = new TL_help_configSimple(); + result->readParams(stream, error); + return result; +} + +void TL_help_configSimple::readParams(NativeByteBuffer *stream, bool &error) { + date = stream->readInt32(&error); + expires = stream->readInt32(&error); + dc_id = stream->readUint32(&error); + int32_t magic = stream->readInt32(&error); + if (magic != 0x1cb5c415) { + error = true; + DEBUG_E("wrong Vector magic, got %x", magic); + return; + } + uint32_t count = stream->readUint32(&error); + for (uint32_t a = 0; a < count; a++) { + TL_ipPort *object = new TL_ipPort(); + object->readParams(stream, error); + if (error) { + return; + } + ip_port_list.push_back(std::unique_ptr(object)); + } +} diff --git a/TMessagesProj/jni/tgnet/MTProtoScheme.h b/TMessagesProj/jni/tgnet/MTProtoScheme.h index 1426e3c59..6bdcbd7ee 100644 --- a/TMessagesProj/jni/tgnet/MTProtoScheme.h +++ b/TMessagesProj/jni/tgnet/MTProtoScheme.h @@ -614,16 +614,41 @@ public: class initConnection : public TLObject { public: - static const uint32_t constructor = 0x69796de9; + static const uint32_t constructor = 0xc7481da6; int32_t api_id; std::string device_model; std::string system_version; std::string app_version; + std::string system_lang_code; + std::string lang_pack; std::string lang_code; std::unique_ptr query; void serializeToStream(NativeByteBuffer *stream); }; +class TL_ipPort : public TLObject { + +public: + std::string ipv4; + uint32_t port; + + void readParams(NativeByteBuffer *stream, bool &error); +}; + +class TL_help_configSimple : public TLObject { + +public: + static const uint32_t constructor = 0xd997c3c5; + + int32_t date; + int32_t expires; + uint32_t dc_id; + std::vector> ip_port_list; + + static TL_help_configSimple *TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool &error); + void readParams(NativeByteBuffer *stream, bool &error); +}; + #endif diff --git a/TMessagesProj/jni/tgnet/Request.cpp b/TMessagesProj/jni/tgnet/Request.cpp index 29a5fdf14..76c19f564 100644 --- a/TMessagesProj/jni/tgnet/Request.cpp +++ b/TMessagesProj/jni/tgnet/Request.cpp @@ -12,13 +12,14 @@ #include "MTProtoScheme.h" #include "ConnectionsManager.h" -Request::Request(int32_t token, ConnectionType type, uint32_t flags, uint32_t datacenter, onCompleteFunc completeFunc, onQuickAckFunc quickAckFunc) { +Request::Request(int32_t token, ConnectionType type, uint32_t flags, uint32_t datacenter, onCompleteFunc completeFunc, onQuickAckFunc quickAckFunc, onWriteToSocketFunc writeToSocketFunc) { requestToken = token; connectionType = type; requestFlags = flags; datacenterId = datacenter; onCompleteRequestCallback = completeFunc; onQuickAckCallback = quickAckFunc; + onWriteToSocketCallback = writeToSocketFunc; dataType = (uint8_t) (requestFlags >> 24); } @@ -32,6 +33,10 @@ Request::~Request() { jniEnv->DeleteGlobalRef(ptr2); ptr2 = nullptr; } + if (ptr3 != nullptr) { + jniEnv->DeleteGlobalRef(ptr3); + ptr3 = nullptr; + } #endif } @@ -59,6 +64,12 @@ void Request::onComplete(TLObject *result, TL_error *error, int32_t networkType) } } +void Request::onWriteToSocket() { + if (onWriteToSocketCallback != nullptr) { + onWriteToSocketCallback(); + } +} + void Request::onQuickAck() { if (onQuickAckCallback != nullptr) { onQuickAckCallback(); diff --git a/TMessagesProj/jni/tgnet/Request.h b/TMessagesProj/jni/tgnet/Request.h index 4c66b142d..96062c27e 100644 --- a/TMessagesProj/jni/tgnet/Request.h +++ b/TMessagesProj/jni/tgnet/Request.h @@ -24,7 +24,7 @@ class TL_error; class Request { public: - Request(int32_t token, ConnectionType type, uint32_t flags, uint32_t datacenter, onCompleteFunc completeFunc, onQuickAckFunc quickAckFunc); + Request(int32_t token, ConnectionType type, uint32_t flags, uint32_t datacenter, onCompleteFunc completeFunc, onQuickAckFunc quickAckFunc, onWriteToSocketFunc writeToSocketFunc); ~Request(); int64_t messageId = 0; @@ -50,17 +50,20 @@ public: std::unique_ptr rpcRequest; onCompleteFunc onCompleteRequestCallback; onQuickAckFunc onQuickAckCallback; + onWriteToSocketFunc onWriteToSocketCallback; void addRespondMessageId(int64_t id); bool respondsToMessageId(int64_t id); void clear(bool time); void onComplete(TLObject *result, TL_error *error, int32_t networkType); void onQuickAck(); + void onWriteToSocket(); TLObject *getRpcRequest(); #ifdef ANDROID jobject ptr1 = nullptr; jobject ptr2 = nullptr; + jobject ptr3 = nullptr; #endif private: diff --git a/TMessagesProj/src/main/AndroidManifest.xml b/TMessagesProj/src/main/AndroidManifest.xml index e4f468ecd..b8818c856 100644 --- a/TMessagesProj/src/main/AndroidManifest.xml +++ b/TMessagesProj/src/main/AndroidManifest.xml @@ -66,6 +66,7 @@ android:allowBackup="false" android:hardwareAccelerated="@bool/useHardwareAcceleration" android:icon="@drawable/ic_launcher" + android:roundIcon="@drawable/ic_launcher" android:largeHeap="true" android:theme="@style/Theme.TMessages.Start" android:manageSpaceActivity="org.telegram.ui.ManageSpaceActivity"> @@ -124,6 +125,8 @@ + + @@ -176,6 +179,15 @@ android:resizeableActivity="false" android:windowSoftInputMode="adjustResize|stateHidden"> + + + + + + + + + + + + + + + + + + @@ -315,7 +338,11 @@ - + + + + + diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java index d9df524cf..b12c964d5 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java @@ -18,6 +18,7 @@ import android.content.ContentUris; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; @@ -111,6 +112,7 @@ public class AndroidUtilities { public static int statusBarHeight = 0; public static float density = 1; public static Point displaySize = new Point(); + public static int roundMessageSize; public static boolean incorrectDisplaySizeFix; public static Integer photoSize = null; public static DisplayMetrics displayMetrics = new DisplayMetrics(); @@ -549,6 +551,13 @@ public class AndroidUtilities { displaySize.y = newSize; } } + if (roundMessageSize == 0) { + if (AndroidUtilities.isTablet()) { + roundMessageSize = (int) (AndroidUtilities.getMinTabletSide() * 0.6f); + } else { + roundMessageSize = (int) (Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) * 0.6f); + } + } FileLog.e("display size = " + displaySize.x + " " + displaySize.y + " " + displayMetrics.xdpi + "x" + displayMetrics.ydpi); } catch (Exception e) { FileLog.e(e); @@ -1606,6 +1615,10 @@ public class AndroidUtilities { } } + public static boolean isBannedForever(int time) { + return Math.abs(time - System.currentTimeMillis() / 1000) > 5 * 365 * 24 * 60 * 60; + } + public static void setRectToRect(Matrix matrix, RectF src, RectF dst, int rotation, Matrix.ScaleToFit align) { float tx, sx; float ty, sy; @@ -1641,4 +1654,104 @@ public class AndroidUtilities { matrix.preScale(sx, sy); matrix.preTranslate(tx, ty); } + + public static boolean handleProxyIntent(Activity activity, Intent intent) { + if (intent == null) { + return false; + } + try { + if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { + return false; + } + Uri data = intent.getData(); + if (data != null) { + String user = null; + String password = null; + String port = null; + String address = null; + String scheme = data.getScheme(); + if (scheme != null) { + if ((scheme.equals("http") || scheme.equals("https"))) { + String host = data.getHost().toLowerCase(); + if (host.equals("telegram.me") || host.equals("t.me") || host.equals("telegram.dog") || host.equals("telesco.pe")) { + String path = data.getPath(); + if (path != null) { + if (path.startsWith("/socks")) { + address = data.getQueryParameter("server"); + port = data.getQueryParameter("port"); + user = data.getQueryParameter("user"); + password = data.getQueryParameter("pass"); + } + } + } + } else if (scheme.equals("tg")) { + String url = data.toString(); + if (url.startsWith("tg:socks") || url.startsWith("tg://socks")) { + url = url.replace("tg:proxy", "tg://telegram.org").replace("tg://proxy", "tg://telegram.org"); + data = Uri.parse(url); + address = data.getQueryParameter("server"); + port = data.getQueryParameter("port"); + user = data.getQueryParameter("user"); + password = data.getQueryParameter("pass"); + } + } + } + if (!TextUtils.isEmpty(address) && !TextUtils.isEmpty(port)) { + if (user == null) { + user = ""; + } + if (password == null) { + password = ""; + } + showProxyAlert(activity, address, port, user, password); + return true; + } + } + } catch (Exception ignore) { + + } + return false; + } + + public static void showProxyAlert(Activity activity, final String address, final String port, final String user, final String password) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(LocaleController.getString("Proxy", R.string.Proxy)); + StringBuilder stringBuilder = new StringBuilder(LocaleController.getString("EnableProxyAlert", R.string.EnableProxyAlert)); + stringBuilder.append("\n\n"); + stringBuilder.append(LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress)).append(": ").append(address).append("\n"); + stringBuilder.append(LocaleController.getString("UseProxyPort", R.string.UseProxyPort)).append(": ").append(port).append("\n"); + if (!TextUtils.isEmpty(user)) { + stringBuilder.append(LocaleController.getString("UseProxyUsername", R.string.UseProxyUsername)).append(": ").append(user).append("\n"); + } + if (!TextUtils.isEmpty(password)) { + stringBuilder.append(LocaleController.getString("UseProxyPassword", R.string.UseProxyPassword)).append(": ").append(password).append("\n"); + } + stringBuilder.append("\n").append(LocaleController.getString("EnableProxyAlert2", R.string.EnableProxyAlert2)); + builder.setMessage(stringBuilder.toString()); + builder.setPositiveButton(LocaleController.getString("ConnectingToProxyEnable", R.string.ConnectingToProxyEnable), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + SharedPreferences.Editor editor = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).edit(); + editor.putBoolean("proxy_enabled", true); + editor.putString("proxy_ip", address); + int p = Utilities.parseInt(port); + editor.putInt("proxy_port", p); + if (TextUtils.isEmpty(password)) { + editor.remove("proxy_pass"); + } else { + editor.putString("proxy_pass", password); + } + if (TextUtils.isEmpty(user)) { + editor.remove("proxy_user"); + } else { + editor.putString("proxy_user", user); + } + editor.commit(); + ConnectionsManager.native_setProxySettings(address, p, user, password); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + builder.show().setCanceledOnTouchOutside(true); + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java index a169bc049..8dd8a379b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java @@ -143,24 +143,27 @@ public class ApplicationLoader extends Application { UserConfig.loadConfig(); String deviceModel; + String systemLangCode; String langCode; String appVersion; String systemVersion; String configPath = getFilesDirFixed().toString(); try { + systemLangCode = LocaleController.getSystemLocaleStringIso639(); langCode = LocaleController.getLocaleStringIso639(); deviceModel = Build.MANUFACTURER + Build.MODEL; PackageInfo pInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0); appVersion = pInfo.versionName + " (" + pInfo.versionCode + ")"; systemVersion = "SDK " + Build.VERSION.SDK_INT; } catch (Exception e) { - langCode = "en"; + systemLangCode = "en"; + langCode = ""; deviceModel = "Android unknown"; appVersion = "App version unknown"; systemVersion = "SDK " + Build.VERSION.SDK_INT; } - if (langCode.trim().length() == 0) { + if (systemLangCode.trim().length() == 0) { langCode = "en"; } if (deviceModel.trim().length() == 0) { @@ -177,7 +180,7 @@ public class ApplicationLoader extends Application { boolean enablePushConnection = preferences.getBoolean("pushConnection", true); MessagesController.getInstance(); - ConnectionsManager.getInstance().init(BuildVars.BUILD_VERSION, TLRPC.LAYER, BuildVars.APP_ID, deviceModel, systemVersion, appVersion, langCode, configPath, FileLog.getNetworkLogPath(), UserConfig.getClientUserId(), enablePushConnection); + ConnectionsManager.getInstance().init(BuildVars.BUILD_VERSION, TLRPC.LAYER, BuildVars.APP_ID, deviceModel, systemVersion, appVersion, langCode, systemLangCode, configPath, FileLog.getNetworkLogPath(), UserConfig.getClientUserId(), enablePushConnection); if (UserConfig.getCurrentUser() != null) { MessagesController.getInstance().putUser(UserConfig.getCurrentUser(), true); ConnectionsManager.getInstance().applyCountryPortNumber(UserConfig.getCurrentUser().phone); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java b/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java index 7e04d7253..94e04d572 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java @@ -180,7 +180,7 @@ public class Bitmaps { if (config != null) { switch (config) { case RGB_565: - newConfig = Bitmap.Config.RGB_565; + newConfig = Bitmap.Config.ARGB_8888; break; case ALPHA_8: newConfig = Bitmap.Config.ALPHA_8; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java b/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java index deb8135c2..b7a3ca908 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java @@ -11,8 +11,8 @@ package org.telegram.messenger; public class BuildVars { public static boolean DEBUG_VERSION = false; public static boolean DEBUG_PRIVATE_VERSION = false; - public static int BUILD_VERSION = 957; - public static String BUILD_VERSION_STRING = "3.18"; + public static int BUILD_VERSION = 1030; + public static String BUILD_VERSION_STRING = "4.1"; public static int APP_ID = 0; //obtain your own APP_ID at https://core.telegram.org/api/obtaining_api_id public static String APP_HASH = ""; //obtain your own APP_HASH at https://core.telegram.org/api/obtaining_api_id public static String HOCKEY_APP_HASH = "your-hockeyapp-api-key-here"; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ChatObject.java b/TMessagesProj/src/main/java/org/telegram/messenger/ChatObject.java index 73477e18f..1f5e59922 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ChatObject.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ChatObject.java @@ -23,7 +23,7 @@ public class ChatObject { } public static boolean isKickedFromChat(TLRPC.Chat chat) { - return chat == null || chat instanceof TLRPC.TL_chatEmpty || chat instanceof TLRPC.TL_chatForbidden || chat instanceof TLRPC.TL_channelForbidden || chat.kicked || chat.deactivated; + return chat == null || chat instanceof TLRPC.TL_chatEmpty || chat instanceof TLRPC.TL_chatForbidden || chat instanceof TLRPC.TL_channelForbidden || chat.kicked || chat.deactivated || chat.banned_rights != null && chat.banned_rights.view_messages; } public static boolean isNotInChat(TLRPC.Chat chat) { @@ -34,6 +34,50 @@ public class ChatObject { return chat instanceof TLRPC.TL_channel || chat instanceof TLRPC.TL_channelForbidden; } + public static boolean hasAdminRights(TLRPC.Chat chat) { + return chat != null && (chat.creator || chat.admin_rights != null && chat.admin_rights.flags != 0); + } + + public static boolean canChangeChatInfo(TLRPC.Chat chat) { + return chat != null && (chat.creator || chat.admin_rights != null && chat.admin_rights.change_info); + } + + public static boolean canAddAdmins(TLRPC.Chat chat) { + return chat != null && (chat.creator || chat.admin_rights != null && chat.admin_rights.add_admins); + } + + public static boolean canBlockUsers(TLRPC.Chat chat) { + return chat != null && (chat.creator || chat.admin_rights != null && chat.admin_rights.ban_users); + } + + public static boolean canSendStickers(TLRPC.Chat chat) { + return chat == null || chat != null && (chat.banned_rights == null || !chat.banned_rights.send_media && !chat.banned_rights.send_stickers); + } + + public static boolean canSendEmbed(TLRPC.Chat chat) { + return chat == null || chat != null && (chat.banned_rights == null || !chat.banned_rights.send_media && !chat.banned_rights.embed_links); + } + + public static boolean canSendMessages(TLRPC.Chat chat) { + return chat == null || chat != null && (chat.banned_rights == null || !chat.banned_rights.send_messages); + } + + public static boolean canPost(TLRPC.Chat chat) { + return chat != null && (chat.creator || chat.admin_rights != null && chat.admin_rights.post_messages); + } + + public static boolean canAddViaLink(TLRPC.Chat chat) { + return chat != null && (chat.creator || chat.admin_rights != null && chat.admin_rights.invite_link); + } + + public static boolean canAddUsers(TLRPC.Chat chat) { + return chat != null && (chat.creator || chat.admin_rights != null && chat.admin_rights.invite_users); + } + + public static boolean canEditInfo(TLRPC.Chat chat) { + return chat != null && (chat.creator || chat.admin_rights != null && chat.admin_rights.change_info); + } + public static boolean isChannel(int chatId) { TLRPC.Chat chat = MessagesController.getInstance().getChat(chatId); return chat instanceof TLRPC.TL_channel || chat instanceof TLRPC.TL_channelForbidden; @@ -41,11 +85,11 @@ public class ChatObject { public static boolean isCanWriteToChannel(int chatId) { TLRPC.Chat chat = MessagesController.getInstance().getChat(chatId); - return chat != null && (chat.creator || chat.editor || chat.megagroup); + return chat != null && (chat.creator || chat.admin_rights != null && chat.admin_rights.post_messages || chat.megagroup); } public static boolean canWriteToChat(TLRPC.Chat chat) { - return !isChannel(chat) || chat.creator || chat.editor || !chat.broadcast; + return !isChannel(chat) || chat.creator || chat.admin_rights != null && chat.admin_rights.post_messages || !chat.broadcast; } public static TLRPC.Chat getChatByDialog(long did) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ClearCacheService.java b/TMessagesProj/src/main/java/org/telegram/messenger/ClearCacheService.java index bc6a35e4e..f1876f669 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ClearCacheService.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ClearCacheService.java @@ -38,7 +38,15 @@ public class ClearCacheService extends IntentService { @Override public void run() { long currentTime = System.currentTimeMillis(); - long diff = 60 * 60 * 1000 * 24 * (keepMedia == 0 ? 7 : 30); + int days; + if (keepMedia == 0) { + days = 7; + } else if (keepMedia == 1) { + days = 30; + } else { + days = 3; + } + long diff = 60 * 60 * 1000 * 24 * days; final HashMap paths = ImageLoader.getInstance().createMediaPaths(); for (HashMap.Entry entry : paths.entrySet()) { if (entry.getKey() == FileLoader.MEDIA_DIR_CACHE) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/DispatchQueue.java b/TMessagesProj/src/main/java/org/telegram/messenger/DispatchQueue.java index ec47e9a29..733a1fccb 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/DispatchQueue.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/DispatchQueue.java @@ -24,7 +24,7 @@ public class DispatchQueue extends Thread { start(); } - private void sendMessage(Message msg, int delay) { + public void sendMessage(Message msg, int delay) { try { syncLatch.await(); if (delay <= 0) { @@ -72,10 +72,19 @@ public class DispatchQueue extends Thread { } } + public void handleMessage(Message inputMessage) { + + } + @Override public void run() { Looper.prepare(); - handler = new Handler(); + handler = new Handler() { + @Override + public void handleMessage(Message msg) { + DispatchQueue.this.handleMessage(msg); + } + }; syncLatch.countDown(); Looper.loop(); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java b/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java index 7872b00f3..5aaefcf8b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java @@ -27,6 +27,7 @@ public class FileLoadOperation { private int offset; private TLRPC.TL_upload_file response; private TLRPC.TL_upload_webFile responseWeb; + private TLRPC.TL_upload_cdnFile responseCdn; } private final static int stateIdle = 0; @@ -56,6 +57,13 @@ public class FileLoadOperation { private int requestsCount; private int renameRetryCount; + private boolean isCdn; + private byte[] cdnIv; + private byte[] cdnKey; + private byte[] cdnToken; + private int cdnDatacenterId; + private boolean reuploadingCdn; + private int nextDownloadOffset; private ArrayList requestInfos; private ArrayList delayedRequestInfos; @@ -383,6 +391,9 @@ public class FileLoadOperation { } else if (requestInfo.responseWeb != null) { requestInfo.responseWeb.disableFree = false; requestInfo.responseWeb.freeResources(); + } else if (requestInfo.responseCdn != null) { + requestInfo.responseCdn.disableFree = false; + requestInfo.responseCdn.freeResources(); } } delayedRequestInfos.clear(); @@ -449,8 +460,10 @@ public class FileLoadOperation { delayedRequestInfos.add(requestInfo); if (requestInfo.response != null) { requestInfo.response.disableFree = true; - } else { + } else if (requestInfo.responseWeb != null) { requestInfo.responseWeb.disableFree = true; + } else if (requestInfo.responseCdn != null) { + requestInfo.responseCdn.disableFree = true; } } return; @@ -459,13 +472,25 @@ public class FileLoadOperation { NativeByteBuffer bytes; if (requestInfo.response != null) { bytes = requestInfo.response.bytes; - } else { + } else if (requestInfo.responseWeb != null) { bytes = requestInfo.responseWeb.bytes; + } else if (requestInfo.responseCdn != null) { + bytes = requestInfo.responseCdn.bytes; + } else { + bytes = null; } if (bytes == null || bytes.limit() == 0) { onFinishLoadingFile(true); return; } + if (requestInfo.responseCdn != null) { + int offset = requestInfo.offset / 16; + cdnIv[15] = (byte) (offset & 0xff); + cdnIv[14] = (byte) ((offset >> 8) & 0xff); + cdnIv[13] = (byte) ((offset >> 16) & 0xff); + cdnIv[12] = (byte) ((offset >> 24) & 0xff); + Utilities.aesCtrDecryption(bytes.buffer, cdnKey, cdnIv, 0, bytes.limit()); + } int currentBytesSize = bytes.limit(); downloadedBytes += currentBytesSize; boolean finishedDownloading = currentBytesSize != currentDownloadChunkSize || (totalBytesCount == downloadedBytes || downloadedBytes % currentDownloadChunkSize != 0) && (totalBytesCount <= 0 || totalBytesCount <= downloadedBytes); @@ -496,9 +521,12 @@ public class FileLoadOperation { if (delayedRequestInfo.response != null) { delayedRequestInfo.response.disableFree = false; delayedRequestInfo.response.freeResources(); - } else { + } else if (delayedRequestInfo.responseWeb != null) { delayedRequestInfo.responseWeb.disableFree = false; delayedRequestInfo.responseWeb.freeResources(); + } else if (delayedRequestInfo.responseCdn != null) { + delayedRequestInfo.responseCdn.disableFree = false; + delayedRequestInfo.responseCdn.freeResources(); } break; } @@ -570,13 +598,45 @@ public class FileLoadOperation { } } + private void clearOperaion(RequestInfo currentInfo) { + int minOffset = Integer.MAX_VALUE; + for (int a = 0; a < requestInfos.size(); a++) { + RequestInfo info = requestInfos.get(a); + minOffset = Math.min(info.offset, minOffset); + if (currentInfo == info) { + continue; + } + if (info.requestToken != 0) { + ConnectionsManager.getInstance().cancelRequest(info.requestToken, true); + } + } + requestInfos.clear(); + for (int a = 0; a < delayedRequestInfos.size(); a++) { + RequestInfo info = delayedRequestInfos.get(a); + if (info.response != null) { + info.response.disableFree = false; + info.response.freeResources(); + } else if (info.responseWeb != null) { + info.responseWeb.disableFree = false; + info.responseWeb.freeResources(); + } else if (info.responseCdn != null) { + info.responseCdn.disableFree = false; + info.responseCdn.freeResources(); + } + minOffset = Math.min(info.offset, minOffset); + } + delayedRequestInfos.clear(); + requestsCount = 0; + nextDownloadOffset = minOffset; + } + private void startDownloadRequest() { if (state != stateDownloading || totalBytesCount > 0 && nextDownloadOffset >= totalBytesCount || requestInfos.size() + delayedRequestInfos.size() >= currentMaxDownloadRequests) { return; } int count = 1; if (totalBytesCount > 0) { - count = Math.max(0, currentMaxDownloadRequests - requestInfos.size()/* - delayedRequestInfos.size()*/); + count = Math.max(0, currentMaxDownloadRequests - requestInfos.size()); } for (int a = 0; a < count; a++) { @@ -584,24 +644,31 @@ public class FileLoadOperation { break; } boolean isLast = totalBytesCount <= 0 || a == count - 1 || totalBytesCount > 0 && nextDownloadOffset + currentDownloadChunkSize >= totalBytesCount; - TLObject request; + final TLObject request; int offset; - int flags; - if (webLocation != null) { - TLRPC.TL_upload_getWebFile req = new TLRPC.TL_upload_getWebFile(); - req.location = webLocation; + int connectionType = requestsCount % 2 == 0 ? ConnectionsManager.ConnectionTypeDownload : ConnectionsManager.ConnectionTypeDownload2; + int flags = (isForceRequest ? ConnectionsManager.RequestFlagForceDownload : 0) | ConnectionsManager.RequestFlagFailOnServerErrors; + if (isCdn) { + TLRPC.TL_upload_getCdnFile req = new TLRPC.TL_upload_getCdnFile(); + req.file_token = cdnToken; req.offset = offset = nextDownloadOffset; req.limit = currentDownloadChunkSize; request = req; - //flags = ConnectionsManager.ConnectionTypeGeneric; - flags = requestsCount % 2 == 0 ? ConnectionsManager.ConnectionTypeDownload : ConnectionsManager.ConnectionTypeDownload2; + flags |= ConnectionsManager.RequestFlagEnableUnauthorized; } else { - TLRPC.TL_upload_getFile req = new TLRPC.TL_upload_getFile(); - req.location = location; - req.offset = offset = nextDownloadOffset; - req.limit = currentDownloadChunkSize; - request = req; - flags = requestsCount % 2 == 0 ? ConnectionsManager.ConnectionTypeDownload : ConnectionsManager.ConnectionTypeDownload2; + if (webLocation != null) { + TLRPC.TL_upload_getWebFile req = new TLRPC.TL_upload_getWebFile(); + req.location = webLocation; + req.offset = offset = nextDownloadOffset; + req.limit = currentDownloadChunkSize; + request = req; + } else { + TLRPC.TL_upload_getFile req = new TLRPC.TL_upload_getFile(); + req.location = location; + req.offset = offset = nextDownloadOffset; + req.limit = currentDownloadChunkSize; + request = req; + } } nextDownloadOffset += currentDownloadChunkSize; final RequestInfo requestInfo = new RequestInfo(); @@ -610,25 +677,85 @@ public class FileLoadOperation { requestInfo.requestToken = ConnectionsManager.getInstance().sendRequest(request, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { - if (response instanceof TLRPC.TL_upload_file) { - requestInfo.response = (TLRPC.TL_upload_file) response; - }else { - requestInfo.responseWeb = (TLRPC.TL_upload_webFile) response; + if (!requestInfos.contains(requestInfo)) { + return; } - if (response != null) { - if (currentType == ConnectionsManager.FileTypeAudio) { - StatsController.getInstance().incrementReceivedBytesCount(response.networkType, StatsController.TYPE_AUDIOS, response.getObjectSize() + 4); - } else if (currentType == ConnectionsManager.FileTypeVideo) { - StatsController.getInstance().incrementReceivedBytesCount(response.networkType, StatsController.TYPE_VIDEOS, response.getObjectSize() + 4); - } else if (currentType == ConnectionsManager.FileTypePhoto) { - StatsController.getInstance().incrementReceivedBytesCount(response.networkType, StatsController.TYPE_PHOTOS, response.getObjectSize() + 4); - } else if (currentType == ConnectionsManager.FileTypeFile) { - StatsController.getInstance().incrementReceivedBytesCount(response.networkType, StatsController.TYPE_FILES, response.getObjectSize() + 4); + if (error != null) { + if (request instanceof TLRPC.TL_upload_getCdnFile) { + if (error.text.equals("FILE_TOKEN_INVALID")) { + isCdn = false; + clearOperaion(requestInfo); + startDownloadRequest(); + return; + } } } - processRequestResult(requestInfo, error); + if (response instanceof TLRPC.TL_upload_fileCdnRedirect) { + TLRPC.TL_upload_fileCdnRedirect res = (TLRPC.TL_upload_fileCdnRedirect) response; + if (res.encryption_iv == null || res.encryption_key == null || res.encryption_iv.length != 16 || res.encryption_key.length != 32) { + error = new TLRPC.TL_error(); + error.text = "bad redirect response"; + error.code = 400; + processRequestResult(requestInfo, error); + } else { + isCdn = true; + cdnDatacenterId = res.dc_id; + cdnIv = res.encryption_iv; + cdnKey = res.encryption_key; + cdnToken = res.file_token; + clearOperaion(requestInfo); + startDownloadRequest(); + } + } else if (response instanceof TLRPC.TL_upload_cdnFileReuploadNeeded) { + if (!reuploadingCdn) { + clearOperaion(requestInfo); + reuploadingCdn = true; + TLRPC.TL_upload_cdnFileReuploadNeeded res = (TLRPC.TL_upload_cdnFileReuploadNeeded) response; + TLRPC.TL_upload_reuploadCdnFile req = new TLRPC.TL_upload_reuploadCdnFile(); + req.file_token = cdnToken; + req.request_token = res.request_token; + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + reuploadingCdn = false; + if (error == null) { + startDownloadRequest(); + } else { + if (error.text.equals("FILE_TOKEN_INVALID") || error.text.equals("REQUEST_TOKEN_INVALID")) { + isCdn = false; + clearOperaion(requestInfo); + startDownloadRequest(); + } else { + onFail(false, 0); + } + } + } + }, null, null, 0, datacenter_id, ConnectionsManager.ConnectionTypeGeneric, true); + } + } else { + if (response instanceof TLRPC.TL_upload_file) { + requestInfo.response = (TLRPC.TL_upload_file) response; + } else if (response instanceof TLRPC.TL_upload_webFile) { + requestInfo.responseWeb = (TLRPC.TL_upload_webFile) response; + } else { + requestInfo.responseCdn = (TLRPC.TL_upload_cdnFile) response; + } + if (response != null) { + if (currentType == ConnectionsManager.FileTypeAudio) { + StatsController.getInstance().incrementReceivedBytesCount(response.networkType, StatsController.TYPE_AUDIOS, response.getObjectSize() + 4); + } else if (currentType == ConnectionsManager.FileTypeVideo) { + StatsController.getInstance().incrementReceivedBytesCount(response.networkType, StatsController.TYPE_VIDEOS, response.getObjectSize() + 4); + } else if (currentType == ConnectionsManager.FileTypePhoto) { + StatsController.getInstance().incrementReceivedBytesCount(response.networkType, StatsController.TYPE_PHOTOS, response.getObjectSize() + 4); + } else if (currentType == ConnectionsManager.FileTypeFile) { + StatsController.getInstance().incrementReceivedBytesCount(response.networkType, StatsController.TYPE_FILES, response.getObjectSize() + 4); + } + } + processRequestResult(requestInfo, error); + } + } - }, null, (isForceRequest ? ConnectionsManager.RequestFlagForceDownload : 0) | ConnectionsManager.RequestFlagFailOnServerErrors, datacenter_id, flags, isLast); + }, null, null, flags, isCdn ? cdnDatacenterId : datacenter_id, connectionType, isLast); requestsCount++; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/FileUploadOperation.java b/TMessagesProj/src/main/java/org/telegram/messenger/FileUploadOperation.java index 457e58a2b..920004179 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/FileUploadOperation.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/FileUploadOperation.java @@ -16,6 +16,7 @@ import org.telegram.tgnet.NativeByteBuffer; import org.telegram.tgnet.RequestDelegate; import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; +import org.telegram.tgnet.WriteToSocketDelegate; import java.io.File; import java.io.FileInputStream; @@ -34,8 +35,12 @@ public class FileUploadOperation { } private boolean isLastPart = false; - private final int maxRequestsCount = 8; - private int uploadChunkSize = 1024 * 128; + private static final int minUploadChunkSize = 64; + private static final int initialRequestsCount = 8; + private static final int maxUploadingKBytes = 1024 * 2; + private int maxRequestsCount; + private int currentUploadingBytes; + private int uploadChunkSize = 64 * 1024; private ArrayList freeRequestIvs; private int requestNum; private String uploadingFilePath; @@ -98,7 +103,7 @@ public class FileUploadOperation { @Override public void run() { preferences = ApplicationLoader.applicationContext.getSharedPreferences("uploadinfo", Activity.MODE_PRIVATE); - for (int a = 0; a < maxRequestsCount; a++) { + for (int a = 0; a < initialRequestsCount; a++) { startUploadRequest(); } } @@ -110,9 +115,14 @@ public class FileUploadOperation { return; } state = 2; - for (Integer num : requestTokens.values()) { - ConnectionsManager.getInstance().cancelRequest(num, true); - } + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + for (Integer num : requestTokens.values()) { + ConnectionsManager.getInstance().cancelRequest(num, true); + } + } + }); delegate.didFailedUploadingFile(this); cleanup(); } @@ -183,12 +193,6 @@ public class FileUploadOperation { try { started = true; if (stream == null) { - if (isEncrypted) { - freeRequestIvs = new ArrayList<>(maxRequestsCount); - for (int a = 0; a < maxRequestsCount; a++) { - freeRequestIvs.add(new byte[32]); - } - } File cacheFile = new File(uploadingFilePath); stream = new FileInputStream(cacheFile); if (estimatedSize != 0) { @@ -206,7 +210,7 @@ public class FileUploadOperation { } } - uploadChunkSize = (int) Math.max(128, (totalFileSize + 1024 * 3000 - 1) / (1024 * 3000)); + uploadChunkSize = (int) Math.max(minUploadChunkSize, (totalFileSize + 1024 * 3000 - 1) / (1024 * 3000)); if (1024 % uploadChunkSize != 0) { int chunkSize = 64; while (uploadChunkSize > chunkSize) { @@ -214,6 +218,14 @@ public class FileUploadOperation { } uploadChunkSize = chunkSize; } + maxRequestsCount = maxUploadingKBytes / uploadChunkSize; + + if (isEncrypted) { + freeRequestIvs = new ArrayList<>(maxRequestsCount); + for (int a = 0; a < maxRequestsCount; a++) { + freeRequestIvs.add(new byte[32]); + } + } uploadChunkSize *= 1024; totalPartsCount = (int) (totalFileSize + uploadChunkSize - 1) / uploadChunkSize; @@ -401,6 +413,9 @@ public class FileUploadOperation { final long currentRequestBytesOffset = readBytesCount; final int currentRequestPartNum = currentPartNum++; final int requestSize = finalRequest.getObjectSize() + 4; + + int connectionType = ConnectionsManager.ConnectionTypeUpload | ((requestNumFinal % 4) << 16); + int requestToken = ConnectionsManager.getInstance().sendRequest(finalRequest, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { @@ -451,6 +466,15 @@ public class FileUploadOperation { delegate.didFinishUploadingFile(FileUploadOperation.this, null, result, key, iv); cleanup(); } + if (currentType == ConnectionsManager.FileTypeAudio) { + StatsController.getInstance().incrementSentItemsCount(ConnectionsManager.getCurrentNetworkType(), StatsController.TYPE_AUDIOS, 1); + } else if (currentType == ConnectionsManager.FileTypeVideo) { + StatsController.getInstance().incrementSentItemsCount(ConnectionsManager.getCurrentNetworkType(), StatsController.TYPE_VIDEOS, 1); + } else if (currentType == ConnectionsManager.FileTypePhoto) { + StatsController.getInstance().incrementSentItemsCount(ConnectionsManager.getCurrentNetworkType(), StatsController.TYPE_PHOTOS, 1); + } else if (currentType == ConnectionsManager.FileTypeFile) { + StatsController.getInstance().incrementSentItemsCount(ConnectionsManager.getCurrentNetworkType(), StatsController.TYPE_FILES, 1); + } } else if (currentUploadRequetsCount < maxRequestsCount) { if (estimatedSize == 0) { if (saveInfoTimes >= 4) { @@ -486,25 +510,27 @@ public class FileUploadOperation { } saveInfoTimes++; } - - startUploadRequest(); } - if (currentType == ConnectionsManager.FileTypeAudio) { - StatsController.getInstance().incrementSentItemsCount(ConnectionsManager.getCurrentNetworkType(), StatsController.TYPE_AUDIOS, 1); - } else if (currentType == ConnectionsManager.FileTypeVideo) { - StatsController.getInstance().incrementSentItemsCount(ConnectionsManager.getCurrentNetworkType(), StatsController.TYPE_VIDEOS, 1); - } else if (currentType == ConnectionsManager.FileTypePhoto) { - StatsController.getInstance().incrementSentItemsCount(ConnectionsManager.getCurrentNetworkType(), StatsController.TYPE_PHOTOS, 1); - } else if (currentType == ConnectionsManager.FileTypeFile) { - StatsController.getInstance().incrementSentItemsCount(ConnectionsManager.getCurrentNetworkType(), StatsController.TYPE_FILES, 1); - } } else { delegate.didFailedUploadingFile(FileUploadOperation.this); cleanup(); } } - }, 0, currentUploadRequetsCount % 2 == 0 ? ConnectionsManager.ConnectionTypeUpload : ConnectionsManager.ConnectionTypeUpload2); + }, null, new WriteToSocketDelegate() { + @Override + public void run() { + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + if (currentUploadRequetsCount < maxRequestsCount) { + startUploadRequest(); + } + } + }); + + } + }, 0, ConnectionsManager.DEFAULT_DATACENTER_ID, connectionType, true); requestTokens.put(requestNumFinal, requestToken); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/GcmPushListenerService.java b/TMessagesProj/src/main/java/org/telegram/messenger/GcmPushListenerService.java index 8553a4ac4..c657ea4fe 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/GcmPushListenerService.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/GcmPushListenerService.java @@ -11,12 +11,16 @@ package org.telegram.messenger; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.os.Build; import android.os.Bundle; import com.google.android.gms.gcm.GcmListenerService; import org.json.JSONObject; import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.TLRPC; + +import static android.support.v4.net.ConnectivityManagerCompat.RESTRICT_BACKGROUND_STATUS_ENABLED; public class GcmPushListenerService extends GcmListenerService { @@ -44,13 +48,52 @@ public class GcmPushListenerService extends GcmListenerService { String ip = parts[0]; int port = Integer.parseInt(parts[1]); ConnectionsManager.getInstance().applyDatacenterAddress(dc, ip, port); - } else { - if (ApplicationLoader.mainInterfacePaused) { - int value = bundle.getInt("badge", -1); - if (value == -1) { + } else if ("MESSAGE_ANNOUNCEMENT".equals(key)) { + Object obj = bundle.get("google.sent_time"); + long time; + try { + if (obj instanceof String) { + time = Utilities.parseLong((String) obj); + } else if (obj instanceof Long) { + time = (Long) obj; + } else { + time = System.currentTimeMillis(); + } + } catch (Exception ignore) { + time = System.currentTimeMillis(); + } + + TLRPC.TL_updateServiceNotification update = new TLRPC.TL_updateServiceNotification(); + update.popup = false; + update.flags = 2; + update.inbox_date = (int) (time / 1000); + update.message = bundle.getString("message"); + update.type = "announcement"; + update.media = new TLRPC.TL_messageMediaEmpty(); + final TLRPC.TL_updates updates = new TLRPC.TL_updates(); + updates.updates.add(update); + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().processUpdates(updates, false); + } + }); + } else if (Build.VERSION.SDK_INT >= 24 && ApplicationLoader.mainInterfacePaused && UserConfig.isClientActivated()) { + Object value = bundle.get("badge"); + if (value == null) { + Object obj = bundle.get("google.sent_time"); + long time; + if (obj instanceof String) { + time = Utilities.parseLong((String) obj); + } else if (obj instanceof Long) { + time = (Long) obj; + } else { + time = -1; + } + if (time == -1 || UserConfig.lastAppPauseTime < time) { ConnectivityManager connectivityManager = (ConnectivityManager) ApplicationLoader.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo(); - if (netInfo == null || !netInfo.isConnected()) { + if (connectivityManager.getRestrictBackgroundStatus() == RESTRICT_BACKGROUND_STATUS_ENABLED && netInfo.getType() == ConnectivityManager.TYPE_MOBILE) { NotificationsController.getInstance().showSingleBackgroundNotification(); } } @@ -59,7 +102,6 @@ public class GcmPushListenerService extends GcmListenerService { } catch (Exception e) { FileLog.e(e); } - ConnectionsManager.onInternalPushReceived(); ConnectionsManager.getInstance().resumeNetworkMaybe(); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java index c107af8ca..297d1b8be 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java @@ -136,8 +136,8 @@ public class ImageLoader { try { URL downloadUrl = new URL(url); httpConnection = downloadUrl.openConnection(); - httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/_BuildID_) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36"); - httpConnection.addRequestProperty("Referer", "google.com"); + 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) { @@ -150,8 +150,8 @@ public class ImageLoader { downloadUrl = new URL(newUrl); httpConnection = downloadUrl.openConnection(); httpConnection.setRequestProperty("Cookie", cookies); - httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/_BuildID_) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36"); - httpConnection.addRequestProperty("Referer", "google.com"); + 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(); @@ -311,8 +311,8 @@ public class ImageLoader { try { URL downloadUrl = new URL(cacheImage.httpUrl); httpConnection = downloadUrl.openConnection(); - httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/_BuildID_) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36"); - httpConnection.addRequestProperty("Referer", "google.com"); + 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) { @@ -1714,15 +1714,16 @@ public class ImageLoader { } if (thumb != 2) { + boolean isEncrypted = imageLocation instanceof TLRPC.TL_documentEncrypted || imageLocation instanceof TLRPC.TL_fileEncryptedLocation; CacheImage img = new CacheImage(); if (httpLocation != null && !httpLocation.startsWith("vthumb") && !httpLocation.startsWith("thumb") && (httpLocation.endsWith("mp4") || httpLocation.endsWith("gif")) || imageLocation instanceof TLRPC.TL_webDocument && ((TLRPC.TL_webDocument) imageLocation).mime_type.equals("image/gif") || - imageLocation instanceof TLRPC.Document && MessageObject.isGifDocument((TLRPC.Document) imageLocation)) { + imageLocation instanceof TLRPC.Document && (MessageObject.isGifDocument((TLRPC.Document) imageLocation) || MessageObject.isRoundVideoDocument((TLRPC.Document) imageLocation))) { img.animatedFile = true; } if (cacheFile == null) { - if (cacheOnly || size == 0 || httpLocation != null) { + if (cacheOnly || size == 0 || httpLocation != null || isEncrypted) { cacheFile = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), url); } else if (imageLocation instanceof TLRPC.Document) { if (MessageObject.isVideoDocument((TLRPC.Document) imageLocation)) { @@ -1867,7 +1868,7 @@ public class ImageLoader { if (thumbKey != null) { thumbUrl = thumbKey + "." + ext; } - saveImageToCache = !MessageObject.isGifDocument(document); + saveImageToCache = !MessageObject.isGifDocument(document) && !MessageObject.isRoundVideoDocument((TLRPC.Document) imageLocation); } if (imageLocation == thumbLocation) { imageLocation = null; @@ -2099,6 +2100,13 @@ public class ImageLoader { } bmOptions.inJustDecodeBounds = false; bmOptions.inSampleSize = (int) scaleFactor; + if (bmOptions.inSampleSize % 2 != 0) { + int sample = 1; + while (sample * 2 < bmOptions.inSampleSize) { + sample *= 2; + } + bmOptions.inSampleSize = sample; + } bmOptions.inPurgeable = Build.VERSION.SDK_INT < 21; String exifPath = null; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java index 93abf9aa7..c7a35c7ba 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java @@ -68,6 +68,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg private Drawable currentThumb; private Drawable staticThumb; private boolean allowStartAnimation = true; + private boolean allowDecodeSingleFrame; private boolean needsQualityThumb; private boolean shouldGenerateQualityThumb; @@ -378,6 +379,9 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg public boolean onAttachedToWindow() { NotificationCenter.getInstance().addObserver(this, NotificationCenter.didReplacedPhotoInMemCache); + if (needsQualityThumb) { + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messageThumbGenerated); + } if (setImageBackup != null && (setImageBackup.fileLocation != null || setImageBackup.httpUrl != null || setImageBackup.thumbLocation != null || setImageBackup.thumb != null)) { setImage(setImageBackup.fileLocation, setImageBackup.httpUrl, setImageBackup.filter, setImageBackup.thumb, setImageBackup.thumbLocation, setImageBackup.thumbFilter, setImageBackup.size, setImageBackup.ext, setImageBackup.cacheOnly); return true; @@ -676,6 +680,10 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg return false; } + public float getCurrentAlpha() { + return currentAlpha; + } + public Bitmap getBitmap() { if (currentImage instanceof AnimatedFileDrawable) { return ((AnimatedFileDrawable) currentImage).getAnimatedBitmap(); @@ -698,6 +706,12 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg return orientation % 360 == 0 || orientation % 360 == 180 ? staticThumb.getIntrinsicWidth() : staticThumb.getIntrinsicHeight(); } Bitmap bitmap = getBitmap(); + if (bitmap == null) { + if (staticThumb != null) { + return staticThumb.getIntrinsicWidth(); + } + return 1; + } return orientation % 360 == 0 || orientation % 360 == 180 ? bitmap.getWidth() : bitmap.getHeight(); } @@ -708,6 +722,12 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg return orientation % 360 == 0 || orientation % 360 == 180 ? staticThumb.getIntrinsicHeight() : staticThumb.getIntrinsicWidth(); } Bitmap bitmap = getBitmap(); + if (bitmap == null) { + if (staticThumb != null) { + return staticThumb.getIntrinsicHeight(); + } + return 1; + } return orientation % 360 == 0 || orientation % 360 == 180 ? bitmap.getHeight() : bitmap.getWidth(); } @@ -768,6 +788,14 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg imageH = height; } + public float getCenterX() { + return imageX + imageW / 2.0f; + } + + public float getCenterY() { + return imageY + imageH / 2.0f; + } + public int getImageX() { return imageX; } @@ -889,6 +917,10 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg allowStartAnimation = value; } + public void setAllowDecodeSingleFrame(boolean value) { + allowDecodeSingleFrame = value; + } + public boolean isAllowStartAnimation() { return allowStartAnimation; } @@ -966,6 +998,8 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg fileDrawable.setParentView(parentView); if (allowStartAnimation) { fileDrawable.start(); + } else { + fileDrawable.setAllowDecodeSingleFrame(allowDecodeSingleFrame); } } @@ -984,7 +1018,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg currentThumb = bitmap; - if (roundRadius != 0 && currentImage == null && bitmap instanceof BitmapDrawable) { + if (roundRadius != 0 && bitmap instanceof BitmapDrawable) { if (bitmap instanceof AnimatedFileDrawable) { ((AnimatedFileDrawable) bitmap).setRoundRadius(roundRadius); } else { @@ -996,9 +1030,13 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg } if (!memCache && crossfadeAlpha != 2) { - currentAlpha = 0.0f; - lastUpdateAlphaTime = System.currentTimeMillis(); - crossfadeWithThumb = staticThumb != null && currentKey == null; + if (parentMessageObject != null && parentMessageObject.isRoundVideo() && parentMessageObject.isSending()) { + currentAlpha = 1.0f; + } else { + currentAlpha = 0.0f; + lastUpdateAlphaTime = System.currentTimeMillis(); + crossfadeWithThumb = staticThumb != null && currentKey == null; + } } else { currentAlpha = 1.0f; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Intro.java b/TMessagesProj/src/main/java/org/telegram/messenger/Intro.java new file mode 100644 index 000000000..4bfd9829e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/Intro.java @@ -0,0 +1,26 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.messenger; + +public class Intro { + public static native void on_draw_frame(); + public static native void set_scroll_offset(float a_offset); + public static native void set_page(int page); + public static native void set_date(float a); + public static native void set_date0(float a); + public static native void set_pages_textures(int a1, int a2, int a3, int a4, int a5, int a6); + public static native void set_ic_textures(int a_ic_bubble_dot, int a_ic_bubble, int a_ic_cam_lens, int a_ic_cam, int a_ic_pencil, int a_ic_pin, int a_ic_smile_eye, int a_ic_smile, int a_ic_videocam); + public static native void set_telegram_textures(int a_telegram_sphere, int a_telegram_plane); + public static native void set_fast_textures(int a_fast_body, int a_fast_spiral, int a_fast_arrow, int a_fast_arrow_shadow); + public static native void set_free_textures(int a_knot_up, int a_knot_down); + public static native void set_powerful_textures(int a_powerful_mask, int a_powerful_star, int a_powerful_infinity, int a_powerful_infinity_white); + public static native void set_private_textures(int a_private_door, int a_private_screw); + public static native void on_surface_created(); + public static native void on_surface_changed(int a_width_px, int a_height_px, float a_scale_factor, int a1, int a2, int a3, int a4, int a5); +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java b/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java index 8cae5aa24..a9f460a87 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java @@ -15,21 +15,24 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Configuration; +import android.text.TextUtils; import android.text.format.DateFormat; import android.util.Xml; import org.telegram.messenger.time.FastDateFormat; import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.RequestDelegate; +import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; import org.xmlpull.v1.XmlPullParser; +import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; +import java.io.FileWriter; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Calendar; -import java.util.Collections; -import java.util.Comparator; import java.util.Currency; import java.util.Date; import java.util.HashMap; @@ -55,6 +58,8 @@ public class LocaleController { public FastDateFormat formatterMonthYear; public FastDateFormat formatterYearMax; public FastDateFormat formatterStats; + public FastDateFormat formatterBannedUntil; + public FastDateFormat formatterBannedUntilThisYear; public FastDateFormat chatDate; public FastDateFormat chatFullDate; @@ -64,10 +69,10 @@ public class LocaleController { private Locale systemDefaultLocale; private PluralRules currentPluralRules; private LocaleInfo currentLocaleInfo; - private LocaleInfo defaultLocalInfo; private HashMap localeValues = new HashMap<>(); private String languageOverride; private boolean changingConfiguration = false; + private boolean reloadLastFile; private HashMap currencyValues; private HashMap translitChars; @@ -87,13 +92,16 @@ public class LocaleController { } public static class LocaleInfo { + public String name; public String nameEnglish; public String shortName; public String pathToFile; + public int version; + public boolean builtIn; public String getSaveString() { - return name + "|" + nameEnglish + "|" + shortName + "|" + pathToFile; + return name + "|" + nameEnglish + "|" + shortName + "|" + pathToFile + "|" + version; } public static LocaleInfo createWithString(String string) { @@ -101,19 +109,51 @@ public class LocaleController { return null; } String[] args = string.split("\\|"); - if (args.length != 4) { - return null; + LocaleInfo localeInfo = null; + if (args.length >= 4) { + localeInfo = new LocaleInfo(); + localeInfo.name = args[0]; + localeInfo.nameEnglish = args[1]; + localeInfo.shortName = args[2]; + localeInfo.pathToFile = args[3]; + if (args.length >= 5) { + localeInfo.version = Utilities.parseInt(args[4]); + } } - LocaleInfo localeInfo = new LocaleInfo(); - localeInfo.name = args[0]; - localeInfo.nameEnglish = args[1]; - localeInfo.shortName = args[2]; - localeInfo.pathToFile = args[3]; return localeInfo; } + + public File getPathToFile() { + if (isRemote()) { + return new File(ApplicationLoader.getFilesDirFixed(), "remote_" + shortName + ".xml"); + } + return !TextUtils.isEmpty(pathToFile) ? new File(pathToFile) : null; + } + + public String getKey() { + if (pathToFile != null && !"remote".equals(pathToFile)) { + return "local_" + shortName; + } + return shortName; + } + + public boolean isRemote() { + return "remote".equals(pathToFile); + } + + public boolean isLocal() { + return !TextUtils.isEmpty(pathToFile) && !isRemote(); + } + + public boolean isBuiltIn() { + return builtIn; + } } - public ArrayList sortedLanguages = new ArrayList<>(); + private boolean loadingRemoteLanguages; + + public ArrayList languages = new ArrayList<>(); + public ArrayList remoteLanguages = new ArrayList<>(); public HashMap languagesDict = new HashMap<>(); private ArrayList otherLanguages = new ArrayList<>(); @@ -162,7 +202,8 @@ public class LocaleController { localeInfo.nameEnglish = "English"; localeInfo.shortName = "en"; localeInfo.pathToFile = null; - sortedLanguages.add(localeInfo); + localeInfo.builtIn = true; + languages.add(localeInfo); languagesDict.put(localeInfo.shortName, localeInfo); localeInfo = new LocaleInfo(); @@ -170,14 +211,16 @@ public class LocaleController { localeInfo.nameEnglish = "Italian"; localeInfo.shortName = "it"; localeInfo.pathToFile = null; - sortedLanguages.add(localeInfo); + localeInfo.builtIn = true; + languages.add(localeInfo); languagesDict.put(localeInfo.shortName, localeInfo); localeInfo = new LocaleInfo(); localeInfo.name = "Español"; localeInfo.nameEnglish = "Spanish"; localeInfo.shortName = "es"; - sortedLanguages.add(localeInfo); + localeInfo.builtIn = true; + languages.add(localeInfo); languagesDict.put(localeInfo.shortName, localeInfo); localeInfo = new LocaleInfo(); @@ -185,7 +228,8 @@ public class LocaleController { localeInfo.nameEnglish = "German"; localeInfo.shortName = "de"; localeInfo.pathToFile = null; - sortedLanguages.add(localeInfo); + localeInfo.builtIn = true; + languages.add(localeInfo); languagesDict.put(localeInfo.shortName, localeInfo); localeInfo = new LocaleInfo(); @@ -193,7 +237,8 @@ public class LocaleController { localeInfo.nameEnglish = "Dutch"; localeInfo.shortName = "nl"; localeInfo.pathToFile = null; - sortedLanguages.add(localeInfo); + localeInfo.builtIn = true; + languages.add(localeInfo); languagesDict.put(localeInfo.shortName, localeInfo); localeInfo = new LocaleInfo(); @@ -201,15 +246,17 @@ public class LocaleController { localeInfo.nameEnglish = "Arabic"; localeInfo.shortName = "ar"; localeInfo.pathToFile = null; - sortedLanguages.add(localeInfo); + localeInfo.builtIn = true; + languages.add(localeInfo); languagesDict.put(localeInfo.shortName, localeInfo); localeInfo = new LocaleInfo(); localeInfo.name = "Português (Brasil)"; localeInfo.nameEnglish = "Portuguese (Brazil)"; - localeInfo.shortName = "pt_BR"; + localeInfo.shortName = "pt_br"; localeInfo.pathToFile = null; - sortedLanguages.add(localeInfo); + localeInfo.builtIn = true; + languages.add(localeInfo); languagesDict.put(localeInfo.shortName, localeInfo); localeInfo = new LocaleInfo(); @@ -217,29 +264,32 @@ public class LocaleController { localeInfo.nameEnglish = "Korean"; localeInfo.shortName = "ko"; localeInfo.pathToFile = null; - sortedLanguages.add(localeInfo); + localeInfo.builtIn = true; + languages.add(localeInfo); languagesDict.put(localeInfo.shortName, localeInfo); loadOtherLanguages(); - - for (LocaleInfo locale : otherLanguages) { - sortedLanguages.add(locale); - languagesDict.put(locale.shortName, locale); + if (remoteLanguages.isEmpty()) { + loadRemoteLanguages(); } - Collections.sort(sortedLanguages, new Comparator() { - @Override - public int compare(LocaleController.LocaleInfo o, LocaleController.LocaleInfo o2) { - return o.name.compareTo(o2.name); - } - }); + for (int a = 0; a < otherLanguages.size(); a++) { + LocaleInfo locale = otherLanguages.get(a); + languages.add(locale); + languagesDict.put(locale.getKey(), locale); + } - defaultLocalInfo = localeInfo = new LocaleController.LocaleInfo(); - localeInfo.name = "System default"; - localeInfo.nameEnglish = "System default"; - localeInfo.shortName = null; - localeInfo.pathToFile = null; - sortedLanguages.add(0, localeInfo); + for (int a = 0; a < remoteLanguages.size(); a++) { + LocaleInfo locale = remoteLanguages.get(a); + LocaleInfo existingLocale = getLanguageFromDict(locale.getKey()); + if (existingLocale != null) { + existingLocale.pathToFile = locale.pathToFile; + existingLocale.version = locale.version; + } else { + languages.add(locale); + languagesDict.put(locale.getKey(), locale); + } + } systemDefaultLocale = Locale.getDefault(); is24HourFormat = DateFormat.is24HourFormat(ApplicationLoader.applicationContext); @@ -250,21 +300,22 @@ public class LocaleController { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); String lang = preferences.getString("language", null); if (lang != null) { - currentInfo = languagesDict.get(lang); + currentInfo = getLanguageFromDict(lang); if (currentInfo != null) { override = true; } } if (currentInfo == null && systemDefaultLocale.getLanguage() != null) { - currentInfo = languagesDict.get(systemDefaultLocale.getLanguage()); + currentInfo = getLanguageFromDict(systemDefaultLocale.getLanguage()); } if (currentInfo == null) { - currentInfo = languagesDict.get(getLocaleString(systemDefaultLocale)); - } - if (currentInfo == null) { - currentInfo = languagesDict.get("en"); + currentInfo = getLanguageFromDict(getLocaleString(systemDefaultLocale)); + if (currentInfo == null) { + currentInfo = getLanguageFromDict("en"); + } } + applyLanguage(currentInfo, override); } catch (Exception e) { FileLog.e(e); @@ -278,6 +329,13 @@ public class LocaleController { } } + private LocaleInfo getLanguageFromDict(String key) { + if (key == null) { + return null; + } + return languagesDict.get(key.toLowerCase().replace("-", "_")); + } + private void addRules(String[] languages, PluralRules rules) { for (String language : languages) { allRules.put(language, rules); @@ -305,6 +363,14 @@ public class LocaleController { return systemDefaultLocale; } + public boolean isCurrentLocalLocale() { + return currentLocaleInfo.isLocal(); + } + + public void reloadCurrentRemoteLocale() { + applyRemoteLanguage(currentLocaleInfo, null, true); + } + private String getLocaleString(Locale locale) { if (locale == null) { return "en"; @@ -328,7 +394,7 @@ public class LocaleController { return result.toString(); } - public static String getLocaleStringIso639() { + public static String getSystemLocaleStringIso639() { Locale locale = getInstance().getSystemDefaultLocale(); if (locale == null) { return "en"; @@ -352,6 +418,30 @@ public class LocaleController { return result.toString(); } + public static String getLocaleStringIso639() { + Locale locale = getInstance().currentLocale; + if (locale == null) { + return "en"; + } + String languageCode = locale.getLanguage(); + String countryCode = locale.getCountry(); + String variantCode = locale.getVariant(); + if (languageCode.length() == 0 && countryCode.length() == 0) { + return "en"; + } + StringBuilder result = new StringBuilder(11); + result.append(languageCode); + if (countryCode.length() > 0 || variantCode.length() > 0) { + result.append('-'); + } + result.append(countryCode); + if (variantCode.length() > 0) { + result.append('_'); + } + result.append(variantCode); + return result.toString(); + } + public boolean applyLanguageFile(File file) { try { HashMap stringMap = getLocaleFileStrings(file); @@ -379,7 +469,7 @@ public class LocaleController { return false; } - LocaleInfo localeInfo = languagesDict.get(languageCode); + LocaleInfo localeInfo = getLanguageFromDict(languageCode); if (localeInfo == null) { localeInfo = new LocaleInfo(); localeInfo.name = languageName; @@ -387,21 +477,10 @@ public class LocaleController { localeInfo.shortName = languageCode; localeInfo.pathToFile = finalFile.getAbsolutePath(); - sortedLanguages.add(localeInfo); + languages.add(localeInfo); languagesDict.put(localeInfo.shortName, localeInfo); otherLanguages.add(localeInfo); - Collections.sort(sortedLanguages, new Comparator() { - @Override - public int compare(LocaleController.LocaleInfo o, LocaleController.LocaleInfo o2) { - if (o.shortName == null) { - return -1; - } else if (o2.shortName == null) { - return 1; - } - return o.name.compareTo(o2.name); - } - }); saveOtherLanguages(); } localeValues = stringMap; @@ -417,30 +496,53 @@ public class LocaleController { private void saveOtherLanguages() { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("langconfig", Activity.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); - String locales = ""; - for (LocaleInfo localeInfo : otherLanguages) { + StringBuilder stringBuilder = new StringBuilder(); + for (int a = 0; a < otherLanguages.size(); a++) { + LocaleInfo localeInfo = otherLanguages.get(a); String loc = localeInfo.getSaveString(); if (loc != null) { - if (locales.length() != 0) { - locales += "&"; + if (stringBuilder.length() != 0) { + stringBuilder.append("&"); } - locales += loc; + stringBuilder.append(loc); } } - editor.putString("locales", locales); + editor.putString("locales", stringBuilder.toString()); + stringBuilder.setLength(0); + for (int a = 0; a < remoteLanguages.size(); a++) { + LocaleInfo localeInfo = remoteLanguages.get(a); + String loc = localeInfo.getSaveString(); + if (loc != null) { + if (stringBuilder.length() != 0) { + stringBuilder.append("&"); + } + stringBuilder.append(loc); + } + } + editor.putString("remote", stringBuilder.toString()); editor.commit(); } public boolean deleteLanguage(LocaleInfo localeInfo) { - if (localeInfo.pathToFile == null) { + if (localeInfo.pathToFile == null || localeInfo.isRemote()) { return false; } if (currentLocaleInfo == localeInfo) { - applyLanguage(defaultLocalInfo, true); + LocaleInfo info = null; + if (systemDefaultLocale.getLanguage() != null) { + info = getLanguageFromDict(systemDefaultLocale.getLanguage()); + } + if (info == null) { + info = getLanguageFromDict(getLocaleString(systemDefaultLocale)); + } + if (info == null) { + info = getLanguageFromDict("en"); + } + applyLanguage(info, true); } otherLanguages.remove(localeInfo); - sortedLanguages.remove(localeInfo); + languages.remove(localeInfo); languagesDict.remove(localeInfo.shortName); File file = new File(localeInfo.pathToFile); file.delete(); @@ -451,21 +553,39 @@ public class LocaleController { private void loadOtherLanguages() { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("langconfig", Activity.MODE_PRIVATE); String locales = preferences.getString("locales", null); - if (locales == null || locales.length() == 0) { - return; + if (!TextUtils.isEmpty(locales)) { + String[] localesArr = locales.split("&"); + for (String locale : localesArr) { + LocaleInfo localeInfo = LocaleInfo.createWithString(locale); + if (localeInfo != null) { + otherLanguages.add(localeInfo); + } + } } - String[] localesArr = locales.split("&"); - for (String locale : localesArr) { - LocaleInfo localeInfo = LocaleInfo.createWithString(locale); - if (localeInfo != null) { - otherLanguages.add(localeInfo); + locales = preferences.getString("remote", null); + if (!TextUtils.isEmpty(locales)) { + String[] localesArr = locales.split("&"); + for (String locale : localesArr) { + LocaleInfo localeInfo = LocaleInfo.createWithString(locale); + localeInfo.shortName = localeInfo.shortName.replace("-", "_"); + if (localeInfo != null) { + remoteLanguages.add(localeInfo); + } } } } private HashMap getLocaleFileStrings(File file) { + return getLocaleFileStrings(file, false); + } + + private HashMap getLocaleFileStrings(File file, boolean preserveEscapes) { FileInputStream stream = null; + reloadLastFile = false; try { + if (!file.exists()) { + return new HashMap<>(); + } HashMap stringMap = new HashMap<>(); XmlPullParser parser = Xml.newPullParser(); stream = new FileInputStream(file); @@ -475,19 +595,28 @@ public class LocaleController { String value = null; String attrName = null; while (eventType != XmlPullParser.END_DOCUMENT) { - if(eventType == XmlPullParser.START_TAG) { + if (eventType == XmlPullParser.START_TAG) { name = parser.getName(); int c = parser.getAttributeCount(); if (c > 0) { attrName = parser.getAttributeValue(0); } - } else if(eventType == XmlPullParser.TEXT) { + } else if (eventType == XmlPullParser.TEXT) { if (attrName != null) { value = parser.getText(); if (value != null) { value = value.trim(); - value = value.replace("\\n", "\n"); - value = value.replace("\\", ""); + if (preserveEscapes) { + value = value.replace("<", "<").replace(">", ">").replace("'", "\\'").replace("& ", "& "); + } else { + value = value.replace("\\n", "\n"); + value = value.replace("\\", ""); + String old = value; + value = value.replace("<", "<"); + if (!reloadLastFile && !value.equals(old)) { + reloadLastFile = true; + } + } } } } else if (eventType == XmlPullParser.END_TAG) { @@ -506,6 +635,7 @@ public class LocaleController { return stringMap; } catch (Exception e) { FileLog.e(e); + reloadLastFile = true; } finally { try { if (stream != null) { @@ -526,64 +656,48 @@ public class LocaleController { if (localeInfo == null) { return; } + File pathToFile = localeInfo.getPathToFile(); + String shortName = localeInfo.shortName; + ConnectionsManager.getInstance().setLangCode(shortName.replace("_", "-")); + if (localeInfo.isRemote() && !pathToFile.exists()) { + applyRemoteLanguage(localeInfo, null, false); + } try { Locale newLocale; - if (localeInfo.shortName != null) { - String[] args = localeInfo.shortName.split("_"); - if (args.length == 1) { - newLocale = new Locale(localeInfo.shortName); - } else { - newLocale = new Locale(args[0], args[1]); - } - if (newLocale != null) { - if (override) { - languageOverride = localeInfo.shortName; - - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("language", localeInfo.shortName); - editor.commit(); - } - } + String[] args = localeInfo.shortName.split("_"); + if (args.length == 1) { + newLocale = new Locale(localeInfo.shortName); } else { - newLocale = systemDefaultLocale; - languageOverride = null; + newLocale = new Locale(args[0], args[1]); + } + if (override) { + languageOverride = localeInfo.shortName; + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); - editor.remove("language"); + editor.putString("language", localeInfo.getKey()); editor.commit(); - - if (newLocale != null) { - LocaleInfo info = null; - if (newLocale.getLanguage() != null) { - info = languagesDict.get(newLocale.getLanguage()); - } - if (info == null) { - info = languagesDict.get(getLocaleString(newLocale)); - } - if (info == null) { - newLocale = Locale.US; - } - } } - if (newLocale != null) { - if (localeInfo.pathToFile == null) { - localeValues.clear(); - } else if (!fromFile) { - localeValues = getLocaleFileStrings(new File(localeInfo.pathToFile)); - } - currentLocale = newLocale; - currentLocaleInfo = localeInfo; - currentPluralRules = allRules.get(currentLocale.getLanguage()); - if (currentPluralRules == null) { - currentPluralRules = allRules.get("en"); - } - changingConfiguration = true; - Locale.setDefault(currentLocale); - android.content.res.Configuration config = new android.content.res.Configuration(); - config.locale = currentLocale; - ApplicationLoader.applicationContext.getResources().updateConfiguration(config, ApplicationLoader.applicationContext.getResources().getDisplayMetrics()); - changingConfiguration = false; + if (pathToFile == null) { + localeValues.clear(); + } else if (!fromFile) { + localeValues = getLocaleFileStrings(pathToFile); + } + currentLocale = newLocale; + currentLocaleInfo = localeInfo; + currentPluralRules = allRules.get(currentLocale.getLanguage()); + if (currentPluralRules == null) { + currentPluralRules = allRules.get("en"); + } + changingConfiguration = true; + Locale.setDefault(currentLocale); + android.content.res.Configuration config = new android.content.res.Configuration(); + config.locale = currentLocale; + ApplicationLoader.applicationContext.getResources().updateConfiguration(config, ApplicationLoader.applicationContext.getResources().getDisplayMetrics()); + changingConfiguration = false; + if (reloadLastFile) { + reloadCurrentRemoteLocale(); + reloadLastFile = false; } } catch (Exception e) { FileLog.e(e); @@ -592,6 +706,10 @@ public class LocaleController { recreateFormatters(); } + public LocaleInfo getCurrentLocaleInfo() { + return currentLocaleInfo; + } + public static String getCurrentLanguageName() { return getString("LanguageName", R.string.LanguageName); } @@ -728,7 +846,7 @@ public class LocaleController { return (discount ? "-" : "") + String.format(type + customFormat, doubleAmount); } - public String formatCurrencyDecimalString(long amount, String type) { + public String formatCurrencyDecimalString(long amount, String type, boolean inludeType) { type = type.toUpperCase(); String customFormat; double doubleAmount; @@ -784,7 +902,7 @@ public class LocaleController { doubleAmount = amount / 100.0; break; } - return String.format(type + customFormat, doubleAmount); + return String.format(inludeType ? type : "" + customFormat, doubleAmount).trim(); } public static String formatStringSimple(String string, Object... args) { @@ -1004,29 +1122,48 @@ public class LocaleController { formatterMonthYear = createFormatter(locale, getStringInternal("formatterMonthYear", R.string.formatterMonthYear), "MMMM yyyy"); formatterDay = createFormatter(lang.toLowerCase().equals("ar") || lang.toLowerCase().equals("ko") ? locale : Locale.US, is24HourFormat ? getStringInternal("formatterDay24H", R.string.formatterDay24H) : getStringInternal("formatterDay12H", R.string.formatterDay12H), is24HourFormat ? "HH:mm" : "h:mm a"); formatterStats = createFormatter(locale, is24HourFormat ? getStringInternal("formatterStats24H", R.string.formatterStats24H) : getStringInternal("formatterStats12H", R.string.formatterStats12H), is24HourFormat ? "MMM dd yyyy, HH:mm" : "MMM dd yyyy, h:mm a"); + formatterBannedUntil = createFormatter(locale, is24HourFormat ? getStringInternal("formatterBannedUntil24H", R.string.formatterBannedUntil24H) : getStringInternal("formatterBannedUntil12H", R.string.formatterBannedUntil12H), is24HourFormat ? "MMM dd yyyy, HH:mm" : "MMM dd yyyy, h:mm a"); + formatterBannedUntilThisYear = createFormatter(locale, is24HourFormat ? getStringInternal("formatterBannedUntilThisYear24H", R.string.formatterBannedUntilThisYear24H) : getStringInternal("formatterBannedUntilThisYear12H", R.string.formatterBannedUntilThisYear12H), is24HourFormat ? "MMM dd, HH:mm" : "MMM dd, h:mm a"); } public static boolean isRTLCharacter(char ch) { return Character.getDirectionality(ch) == Character.DIRECTIONALITY_RIGHT_TO_LEFT || Character.getDirectionality(ch) == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC || Character.getDirectionality(ch) == Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING || Character.getDirectionality(ch) == Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE; } + public static String formatDateForBan(long date) { + try { + date *= 1000; + Calendar rightNow = Calendar.getInstance(); + int year = rightNow.get(Calendar.YEAR); + rightNow.setTimeInMillis(date); + int dateYear = rightNow.get(Calendar.YEAR); + + if (year == dateYear) { + return getInstance().formatterBannedUntilThisYear.format(new Date(date)); + } else { + return getInstance().formatterBannedUntil.format(new Date(date)); + } + } catch (Exception e) { + FileLog.e(e); + } + return "LOC_ERR"; + } + public static String stringForMessageListDate(long date) { try { date *= 1000; Calendar rightNow = Calendar.getInstance(); int day = rightNow.get(Calendar.DAY_OF_YEAR); - int year = rightNow.get(Calendar.YEAR); rightNow.setTimeInMillis(date); int dateDay = rightNow.get(Calendar.DAY_OF_YEAR); - int dateYear = rightNow.get(Calendar.YEAR); if (Math.abs(System.currentTimeMillis() - date) >= 31536000000L) { return getInstance().formatterYear.format(new Date(date)); } else { int dayDiff = dateDay - day; - if(dayDiff == 0 || dayDiff == -1 && System.currentTimeMillis() - date < 60 * 60 * 8 * 1000) { + if (dayDiff == 0 || dayDiff == -1 && System.currentTimeMillis() - date < 60 * 60 * 8 * 1000) { return getInstance().formatterDay.format(new Date(date)); - } else if(dayDiff > -7 && dayDiff <= -1) { + } else if (dayDiff > -7 && dayDiff <= -1) { return getInstance().formatterWeek.format(new Date(date)); } else { return getInstance().formatterMonth.format(new Date(date)); @@ -1105,6 +1242,208 @@ public class LocaleController { } } + public void saveRemoteLocaleStrings(final TLRPC.TL_langPackDifference difference) { + File finalFile = new File(ApplicationLoader.getFilesDirFixed(), "remote_" + difference.lang_code + ".xml"); + try { + final HashMap values; + if (difference.from_version == 0) { + values = new HashMap<>(); + } else { + values = getLocaleFileStrings(finalFile, true); + } + for (int a = 0; a < difference.strings.size(); a++) { + TLRPC.LangPackString string = difference.strings.get(a); + if (string instanceof TLRPC.TL_langPackString) { + values.put(string.key, string.value); + } else if (string instanceof TLRPC.TL_langPackStringPluralized) { + values.put(string.key + "_zero", string.zero_value != null ? string.zero_value : ""); + values.put(string.key + "_one", string.one_value != null ? string.one_value : ""); + values.put(string.key + "_two", string.two_value != null ? string.two_value : ""); + values.put(string.key + "_few", string.few_value != null ? string.few_value : ""); + values.put(string.key + "_many", string.many_value != null ? string.many_value : ""); + values.put(string.key + "_other", string.other_value != null ? string.other_value : ""); + } else if (string instanceof TLRPC.TL_langPackStringDeleted) { + values.remove(string.key); + } + } + BufferedWriter writer = new BufferedWriter(new FileWriter(finalFile)); + writer.write("\n"); + writer.write("\n"); + for (HashMap.Entry entry : values.entrySet()) { + writer.write(String.format("%2$s\n", entry.getKey(), entry.getValue())); + } + writer.write(""); + writer.close(); + final HashMap valuesToSet = getLocaleFileStrings(finalFile); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + LocaleInfo localeInfo = getLanguageFromDict(difference.lang_code); + if (localeInfo != null) { + localeInfo.version = difference.version; + } + saveOtherLanguages(); + try { + Locale newLocale; + String[] args = localeInfo.shortName.split("_"); + if (args.length == 1) { + newLocale = new Locale(localeInfo.shortName); + } else { + newLocale = new Locale(args[0], args[1]); + } + if (newLocale != null) { + languageOverride = localeInfo.shortName; + + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.putString("language", localeInfo.getKey()); + editor.commit(); + } + if (newLocale != null) { + localeValues = valuesToSet; + currentLocale = newLocale; + currentLocaleInfo = localeInfo; + currentPluralRules = allRules.get(currentLocale.getLanguage()); + if (currentPluralRules == null) { + currentPluralRules = allRules.get("en"); + } + changingConfiguration = true; + Locale.setDefault(currentLocale); + android.content.res.Configuration config = new android.content.res.Configuration(); + config.locale = currentLocale; + ApplicationLoader.applicationContext.getResources().updateConfiguration(config, ApplicationLoader.applicationContext.getResources().getDisplayMetrics()); + changingConfiguration = false; + } + } catch (Exception e) { + FileLog.e(e); + changingConfiguration = false; + } + recreateFormatters(); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadInterface); + } + }); + } catch (Exception ignore) { + + } + } + + public void loadRemoteLanguages() { + if (loadingRemoteLanguages) { + return; + } + loadingRemoteLanguages = true; + TLRPC.TL_langpack_getLanguages req = new TLRPC.TL_langpack_getLanguages(); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, TLRPC.TL_error error) { + if (response != null) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + loadingRemoteLanguages = false; + TLRPC.Vector res = (TLRPC.Vector) response; + HashMap remoteLoaded = new HashMap<>(); + remoteLanguages.clear(); + for (int a = 0; a < res.objects.size(); a++) { + TLRPC.TL_langPackLanguage language = (TLRPC.TL_langPackLanguage) res.objects.get(a); + LocaleInfo localeInfo = new LocaleInfo(); + localeInfo.nameEnglish = language.name; + localeInfo.name = language.native_name; + localeInfo.shortName = language.lang_code.replace('-', '_').toLowerCase(); + localeInfo.pathToFile = "remote"; + + LocaleInfo existing = getLanguageFromDict(localeInfo.getKey()); + if (existing == null) { + languages.add(localeInfo); + languagesDict.put(localeInfo.getKey(), localeInfo); + } else { + existing.nameEnglish = localeInfo.nameEnglish; + existing.name = localeInfo.name; + existing.pathToFile = localeInfo.pathToFile; + } + remoteLanguages.add(localeInfo); + remoteLoaded.put(localeInfo.getKey(), existing); + } + for (int a = 0; a < languages.size(); a++) { + LocaleInfo info = languages.get(a); + if (info.isBuiltIn() || !info.isRemote()) { + continue; + } + LocaleInfo existing = remoteLoaded.get(info.getKey()); + if (existing == null) { + languages.remove(a); + languagesDict.remove(info.getKey()); + a--; + if (info == currentLocaleInfo) { + if (systemDefaultLocale.getLanguage() != null) { + info = getLanguageFromDict(systemDefaultLocale.getLanguage()); + } + if (info == null) { + info = getLanguageFromDict(getLocaleString(systemDefaultLocale)); + } + if (info == null) { + info = getLanguageFromDict("en"); + } + applyLanguage(info, true); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadInterface); + } + } + } + saveOtherLanguages(); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.suggestedLangpack); + applyLanguage(currentLocaleInfo, true); + } + }); + } + } + }, ConnectionsManager.RequestFlagWithoutLogin); + } + + public void applyRemoteLanguage(LocaleInfo localeInfo, TLRPC.TL_langPackLanguage language, boolean force) { + if (localeInfo == null && language == null || localeInfo != null && !localeInfo.isRemote()) { + return; + } + if (localeInfo.version != 0 && !BuildVars.DEBUG_VERSION && !force) { + TLRPC.TL_langpack_getDifference req = new TLRPC.TL_langpack_getDifference(); + req.from_version = localeInfo.version; + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, TLRPC.TL_error error) { + if (response != null) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + saveRemoteLocaleStrings((TLRPC.TL_langPackDifference) response); + } + }); + } + } + }, ConnectionsManager.RequestFlagWithoutLogin); + } else { + ConnectionsManager.getInstance().setLangCode(localeInfo != null ? localeInfo.shortName : language.lang_code); + TLRPC.TL_langpack_getLangPack req = new TLRPC.TL_langpack_getLangPack(); + if (language == null) { + req.lang_code = localeInfo.shortName; + } else { + req.lang_code = language.lang_code; + } + req.lang_code = req.lang_code.replace("_", "-"); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, TLRPC.TL_error error) { + if (response != null) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + saveRemoteLocaleStrings((TLRPC.TL_langPackDifference) response); + } + }); + } + } + }, ConnectionsManager.RequestFlagWithoutLogin); + } + } + public String getTranslitString(String src) { if (translitChars == null) { translitChars = new HashMap<>(520); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java b/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java index 0613818ef..9ba9a05c6 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java @@ -24,6 +24,7 @@ import android.database.ContentObserver; import android.database.Cursor; import android.graphics.BitmapFactory; import android.graphics.Point; +import android.graphics.SurfaceTexture; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; @@ -49,8 +50,14 @@ import android.provider.MediaStore; import android.provider.OpenableColumns; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; +import android.view.TextureView; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; import org.telegram.messenger.audioinfo.AudioInfo; +import org.telegram.messenger.exoplayer2.ExoPlayer; +import org.telegram.messenger.exoplayer2.ui.AspectRatioFrameLayout; import org.telegram.messenger.query.SharedMediaQuery; import org.telegram.messenger.video.InputSurface; import org.telegram.messenger.video.MP4Builder; @@ -62,6 +69,8 @@ import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ChatActivity; import org.telegram.ui.Components.EmbedBottomSheet; +import org.telegram.ui.Components.PipRoundVideoView; +import org.telegram.ui.Components.VideoPlayer; import org.telegram.ui.PhotoViewer; import java.io.File; @@ -73,6 +82,8 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.Locale; import java.util.Timer; @@ -130,7 +141,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, MediaStore.Video.Media.BUCKET_ID, MediaStore.Video.Media.BUCKET_DISPLAY_NAME, MediaStore.Video.Media.DATA, - MediaStore.Video.Media.DATE_TAKEN + MediaStore.Video.Media.DATE_TAKEN, + MediaStore.Video.Media.DURATION }; public static class AudioEntry { @@ -149,13 +161,11 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public PhotoEntry coverPhoto; public ArrayList photos = new ArrayList<>(); public HashMap photosByIds = new HashMap<>(); - public boolean isVideo; - public AlbumEntry(int bucketId, String bucketName, PhotoEntry coverPhoto, boolean isVideo) { + public AlbumEntry(int bucketId, String bucketName, PhotoEntry coverPhoto) { this.bucketId = bucketId; this.bucketName = bucketName; this.coverPhoto = coverPhoto; - this.isVideo = isVideo; } public void addPhoto(PhotoEntry photoEntry) { @@ -168,10 +178,12 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public int bucketId; public int imageId; public long dateTaken; + public int duration; public String path; public int orientation; public String thumbPath; public String imagePath; + public VideoEditedInfo editedInfo; public boolean isVideo; public CharSequence caption; public ArrayList stickers = new ArrayList<>(); @@ -181,7 +193,11 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, this.imageId = imageId; this.dateTaken = dateTaken; this.path = path; - this.orientation = orientation; + if (isVideo) { + this.duration = orientation; + } else { + this.orientation = orientation; + } this.isVideo = isVideo; } } @@ -269,12 +285,14 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public static final int AUTODOWNLOAD_MASK_DOCUMENT = 8; public static final int AUTODOWNLOAD_MASK_MUSIC = 16; public static final int AUTODOWNLOAD_MASK_GIF = 32; + public static final int AUTODOWNLOAD_MASK_VIDEOMESSAGE = 64; public int mobileDataDownloadMask = 0; public int wifiDownloadMask = 0; public int roamingDownloadMask = 0; private int lastCheckMask = 0; private ArrayList photoDownloadQueue = new ArrayList<>(); private ArrayList audioDownloadQueue = new ArrayList<>(); + private ArrayList videoMessageDownloadQueue = new ArrayList<>(); private ArrayList documentDownloadQueue = new ArrayList<>(); private ArrayList musicDownloadQueue = new ArrayList<>(); private ArrayList gifDownloadQueue = new ArrayList<>(); @@ -290,6 +308,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, private int repeatMode; private Runnable refreshGalleryRunnable; + public static AlbumEntry allMediaAlbumEntry; public static AlbumEntry allPhotosAlbumEntry; private static Runnable broadcastPhotosRunnable; @@ -304,7 +323,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, private boolean isPaused = false; private MediaPlayer audioPlayer = null; private AudioTrack audioTrackPlayer = null; - private int lastProgress = 0; + private long lastProgress = 0; private MessageObject playingMessageObject; private int playerBufferSize = 0; private boolean decodingFinished = false; @@ -321,10 +340,21 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, private boolean downloadingCurrentMessage; private boolean playMusicAgain; private AudioInfo audioInfo; + private VideoPlayer videoPlayer; + private TextureView currentTextureView; + private PipRoundVideoView pipRoundVideoView; + private int pipSwitchingState; + private Activity baseActivity; + private AspectRatioFrameLayout currentAspectRatioFrameLayout; + private boolean isDrawingWasReady; + private FrameLayout currentTextureViewContainer; + private int currentAspectRatioFrameLayoutRotation; + private float currentAspectRatioFrameLayoutRatio; + private boolean currentAspectRatioFrameLayoutReady; - private AudioRecord audioRecorder = null; - private TLRPC.TL_document recordingAudio = null; - private File recordingAudioFile = null; + private AudioRecord audioRecorder; + private TLRPC.TL_document recordingAudio; + private File recordingAudioFile; private long recordStartTime; private long recordTimeCount; private long recordDialogId; @@ -618,8 +648,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, fileDecodingQueue = new DispatchQueue("fileDecodingQueue"); SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - mobileDataDownloadMask = preferences.getInt("mobileDataDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | AUTODOWNLOAD_MASK_GIF); - wifiDownloadMask = preferences.getInt("wifiDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | AUTODOWNLOAD_MASK_GIF); + mobileDataDownloadMask = preferences.getInt("mobileDataDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | AUTODOWNLOAD_MASK_GIF | AUTODOWNLOAD_MASK_VIDEOMESSAGE); + wifiDownloadMask = preferences.getInt("wifiDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | AUTODOWNLOAD_MASK_GIF | AUTODOWNLOAD_MASK_VIDEOMESSAGE); roamingDownloadMask = preferences.getInt("roamingDownloadMask", 0); saveToGallery = preferences.getBoolean("save_gallery", false); autoplayGifs = preferences.getBoolean("autoplay_gif", true); @@ -697,8 +727,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, @Override public void run() { if (state == TelephonyManager.CALL_STATE_RINGING) { - if (isPlayingAudio(getPlayingMessageObject()) && !isAudioPaused()) { - pauseAudio(getPlayingMessageObject()); + if (isPlayingMessage(getPlayingMessageObject()) && !isMessagePaused()) { + pauseMessage(getPlayingMessageObject()); } else if (recordStartRunnable != null || recordingAudio != null) { stopRecording(2); } @@ -732,8 +762,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, @Override public void onAudioFocusChange(int focusChange) { if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { - if (isPlayingAudio(getPlayingMessageObject()) && !isAudioPaused()) { - pauseAudio(getPlayingMessageObject()); + if (isPlayingMessage(getPlayingMessageObject()) && !isMessagePaused()) { + pauseMessage(getPlayingMessageObject()); } hasAudioFocus = 0; audioFocus = AUDIO_NO_FOCUS_NO_DUCK; @@ -741,16 +771,16 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, audioFocus = AUDIO_FOCUSED; if (resumeAudioOnFocusGain) { resumeAudioOnFocusGain = false; - if (isPlayingAudio(getPlayingMessageObject()) && isAudioPaused()) { - playAudio(getPlayingMessageObject()); + if (isPlayingMessage(getPlayingMessageObject()) && isMessagePaused()) { + playMessage(getPlayingMessageObject()); } } } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { audioFocus = AUDIO_NO_FOCUS_CAN_DUCK; } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { audioFocus = AUDIO_NO_FOCUS_NO_DUCK; - if (isPlayingAudio(getPlayingMessageObject()) && !isAudioPaused()) { - pauseAudio(getPlayingMessageObject()); + if (isPlayingMessage(getPlayingMessageObject()) && !isMessagePaused()) { + pauseMessage(getPlayingMessageObject()); resumeAudioOnFocusGain = true; } } @@ -769,6 +799,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, audioPlayer.setVolume(volume, volume); } else if (audioTrackPlayer != null) { audioTrackPlayer.setStereoVolume(volume, volume); + } else if (videoPlayer != null) { + videoPlayer.setVolume(volume); } } catch (Exception e) { FileLog.e(e); @@ -793,15 +825,24 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - if (currentPlayingMessageObject != null && (audioPlayer != null || audioTrackPlayer != null) && !isPaused) { + if (currentPlayingMessageObject != null && (audioPlayer != null || audioTrackPlayer != null || videoPlayer != null) && !isPaused) { try { if (ignoreFirstProgress != 0) { ignoreFirstProgress--; return; } - int progress; + long progress; float value; - if (audioPlayer != null) { + if (videoPlayer != null) { + progress = videoPlayer.getCurrentPosition(); + value = (float) lastProgress / (float) videoPlayer.getDuration(); + if (progress <= lastProgress) { + return; + } + if (value >= 1) { + return; + } + } else if (audioPlayer != null) { progress = audioPlayer.getCurrentPosition(); value = (float) lastProgress / (float) audioPlayer.getDuration(); if (progress <= lastProgress) { @@ -816,8 +857,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } lastProgress = progress; currentPlayingMessageObject.audioProgress = value; - currentPlayingMessageObject.audioProgressSec = lastProgress / 1000; - NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioProgressDidChanged, currentPlayingMessageObject.getId(), value); + currentPlayingMessageObject.audioProgressSec = (int) (lastProgress / 1000); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingProgressDidChanged, currentPlayingMessageObject.getId(), value); } catch (Exception e) { FileLog.e(e); } @@ -849,6 +890,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, playMusicAgain = false; photoDownloadQueue.clear(); audioDownloadQueue.clear(); + videoMessageDownloadQueue.clear(); documentDownloadQueue.clear(); videoDownloadQueue.clear(); musicDownloadQueue.clear(); @@ -872,6 +914,9 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if ((mobileDataDownloadMask & AUTODOWNLOAD_MASK_AUDIO) != 0 || (wifiDownloadMask & AUTODOWNLOAD_MASK_AUDIO) != 0 || (roamingDownloadMask & AUTODOWNLOAD_MASK_AUDIO) != 0) { mask |= AUTODOWNLOAD_MASK_AUDIO; } + if ((mobileDataDownloadMask & AUTODOWNLOAD_MASK_VIDEOMESSAGE) != 0 || (wifiDownloadMask & AUTODOWNLOAD_MASK_VIDEOMESSAGE) != 0 || (roamingDownloadMask & AUTODOWNLOAD_MASK_VIDEOMESSAGE) != 0) { + mask |= AUTODOWNLOAD_MASK_VIDEOMESSAGE; + } if ((mobileDataDownloadMask & AUTODOWNLOAD_MASK_VIDEO) != 0 || (wifiDownloadMask & AUTODOWNLOAD_MASK_VIDEO) != 0 || (roamingDownloadMask & AUTODOWNLOAD_MASK_VIDEO) != 0) { mask |= AUTODOWNLOAD_MASK_VIDEO; } @@ -915,6 +960,17 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } audioDownloadQueue.clear(); } + if ((currentMask & AUTODOWNLOAD_MASK_VIDEOMESSAGE) != 0) { + if (videoMessageDownloadQueue.isEmpty()) { + newDownloadObjectsAvailable(AUTODOWNLOAD_MASK_VIDEOMESSAGE); + } + } else { + for (int a = 0; a < videoMessageDownloadQueue.size(); a++) { + DownloadObject downloadObject = videoMessageDownloadQueue.get(a); + FileLoader.getInstance().cancelLoadFile((TLRPC.Document) downloadObject.object); + } + videoMessageDownloadQueue.clear(); + } if ((currentMask & AUTODOWNLOAD_MASK_DOCUMENT) != 0) { if (documentDownloadQueue.isEmpty()) { newDownloadObjectsAvailable(AUTODOWNLOAD_MASK_DOCUMENT); @@ -973,6 +1029,9 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if ((mask & AUTODOWNLOAD_MASK_AUDIO) == 0) { MessagesStorage.getInstance().clearDownloadQueue(AUTODOWNLOAD_MASK_AUDIO); } + if ((mask & AUTODOWNLOAD_MASK_VIDEOMESSAGE) == 0) { + MessagesStorage.getInstance().clearDownloadQueue(AUTODOWNLOAD_MASK_VIDEOMESSAGE); + } if ((mask & AUTODOWNLOAD_MASK_VIDEO) == 0) { MessagesStorage.getInstance().clearDownloadQueue(AUTODOWNLOAD_MASK_VIDEO); } @@ -1011,6 +1070,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, queue = photoDownloadQueue; } else if (type == AUTODOWNLOAD_MASK_AUDIO) { queue = audioDownloadQueue; + } else if (type == AUTODOWNLOAD_MASK_VIDEOMESSAGE) { + queue = videoMessageDownloadQueue; } else if (type == AUTODOWNLOAD_MASK_VIDEO) { queue = videoDownloadQueue; } else if (type == AUTODOWNLOAD_MASK_DOCUMENT) { @@ -1057,6 +1118,9 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if ((mask & AUTODOWNLOAD_MASK_AUDIO) != 0 && (downloadMask & AUTODOWNLOAD_MASK_AUDIO) != 0 && audioDownloadQueue.isEmpty()) { MessagesStorage.getInstance().getDownloadQueue(AUTODOWNLOAD_MASK_AUDIO); } + if ((mask & AUTODOWNLOAD_MASK_VIDEOMESSAGE) != 0 && (downloadMask & AUTODOWNLOAD_MASK_VIDEOMESSAGE) != 0 && videoMessageDownloadQueue.isEmpty()) { + MessagesStorage.getInstance().getDownloadQueue(AUTODOWNLOAD_MASK_VIDEOMESSAGE); + } if ((mask & AUTODOWNLOAD_MASK_VIDEO) != 0 && (downloadMask & AUTODOWNLOAD_MASK_VIDEO) != 0 && videoDownloadQueue.isEmpty()) { MessagesStorage.getInstance().getDownloadQueue(AUTODOWNLOAD_MASK_VIDEO); } @@ -1088,6 +1152,11 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if (audioDownloadQueue.isEmpty()) { newDownloadObjectsAvailable(AUTODOWNLOAD_MASK_AUDIO); } + } else if (downloadObject.type == AUTODOWNLOAD_MASK_VIDEOMESSAGE) { + videoMessageDownloadQueue.remove(downloadObject); + if (videoMessageDownloadQueue.isEmpty()) { + newDownloadObjectsAvailable(AUTODOWNLOAD_MASK_VIDEOMESSAGE); + } } else if (downloadObject.type == AUTODOWNLOAD_MASK_VIDEO) { videoDownloadQueue.remove(downloadObject); if (videoDownloadQueue.isEmpty()) { @@ -1320,7 +1389,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, String file = FileLoader.getAttachFileName(playingMessageObject.getDocument()); if (file.equals(fileName)) { playMusicAgain = true; - playAudio(playingMessageObject); + playMessage(playingMessageObject); } } ArrayList messageObjects = loadingFileMessagesObservers.get(fileName); @@ -1383,7 +1452,9 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, long dialog_id = delayedMessage.obj.getDialogId(); Long lastTime = typingTimes.get(dialog_id); if (lastTime == null || lastTime + 4000 < System.currentTimeMillis()) { - if (MessageObject.isVideoDocument(delayedMessage.documentLocation)) { + if (MessageObject.isRoundVideoDocument(delayedMessage.documentLocation)) { + MessagesController.getInstance().sendTyping(dialog_id, 8, 0); + } else if (MessageObject.isVideoDocument(delayedMessage.documentLocation)) { MessagesController.getInstance().sendTyping(dialog_id, 5, 0); } else if (delayedMessage.documentLocation != null) { MessagesController.getInstance().sendTyping(dialog_id, 3, 0); @@ -1444,7 +1515,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, ArrayList arr = (ArrayList) args[1]; for (int a = 0; a < arr.size(); a++) { messageObject = arr.get(a); - if (messageObject.isVoice() && (!voiceMessagesPlaylistUnread || messageObject.isContentUnread() && !messageObject.isOut())) { + if ((messageObject.isVoice() || messageObject.isRoundVideo()) && (!voiceMessagesPlaylistUnread || messageObject.isContentUnread() && !messageObject.isOut())) { voiceMessagesPlaylist.add(messageObject); voiceMessagesPlaylistMap.put(messageObject.getId(), messageObject); } @@ -1680,18 +1751,21 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, useFrontSpeaker = false; startRecording(raiseChat.getDialogId(), null); } + if (useFrontSpeaker) { + setUseFrontSpeaker(true); + } ignoreOnPause = true; if (proximityHasDifferentValues && proximityWakeLock != null && !proximityWakeLock.isHeld()) { proximityWakeLock.acquire(); } } - } else if (playingMessageObject != null && playingMessageObject.isVoice()) { + } else if (playingMessageObject != null && (playingMessageObject.isVoice() || playingMessageObject.isRoundVideo())) { if (!useFrontSpeaker) { FileLog.e("start listen"); if (proximityHasDifferentValues && proximityWakeLock != null && !proximityWakeLock.isHeld()) { proximityWakeLock.acquire(); } - useFrontSpeaker = true; + setUseFrontSpeaker(true); startAudioAgain(false); ignoreOnPause = true; } @@ -1700,13 +1774,13 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, raisedToTop = 0; countLess = 0; } else if (proximityTouched) { - if (playingMessageObject != null && playingMessageObject.isVoice()) { + if (playingMessageObject != null && (playingMessageObject.isVoice() || playingMessageObject.isRoundVideo())) { if (!useFrontSpeaker) { FileLog.e("start listen by proximity only"); if (proximityHasDifferentValues && proximityWakeLock != null && !proximityWakeLock.isHeld()) { proximityWakeLock.acquire(); } - useFrontSpeaker = true; + setUseFrontSpeaker(true); startAudioAgain(false); ignoreOnPause = true; } @@ -1738,6 +1812,17 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } } + private void setUseFrontSpeaker(boolean value) { + useFrontSpeaker = value; + AudioManager audioManager = NotificationsController.getInstance().audioManager; + if (useFrontSpeaker) { + audioManager.setBluetoothScoOn(false); + audioManager.setSpeakerphoneOn(false); + } else { + audioManager.setSpeakerphoneOn(true); + } + } + public void startRecordingIfFromSpeaker() { if (!useFrontSpeaker || raiseChat == null || !allowStartRecord) { return; @@ -1751,23 +1836,33 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if (playingMessageObject == null) { return; } - boolean post = audioPlayer != null; + NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioRouteChanged, useFrontSpeaker); - final MessageObject currentMessageObject = playingMessageObject; - float progress = playingMessageObject.audioProgress; - cleanupPlayer(false, true); - currentMessageObject.audioProgress = progress; - playAudio(currentMessageObject); - if (paused) { - if (post) { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - pauseAudio(currentMessageObject); - } - }, 100); + if (videoPlayer != null) { + videoPlayer.setStreamType(useFrontSpeaker ? AudioManager.STREAM_VOICE_CALL : AudioManager.STREAM_MUSIC); + if (!paused) { + videoPlayer.play(); } else { - pauseAudio(currentMessageObject); + videoPlayer.pause(); + } + } else { + boolean post = audioPlayer != null; + final MessageObject currentMessageObject = playingMessageObject; + float progress = playingMessageObject.audioProgress; + cleanupPlayer(false, true); + currentMessageObject.audioProgress = progress; + playMessage(currentMessageObject); + if (paused) { + if (post) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + pauseMessage(currentMessageObject); + } + }, 100); + } else { + pauseMessage(currentMessageObject); + } } } } @@ -1790,7 +1885,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, return; } raiseChat = chatActivity; - if (!raiseToSpeak && (playingMessageObject == null || !playingMessageObject.isVoice())) { + if (!raiseToSpeak && (playingMessageObject == null || !playingMessageObject.isVoice() && !playingMessageObject.isRoundVideo())) { return; } if (!sensorsStarted) { @@ -1874,10 +1969,10 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } try { audioPlayer.release(); - audioPlayer = null; } catch (Exception e) { FileLog.e(e); } + audioPlayer = null; } else if (audioTrackPlayer != null) { synchronized (playerObjectSync) { try { @@ -1888,10 +1983,22 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } try { audioTrackPlayer.release(); - audioTrackPlayer = null; } catch (Exception e) { FileLog.e(e); } + audioTrackPlayer = null; + } + } else if (videoPlayer != null) { + currentAspectRatioFrameLayout = null; + currentTextureViewContainer = null; + currentAspectRatioFrameLayoutReady = false; + currentTextureView = null; + videoPlayer.releasePlayer(); + videoPlayer = null; + try { + baseActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } catch (Exception e) { + FileLog.e(e); } } stopProgressTimer(); @@ -1905,7 +2012,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, MessageObject lastFile = playingMessageObject; playingMessageObject.audioProgress = 0.0f; playingMessageObject.audioProgressSec = 0; - NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioProgressDidChanged, playingMessageObject.getId(), 0); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingProgressDidChanged, playingMessageObject.getId(), 0); playingMessageObject = null; downloadingCurrentMessage = false; if (notify) { @@ -1927,12 +2034,21 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, boolean next = false; if (voiceMessagesPlaylist != null) { MessageObject nextVoiceMessage = voiceMessagesPlaylist.get(0); - playAudio(nextVoiceMessage); + playMessage(nextVoiceMessage); + if (!nextVoiceMessage.isRoundVideo() && pipRoundVideoView != null) { + pipRoundVideoView.close(true); + pipRoundVideoView = null; + } } else { - if (lastFile.isVoice() && lastFile.getId() != 0) { + if ((lastFile.isVoice() || lastFile.isRoundVideo()) && lastFile.getId() != 0) { startRecordingIfFromSpeaker(); } - NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioDidReset, lastFile.getId(), stopService); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingDidReset, lastFile.getId(), stopService); + pipSwitchingState = 0; + if (pipRoundVideoView != null) { + pipRoundVideoView.close(true); + pipRoundVideoView = null; + } } } if (stopService) { @@ -1982,7 +2098,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } public boolean seekToProgress(MessageObject messageObject, float progress) { - if (audioTrackPlayer == null && audioPlayer == null || messageObject == null || playingMessageObject == null || playingMessageObject != null && playingMessageObject.getId() != messageObject.getId()) { + if (audioTrackPlayer == null && audioPlayer == null && videoPlayer == null || messageObject == null || playingMessageObject == null || playingMessageObject != null && playingMessageObject.getId() != messageObject.getId()) { return false; } try { @@ -1992,6 +2108,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, lastProgress = seekTo; } else if (audioTrackPlayer != null) { seekOpusPlayer(progress); + } else if (videoPlayer != null) { + videoPlayer.seekTo((long) (videoPlayer.getDuration() * progress)); } } catch (Exception e) { FileLog.e(e); @@ -2033,7 +2151,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public boolean setPlaylist(ArrayList messageObjects, MessageObject current, boolean loadMusic) { if (playingMessageObject == current) { - return playAudio(current); + return playMessage(current); } forceLoopCurrentPlaylist = !loadMusic; playMusicAgain = !playlist.isEmpty(); @@ -2060,7 +2178,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, SharedMediaQuery.loadMusic(current.getDialogId(), playlist.get(0).getId()); } } - return playAudio(current); + return playMessage(current); } public void playNextMessage() { @@ -2073,7 +2191,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } currentPlaylistNum = index; playMusicAgain = true; - playAudio(playlist.get(currentPlaylistNum)); + playMessage(playlist.get(currentPlaylistNum)); } private void playNextMessage(boolean byStop) { @@ -2081,14 +2199,14 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if (byStop && repeatMode == 2 && !forceLoopCurrentPlaylist) { cleanupPlayer(false, false); - playAudio(currentPlayList.get(currentPlaylistNum)); + playMessage(currentPlayList.get(currentPlaylistNum)); return; } currentPlaylistNum++; if (currentPlaylistNum >= currentPlayList.size()) { currentPlaylistNum = 0; if (byStop && repeatMode == 0 && !forceLoopCurrentPlaylist) { - if (audioPlayer != null || audioTrackPlayer != null) { + if (audioPlayer != null || audioTrackPlayer != null || videoPlayer != null) { if (audioPlayer != null) { try { audioPlayer.reset(); @@ -2102,10 +2220,10 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } try { audioPlayer.release(); - audioPlayer = null; } catch (Exception e) { FileLog.e(e); } + audioPlayer = null; } else if (audioTrackPlayer != null) { synchronized (playerObjectSync) { try { @@ -2116,10 +2234,22 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } try { audioTrackPlayer.release(); - audioTrackPlayer = null; } catch (Exception e) { FileLog.e(e); } + audioTrackPlayer = null; + } + } else if (videoPlayer != null) { + currentAspectRatioFrameLayout = null; + currentTextureViewContainer = null; + currentAspectRatioFrameLayoutReady = false; + currentTextureView = null; + videoPlayer.releasePlayer(); + videoPlayer = null; + try { + baseActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } catch (Exception e) { + FileLog.e(e); } } stopProgressTimer(); @@ -2128,8 +2258,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, isPaused = true; playingMessageObject.audioProgress = 0.0f; playingMessageObject.audioProgressSec = 0; - NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioProgressDidChanged, playingMessageObject.getId(), 0); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioPlayStateChanged, playingMessageObject.getId()); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingProgressDidChanged, playingMessageObject.getId(), 0); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingPlayStateChanged, playingMessageObject.getId()); } return; } @@ -2138,7 +2268,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, return; } playMusicAgain = true; - playAudio(currentPlayList.get(currentPlaylistNum)); + playMessage(currentPlayList.get(currentPlaylistNum)); } public void playPreviousMessage() { @@ -2160,7 +2290,26 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, return; } playMusicAgain = true; - playAudio(currentPlayList.get(currentPlaylistNum)); + playMessage(currentPlayList.get(currentPlaylistNum)); + } + + private void checkIsNextVoiceFileDownloaded() { + if (voiceMessagesPlaylist == null || voiceMessagesPlaylist.size() < 2) { + return; + } + MessageObject nextAudio = voiceMessagesPlaylist.get(1); + File file = null; + if (nextAudio.messageOwner.attachPath != null && nextAudio.messageOwner.attachPath.length() > 0) { + file = new File(nextAudio.messageOwner.attachPath); + if (!file.exists()) { + file = null; + } + } + final File cacheFile = file != null ? file : FileLoader.getPathToMessage(nextAudio.messageOwner); + boolean exist = cacheFile != null && cacheFile.exists(); + if (cacheFile != null && cacheFile != file && !cacheFile.exists()) { + FileLoader.getInstance().loadFile(nextAudio.getDocument(), false, false); + } } private void checkIsNextMusicFileDownloaded() { @@ -2204,7 +2353,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, private void checkAudioFocus(MessageObject messageObject) { int neededAudioFocus; - if (messageObject.isVoice()) { + if (messageObject.isVoice() || messageObject.isRoundVideo()) { if (useFrontSpeaker) { neededAudioFocus = 3; } else { @@ -2227,11 +2376,89 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } } - public boolean playAudio(final MessageObject messageObject) { + public void setCurrentRoundVisible(boolean visible) { + if (currentAspectRatioFrameLayout == null) { + return; + } + if (visible) { + if (pipRoundVideoView != null) { + pipSwitchingState = 2; + pipRoundVideoView.close(true); + pipRoundVideoView = null; + } else if (currentAspectRatioFrameLayout != null) { + if (currentAspectRatioFrameLayout.getParent() == null) { + currentTextureViewContainer.addView(currentAspectRatioFrameLayout); + } + videoPlayer.setTextureView(currentTextureView); + } + } else { + if (currentAspectRatioFrameLayout.getParent() != null) { + pipSwitchingState = 1; + currentTextureViewContainer.removeView(currentAspectRatioFrameLayout); + } else { + if (pipRoundVideoView == null) { + try { + pipRoundVideoView = new PipRoundVideoView(); + pipRoundVideoView.show(baseActivity, new Runnable() { + @Override + public void run() { + cleanupPlayer(true, true); + } + }); + } catch (Exception e) { + pipRoundVideoView = null; + } + } + if (pipRoundVideoView != null) { + videoPlayer.setTextureView(pipRoundVideoView.getTextureView()); + } + } + } + } + + public void setTextureView(TextureView textureView, AspectRatioFrameLayout aspectRatioFrameLayout, FrameLayout container, boolean set) { + if (!set && currentTextureView == textureView) { + pipSwitchingState = 1; + currentTextureView = null; + currentAspectRatioFrameLayout = null; + currentTextureViewContainer = null; + return; + } + if (videoPlayer == null || textureView == currentTextureView) { + return; + } + isDrawingWasReady = aspectRatioFrameLayout != null && aspectRatioFrameLayout.isDrawingReady(); + currentTextureView = textureView; + if (pipRoundVideoView != null) { + videoPlayer.setTextureView(pipRoundVideoView.getTextureView()); + } else { + videoPlayer.setTextureView(currentTextureView); + } + currentAspectRatioFrameLayout = aspectRatioFrameLayout; + currentTextureViewContainer = container; + if (currentAspectRatioFrameLayoutReady && currentAspectRatioFrameLayout != null) { + if (currentAspectRatioFrameLayout != null) { + currentAspectRatioFrameLayout.setAspectRatio(currentAspectRatioFrameLayoutRatio, currentAspectRatioFrameLayoutRotation); + } + if (currentTextureViewContainer.getVisibility() != View.VISIBLE) { + currentTextureViewContainer.setVisibility(View.VISIBLE); + } + } + } + + public void setBaseActivity(Activity activity, boolean set) { + if (set) { + baseActivity = activity; + } else if (baseActivity == activity) { + baseActivity = null; + } + } + + public boolean playMessage(final MessageObject messageObject) { if (messageObject == null) { return false; } - if ((audioTrackPlayer != null || audioPlayer != null) && playingMessageObject != null && messageObject.getId() == playingMessageObject.getId()) { + if ((audioTrackPlayer != null || audioPlayer != null || videoPlayer != null) && playingMessageObject != null && messageObject.getId() == playingMessageObject.getId()) { if (isPaused) { resumeAudio(messageObject); } @@ -2257,7 +2484,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } } final File cacheFile = file != null ? file : FileLoader.getPathToMessage(messageObject.messageOwner); - if (cacheFile != null && cacheFile != file && !cacheFile.exists() && messageObject.isMusic()) { + if (cacheFile != null && cacheFile != file && !cacheFile.exists()) { FileLoader.getInstance().loadFile(messageObject.getDocument(), false, false); downloadingCurrentMessage = true; isPaused = false; @@ -2272,16 +2499,164 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, Intent intent = new Intent(ApplicationLoader.applicationContext, MusicPlayerService.class); ApplicationLoader.applicationContext.stopService(intent); } - NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioPlayStateChanged, playingMessageObject.getId()); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingPlayStateChanged, playingMessageObject.getId()); return true; } else { downloadingCurrentMessage = false; } if (messageObject.isMusic()) { checkIsNextMusicFileDownloaded(); + } else { + checkIsNextVoiceFileDownloaded(); } - if (isOpusFile(cacheFile.getAbsolutePath()) == 1) { + if (currentAspectRatioFrameLayout != null) { + isDrawingWasReady = false; + currentAspectRatioFrameLayout.setDrawingReady(false); + } + if (messageObject.isRoundVideo()) { + playlist.clear(); + shuffledPlaylist.clear(); + videoPlayer = new VideoPlayer(); + videoPlayer.setDelegate(new VideoPlayer.VideoPlayerDelegate() { + @Override + public void onStateChanged(boolean playWhenReady, int playbackState) { + if (videoPlayer == null) { + return; + } + if (playbackState != ExoPlayer.STATE_ENDED && playbackState != ExoPlayer.STATE_IDLE) { + try { + baseActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } catch (Exception e) { + FileLog.e(e); + } + } else { + try { + baseActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } catch (Exception e) { + FileLog.e(e); + } + } + if (playbackState == ExoPlayer.STATE_READY) { + currentAspectRatioFrameLayoutReady = true; + if (currentTextureViewContainer != null && currentTextureViewContainer.getVisibility() != View.VISIBLE) { + currentTextureViewContainer.setVisibility(View.VISIBLE); + } + } else if (videoPlayer.isPlaying() && playbackState == ExoPlayer.STATE_ENDED) { + cleanupPlayer(true, true, true); + } + } + + @Override + public void onError(Exception e) { + FileLog.e(e); + } + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + currentAspectRatioFrameLayoutRotation = unappliedRotationDegrees; + if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) { + int temp = width; + width = height; + height = temp; + } + currentAspectRatioFrameLayoutRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; + + if (currentAspectRatioFrameLayout != null) { + currentAspectRatioFrameLayout.setAspectRatio(currentAspectRatioFrameLayoutRatio, currentAspectRatioFrameLayoutRotation); + } + } + + @Override + public void onRenderedFirstFrame() { + if (currentAspectRatioFrameLayout != null && !currentAspectRatioFrameLayout.isDrawingReady()) { + isDrawingWasReady = true; + currentAspectRatioFrameLayout.setDrawingReady(true); + if (currentTextureViewContainer != null && currentTextureViewContainer.getVisibility() != View.VISIBLE) { + currentTextureViewContainer.setVisibility(View.VISIBLE); + } + } + } + + @TargetApi(16) + @Override + public boolean onSurfaceDestroyed(SurfaceTexture surfaceTexture) { + if (videoPlayer == null) { + return false; + } + if (pipSwitchingState == 2) { + if (currentAspectRatioFrameLayout != null) { + if (isDrawingWasReady) { + currentAspectRatioFrameLayout.setDrawingReady(true); + } + if (currentAspectRatioFrameLayout.getParent() == null) { + currentTextureViewContainer.addView(currentAspectRatioFrameLayout); + } + if (currentTextureView.getSurfaceTexture() != surfaceTexture) { + currentTextureView.setSurfaceTexture(surfaceTexture); + } + videoPlayer.setTextureView(currentTextureView); + } + pipSwitchingState = 0; + return true; + } else if (pipSwitchingState == 1) { + if (baseActivity != null) { + if (pipRoundVideoView == null) { + try { + pipRoundVideoView = new PipRoundVideoView(); + pipRoundVideoView.show(baseActivity, new Runnable() { + @Override + public void run() { + cleanupPlayer(true, true); + } + }); + } catch (Exception e) { + pipRoundVideoView = null; + } + } + if (pipRoundVideoView != null) { + if (pipRoundVideoView.getTextureView().getSurfaceTexture() != surfaceTexture) { + pipRoundVideoView.getTextureView().setSurfaceTexture(surfaceTexture); + } + videoPlayer.setTextureView(pipRoundVideoView.getTextureView()); + } + } + pipSwitchingState = 0; + return true; + } + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + + } + }); + currentAspectRatioFrameLayoutReady = false; + if (pipRoundVideoView != null || !MessagesController.getInstance().isDialogCreated(messageObject.getDialogId())) { + if (pipRoundVideoView == null) { + try { + pipRoundVideoView = new PipRoundVideoView(); + pipRoundVideoView.show(baseActivity, new Runnable() { + @Override + public void run() { + cleanupPlayer(true, true); + } + }); + } catch (Exception e) { + pipRoundVideoView = null; + } + } + if (pipRoundVideoView != null) { + videoPlayer.setTextureView(pipRoundVideoView.getTextureView()); + } + } else if (currentTextureView != null) { + videoPlayer.setTextureView(currentTextureView); + } + videoPlayer.preparePlayer(Uri.fromFile(cacheFile), "other"); + videoPlayer.setStreamType(useFrontSpeaker ? AudioManager.STREAM_VOICE_CALL : AudioManager.STREAM_MUSIC); + videoPlayer.play(); + } else if (isOpusFile(cacheFile.getAbsolutePath()) == 1) { playlist.clear(); shuffledPlaylist.clear(); synchronized (playerObjectSync) { @@ -2359,7 +2734,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } } catch (Exception e) { FileLog.e(e); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioPlayStateChanged, playingMessageObject != null ? playingMessageObject.getId() : 0); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingPlayStateChanged, playingMessageObject != null ? playingMessageObject.getId() : 0); if (audioPlayer != null) { audioPlayer.release(); audioPlayer = null; @@ -2381,9 +2756,21 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, startRaiseToEarSensors(raiseChat); } startProgressTimer(playingMessageObject); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioDidStarted, messageObject); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingDidStarted, messageObject); - if (audioPlayer != null) { + if (videoPlayer != null) { + try { + if (playingMessageObject.audioProgress != 0) { + int seekTo = (int) (videoPlayer.getDuration() * playingMessageObject.audioProgress); + videoPlayer.seekTo(seekTo); + } + } catch (Exception e2) { + playingMessageObject.audioProgress = 0; + playingMessageObject.audioProgressSec = 0; + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingProgressDidChanged, playingMessageObject.getId(), 0); + FileLog.e(e2); + } + } else if (audioPlayer != null) { try { if (playingMessageObject.audioProgress != 0) { int seekTo = (int) (audioPlayer.getDuration() * playingMessageObject.audioProgress); @@ -2392,7 +2779,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } catch (Exception e2) { playingMessageObject.audioProgress = 0; playingMessageObject.audioProgressSec = 0; - NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioProgressDidChanged, playingMessageObject.getId(), 0); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingProgressDidChanged, playingMessageObject.getId(), 0); FileLog.e(e2); } } else if (audioTrackPlayer != null) { @@ -2432,7 +2819,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } public void stopAudio() { - if (audioTrackPlayer == null && audioPlayer == null || playingMessageObject == null) { + if (audioTrackPlayer == null && audioPlayer == null && videoPlayer == null || playingMessageObject == null) { return; } try { @@ -2446,6 +2833,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } else if (audioTrackPlayer != null) { audioTrackPlayer.pause(); audioTrackPlayer.flush(); + } else if (videoPlayer != null) { + videoPlayer.pause(); } } catch (Exception e) { FileLog.e(e); @@ -2459,6 +2848,18 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, audioTrackPlayer.release(); audioTrackPlayer = null; } + } else if (videoPlayer != null) { + currentAspectRatioFrameLayout = null; + currentTextureViewContainer = null; + currentAspectRatioFrameLayoutReady = false; + currentTextureView = null; + videoPlayer.releasePlayer(); + videoPlayer = null; + try { + baseActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } catch (Exception e) { + FileLog.e(e); + } } } catch (Exception e) { FileLog.e(e); @@ -2516,8 +2917,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, editor.commit(); } - public boolean pauseAudio(MessageObject messageObject) { - if (audioTrackPlayer == null && audioPlayer == null || messageObject == null || playingMessageObject == null || playingMessageObject != null && playingMessageObject.getId() != messageObject.getId()) { + public boolean pauseMessage(MessageObject messageObject) { + if (audioTrackPlayer == null && audioPlayer == null && videoPlayer == null || messageObject == null || playingMessageObject == null || playingMessageObject != null && playingMessageObject.getId() != messageObject.getId()) { return false; } stopProgressTimer(); @@ -2526,9 +2927,11 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, audioPlayer.pause(); } else if (audioTrackPlayer != null) { audioTrackPlayer.pause(); + } else if (videoPlayer != null) { + videoPlayer.pause(); } isPaused = true; - NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioPlayStateChanged, playingMessageObject.getId()); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingPlayStateChanged, playingMessageObject.getId()); } catch (Exception e) { FileLog.e(e); isPaused = false; @@ -2538,21 +2941,23 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } public boolean resumeAudio(MessageObject messageObject) { - if (audioTrackPlayer == null && audioPlayer == null || messageObject == null || playingMessageObject == null || playingMessageObject != null && playingMessageObject.getId() != messageObject.getId()) { + if (audioTrackPlayer == null && audioPlayer == null && videoPlayer == null || messageObject == null || playingMessageObject == null || playingMessageObject != null && playingMessageObject.getId() != messageObject.getId()) { return false; } try { - startProgressTimer(messageObject); + startProgressTimer(playingMessageObject); if (audioPlayer != null) { audioPlayer.start(); } else if (audioTrackPlayer != null) { audioTrackPlayer.play(); checkPlayerQueue(); + } else if (videoPlayer != null) { + videoPlayer.play(); } checkAudioFocus(messageObject); isPaused = false; - NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioPlayStateChanged, playingMessageObject.getId()); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagePlayingPlayStateChanged, playingMessageObject.getId()); } catch (Exception e) { FileLog.e(e); return false; @@ -2560,11 +2965,15 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, return true; } - public boolean isPlayingAudio(MessageObject messageObject) { - return !(audioTrackPlayer == null && audioPlayer == null || messageObject == null || playingMessageObject == null || playingMessageObject != null && (playingMessageObject.getId() != messageObject.getId() || downloadingCurrentMessage)); + public boolean isRoundVideoDrawingReady() { + return currentAspectRatioFrameLayout != null && currentAspectRatioFrameLayout.isDrawingReady(); } - public boolean isAudioPaused() { + public boolean isPlayingMessage(MessageObject messageObject) { + return !(audioTrackPlayer == null && audioPlayer == null && videoPlayer == null || messageObject == null || playingMessageObject == null || playingMessageObject != null && (playingMessageObject.eventId != messageObject.eventId || playingMessageObject.getId() != messageObject.getId() || downloadingCurrentMessage)); + } + + public boolean isMessagePaused() { return isPaused || downloadingCurrentMessage; } @@ -2574,9 +2983,9 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public void startRecording(final long dialog_id, final MessageObject reply_to_msg) { boolean paused = false; - if (playingMessageObject != null && isPlayingAudio(playingMessageObject) && !isAudioPaused()) { + if (playingMessageObject != null && isPlayingMessage(playingMessageObject) && !isMessagePaused()) { paused = true; - pauseAudio(playingMessageObject); + pauseMessage(playingMessageObject); } try { @@ -2724,7 +3133,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, audioToSend.size = (int) recordingAudioFileToSend.length(); TLRPC.TL_documentAttributeAudio attributeAudio = new TLRPC.TL_documentAttributeAudio(); attributeAudio.voice = true; - attributeAudio.waveform = getWaveform2(recordSamples, recordSamples.length); //getWaveform(recordingAudioFileToSend.getAbsolutePath()); + attributeAudio.waveform = getWaveform2(recordSamples, recordSamples.length); if (attributeAudio.waveform != null) { attributeAudio.flags |= 4; } @@ -2788,7 +3197,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - NotificationCenter.getInstance().postNotificationName(NotificationCenter.recordStopped); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.recordStopped, send == 2 ? 1 : 0); } }); } @@ -3169,18 +3578,20 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, Thread thread = new Thread(new Runnable() { @Override public void run() { - final ArrayList albumsSorted = new ArrayList<>(); - final ArrayList videoAlbumsSorted = new ArrayList<>(); - HashMap albums = new HashMap<>(); + final ArrayList mediaAlbumsSorted = new ArrayList<>(); + final ArrayList photoAlbumsSorted = new ArrayList<>(); + HashMap mediaAlbums = new HashMap<>(); + HashMap photoAlbums = new HashMap<>(); AlbumEntry allPhotosAlbum = null; + AlbumEntry allMediaAlbum = null; String cameraFolder = null; try { cameraFolder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + "/" + "Camera/"; } catch (Exception e) { FileLog.e(e); } - Integer cameraAlbumId = null; - Integer cameraAlbumVideoId = null; + Integer mediaCameraAlbumId = null; + Integer photoCameraAlbumId = null; Cursor cursor = null; try { @@ -3209,25 +3620,40 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, PhotoEntry photoEntry = new PhotoEntry(bucketId, imageId, dateTaken, path, orientation, false); if (allPhotosAlbum == null) { - allPhotosAlbum = new AlbumEntry(0, LocaleController.getString("AllPhotos", R.string.AllPhotos), photoEntry, false); - albumsSorted.add(0, allPhotosAlbum); + allPhotosAlbum = new AlbumEntry(0, LocaleController.getString("AllPhotos", R.string.AllPhotos), photoEntry); + photoAlbumsSorted.add(0, allPhotosAlbum); } - if (allPhotosAlbum != null) { - allPhotosAlbum.addPhoto(photoEntry); + if (allMediaAlbum == null) { + allMediaAlbum = new AlbumEntry(0, LocaleController.getString("AllMedia", R.string.AllMedia), photoEntry); + mediaAlbumsSorted.add(0, allMediaAlbum); } + allPhotosAlbum.addPhoto(photoEntry); + allMediaAlbum.addPhoto(photoEntry); - AlbumEntry albumEntry = albums.get(bucketId); + AlbumEntry albumEntry = mediaAlbums.get(bucketId); if (albumEntry == null) { - albumEntry = new AlbumEntry(bucketId, bucketName, photoEntry, false); - albums.put(bucketId, albumEntry); - if (cameraAlbumId == null && cameraFolder != null && path != null && path.startsWith(cameraFolder)) { - albumsSorted.add(0, albumEntry); - cameraAlbumId = bucketId; + albumEntry = new AlbumEntry(bucketId, bucketName, photoEntry); + mediaAlbums.put(bucketId, albumEntry); + if (mediaCameraAlbumId == null && cameraFolder != null && path != null && path.startsWith(cameraFolder)) { + mediaAlbumsSorted.add(0, albumEntry); + mediaCameraAlbumId = bucketId; } else { - albumsSorted.add(albumEntry); + mediaAlbumsSorted.add(albumEntry); } } + albumEntry.addPhoto(photoEntry); + albumEntry = photoAlbums.get(bucketId); + if (albumEntry == null) { + albumEntry = new AlbumEntry(bucketId, bucketName, photoEntry); + photoAlbums.put(bucketId, albumEntry); + if (photoCameraAlbumId == null && cameraFolder != null && path != null && path.startsWith(cameraFolder)) { + photoAlbumsSorted.add(0, albumEntry); + photoCameraAlbumId = bucketId; + } else { + photoAlbumsSorted.add(albumEntry); + } + } albumEntry.addPhoto(photoEntry); } } @@ -3246,8 +3672,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, try { if (Build.VERSION.SDK_INT < 23 || Build.VERSION.SDK_INT >= 23 && ApplicationLoader.applicationContext.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { - albums.clear(); - AlbumEntry allVideosAlbum = null; cursor = MediaStore.Images.Media.query(ApplicationLoader.applicationContext.getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projectionVideo, null, null, MediaStore.Video.Media.DATE_TAKEN + " DESC"); if (cursor != null) { int imageIdColumn = cursor.getColumnIndex(MediaStore.Video.Media._ID); @@ -3255,6 +3679,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, int bucketNameColumn = cursor.getColumnIndex(MediaStore.Video.Media.BUCKET_DISPLAY_NAME); int dataColumn = cursor.getColumnIndex(MediaStore.Video.Media.DATA); int dateColumn = cursor.getColumnIndex(MediaStore.Video.Media.DATE_TAKEN); + int durationColumn = cursor.getColumnIndex(MediaStore.Video.Media.DURATION); while (cursor.moveToNext()) { int imageId = cursor.getInt(imageIdColumn); @@ -3262,30 +3687,30 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, String bucketName = cursor.getString(bucketNameColumn); String path = cursor.getString(dataColumn); long dateTaken = cursor.getLong(dateColumn); + long duration = cursor.getLong(durationColumn); + if (path == null || path.length() == 0) { continue; } - PhotoEntry photoEntry = new PhotoEntry(bucketId, imageId, dateTaken, path, 0, true); + PhotoEntry photoEntry = new PhotoEntry(bucketId, imageId, dateTaken, path, (int) (duration / 1000), true); - if (allVideosAlbum == null) { - allVideosAlbum = new AlbumEntry(0, LocaleController.getString("AllVideo", R.string.AllVideo), photoEntry, true); - videoAlbumsSorted.add(0, allVideosAlbum); - } - if (allVideosAlbum != null) { - allVideosAlbum.addPhoto(photoEntry); + if (allMediaAlbum == null) { + allMediaAlbum = new AlbumEntry(0, LocaleController.getString("AllMedia", R.string.AllMedia), photoEntry); + mediaAlbumsSorted.add(0, allMediaAlbum); } + allMediaAlbum.addPhoto(photoEntry); - AlbumEntry albumEntry = albums.get(bucketId); + AlbumEntry albumEntry = mediaAlbums.get(bucketId); if (albumEntry == null) { - albumEntry = new AlbumEntry(bucketId, bucketName, photoEntry, true); - albums.put(bucketId, albumEntry); - if (cameraAlbumVideoId == null && cameraFolder != null && path != null && path.startsWith(cameraFolder)) { - videoAlbumsSorted.add(0, albumEntry); - cameraAlbumVideoId = bucketId; + albumEntry = new AlbumEntry(bucketId, bucketName, photoEntry); + mediaAlbums.put(bucketId, albumEntry); + if (mediaCameraAlbumId == null && cameraFolder != null && path != null && path.startsWith(cameraFolder)) { + mediaAlbumsSorted.add(0, albumEntry); + mediaCameraAlbumId = bucketId; } else { - videoAlbumsSorted.add(albumEntry); + mediaAlbumsSorted.add(albumEntry); } } @@ -3304,14 +3729,27 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } } } - broadcastNewPhotos(guid, albumsSorted, cameraAlbumId, videoAlbumsSorted, cameraAlbumVideoId, allPhotosAlbum, 0); + for (int a = 0; a < mediaAlbumsSorted.size(); a++) { + Collections.sort(mediaAlbumsSorted.get(a).photos, new Comparator() { + @Override + public int compare(PhotoEntry o1, PhotoEntry o2) { + if (o1.dateTaken < o2.dateTaken) { + return 1; + } else if (o1.dateTaken > o2.dateTaken) { + return -1; + } + return 0; + } + }); + } + broadcastNewPhotos(guid, mediaAlbumsSorted, photoAlbumsSorted, mediaCameraAlbumId, allMediaAlbum, allPhotosAlbum, 0); } }); thread.setPriority(Thread.MIN_PRIORITY); thread.start(); } - private static void broadcastNewPhotos(final int guid, final ArrayList albumsSorted, final Integer cameraAlbumIdFinal, final ArrayList videoAlbumsSorted, final Integer cameraAlbumVideoIdFinal, final AlbumEntry allPhotosAlbumFinal, int delay) { + private static void broadcastNewPhotos(final int guid, final ArrayList mediaAlbumsSorted, final ArrayList photoAlbumsSorted, final Integer cameraAlbumIdFinal, final AlbumEntry allMediaAlbumFinal, final AlbumEntry allPhotosAlbumFinal, int delay) { if (broadcastPhotosRunnable != null) { AndroidUtilities.cancelRunOnUIThread(broadcastPhotosRunnable); } @@ -3319,12 +3757,13 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, @Override public void run() { if (PhotoViewer.getInstance().isVisible()) { - broadcastNewPhotos(guid, albumsSorted, cameraAlbumIdFinal, videoAlbumsSorted, cameraAlbumVideoIdFinal, allPhotosAlbumFinal, 1000); + broadcastNewPhotos(guid, mediaAlbumsSorted, photoAlbumsSorted, cameraAlbumIdFinal, allMediaAlbumFinal, allPhotosAlbumFinal, 1000); return; } broadcastPhotosRunnable = null; allPhotosAlbumEntry = allPhotosAlbumFinal; - NotificationCenter.getInstance().postNotificationName(NotificationCenter.albumsDidLoaded, guid, albumsSorted, cameraAlbumIdFinal, videoAlbumsSorted, cameraAlbumVideoIdFinal); + allMediaAlbumEntry = allMediaAlbumFinal; + NotificationCenter.getInstance().postNotificationName(NotificationCenter.albumsDidLoaded, guid, mediaAlbumsSorted, photoAlbumsSorted, cameraAlbumIdFinal); } }, delay); } @@ -3690,11 +4129,11 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, movie.setSize(resultWidth, resultHeight); mediaMuxer = new MP4Builder().createMovie(movie); extractor = new MediaExtractor(); - extractor.setDataSource(inputFile.toString()); + extractor.setDataSource(videoPath); checkConversionCanceled(); - if (resultWidth != originalWidth || resultHeight != originalHeight || rotateRender != 0) { + if (resultWidth != originalWidth || resultHeight != originalHeight || rotateRender != 0 || messageObject.videoEditedInfo.roundVideo) { int videoIndex; videoIndex = selectTrack(extractor, false); if (videoIndex >= 0) { @@ -4048,7 +4487,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } if (mediaMuxer != null) { try { - mediaMuxer.finishMovie(false); + mediaMuxer.finishMovie(); } catch (Exception e) { FileLog.e(e); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java index 9e514bdf5..b27f6208a 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java @@ -9,6 +9,7 @@ package org.telegram.messenger; import android.graphics.Typeface; +import android.os.Build; import android.text.Layout; import android.text.Spannable; import android.text.SpannableString; @@ -37,6 +38,7 @@ import java.util.AbstractMap; import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -52,11 +54,14 @@ public class MessageObject { public CharSequence caption; public MessageObject replyMessageObject; public int type = 1000; + private int isRoundVideoCached; + public long eventId; public int contentType; public String dateKey; public String monthKey; public boolean deleted; public float audioProgress; + public float gifState; public int audioProgressSec; public boolean isDateObject; public ArrayList photoThumbs; @@ -106,9 +111,14 @@ public class MessageObject { } public MessageObject(TLRPC.Message message, AbstractMap users, AbstractMap chats, boolean generateLayout) { + this(message, users, chats, generateLayout, 0); + } + + public MessageObject(TLRPC.Message message, AbstractMap users, AbstractMap chats, boolean generateLayout, long eid) { Theme.createChatResources(null, true); messageOwner = message; + eventId = eid; if (message.replyMessage != null) { replyMessageObject = new MessageObject(message.replyMessage, users, chats, false); @@ -380,6 +390,8 @@ public class MessageObject { messageText = LocaleController.getString("AttachVideo", R.string.AttachVideo); } else if (isVoice()) { messageText = LocaleController.getString("AttachAudio", R.string.AttachAudio); + } else if (isRoundVideo()) { + messageText = LocaleController.getString("AttachRound", R.string.AttachRound); } else if (message.media instanceof TLRPC.TL_messageMediaGeo || message.media instanceof TLRPC.TL_messageMediaVenue) { messageText = LocaleController.getString("AttachLocation", R.string.AttachLocation); } else if (message.media instanceof TLRPC.TL_messageMediaContact) { @@ -429,10 +441,12 @@ public class MessageObject { dateKey = String.format("%d_%02d_%02d", dateYear, dateMonth, dateDay); monthKey = String.format("%d_%02d", dateYear, dateMonth); - if (messageOwner.message != null && messageOwner.id < 0 && messageOwner.message.length() > 6 && (isVideo() || isNewGif())) { + if (messageOwner.message != null && messageOwner.id < 0 && messageOwner.message.length() > 6 && (isVideo() || isNewGif() || isRoundVideo())) { videoEditedInfo = new VideoEditedInfo(); if (!videoEditedInfo.parseString(messageOwner.message)) { videoEditedInfo = null; + } else { + videoEditedInfo.roundVideo = isRoundVideo(); } } @@ -478,6 +492,496 @@ public class MessageObject { checkMediaExistance(); } + public MessageObject(TLRPC.TL_channelAdminLogEvent event, ArrayList messageObjects, HashMap> messagesByDays, TLRPC.Chat chat, int[] mid) { + TLRPC.User fromUser = null; + if (event.user_id > 0) { + if (fromUser == null) { + fromUser = MessagesController.getInstance().getUser(event.user_id); + } + } + + Calendar rightNow = new GregorianCalendar(); + rightNow.setTimeInMillis((long) (event.date) * 1000); + int dateDay = rightNow.get(Calendar.DAY_OF_YEAR); + int dateYear = rightNow.get(Calendar.YEAR); + int dateMonth = rightNow.get(Calendar.MONTH); + dateKey = String.format("%d_%02d_%02d", dateYear, dateMonth, dateDay); + monthKey = String.format("%d_%02d", dateYear, dateMonth); + + ArrayList dayArray = messagesByDays.get(dateKey); + + TLRPC.Peer to_id = new TLRPC.TL_peerChannel(); + to_id.channel_id = chat.id; + + if (dayArray == null) { + dayArray = new ArrayList<>(); + messagesByDays.put(dateKey, dayArray); + TLRPC.Message dateMsg = new TLRPC.Message(); + dateMsg.message = LocaleController.formatDateChat(event.date); + dateMsg.id = 0; + dateMsg.date = event.date; + MessageObject dateObj = new MessageObject(dateMsg, null, false); + dateObj.type = 10; + dateObj.contentType = 1; + dateObj.isDateObject = true; + messageObjects.add(dateObj); + } + + TLRPC.Message message = null; + if (event.action instanceof TLRPC.TL_channelAdminLogEventActionChangeTitle) { + String title = ((TLRPC.TL_channelAdminLogEventActionChangeTitle) event.action).new_value; + if (chat.megagroup) { + messageText = replaceWithLink(LocaleController.formatString("EventLogEditedGroupTitle", R.string.EventLogEditedGroupTitle, title), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.formatString("EventLogEditedChannelTitle", R.string.EventLogEditedChannelTitle, title), "un1", fromUser); + } + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionChangePhoto) { + messageOwner = new TLRPC.TL_messageService(); + if (event.action.new_photo instanceof TLRPC.TL_chatPhotoEmpty) { + messageOwner.action = new TLRPC.TL_messageActionChatDeletePhoto(); + if (chat.megagroup) { + messageText = replaceWithLink(LocaleController.getString("EventLogRemovedWGroupPhoto", R.string.EventLogRemovedWGroupPhoto), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogRemovedChannelPhoto", R.string.EventLogRemovedChannelPhoto), "un1", fromUser); + } + } else { + messageOwner.action = new TLRPC.TL_messageActionChatEditPhoto(); + messageOwner.action.photo = new TLRPC.TL_photo(); + TLRPC.TL_photoSize photoSize = new TLRPC.TL_photoSize(); + photoSize.location = event.action.new_photo.photo_small; + photoSize.type = "s"; + photoSize.w = photoSize.h = 80; + messageOwner.action.photo.sizes.add(photoSize); + photoSize = new TLRPC.TL_photoSize(); + photoSize.location = event.action.new_photo.photo_big; + photoSize.type = "m"; + photoSize.w = photoSize.h = 640; + messageOwner.action.photo.sizes.add(photoSize); + + if (chat.megagroup) { + messageText = replaceWithLink(LocaleController.getString("EventLogEditedGroupPhoto", R.string.EventLogEditedGroupPhoto), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogEditedChannelPhoto", R.string.EventLogEditedChannelPhoto), "un1", fromUser); + } + } + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionParticipantJoin) { + if (chat.megagroup) { + messageText = replaceWithLink(LocaleController.getString("EventLogGroupJoined", R.string.EventLogGroupJoined), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogChannelJoined", R.string.EventLogChannelJoined), "un1", fromUser); + } + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionParticipantLeave) { + messageOwner = new TLRPC.TL_messageService(); + messageOwner.action = new TLRPC.TL_messageActionChatDeleteUser(); + messageOwner.action.user_id = event.user_id; + if (chat.megagroup) { + messageText = replaceWithLink(LocaleController.getString("EventLogLeftGroup", R.string.EventLogLeftGroup), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogLeftChannel", R.string.EventLogLeftChannel), "un1", fromUser); + } + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionParticipantInvite) { + messageOwner = new TLRPC.TL_messageService(); + messageOwner.action = new TLRPC.TL_messageActionChatAddUser(); + TLRPC.User whoUser = MessagesController.getInstance().getUser(event.action.participant.user_id); + if (event.action.participant.user_id == messageOwner.from_id) { + if (chat.megagroup) { + messageText = replaceWithLink(LocaleController.getString("EventLogGroupJoined", R.string.EventLogGroupJoined), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogChannelJoined", R.string.EventLogChannelJoined), "un1", fromUser); + } + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogAdded", R.string.EventLogAdded), "un2", whoUser); + messageText = replaceWithLink(messageText, "un1", fromUser); + } + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionParticipantToggleAdmin) { + messageOwner = new TLRPC.TL_message(); + + TLRPC.User whoUser = MessagesController.getInstance().getUser(event.action.prev_participant.user_id); + String str = LocaleController.getString("EventLogPromoted", R.string.EventLogPromoted); + int offset = str.indexOf("%1$s"); + StringBuilder rights = new StringBuilder(String.format(str, getUserName(whoUser, messageOwner.entities, offset))); + rights.append("\n"); + + TLRPC.TL_channelAdminRights o = event.action.prev_participant.admin_rights; + TLRPC.TL_channelAdminRights n = event.action.new_participant.admin_rights; + if (o == null) { + o = new TLRPC.TL_channelAdminRights(); + } + if (n == null) { + n = new TLRPC.TL_channelAdminRights(); + } + if (o.change_info != n.change_info) { + rights.append('\n').append(n.change_info ? '+' : '-').append(' '); + rights.append(chat.megagroup ? LocaleController.getString("EventLogPromotedChangeGroupInfo", R.string.EventLogPromotedChangeGroupInfo) : LocaleController.getString("EventLogPromotedChangeChannelInfo", R.string.EventLogPromotedChangeChannelInfo)); + } + if (!chat.megagroup) { + if (o.post_messages != n.post_messages) { + rights.append('\n').append(n.post_messages ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogPromotedPostMessages", R.string.EventLogPromotedPostMessages)); + } + if (o.edit_messages != n.edit_messages) { + rights.append('\n').append(n.edit_messages ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogPromotedEditMessages", R.string.EventLogPromotedEditMessages)); + } + } + if (o.delete_messages != n.delete_messages) { + rights.append('\n').append(n.delete_messages ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogPromotedDeleteMessages", R.string.EventLogPromotedDeleteMessages)); + } + if (o.add_admins != n.add_admins) { + rights.append('\n').append(n.add_admins ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogPromotedAddAdmins", R.string.EventLogPromotedAddAdmins)); + } + if (chat.megagroup) { + if (o.ban_users != n.ban_users) { + rights.append('\n').append(n.ban_users ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogPromotedBanUsers", R.string.EventLogPromotedBanUsers)); + } + } + if (o.invite_users != n.invite_users) { + rights.append('\n').append(n.invite_users ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogPromotedAddUsers", R.string.EventLogPromotedAddUsers)); + } + if (chat.megagroup) { + if (o.pin_messages != n.pin_messages) { + rights.append('\n').append(n.pin_messages ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogPromotedPinMessages", R.string.EventLogPromotedPinMessages)); + } + } + messageText = rights.toString(); + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionParticipantToggleBan) { + messageOwner = new TLRPC.TL_message(); + TLRPC.User whoUser = MessagesController.getInstance().getUser(event.action.prev_participant.user_id); + TLRPC.TL_channelBannedRights o = event.action.prev_participant.banned_rights; + TLRPC.TL_channelBannedRights n = event.action.new_participant.banned_rights; + if (chat.megagroup && (n == null || !n.view_messages || n != null && o != null && n.until_date != o.until_date)) { + StringBuilder rights; + String bannedDuration; + if (n != null && !AndroidUtilities.isBannedForever(n.until_date)) { + bannedDuration = ""; + int duration = n.until_date - event.date; + int days = duration / 60 / 60 / 24; + duration -= days * 60 * 60 * 24; + int hours = duration / 60 / 60; + duration -= hours * 60 * 60; + int minutes = duration / 60; + int count = 0; + for (int a = 0; a < 3; a++) { + String addStr = null; + if (a == 0) { + if (days != 0) { + addStr = LocaleController.formatPluralString("Days", days); + count++; + } + } else if (a == 1) { + if (hours != 0) { + addStr = LocaleController.formatPluralString("Hours", hours); + count++; + } + } else { + if (minutes != 0) { + addStr = LocaleController.formatPluralString("Minutes", minutes); + count++; + } + } + if (addStr != null) { + if (bannedDuration.length() > 0) { + bannedDuration += ", "; + } + bannedDuration += addStr; + } + if (count == 2) { + break; + } + } + } else { + bannedDuration = LocaleController.getString("UserRestrictionsUntilForever", R.string.UserRestrictionsUntilForever); + } + String str = LocaleController.getString("EventLogRestrictedUntil", R.string.EventLogRestrictedUntil); + int offset = str.indexOf("%1$s"); + rights = new StringBuilder(String.format(str, getUserName(whoUser, messageOwner.entities, offset), bannedDuration)); + boolean added = false; + if (o == null) { + o = new TLRPC.TL_channelBannedRights(); + } + if (n == null) { + n = new TLRPC.TL_channelBannedRights(); + } + if (o.view_messages != n.view_messages) { + if (!added) { + rights.append('\n'); + added = true; + } + rights.append('\n').append(!n.view_messages ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogRestrictedReadMessages", R.string.EventLogRestrictedReadMessages)); + } + if (o.send_messages != n.send_messages) { + if (!added) { + rights.append('\n'); + added = true; + } + rights.append('\n').append(!n.send_messages ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogRestrictedSendMessages", R.string.EventLogRestrictedSendMessages)); + } + if (o.send_stickers != n.send_stickers || o.send_inline != n.send_inline || o.send_gifs != n.send_gifs || o.send_games != n.send_games) { + if (!added) { + rights.append('\n'); + added = true; + } + rights.append('\n').append(!n.send_stickers ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogRestrictedSendStickers", R.string.EventLogRestrictedSendStickers)); + } + if (o.send_media != n.send_media) { + if (!added) { + rights.append('\n'); + added = true; + } + rights.append('\n').append(!n.send_media ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogRestrictedSendMedia", R.string.EventLogRestrictedSendMedia)); + } + if (o.embed_links != n.embed_links) { + if (!added) { + rights.append('\n'); + } + rights.append('\n').append(!n.embed_links ? '+' : '-').append(' '); + rights.append(LocaleController.getString("EventLogRestrictedSendEmbed", R.string.EventLogRestrictedSendEmbed)); + } + messageText = rights.toString(); + } else { + String str; + if (n != null && (o == null || n.view_messages)) { + str = LocaleController.getString("EventLogChannelRestricted", R.string.EventLogChannelRestricted); + } else { + str = LocaleController.getString("EventLogChannelUnrestricted", R.string.EventLogChannelUnrestricted); + } + int offset = str.indexOf("%1$s"); + messageText = String.format(str, getUserName(whoUser, messageOwner.entities, offset)); + } + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionUpdatePinned) { + if (event.action.message instanceof TLRPC.TL_messageEmpty) { + messageText = replaceWithLink(LocaleController.getString("EventLogUnpinnedMessages", R.string.EventLogUnpinnedMessages), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogPinnedMessages", R.string.EventLogPinnedMessages), "un1", fromUser); + } + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionToggleSignatures) { + if (((TLRPC.TL_channelAdminLogEventActionToggleSignatures) event.action).new_value) { + messageText = replaceWithLink(LocaleController.getString("EventLogToggledSignaturesOn", R.string.EventLogToggledSignaturesOn), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogToggledSignaturesOff", R.string.EventLogToggledSignaturesOff), "un1", fromUser); + } + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionToggleInvites) { + if (((TLRPC.TL_channelAdminLogEventActionToggleInvites) event.action).new_value) { + messageText = replaceWithLink(LocaleController.getString("EventLogToggledInvitesOn", R.string.EventLogToggledInvitesOn), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogToggledInvitesOff", R.string.EventLogToggledInvitesOff), "un1", fromUser); + } + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionDeleteMessage) { + messageText = replaceWithLink(LocaleController.getString("EventLogDeletedMessages", R.string.EventLogDeletedMessages), "un1", fromUser); + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionChangeAbout) { + messageText = replaceWithLink(chat.megagroup ? LocaleController.getString("EventLogEditedGroupDescription", R.string.EventLogEditedGroupDescription) : LocaleController.getString("EventLogEditedChannelDescription", R.string.EventLogEditedChannelDescription), "un1", fromUser); + message = new TLRPC.TL_message(); + message.out = false; + message.unread = false; + message.from_id = event.user_id; + message.to_id = to_id; + message.date = event.date; + message.message = ((TLRPC.TL_channelAdminLogEventActionChangeAbout) event.action).new_value; + if (!TextUtils.isEmpty(((TLRPC.TL_channelAdminLogEventActionChangeAbout) event.action).prev_value)) { + message.media = new TLRPC.TL_messageMediaWebPage(); + message.media.webpage = new TLRPC.TL_webPage(); + message.media.webpage.flags = 10; + message.media.webpage.display_url = ""; + message.media.webpage.url = ""; + message.media.webpage.site_name = LocaleController.getString("EventLogPreviousGroupDescription", R.string.EventLogPreviousGroupDescription); + message.media.webpage.description = ((TLRPC.TL_channelAdminLogEventActionChangeAbout) event.action).prev_value; + } else { + message.media = new TLRPC.TL_messageMediaEmpty(); + } + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionChangeUsername) { + String newLink = ((TLRPC.TL_channelAdminLogEventActionChangeUsername) event.action).new_value; + if (!TextUtils.isEmpty(newLink)) { + messageText = replaceWithLink(chat.megagroup ? LocaleController.getString("EventLogChangedGroupLink", R.string.EventLogChangedGroupLink) : LocaleController.getString("EventLogChangedChannelLink", R.string.EventLogChangedChannelLink), "un1", fromUser); + } else { + messageText = replaceWithLink(chat.megagroup ? LocaleController.getString("EventLogRemovedGroupLink", R.string.EventLogRemovedGroupLink) : LocaleController.getString("EventLogRemovedChannelLink", R.string.EventLogRemovedChannelLink), "un1", fromUser); + } + message = new TLRPC.TL_message(); + message.out = false; + message.unread = false; + message.from_id = event.user_id; + message.to_id = to_id; + message.date = event.date; + if (!TextUtils.isEmpty(newLink)) { + message.message = "https://" + MessagesController.getInstance().linkPrefix + "/" + newLink; + } else { + message.message = ""; + } + TLRPC.TL_messageEntityUrl url = new TLRPC.TL_messageEntityUrl(); + url.offset = 0; + url.length = message.message.length(); + message.entities.add(url); + if (!TextUtils.isEmpty(((TLRPC.TL_channelAdminLogEventActionChangeUsername) event.action).prev_value)) { + message.media = new TLRPC.TL_messageMediaWebPage(); + message.media.webpage = new TLRPC.TL_webPage(); + message.media.webpage.flags = 10; + message.media.webpage.display_url = ""; + message.media.webpage.url = ""; + message.media.webpage.site_name = LocaleController.getString("EventLogPreviousLink", R.string.EventLogPreviousLink); + message.media.webpage.description = "https://" + MessagesController.getInstance().linkPrefix + "/" + ((TLRPC.TL_channelAdminLogEventActionChangeUsername) event.action).prev_value; + } else { + message.media = new TLRPC.TL_messageMediaEmpty(); + } + } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionEditMessage) { + message = new TLRPC.TL_message(); + message.out = false; + message.unread = false; + message.from_id = event.user_id; + message.to_id = to_id; + message.date = event.date; + TLRPC.Message newMessage = ((TLRPC.TL_channelAdminLogEventActionEditMessage) event.action).new_message; + TLRPC.Message oldMessage = ((TLRPC.TL_channelAdminLogEventActionEditMessage) event.action).prev_message; + if (newMessage.media != null && !(newMessage.media instanceof TLRPC.TL_messageMediaEmpty) && !(newMessage.media instanceof TLRPC.TL_messageMediaWebPage) && TextUtils.isEmpty(newMessage.message)) { + messageText = replaceWithLink(LocaleController.getString("EventLogEditedCaption", R.string.EventLogEditedCaption), "un1", fromUser); + message.media = newMessage.media; + message.media.webpage = new TLRPC.TL_webPage(); + message.media.webpage.site_name = LocaleController.getString("EventLogOriginalCaption", R.string.EventLogOriginalCaption); + if (TextUtils.isEmpty(oldMessage.media.caption)) { + message.media.webpage.description = LocaleController.getString("EventLogOriginalCaptionEmpty", R.string.EventLogOriginalCaptionEmpty); + } else { + message.media.webpage.description = oldMessage.media.caption; + } + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogEditedMessages", R.string.EventLogEditedMessages), "un1", fromUser); + message.message = newMessage.message; + message.media = new TLRPC.TL_messageMediaWebPage(); + message.media.webpage = new TLRPC.TL_webPage(); + message.media.webpage.site_name = LocaleController.getString("EventLogOriginalMessages", R.string.EventLogOriginalMessages); + message.media.webpage.description = oldMessage.message; + } + message.reply_markup = newMessage.reply_markup; + message.media.webpage.flags = 10; + message.media.webpage.display_url = ""; + message.media.webpage.url = ""; + } else { + messageText = "unsupported " + event.action; + } + if (messageOwner == null) { + messageOwner = new TLRPC.TL_messageService(); + } + messageOwner.message = messageText.toString(); + messageOwner.from_id = event.user_id; + messageOwner.date = event.date; + messageOwner.id = mid[0]++; + eventId = event.id; + //messageOwner.out = event.user_id == UserConfig.getClientUserId(); + messageOwner.out = false; + messageOwner.to_id = new TLRPC.TL_peerChannel(); + messageOwner.to_id.channel_id = chat.id; + messageOwner.unread = false; + if (chat.megagroup) { + messageOwner.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; + } + + if (event.action.message != null && !(event.action.message instanceof TLRPC.TL_messageEmpty)) { + message = event.action.message; + } + + if (message != null) { + message.out = false; + message.id = mid[0]++; + message.reply_to_msg_id = 0; + message.flags = message.flags &~ TLRPC.MESSAGE_FLAG_EDITED; + if (chat.megagroup) { + message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; + } + MessageObject messageObject = new MessageObject(message, null, null, true, eventId); + messageObjects.add(messageObjects.size() - 1, messageObject); + } + messageObjects.add(messageObjects.size() - 1, this); + + if (messageText == null) { + messageText = ""; + } + + setType(); + measureInlineBotButtons(); + + if (messageOwner.message != null && messageOwner.id < 0 && messageOwner.message.length() > 6 && (isVideo() || isNewGif() || isRoundVideo())) { + videoEditedInfo = new VideoEditedInfo(); + if (!videoEditedInfo.parseString(messageOwner.message)) { + videoEditedInfo = null; + } else { + videoEditedInfo.roundVideo = isRoundVideo(); + } + } + + generateCaption(); + + TextPaint paint; + if (messageOwner.media instanceof TLRPC.TL_messageMediaGame) { + paint = Theme.chat_msgGameTextPaint; + } else { + paint = Theme.chat_msgTextPaint; + } + int[] emojiOnly = MessagesController.getInstance().allowBigEmoji ? new int[1] : null; + messageText = Emoji.replaceEmoji(messageText, paint.getFontMetricsInt(), AndroidUtilities.dp(20), false, emojiOnly); + if (emojiOnly != null && emojiOnly[0] >= 1 && emojiOnly[0] <= 3) { + TextPaint emojiPaint; + int size; + switch (emojiOnly[0]) { + case 1: + emojiPaint = Theme.chat_msgTextPaintOneEmoji; + size = AndroidUtilities.dp(32); + break; + case 2: + emojiPaint = Theme.chat_msgTextPaintTwoEmoji; + size = AndroidUtilities.dp(28); + break; + case 3: + default: + emojiPaint = Theme.chat_msgTextPaintThreeEmoji; + size = AndroidUtilities.dp(24); + break; + } + Emoji.EmojiSpan[] spans = ((Spannable) messageText).getSpans(0, messageText.length(), Emoji.EmojiSpan.class); + if (spans != null && spans.length > 0) { + for (int a = 0; a < spans.length; a++) { + spans[a].replaceFontMetrics(emojiPaint.getFontMetricsInt(), size); + } + } + } + generateLayout(fromUser); + layoutCreated = true; + generateThumbs(false); + checkMediaExistance(); + } + + private String getUserName(TLRPC.User user, ArrayList entities, int offset) { + String name; + if (user == null) { + name = ""; + } else { + name = ContactsController.formatName(user.first_name, user.last_name); + } + if (offset >= 0) { + TLRPC.TL_messageEntityMentionName entity = new TLRPC.TL_messageEntityMentionName(); + entity.user_id = user.id; + entity.offset = offset; + entity.length = name.length(); + entities.add(entity); + } + if (!TextUtils.isEmpty(user.username)) { + if (offset >= 0) { + TLRPC.TL_messageEntityMentionName entity = new TLRPC.TL_messageEntityMentionName(); + entity.user_id = user.id; + entity.offset = offset + name.length() + 2; + entity.length = user.username.length() + 1; + entities.add(entity); + } + return String.format("%1$s (@%2$s)", name, user.username); + } + return name; + } + public void applyNewText() { if (TextUtils.isEmpty(messageOwner.message)) { return; @@ -560,6 +1064,8 @@ public class MessageObject { messageText = replaceWithLink(LocaleController.getString("ActionPinnedGif", R.string.ActionPinnedGif), "un1", fromUser != null ? fromUser : chat); } else if (replyMessageObject.isVoice()) { messageText = replaceWithLink(LocaleController.getString("ActionPinnedVoice", R.string.ActionPinnedVoice), "un1", fromUser != null ? fromUser : chat); + } else if (replyMessageObject.isRoundVideo()) { + messageText = replaceWithLink(LocaleController.getString("ActionPinnedRound", R.string.ActionPinnedRound), "un1", fromUser != null ? fromUser : chat); } else if (replyMessageObject.isSticker()) { messageText = replaceWithLink(LocaleController.getString("ActionPinnedSticker", R.string.ActionPinnedSticker), "un1", fromUser != null ? fromUser : chat); } else if (replyMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaDocument) { @@ -612,7 +1118,12 @@ public class MessageObject { } StaticLayout staticLayout = new StaticLayout(text, Theme.chat_msgBotButtonPaint, AndroidUtilities.dp(2000), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); if (staticLayout.getLineCount() > 0) { - maxButtonSize = Math.max(maxButtonSize, (int) Math.ceil(staticLayout.getLineWidth(0) - staticLayout.getLineLeft(0)) + AndroidUtilities.dp(4)); + float width = staticLayout.getLineWidth(0); + float left = staticLayout.getLineLeft(0); + if (left < width) { + width -= left; + } + maxButtonSize = Math.max(maxButtonSize, (int) Math.ceil(width) + AndroidUtilities.dp(4)); } } wantedBotKeyboardWidth = Math.max(wantedBotKeyboardWidth, (maxButtonSize + AndroidUtilities.dp(12)) * size + AndroidUtilities.dp(5) * (size - 1)); @@ -624,13 +1135,15 @@ public class MessageObject { if (messageOwner instanceof TLRPC.TL_message || messageOwner instanceof TLRPC.TL_messageForwarded_old2) { if (isMediaEmpty()) { type = 0; - if (messageText == null || messageText.length() == 0) { + if (TextUtils.isEmpty(messageText) && eventId == 0) { messageText = "Empty message"; } } else if (messageOwner.media instanceof TLRPC.TL_messageMediaPhoto) { type = 1; } else if (messageOwner.media instanceof TLRPC.TL_messageMediaGeo || messageOwner.media instanceof TLRPC.TL_messageMediaVenue) { type = 4; + } else if (isRoundVideo()) { + type = 5; } else if (isVideo()) { type = 3; } else if (isVoice()) { @@ -726,6 +1239,12 @@ public class MessageObject { } } else if (messageOwner.media instanceof TLRPC.TL_messageMediaPhoto) { return "image/jpeg"; + } else if (messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { + if (messageOwner.media.webpage.document != null) { + return messageOwner.media.document.mime_type; + } else if (messageOwner.media.webpage.photo != null) { + return "image/jpeg"; + } } return ""; } @@ -734,21 +1253,23 @@ public class MessageObject { return document != null && document.thumb != null && document.mime_type != null && (document.mime_type.equals("image/gif") || isNewGifDocument(document)); } - public static boolean isVoiceVideoDocument(TLRPC.Document document) { + public static boolean isRoundVideoDocument(TLRPC.Document document) { + if (Build.VERSION.SDK_INT < 16) { + return false; + } if (document != null && document.mime_type != null && document.mime_type.equals("video/mp4")) { int width = 0; int height = 0; - boolean animated = false; + boolean round = false; for (int a = 0; a < document.attributes.size(); a++) { TLRPC.DocumentAttribute attribute = document.attributes.get(a); - if (attribute instanceof TLRPC.TL_documentAttributeAnimated) { - animated = true; - } else if (attribute instanceof TLRPC.TL_documentAttributeVideo) { + if (attribute instanceof TLRPC.TL_documentAttributeVideo) { width = attribute.w; height = attribute.w; + round = attribute.round_message; } } - if (animated && width <= 1280 && height <= 1280) { + if (round && width <= 1280 && height <= 1280) { return true; } } @@ -943,6 +1464,7 @@ public class MessageObject { name = ""; id = "0"; } + name = name.replace('\n', ' '); SpannableStringBuilder builder = new SpannableStringBuilder(TextUtils.replace(source, new String[]{param}, new String[]{name})); builder.setSpan(new URLSpanNoUnderlineBold("" + id), start, start + name.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return builder; @@ -1173,6 +1695,7 @@ public class MessageObject { } boolean useManualParse = !hasEntities && ( + eventId != 0 || messageOwner instanceof TLRPC.TL_message_old || messageOwner instanceof TLRPC.TL_message_old2 || messageOwner instanceof TLRPC.TL_message_old3 || @@ -1207,16 +1730,23 @@ public class MessageObject { } else if (entity.offset + entity.length > messageOwner.message.length()) { entity.length = messageOwner.message.length() - entity.offset; } - if (spans != null && spans.length > 0) { - for (int b = 0; b < spans.length; b++) { - if (spans[b] == null) { - continue; - } - int start = spannable.getSpanStart(spans[b]); - int end = spannable.getSpanEnd(spans[b]); - if (entity.offset <= start && entity.offset + entity.length >= start || entity.offset <= end && entity.offset + entity.length >= end) { - spannable.removeSpan(spans[b]); - spans[b] = null; + if (entity instanceof TLRPC.TL_messageEntityBold || + entity instanceof TLRPC.TL_messageEntityItalic || + entity instanceof TLRPC.TL_messageEntityCode || + entity instanceof TLRPC.TL_messageEntityPre || + entity instanceof TLRPC.TL_messageEntityMentionName || + entity instanceof TLRPC.TL_inputMessageEntityMentionName) { + if (spans != null && spans.length > 0) { + for (int b = 0; b < spans.length; b++) { + if (spans[b] == null) { + continue; + } + int start = spannable.getSpanStart(spans[b]); + int end = spannable.getSpanEnd(spans[b]); + if (entity.offset <= start && entity.offset + entity.length >= start || entity.offset <= end && entity.offset + entity.length >= end) { + spannable.removeSpan(spans[b]); + spans[b] = null; + } } } } @@ -1239,7 +1769,7 @@ public class MessageObject { } else if (entity instanceof TLRPC.TL_messageEntityEmail) { spannable.setSpan(new URLSpanReplacement("mailto:" + url), entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } else if (entity instanceof TLRPC.TL_messageEntityUrl) { - if (!url.toLowerCase().startsWith("http")) { + if (!url.toLowerCase().startsWith("http") && !url.toLowerCase().startsWith("tg://")) { spannable.setSpan(new URLSpan("http://" + url), entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } else { spannable.setSpan(new URLSpan(url), entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -1252,9 +1782,9 @@ public class MessageObject { } int maxWidth; - boolean needShare = messageOwner.from_id > 0 && (messageOwner.to_id.channel_id != 0 || messageOwner.to_id.chat_id != 0 || messageOwner.media instanceof TLRPC.TL_messageMediaGame || messageOwner.media instanceof TLRPC.TL_messageMediaInvoice) && !isOut(); + boolean needShare = eventId == 0 && messageOwner.from_id > 0 && (messageOwner.to_id.channel_id != 0 || messageOwner.to_id.chat_id != 0 || messageOwner.media instanceof TLRPC.TL_messageMediaGame || messageOwner.media instanceof TLRPC.TL_messageMediaInvoice) && !isOut(); generatedWithMinSize = AndroidUtilities.isTablet() ? AndroidUtilities.getMinTabletSide() : AndroidUtilities.displaySize.x; - maxWidth = generatedWithMinSize - AndroidUtilities.dp(needShare ? 122 : 80); + maxWidth = generatedWithMinSize - AndroidUtilities.dp(needShare || eventId != 0 ? 122 : 80); if (fromUser != null && fromUser.bot || (isMegagroup() || messageOwner.fwd_from != null && messageOwner.fwd_from.channel_id != 0) && !isOut()) { maxWidth -= AndroidUtilities.dp(20); } @@ -1272,7 +1802,15 @@ public class MessageObject { } try { - textLayout = new StaticLayout(messageText, paint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + textLayout = StaticLayout.Builder.obtain(messageText, 0, messageText.length(), paint, maxWidth) + .setBreakStrategy(StaticLayout.BREAK_STRATEGY_HIGH_QUALITY) + .setHyphenationFrequency(StaticLayout.HYPHENATION_FREQUENCY_NONE) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .build(); + } else { + textLayout = new StaticLayout(messageText, paint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + } } catch (Exception e) { FileLog.e(e); return; @@ -1303,7 +1841,15 @@ public class MessageObject { block.charactersOffset = startCharacter; block.charactersEnd = endCharacter; try { - block.textLayout = new StaticLayout(messageText, startCharacter, endCharacter, paint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + block.textLayout = StaticLayout.Builder.obtain(messageText, startCharacter, endCharacter, paint, maxWidth) + .setBreakStrategy(StaticLayout.BREAK_STRATEGY_HIGH_QUALITY) + .setHyphenationFrequency(StaticLayout.HYPHENATION_FREQUENCY_NONE) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .build(); + } else { + block.textLayout = new StaticLayout(messageText, startCharacter, endCharacter, paint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + } block.textYOffset = textLayout.getLineTop(linesOffset); if (a != 0) { block.height = (int) (block.textYOffset - prevOffset); @@ -1434,6 +1980,10 @@ public class MessageObject { return messageOwner.out && messageOwner.from_id > 0 && !messageOwner.post; } + public boolean needDrawAvatar() { + return isFromUser() || eventId != 0; + } + public boolean isFromUser() { return messageOwner.from_id > 0 && !messageOwner.post; } @@ -1474,12 +2024,12 @@ public class MessageObject { } public boolean isSecretPhoto() { - return messageOwner instanceof TLRPC.TL_message_secret && messageOwner.media instanceof TLRPC.TL_messageMediaPhoto && messageOwner.ttl > 0 && messageOwner.ttl <= 60; + return messageOwner instanceof TLRPC.TL_message_secret && (messageOwner.media instanceof TLRPC.TL_messageMediaPhoto || isRoundVideo()) && messageOwner.ttl > 0 && messageOwner.ttl <= 60; } public boolean isSecretMedia() { return messageOwner instanceof TLRPC.TL_message_secret && - (messageOwner.media instanceof TLRPC.TL_messageMediaPhoto && messageOwner.ttl > 0 && messageOwner.ttl <= 60 || isVoice() || isVideo()); + ((messageOwner.media instanceof TLRPC.TL_messageMediaPhoto || isRoundVideo()) && messageOwner.ttl > 0 && messageOwner.ttl <= 60 || isVoice() || isVideo()); } public static void setUnreadFlags(TLRPC.Message message, int flag) { @@ -1635,10 +2185,12 @@ public class MessageObject { boolean isVideo = false; int width = 0; int height = 0; - for (int a = 0; a < document.attributes.size(); a++) { TLRPC.DocumentAttribute attribute = document.attributes.get(a); if (attribute instanceof TLRPC.TL_documentAttributeVideo) { + if (Build.VERSION.SDK_INT >= 16 && attribute.round_message) { + return false; + } isVideo = true; width = attribute.w; height = attribute.h; @@ -1676,8 +2228,14 @@ public class MessageObject { return message.media != null && message.media.document != null && isMusicDocument(message.media.document); } - public static boolean isVideoVoiceMessage(TLRPC.Message message) { - return message.media != null && message.media.document != null && isVoiceVideoDocument(message.media.document); + public static boolean isRoundVideoMessage(TLRPC.Message message) { + if (Build.VERSION.SDK_INT < 16) { + return false; + } + if (message.media instanceof TLRPC.TL_messageMediaWebPage) { + return isRoundVideoDocument(message.media.webpage.document); + } + return message.media != null && message.media.document != null && isRoundVideoDocument(message.media.document); } public static boolean isVoiceMessage(TLRPC.Message message) { @@ -1755,6 +2313,8 @@ public class MessageObject { return AndroidUtilities.dp(30); } else if (type == 11) { return AndroidUtilities.dp(50); + } else if (type == 5) { + return AndroidUtilities.roundMessageSize; } else if (type == 13) { float maxHeight = AndroidUtilities.displaySize.y * 0.4f; float maxWidth; @@ -1867,8 +2427,14 @@ public class MessageObject { return isInvoiceMessage(messageOwner); } - public boolean isVideoVoice() { - return isVideoVoiceMessage(messageOwner) && BuildVars.DEBUG_PRIVATE_VERSION; + public boolean isRoundVideo() { + if (Build.VERSION.SDK_INT < 16) { + return false; + } + if (isRoundVideoCached == 0) { + isRoundVideoCached = type == 5 || isRoundVideoMessage(messageOwner) ? 1 : 2; + } + return isRoundVideoCached == 1; } public boolean hasPhotoStickers() { @@ -1888,6 +2454,10 @@ public class MessageObject { } public String getMusicTitle() { + return getMusicTitle(true); + } + + public String getMusicTitle(boolean unknown) { TLRPC.Document document; if (type == 0) { document = messageOwner.media.webpage.document; @@ -1898,16 +2468,23 @@ public class MessageObject { TLRPC.DocumentAttribute attribute = document.attributes.get(a); if (attribute instanceof TLRPC.TL_documentAttributeAudio) { if (attribute.voice) { + if (!unknown) { + return null; + } return LocaleController.formatDateAudio(messageOwner.date); } String title = attribute.title; if (title == null || title.length() == 0) { title = FileLoader.getDocumentFileName(document); - if (title == null || title.length() == 0) { + if (TextUtils.isEmpty(title) && unknown) { title = LocaleController.getString("AudioUnknownTitle", R.string.AudioUnknownTitle); } } return title; + } else if (attribute instanceof TLRPC.TL_documentAttributeVideo) { + if (attribute.round_message) { + return LocaleController.formatDateAudio(messageOwner.date); + } } } return ""; @@ -1930,41 +2507,57 @@ public class MessageObject { } public String getMusicAuthor() { + return getMusicAuthor(true); + } + + public String getMusicAuthor(boolean unknown) { TLRPC.Document document; if (type == 0) { document = messageOwner.media.webpage.document; } else { document = messageOwner.media.document; } + boolean isVoice = false; for (int a = 0; a < document.attributes.size(); a++) { TLRPC.DocumentAttribute attribute = document.attributes.get(a); if (attribute instanceof TLRPC.TL_documentAttributeAudio) { if (attribute.voice) { - if (isOutOwner() || messageOwner.fwd_from != null && messageOwner.fwd_from.from_id == UserConfig.getClientUserId()) { - return LocaleController.getString("FromYou", R.string.FromYou); - } - TLRPC.User user = null; - TLRPC.Chat chat = null; - if (messageOwner.fwd_from != null && messageOwner.fwd_from.channel_id != 0) { - chat = MessagesController.getInstance().getChat(messageOwner.fwd_from.channel_id); - } else if (messageOwner.fwd_from != null && messageOwner.fwd_from.from_id != 0) { - user = MessagesController.getInstance().getUser(messageOwner.fwd_from.from_id); - } else if (messageOwner.from_id < 0) { - chat = MessagesController.getInstance().getChat(-messageOwner.from_id); - } else { - user = MessagesController.getInstance().getUser(messageOwner.from_id); - } - if (user != null) { - return UserObject.getUserName(user); - } else if (chat != null) { - return chat.title; + isVoice = true; + } else { + String performer = attribute.performer; + if (TextUtils.isEmpty(performer) && unknown) { + performer = LocaleController.getString("AudioUnknownArtist", R.string.AudioUnknownArtist); } + return performer; } - String performer = attribute.performer; - if (performer == null || performer.length() == 0) { - performer = LocaleController.getString("AudioUnknownArtist", R.string.AudioUnknownArtist); + } else if (attribute instanceof TLRPC.TL_documentAttributeVideo) { + if (attribute.round_message) { + isVoice = true; + } + } + if (isVoice) { + if (!unknown) { + return null; + } + if (isOutOwner() || messageOwner.fwd_from != null && messageOwner.fwd_from.from_id == UserConfig.getClientUserId()) { + return LocaleController.getString("FromYou", R.string.FromYou); + } + TLRPC.User user = null; + TLRPC.Chat chat = null; + if (messageOwner.fwd_from != null && messageOwner.fwd_from.channel_id != 0) { + chat = MessagesController.getInstance().getChat(messageOwner.fwd_from.channel_id); + } else if (messageOwner.fwd_from != null && messageOwner.fwd_from.from_id != 0) { + user = MessagesController.getInstance().getUser(messageOwner.fwd_from.from_id); + } else if (messageOwner.from_id < 0) { + chat = MessagesController.getInstance().getChat(-messageOwner.from_id); + } else { + user = MessagesController.getInstance().getUser(messageOwner.from_id); + } + if (user != null) { + return UserObject.getUserName(user); + } else if (chat != null) { + return chat.title; } - return performer; } } return ""; @@ -1979,7 +2572,7 @@ public class MessageObject { } public static boolean isForwardedMessage(TLRPC.Message message) { - return (message.flags & TLRPC.MESSAGE_FLAG_FWD) != 0; + return (message.flags & TLRPC.MESSAGE_FLAG_FWD) != 0 && message.fwd_from != null; } public boolean isReply() { @@ -1999,7 +2592,7 @@ public class MessageObject { } public static boolean canEditMessage(TLRPC.Message message, TLRPC.Chat chat) { - if (message == null || message.to_id == null || message.action != null && !(message.action instanceof TLRPC.TL_messageActionEmpty) || isForwardedMessage(message) || message.via_bot_id != 0 || message.id < 0) { + if (message == null || message.to_id == null || message.media != null && isRoundVideoDocument(message.media.document) || message.action != null && !(message.action instanceof TLRPC.TL_messageActionEmpty) || isForwardedMessage(message) || message.via_bot_id != 0 || message.id < 0) { return false; } if (message.from_id == message.to_id.user_id && message.from_id == UserConfig.getClientUserId()) { @@ -2021,7 +2614,7 @@ public class MessageObject { return false; } } - if (chat.megagroup && message.out || !chat.megagroup && (chat.creator || chat.editor && isOut(message)) && message.post) { + if (chat.megagroup && message.out || !chat.megagroup && (chat.creator || chat.admin_rights != null && chat.admin_rights.edit_messages) && message.post) { if (message.media instanceof TLRPC.TL_messageMediaPhoto || message.media instanceof TLRPC.TL_messageMediaDocument && !isStickerMessage(message) || message.media instanceof TLRPC.TL_messageMediaEmpty || @@ -2034,7 +2627,7 @@ public class MessageObject { } public boolean canDeleteMessage(TLRPC.Chat chat) { - return canDeleteMessage(messageOwner, chat); + return eventId == 0 && canDeleteMessage(messageOwner, chat); } public static boolean canDeleteMessage(TLRPC.Message message, TLRPC.Chat chat) { @@ -2045,22 +2638,7 @@ public class MessageObject { chat = MessagesController.getInstance().getChat(message.to_id.channel_id); } if (ChatObject.isChannel(chat)) { - if (message.id == 1) { - return false; - } - if (chat.creator) { - return true; - } else if (chat.editor) { - if (isOut(message) || message.from_id > 0 && !message.post) { - return true; - } - } else if (chat.moderator) { - if (message.from_id > 0 && !message.post) { - return true; - } - } else { - return chat.megagroup && isOut(message) && message.from_id > 0; - } + return message.id != 1 && (chat.creator || chat.admin_rights != null && chat.admin_rights.delete_messages || chat.megagroup && isOut(message) && message.from_id > 0); } return isOut(message) || !ChatObject.isChannel(chat); } @@ -2091,7 +2669,7 @@ public class MessageObject { if (currentPhotoObject != null) { mediaExists = FileLoader.getPathToMessage(messageOwner).exists(); } - } else if (type == 8 || type == 3 || type == 9 || type == 2 || type == 14) { + } else if (type == 8 || type == 3 || type == 9 || type == 2 || type == 14 || type == 5) { if (messageOwner.attachPath != null && messageOwner.attachPath.length() > 0) { File f = new File(messageOwner.attachPath); attachPathExists = f.exists(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java index 70dcc893e..1c18d3b5e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java @@ -53,7 +53,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter private ConcurrentHashMap chats = new ConcurrentHashMap<>(100, 1.0f, 2); private ConcurrentHashMap encryptedChats = new ConcurrentHashMap<>(10, 1.0f, 2); private ConcurrentHashMap users = new ConcurrentHashMap<>(100, 1.0f, 2); - private ConcurrentHashMap usersByUsernames = new ConcurrentHashMap<>(100, 1.0f, 2); + private ConcurrentHashMap objectsByUsernames = new ConcurrentHashMap<>(100, 1.0f, 2); private ArrayList joiningToChannels = new ArrayList<>(); @@ -79,6 +79,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter private HashMap loadingPeerSettings = new HashMap<>(); private ArrayList createdDialogIds = new ArrayList<>(); + private ArrayList createdDialogMainThreadIds = new ArrayList<>(); private SparseIntArray shortPollChannels = new SparseIntArray(); private SparseIntArray needShortPollChannels = new SparseIntArray(); @@ -147,7 +148,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter public int fontSize = AndroidUtilities.dp(16); public int maxGroupCount = 200; public int maxBroadcastCount = 100; - public int maxMegagroupCount = 5000; + public int maxMegagroupCount = 10000; public int minGroupConvertSize = 200; public int maxEditTime = 172800; public int groupBigSize; @@ -276,7 +277,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); secretWebpagePreview = preferences.getInt("secretWebpage2", 2); maxGroupCount = preferences.getInt("maxGroupCount", 200); - maxMegagroupCount = preferences.getInt("maxMegagroupCount", 1000); + maxMegagroupCount = preferences.getInt("maxMegagroupCount", 10000); maxRecentGifsCount = preferences.getInt("maxRecentGifsCount", 200); maxRecentStickersCount = preferences.getInt("maxRecentStickersCount", 30); maxEditTime = preferences.getInt("maxEditTime", 3600); @@ -317,6 +318,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { + LocaleController.getInstance().loadRemoteLanguages(); //maxBroadcastCount = config.broadcast_size_max; maxMegagroupCount = config.megagroup_size_max; maxGroupCount = config.chat_size_max; @@ -605,7 +607,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter dialogMessagesByIds.clear(); dialogMessagesByRandomIds.clear(); users.clear(); - usersByUsernames.clear(); + objectsByUsernames.clear(); chats.clear(); dialogMessage.clear(); printingUsers.clear(); @@ -629,6 +631,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter gettingDifference = false; } }); + createdDialogMainThreadIds.clear(); blockedUsers.clear(); sendingTypings.clear(); loadingFullUsers.clear(); @@ -681,11 +684,11 @@ public class MessagesController implements NotificationCenter.NotificationCenter return users.get(id); } - public TLRPC.User getUser(String username) { + public TLObject getUserOrChat(String username) { if (username == null || username.length() == 0) { return null; } - return usersByUsernames.get(username.toLowerCase()); + return objectsByUsernames.get(username.toLowerCase()); } public ConcurrentHashMap getUsers() { @@ -721,7 +724,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter return chat; } + public boolean isDialogCreated(long dialog_id) { + return createdDialogMainThreadIds.contains(dialog_id); + } + public void setLastCreatedDialogId(final long dialog_id, final boolean set) { + if (set) { + createdDialogMainThreadIds.add(dialog_id); + } else { + createdDialogMainThreadIds.remove(dialog_id); + } Utilities.stageQueue.postRunnable(new Runnable() { @Override public void run() { @@ -745,10 +757,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter fromCache = fromCache && user.id / 1000 != 333 && user.id != 777000; TLRPC.User oldUser = users.get(user.id); if (oldUser != null && !TextUtils.isEmpty(oldUser.username)) { - usersByUsernames.remove(oldUser.username.toLowerCase()); + objectsByUsernames.remove(oldUser.username.toLowerCase()); } if (!TextUtils.isEmpty(user.username)) { - usersByUsernames.put(user.username.toLowerCase(), user); + objectsByUsernames.put(user.username.toLowerCase(), user); } if (user.min) { if (oldUser != null) { @@ -827,12 +839,17 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } - public void putChat(TLRPC.Chat chat, boolean fromCache) { + public void putChat(final TLRPC.Chat chat, boolean fromCache) { if (chat == null) { return; } TLRPC.Chat oldChat = chats.get(chat.id); - + if (oldChat != null && !TextUtils.isEmpty(oldChat.username)) { + objectsByUsernames.remove(oldChat.username.toLowerCase()); + } + if (!TextUtils.isEmpty(chat.username)) { + objectsByUsernames.put(chat.username.toLowerCase(), chat); + } if (chat.min) { if (oldChat != null) { if (!fromCache) { @@ -855,8 +872,20 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } else { if (!fromCache) { - if (oldChat != null && chat.version != oldChat.version) { - loadedFullChats.remove((Integer) chat.id); + if (oldChat != null) { + if (chat.version != oldChat.version) { + loadedFullChats.remove((Integer) chat.id); + } + int oldFlags = oldChat.banned_rights != null ? oldChat.banned_rights.flags : 0; + int newFlags = chat.banned_rights != null ? chat.banned_rights.flags : 0; + if (oldFlags != newFlags) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + NotificationCenter.getInstance().postNotificationName(NotificationCenter.channelRightsUpdated, chat); + } + }); + } } chats.put(chat.id, chat); } else if (oldChat == null) { @@ -1360,8 +1389,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter return; } loadingPeerSettings.put(dialogId, true); + FileLog.d("request spam button for " + dialogId); SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE); if (preferences.getInt("spam3_" + dialogId, 0) == 1) { + FileLog.d("spam button already hidden for " + dialogId); return; } boolean hidden = preferences.getBoolean("spam_" + dialogId, false); @@ -1408,12 +1439,15 @@ public class MessagesController implements NotificationCenter.NotificationCenter SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); if (!res.report_spam) { + FileLog.d("don't show spam button for " + dialogId); editor.putInt("spam3_" + dialogId, 1); + editor.commit(); } else { + FileLog.d("show spam button for " + dialogId); editor.putInt("spam3_" + dialogId, 2); + editor.commit(); NotificationCenter.getInstance().postNotificationName(NotificationCenter.peerSettingsDidLoaded, dialogId); } - editor.commit(); } } }); @@ -1682,6 +1716,68 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); } + public static void setUserBannedRole(final int chatId, TLRPC.User user, TLRPC.TL_channelBannedRights rights, final boolean isMegagroup, final BaseFragment parentFragment) { + if (user == null || rights == null) { + return; + } + final TLRPC.TL_channels_editBanned req = new TLRPC.TL_channels_editBanned(); + req.channel = MessagesController.getInputChannel(chatId); + req.user_id = MessagesController.getInputUser(user); + req.banned_rights = rights; + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, final TLRPC.TL_error error) { + if (error == null) { + MessagesController.getInstance().processUpdates((TLRPC.Updates) response, false); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().loadFullChat(chatId, 0, true); + } + }, 1000); + } else { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + AlertsCreator.processError(error, parentFragment, req, !isMegagroup); + } + }); + } + } + }); + } + + public static void setUserAdminRole(final int chatId, TLRPC.User user, TLRPC.TL_channelAdminRights rights, final boolean isMegagroup, final BaseFragment parentFragment) { + if (user == null || rights == null) { + return; + } + final TLRPC.TL_channels_editAdmin req = new TLRPC.TL_channels_editAdmin(); + req.channel = MessagesController.getInputChannel(chatId); + req.user_id = MessagesController.getInputUser(user); + req.admin_rights = rights; + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, final TLRPC.TL_error error) { + if (error == null) { + MessagesController.getInstance().processUpdates((TLRPC.Updates) response, false); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().loadFullChat(chatId, 0, true); + } + }, 1000); + } else { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + AlertsCreator.processError(error, parentFragment, req, !isMegagroup); + } + }); + } + } + }); + } + public void unblockUser(int user_id) { TLRPC.TL_contacts_unblock req = new TLRPC.TL_contacts_unblock(); final TLRPC.User user = getUser(user_id); @@ -2437,6 +2533,13 @@ public class MessagesController implements NotificationCenter.NotificationCenter newPrintingStrings.put(key, LocaleController.getString("RecordingAudio", R.string.RecordingAudio)); } newPrintingStringsTypes.put(key, 1); + } else if (pu.action instanceof TLRPC.TL_sendMessageRecordRoundAction || pu.action instanceof TLRPC.TL_sendMessageUploadRoundAction) { + if (lower_id < 0) { + newPrintingStrings.put(key, LocaleController.formatString("IsRecordingRound", R.string.IsRecordingRound, getUserNameForTyping(user))); + } else { + newPrintingStrings.put(key, LocaleController.getString("RecordingRound", R.string.RecordingRound)); + } + newPrintingStringsTypes.put(key, 4); } else if (pu.action instanceof TLRPC.TL_sendMessageUploadAudioAction) { if (lower_id < 0) { newPrintingStrings.put(key, LocaleController.formatString("IsSendingAudio", R.string.IsSendingAudio, getUserNameForTyping(user))); @@ -2573,6 +2676,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter req.action = new TLRPC.TL_sendMessageUploadVideoAction(); } else if (action == 6) { req.action = new TLRPC.TL_sendMessageGamePlayAction(); + } else if (action == 7) { + req.action = new TLRPC.TL_sendMessageRecordRoundAction(); + } else if (action == 8) { + req.action = new TLRPC.TL_sendMessageUploadRoundAction(); } typings.put(dialog_id, true); int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @@ -2819,6 +2926,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.User user = usersDict.get(message.action.user_id); if (user != null && user.bot) { message.reply_markup = new TLRPC.TL_replyKeyboardHide(); + message.flags |= 64; } } if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { @@ -2901,35 +3009,62 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.TL_messages_getDialogs req = new TLRPC.TL_messages_getDialogs(); req.limit = count; req.exclude_pinned = true; - boolean found = false; - for (int a = dialogs.size() - 1; a >= 0; a--) { - TLRPC.TL_dialog dialog = dialogs.get(a); - if (dialog.pinned) { - continue; + if (UserConfig.dialogsLoadOffsetId != -1) { + if (UserConfig.dialogsLoadOffsetId == Integer.MAX_VALUE) { + dialogsEndReached = true; + serverDialogsEndReached = true; + loadingDialogs = false; + NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); + return; } - int lower_id = (int) dialog.id; - int high_id = (int) (dialog.id >> 32); - if (lower_id != 0 && high_id != 1 && dialog.top_message > 0) { - MessageObject message = dialogMessage.get(dialog.id); - if (message != null && message.getId() > 0) { - req.offset_date = message.messageOwner.date; - req.offset_id = message.messageOwner.id; - int id; - if (message.messageOwner.to_id.channel_id != 0) { - id = -message.messageOwner.to_id.channel_id; - } else if (message.messageOwner.to_id.chat_id != 0) { - id = -message.messageOwner.to_id.chat_id; - } else { - id = message.messageOwner.to_id.user_id; + req.offset_id = UserConfig.dialogsLoadOffsetId; + req.offset_date = UserConfig.dialogsLoadOffsetDate; + if (req.offset_id == 0) { + req.offset_peer = new TLRPC.TL_inputPeerEmpty(); + } else { + if (UserConfig.dialogsLoadOffsetChannelId != 0) { + req.offset_peer = new TLRPC.TL_inputPeerChannel(); + req.offset_peer.channel_id = UserConfig.dialogsLoadOffsetChannelId; + } else if (UserConfig.dialogsLoadOffsetUserId != 0) { + req.offset_peer = new TLRPC.TL_inputPeerUser(); + req.offset_peer.user_id = UserConfig.dialogsLoadOffsetUserId; + } else { + req.offset_peer = new TLRPC.TL_inputPeerChat(); + req.offset_peer.chat_id = UserConfig.dialogsLoadOffsetChatId; + } + req.offset_peer.access_hash = UserConfig.dialogsLoadOffsetAccess; + } + } else { + boolean found = false; + for (int a = dialogs.size() - 1; a >= 0; a--) { + TLRPC.TL_dialog dialog = dialogs.get(a); + if (dialog.pinned) { + continue; + } + int lower_id = (int) dialog.id; + int high_id = (int) (dialog.id >> 32); + if (lower_id != 0 && high_id != 1 && dialog.top_message > 0) { + MessageObject message = dialogMessage.get(dialog.id); + if (message != null && message.getId() > 0) { + req.offset_date = message.messageOwner.date; + req.offset_id = message.messageOwner.id; + int id; + if (message.messageOwner.to_id.channel_id != 0) { + id = -message.messageOwner.to_id.channel_id; + } else if (message.messageOwner.to_id.chat_id != 0) { + id = -message.messageOwner.to_id.chat_id; + } else { + id = message.messageOwner.to_id.user_id; + } + req.offset_peer = getInputPeer(id); + found = true; + break; } - req.offset_peer = getInputPeer(id); - found = true; - break; } } - } - if (!found) { - req.offset_peer = new TLRPC.TL_inputPeerEmpty(); + if (!found) { + req.offset_peer = new TLRPC.TL_inputPeerEmpty(); + } } ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override @@ -2950,9 +3085,11 @@ public class MessagesController implements NotificationCenter.NotificationCenter migratingDialogs = true; TLRPC.TL_messages_getDialogs req = new TLRPC.TL_messages_getDialogs(); + req.exclude_pinned = true; req.limit = 100; req.offset_id = offset; req.offset_date = offsetDate; + FileLog.e("start migrate with id " + offset + " date " + LocaleController.getInstance().formatterStats.format((long) offsetDate * 1000)); if (offset == 0) { req.offset_peer = new TLRPC.TL_inputPeerEmpty(); } else { @@ -2978,51 +3115,26 @@ public class MessagesController implements NotificationCenter.NotificationCenter public void run() { try { int offsetId; - if (dialogsRes.dialogs.size() == 100) { - TLRPC.Message lastMessage = null; - for (int a = 0; a < dialogsRes.messages.size(); a++) { - TLRPC.Message message = dialogsRes.messages.get(a); - if (lastMessage == null || message.date < lastMessage.date) { - lastMessage = message; - } + UserConfig.totalDialogsLoadCount += dialogsRes.dialogs.size(); + TLRPC.Message lastMessage = null; + for (int a = 0; a < dialogsRes.messages.size(); a++) { + TLRPC.Message message = dialogsRes.messages.get(a); + FileLog.e("search migrate id " + message.id + " date " + LocaleController.getInstance().formatterStats.format((long) message.date * 1000)); + if (lastMessage == null || message.date < lastMessage.date) { + lastMessage = message; } + } + FileLog.e("migrate step with id " + lastMessage.id + " date " + LocaleController.getInstance().formatterStats.format((long) lastMessage.date * 1000)); + if (dialogsRes.dialogs.size() >= 100) { offsetId = lastMessage.id; - UserConfig.migrateOffsetDate = lastMessage.date; - if (lastMessage.to_id.channel_id != 0) { - UserConfig.migrateOffsetChannelId = lastMessage.to_id.channel_id; - UserConfig.migrateOffsetChatId = 0; - UserConfig.migrateOffsetUserId = 0; - for (int a = 0; a < dialogsRes.chats.size(); a++) { - TLRPC.Chat chat = dialogsRes.chats.get(a); - if (chat.id == UserConfig.migrateOffsetChannelId) { - UserConfig.migrateOffsetAccess = chat.access_hash; - break; - } - } - } else if (lastMessage.to_id.chat_id != 0) { - UserConfig.migrateOffsetChatId = lastMessage.to_id.chat_id; - UserConfig.migrateOffsetChannelId = 0; - UserConfig.migrateOffsetUserId = 0; - for (int a = 0; a < dialogsRes.chats.size(); a++) { - TLRPC.Chat chat = dialogsRes.chats.get(a); - if (chat.id == UserConfig.migrateOffsetChatId) { - UserConfig.migrateOffsetAccess = chat.access_hash; - break; - } - } - } else if (lastMessage.to_id.user_id != 0) { - UserConfig.migrateOffsetUserId = lastMessage.to_id.user_id; - UserConfig.migrateOffsetChatId = 0; - UserConfig.migrateOffsetChannelId = 0; - for (int a = 0; a < dialogsRes.users.size(); a++) { - TLRPC.User user = dialogsRes.users.get(a); - if (user.id == UserConfig.migrateOffsetUserId) { - UserConfig.migrateOffsetAccess = user.access_hash; - break; - } - } - } } else { + FileLog.e("migrate stop due to not 100 dialogs"); + UserConfig.dialogsLoadOffsetId = Integer.MAX_VALUE; + UserConfig.dialogsLoadOffsetDate = UserConfig.migrateOffsetDate; + UserConfig.dialogsLoadOffsetUserId = UserConfig.migrateOffsetUserId; + UserConfig.dialogsLoadOffsetChatId = UserConfig.migrateOffsetChatId; + UserConfig.dialogsLoadOffsetChannelId = UserConfig.migrateOffsetChannelId; + UserConfig.dialogsLoadOffsetAccess = UserConfig.migrateOffsetAccess; offsetId = -1; } @@ -3058,21 +3170,29 @@ public class MessagesController implements NotificationCenter.NotificationCenter a--; if (message.id == dialog.top_message) { dialog.top_message = 0; - } - if (dialog.top_message == 0) { break; } } } } cursor.dispose(); + FileLog.e("migrate found missing dialogs " + dialogsRes.dialogs.size()); cursor = MessagesStorage.getInstance().getDatabase().queryFinalized("SELECT min(date) FROM dialogs WHERE date != 0 AND did >> 32 IN (0, -1)"); if (cursor.next()) { int date = Math.max(1441062000, cursor.intValue(0)); for (int a = 0; a < dialogsRes.messages.size(); a++) { TLRPC.Message message = dialogsRes.messages.get(a); if (message.date < date) { - offsetId = -1; + if (offset != -1) { + UserConfig.dialogsLoadOffsetId = UserConfig.migrateOffsetId; + UserConfig.dialogsLoadOffsetDate = UserConfig.migrateOffsetDate; + UserConfig.dialogsLoadOffsetUserId = UserConfig.migrateOffsetUserId; + UserConfig.dialogsLoadOffsetChatId = UserConfig.migrateOffsetChatId; + UserConfig.dialogsLoadOffsetChannelId = UserConfig.migrateOffsetChannelId; + UserConfig.dialogsLoadOffsetAccess = UserConfig.migrateOffsetAccess; + offsetId = -1; + FileLog.e("migrate stop due to reached loaded dialogs " + LocaleController.getInstance().formatterStats.format((long) date * 1000)); + } dialogsRes.messages.remove(a); a--; TLRPC.TL_dialog dialog = dialogHashMap.remove(MessageObject.getDialogId(message)); @@ -3081,9 +3201,55 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } + if (lastMessage != null && lastMessage.date < date && offset != -1) { + UserConfig.dialogsLoadOffsetId = UserConfig.migrateOffsetId; + UserConfig.dialogsLoadOffsetDate = UserConfig.migrateOffsetDate; + UserConfig.dialogsLoadOffsetUserId = UserConfig.migrateOffsetUserId; + UserConfig.dialogsLoadOffsetChatId = UserConfig.migrateOffsetChatId; + UserConfig.dialogsLoadOffsetChannelId = UserConfig.migrateOffsetChannelId; + UserConfig.dialogsLoadOffsetAccess = UserConfig.migrateOffsetAccess; + offsetId = -1; + FileLog.e("migrate stop due to reached loaded dialogs " + LocaleController.getInstance().formatterStats.format((long) date * 1000)); + } } cursor.dispose(); + UserConfig.migrateOffsetDate = lastMessage.date; + if (lastMessage.to_id.channel_id != 0) { + UserConfig.migrateOffsetChannelId = lastMessage.to_id.channel_id; + UserConfig.migrateOffsetChatId = 0; + UserConfig.migrateOffsetUserId = 0; + for (int a = 0; a < dialogsRes.chats.size(); a++) { + TLRPC.Chat chat = dialogsRes.chats.get(a); + if (chat.id == UserConfig.migrateOffsetChannelId) { + UserConfig.migrateOffsetAccess = chat.access_hash; + break; + } + } + } else if (lastMessage.to_id.chat_id != 0) { + UserConfig.migrateOffsetChatId = lastMessage.to_id.chat_id; + UserConfig.migrateOffsetChannelId = 0; + UserConfig.migrateOffsetUserId = 0; + for (int a = 0; a < dialogsRes.chats.size(); a++) { + TLRPC.Chat chat = dialogsRes.chats.get(a); + if (chat.id == UserConfig.migrateOffsetChatId) { + UserConfig.migrateOffsetAccess = chat.access_hash; + break; + } + } + } else if (lastMessage.to_id.user_id != 0) { + UserConfig.migrateOffsetUserId = lastMessage.to_id.user_id; + UserConfig.migrateOffsetChatId = 0; + UserConfig.migrateOffsetChannelId = 0; + for (int a = 0; a < dialogsRes.users.size(); a++) { + TLRPC.User user = dialogsRes.users.get(a); + if (user.id == UserConfig.migrateOffsetUserId) { + UserConfig.migrateOffsetAccess = user.access_hash; + break; + } + } + } + processLoadedDialogs(dialogsRes, null, offsetId, 0, 0, false, true, false); } catch (Exception e) { FileLog.e(e); @@ -3127,9 +3293,13 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (resetEnd) { dialogsEndReached = false; serverDialogsEndReached = false; + } else if (UserConfig.dialogsLoadOffsetId == Integer.MAX_VALUE) { + dialogsEndReached = true; + serverDialogsEndReached = true; + } else { + loadDialogs(0, count, false); } NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); - loadDialogs(0, count, false); } }); return; @@ -3152,8 +3322,12 @@ public class MessagesController implements NotificationCenter.NotificationCenter nextDialogsCacheOffset = offset + count; } + TLRPC.Message lastMessage = null; for (int a = 0; a < dialogsRes.messages.size(); a++) { TLRPC.Message message = dialogsRes.messages.get(a); + if (lastMessage == null || message.date < lastMessage.date) { + lastMessage = message; + } if (message.to_id.channel_id != 0) { TLRPC.Chat chat = chatsDict.get(message.to_id.channel_id); if (chat != null && chat.left) { @@ -3175,6 +3349,51 @@ public class MessagesController implements NotificationCenter.NotificationCenter new_dialogMessage.put(messageObject.getDialogId(), messageObject); } + if (!fromCache && !migrate && UserConfig.dialogsLoadOffsetId != -1 && loadType == 0) { + if (lastMessage != null && lastMessage.id != UserConfig.dialogsLoadOffsetId) { + UserConfig.totalDialogsLoadCount += dialogsRes.dialogs.size(); + UserConfig.dialogsLoadOffsetId = lastMessage.id; + UserConfig.dialogsLoadOffsetDate = lastMessage.date; + if (lastMessage.to_id.channel_id != 0) { + UserConfig.dialogsLoadOffsetChannelId = lastMessage.to_id.channel_id; + UserConfig.dialogsLoadOffsetChatId = 0; + UserConfig.dialogsLoadOffsetUserId = 0; + for (int a = 0; a < dialogsRes.chats.size(); a++) { + TLRPC.Chat chat = dialogsRes.chats.get(a); + if (chat.id == UserConfig.dialogsLoadOffsetChannelId) { + UserConfig.dialogsLoadOffsetAccess = chat.access_hash; + break; + } + } + } else if (lastMessage.to_id.chat_id != 0) { + UserConfig.dialogsLoadOffsetChatId = lastMessage.to_id.chat_id; + UserConfig.dialogsLoadOffsetChannelId = 0; + UserConfig.dialogsLoadOffsetUserId = 0; + for (int a = 0; a < dialogsRes.chats.size(); a++) { + TLRPC.Chat chat = dialogsRes.chats.get(a); + if (chat.id == UserConfig.dialogsLoadOffsetChatId) { + UserConfig.dialogsLoadOffsetAccess = chat.access_hash; + break; + } + } + } else if (lastMessage.to_id.user_id != 0) { + UserConfig.dialogsLoadOffsetUserId = lastMessage.to_id.user_id; + UserConfig.dialogsLoadOffsetChatId = 0; + UserConfig.dialogsLoadOffsetChannelId = 0; + for (int a = 0; a < dialogsRes.users.size(); a++) { + TLRPC.User user = dialogsRes.users.get(a); + if (user.id == UserConfig.dialogsLoadOffsetUserId) { + UserConfig.dialogsLoadOffsetAccess = user.access_hash; + break; + } + } + } + } else { + UserConfig.dialogsLoadOffsetId = Integer.MAX_VALUE; + } + UserConfig.saveConfig(false); + } + final ArrayList dialogsToReload = new ArrayList<>(); for (int a = 0; a < dialogsRes.dialogs.size(); a++) { TLRPC.TL_dialog d = dialogsRes.dialogs.get(a); @@ -3242,6 +3461,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.User user = usersDict.get(message.action.user_id); if (user != null && user.bot) { message.reply_markup = new TLRPC.TL_replyKeyboardHide(); + message.flags |= 64; } } @@ -3370,6 +3590,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } + if (!fromCache && !migrate && UserConfig.totalDialogsLoadCount < 400 && UserConfig.dialogsLoadOffsetId != -1 && UserConfig.dialogsLoadOffsetId != Integer.MAX_VALUE) { + loadDialogs(0, 100, false); + } NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); if (migrate) { @@ -3512,7 +3735,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (taskId == 0) { NativeByteBuffer data = null; try { - data = new NativeByteBuffer(48 + peer.getObjectSize()); + data = new NativeByteBuffer(48 + req.peer.getObjectSize()); data.writeInt32(5); data.writeInt64(dialog.id); data.writeInt32(dialog.top_message); @@ -4321,18 +4544,27 @@ public class MessagesController implements NotificationCenter.NotificationCenter request = req; joiningToChannels.add(chat_id); } else { - if (user.bot && !isMegagroup) { + /*if (user.bot && !isMegagroup) { TODO TLRPC.TL_channels_editAdmin req = new TLRPC.TL_channels_editAdmin(); req.channel = getInputChannel(chat_id); req.user_id = getInputUser(user); - req.role = new TLRPC.TL_channelRoleEditor(); + req.rights = new TLRPC.TL_channelAdminRights(); + req.rights.change_info = true; + req.rights.post_messages = true; + req.rights.delete_messages = true; + req.rights.ban_users = true; + req.rights.invite_users = true; + req.rights.invite_link = true; + req.rights.pin_messages = true; + req.rights.start_calls = true; + req.rights.add_admins = true; request = req; - } else { + } else {*/ TLRPC.TL_channels_inviteToChannel req = new TLRPC.TL_channels_inviteToChannel(); req.channel = getInputChannel(chat_id); req.users.add(inputUser); request = req; - } + //} } } else { TLRPC.TL_messages_addChatUser req = new TLRPC.TL_messages_addChatUser(); @@ -4450,10 +4682,18 @@ public class MessagesController implements NotificationCenter.NotificationCenter request = req; } } else { - TLRPC.TL_channels_kickFromChannel req = new TLRPC.TL_channels_kickFromChannel(); + TLRPC.TL_channels_editBanned req = new TLRPC.TL_channels_editBanned(); req.channel = getInputChannel(chat); req.user_id = inputUser; - req.kicked = true; + req.banned_rights = new TLRPC.TL_channelBannedRights(); + req.banned_rights.view_messages = true; + req.banned_rights.send_media = true; + req.banned_rights.send_messages = true; + req.banned_rights.send_stickers = true; + req.banned_rights.send_gifs = true; + req.banned_rights.send_games = true; + req.banned_rights.send_inline = true; + req.banned_rights.embed_links = true; request = req; } } else { @@ -5399,6 +5639,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.User user = usersDict.get(message.action.user_id); if (user != null && user.bot) { message.reply_markup = new TLRPC.TL_replyKeyboardHide(); + message.flags |= 64; } } if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { @@ -6166,9 +6407,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.Chat existChat = getChat(chat.id); if (existChat == null || existChat.min) { TLRPC.Chat cacheChat = MessagesStorage.getInstance().getChatSync(updates.chat_id); - if (existChat == null) { - putChat(cacheChat, true); - } + putChat(cacheChat, true); existChat = cacheChat; } if (existChat == null || existChat.min) { @@ -6357,10 +6596,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (processUpdate) { processUpdateArray(updates.updates, updates.users, updates.chats, false); - if (updates.date != 0) { - MessagesStorage.lastDateValue = updates.date; - } if (updates.seq != 0) { + if (updates.date != 0) { + MessagesStorage.lastDateValue = updates.date; + } MessagesStorage.lastSeqValue = updates.seq; } } else { @@ -6596,6 +6835,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.User user = usersDict.get(message.action.user_id); if (user != null && user.bot) { message.reply_markup = new TLRPC.TL_replyKeyboardHide(); + message.flags |= 64; } else if (message.from_id == UserConfig.getClientUserId() && message.action.user_id == UserConfig.getClientUserId()) { continue; } @@ -6896,7 +7136,11 @@ public class MessagesController implements NotificationCenter.NotificationCenter UserConfig.saveConfig(false); newMessage.unread = true; newMessage.flags = TLRPC.MESSAGE_FLAG_HAS_FROM_ID; - newMessage.date = notification.inbox_date; + if (notification.inbox_date != 0) { + newMessage.date = notification.inbox_date; + } else { + newMessage.date = (int) (System.currentTimeMillis() / 1000); + } newMessage.from_id = 777000; newMessage.to_id = new TLRPC.TL_peerUser(); newMessage.to_id.user_id = UserConfig.getClientUserId(); @@ -7120,6 +7364,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter updatesOnMainThread.add(update); } else if (update instanceof TLRPC.TL_updatePhoneCall) { updatesOnMainThread.add(update); + } else if (update instanceof TLRPC.TL_updateLangPack) { + LocaleController.getInstance().saveRemoteLocaleStrings(update.difference); + } else if (update instanceof TLRPC.TL_updateLangPackTooLong) { + LocaleController.getInstance().reloadCurrentRemoteLocale(); } } if (!messages.isEmpty()) { @@ -7223,10 +7471,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter currentUser.last_name = update.last_name; } if (currentUser.username != null && currentUser.username.length() > 0) { - usersByUsernames.remove(currentUser.username); + objectsByUsernames.remove(currentUser.username); } if (update.username != null && update.username.length() > 0) { - usersByUsernames.put(update.username, currentUser); + objectsByUsernames.put(update.username, currentUser); } currentUser.username = update.username; } @@ -7426,12 +7674,12 @@ public class MessagesController implements NotificationCenter.NotificationCenter intent.putExtra("user_id", call.participant_id == UserConfig.getClientUserId() ? call.admin_id : call.participant_id); ApplicationLoader.applicationContext.startService(intent); } else { - if (svc != null && call!=null) { + if (svc != null && call != null) { svc.onCallUpdated(call); - }else if(VoIPService.callIShouldHavePutIntoIntent!=null){ + } else if (VoIPService.callIShouldHavePutIntoIntent != null) { FileLog.d("Updated the call while the service is starting"); - if(call.id==VoIPService.callIShouldHavePutIntoIntent.id){ - VoIPService.callIShouldHavePutIntoIntent=call; + if (call.id == VoIPService.callIShouldHavePutIntoIntent.id) { + VoIPService.callIShouldHavePutIntoIntent = call; } } } @@ -7543,17 +7791,23 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (markAsReadMessagesInbox.size() != 0 || markAsReadMessagesOutbox.size() != 0) { NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesRead, markAsReadMessagesInbox, markAsReadMessagesOutbox); NotificationsController.getInstance().processReadMessages(markAsReadMessagesInbox, 0, 0, 0, false); - for (int b = 0; b < markAsReadMessagesInbox.size(); b++) { - int key = markAsReadMessagesInbox.keyAt(b); - int messageId = (int) ((long) markAsReadMessagesInbox.get(key)); - TLRPC.TL_dialog dialog = dialogs_dict.get((long) key); - if (dialog != null && dialog.top_message <= messageId) { - MessageObject obj = dialogMessage.get(dialog.id); - if (obj != null && !obj.isOut()) { - obj.setIsRead(); - updateMask |= UPDATE_MASK_READ_DIALOG_MESSAGE; + if (markAsReadMessagesInbox.size() != 0) { + SharedPreferences.Editor editor = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE).edit(); + for (int b = 0; b < markAsReadMessagesInbox.size(); b++) { + int key = markAsReadMessagesInbox.keyAt(b); + int messageId = (int) ((long) markAsReadMessagesInbox.get(key)); + TLRPC.TL_dialog dialog = dialogs_dict.get((long) key); + if (dialog != null && dialog.top_message <= messageId) { + MessageObject obj = dialogMessage.get(dialog.id); + if (obj != null && !obj.isOut()) { + obj.setIsRead(); + updateMask |= UPDATE_MASK_READ_DIALOG_MESSAGE; + } } + editor.remove("diditem" + key); + editor.remove("diditemo" + key); } + editor.commit(); } for (int b = 0; b < markAsReadMessagesOutbox.size(); b++) { int key = markAsReadMessagesOutbox.keyAt(b); @@ -7843,7 +8097,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter dialogsServerOnly.add(d); if (DialogObject.isChannel(d)) { TLRPC.Chat chat = getChat(-lower_id); - if (chat != null && (chat.megagroup && chat.editor || chat.creator)) { + if (chat != null && (chat.megagroup && (chat.admin_rights != null && chat.admin_rights.post_messages) || chat.creator)) { dialogsGroupsOnly.add(d); } } else if (lower_id < 0) { @@ -7940,6 +8194,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (type == 0) { fragment.presentFragment(new ProfileActivity(args)); + } else if (type == 2) { + fragment.presentFragment(new ChatActivity(args), true, true); } else { fragment.presentFragment(new ChatActivity(args), closeLast); } @@ -7950,9 +8206,24 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (username == null || fragment == null) { return; } - TLRPC.User user = getInstance().getUser(username); + TLObject object = getInstance().getUserOrChat(username); + TLRPC.User user = null; + TLRPC.Chat chat = null; + if (object instanceof TLRPC.User) { + user = (TLRPC.User) object; + if (user.min) { + user = null; + } + } else if (object instanceof TLRPC.Chat) { + chat = (TLRPC.Chat) object; + if (chat.min) { + chat = null; + } + } if (user != null) { openChatOrProfileWith(user, null, fragment, type, false); + } else if (chat != null) { + openChatOrProfileWith(null, chat, fragment, 1, false); } else { if (fragment.getParentActivity() == null) { return; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java index e6d0537ec..91625cafb 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java @@ -4008,7 +4008,8 @@ public class MessagesStorage { if (message instanceof TLRPC.TL_message_secret && ( message.media instanceof TLRPC.TL_messageMediaPhoto && message.ttl > 0 && message.ttl <= 60 || MessageObject.isVoiceMessage(message) || - MessageObject.isVideoMessage(message))) { + MessageObject.isVideoMessage(message) || + MessageObject.isRoundVideoMessage(message))) { return 1; } else if (message.media instanceof TLRPC.TL_messageMediaPhoto || MessageObject.isVideoMessage(message)) { return 0; @@ -4437,13 +4438,21 @@ public class MessagesStorage { long id = 0; TLRPC.MessageMedia object = null; if (MessageObject.isVoiceMessage(message)) { - if ((downloadMask & MediaController.AUTODOWNLOAD_MASK_AUDIO) != 0 && message.media.document.size < 1024 * 1024 * 5) { + if ((downloadMask & MediaController.AUTODOWNLOAD_MASK_AUDIO) != 0 && message.media.document.size < 1024 * 1024 * 2) { id = message.media.document.id; type = MediaController.AUTODOWNLOAD_MASK_AUDIO; object = new TLRPC.TL_messageMediaDocument(); object.caption = ""; object.document = message.media.document; } + } else if (MessageObject.isRoundVideoMessage(message)) { + if ((downloadMask & MediaController.AUTODOWNLOAD_MASK_VIDEOMESSAGE) != 0 && message.media.document.size < 1024 * 1024 * 5) { + id = message.media.document.id; + type = MediaController.AUTODOWNLOAD_MASK_VIDEOMESSAGE; + object = new TLRPC.TL_messageMediaDocument(); + object.caption = ""; + object.document = message.media.document; + } } else if (message.media instanceof TLRPC.TL_messageMediaPhoto) { if ((downloadMask & MediaController.AUTODOWNLOAD_MASK_PHOTO) != 0) { TLRPC.PhotoSize photoSize = FileLoader.getClosestPhotoSizeWithSize(message.media.photo.sizes, AndroidUtilities.getPhotoSize()); @@ -6056,36 +6065,9 @@ public class MessagesStorage { } cursor.dispose(); if (!unpinnedDialogs.isEmpty()) { - int minDate = 0; - cursor = database.queryFinalized("SELECT min(date), min(date_i) FROM dialogs WHERE (date != 0 OR date_i != 0) AND pinned = 0"); - if (cursor.next()) { - int date = cursor.intValue(0); - int date_i = cursor.intValue(1); - if (date != 0 && date_i != 0) { - minDate = Math.min(date, date_i); - } else if (date == 0) { - minDate = date_i; - } else { - minDate = date; - } - } - cursor.dispose(); - SQLitePreparedStatement state = database.executeFast("UPDATE dialogs SET pinned = ? WHERE did = ?"); for (int a = 0; a < unpinnedDialogs.size(); a++) { long did = unpinnedDialogs.get(a); - - int dialogDate = 0; - cursor = database.queryFinalized("SELECT date FROM dialogs WHERE did = " + did); - if (cursor.next()) { - dialogDate = cursor.intValue(0); - } - cursor.dispose(); - - if (dialogDate <= minDate) { - database.executeFast("DELETE FROM dialogs WHERE did = " + did).stepThis().dispose(); - continue; - } state.requery(); state.bindInteger(1, 0); state.bindLong(2, did); @@ -6105,24 +6087,6 @@ public class MessagesStorage { @Override public void run() { try { - if (pinned == 0 && (int) did != 0) { - int dialogDate = 0; - int minDate = 0; - SQLiteCursor cursor = database.queryFinalized("SELECT date FROM dialogs WHERE did = " + did); - if (cursor.next()) { - dialogDate = cursor.intValue(0); - } - cursor.dispose(); - cursor = database.queryFinalized("SELECT min(date) FROM dialogs WHERE date != 0 AND pinned = 0"); - if (cursor.next()) { - minDate = cursor.intValue(0); - } - cursor.dispose(); - if (dialogDate <= minDate) { - database.executeFast("DELETE FROM dialogs WHERE did = " + did).stepThis().dispose(); - return; - } - } SQLitePreparedStatement state = database.executeFast("UPDATE dialogs SET pinned = ? WHERE did = ?"); state.bindInteger(1, pinned); state.bindLong(2, did); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java b/TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java index b5f6039f1..d78dca19b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java @@ -108,9 +108,9 @@ public class MusicBrowserService extends MediaBrowserService implements Notifica updatePlaybackState(null); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioPlayStateChanged); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidStarted); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidReset); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingPlayStateChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidStarted); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidReset); } @Override @@ -363,7 +363,7 @@ public class MusicBrowserService extends MediaBrowserService implements Notifica if (messageObject == null) { onPlayFromMediaId(lastSelectedDialog + "_" + 0, null); } else { - MediaController.getInstance().playAudio(messageObject); + MediaController.getInstance().playMessage(messageObject); } } @@ -486,7 +486,7 @@ public class MusicBrowserService extends MediaBrowserService implements Notifica if (MediaController.getInstance().isDownloadingCurrentMessage()) { state = PlaybackState.STATE_BUFFERING; } else { - state = MediaController.getInstance().isAudioPaused() ? PlaybackState.STATE_PAUSED : PlaybackState.STATE_PLAYING; + state = MediaController.getInstance().isMessagePaused() ? PlaybackState.STATE_PAUSED : PlaybackState.STATE_PLAYING; } } @@ -508,7 +508,7 @@ public class MusicBrowserService extends MediaBrowserService implements Notifica long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_PLAY_FROM_SEARCH; MessageObject playingMessageObject = MediaController.getInstance().getPlayingMessageObject(); if (playingMessageObject != null) { - if (!MediaController.getInstance().isAudioPaused()) { + if (!MediaController.getInstance().isMessagePaused()) { actions |= PlaybackState.ACTION_PAUSE; } actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; @@ -523,9 +523,9 @@ public class MusicBrowserService extends MediaBrowserService implements Notifica updatePlaybackState(withError); stopSelf(); serviceStarted = false; - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioPlayStateChanged); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidStarted); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidReset); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingPlayStateChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidStarted); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidReset); } private void handlePlayRequest() { @@ -558,7 +558,7 @@ public class MusicBrowserService extends MediaBrowserService implements Notifica } private void handlePauseRequest() { - MediaController.getInstance().pauseAudio(MediaController.getInstance().getPlayingMessageObject()); + MediaController.getInstance().pauseMessage(MediaController.getInstance().getPlayingMessageObject()); delayedStopHandler.removeCallbacksAndMessages(null); delayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY); } @@ -581,7 +581,7 @@ public class MusicBrowserService extends MediaBrowserService implements Notifica MusicBrowserService service = mWeakReference.get(); if (service != null) { MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); - if (messageObject != null && !MediaController.getInstance().isAudioPaused()) { + if (messageObject != null && !MediaController.getInstance().isMessagePaused()) { return; } service.stopSelf(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerReceiver.java index e6738cad3..b187bc312 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerReceiver.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerReceiver.java @@ -31,17 +31,17 @@ public class MusicPlayerReceiver extends BroadcastReceiver { switch (keyEvent.getKeyCode()) { case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - if (MediaController.getInstance().isAudioPaused()) { - MediaController.getInstance().playAudio(MediaController.getInstance().getPlayingMessageObject()); + if (MediaController.getInstance().isMessagePaused()) { + MediaController.getInstance().playMessage(MediaController.getInstance().getPlayingMessageObject()); } else { - MediaController.getInstance().pauseAudio(MediaController.getInstance().getPlayingMessageObject()); + MediaController.getInstance().pauseMessage(MediaController.getInstance().getPlayingMessageObject()); } break; case KeyEvent.KEYCODE_MEDIA_PLAY: - MediaController.getInstance().playAudio(MediaController.getInstance().getPlayingMessageObject()); + MediaController.getInstance().playMessage(MediaController.getInstance().getPlayingMessageObject()); break; case KeyEvent.KEYCODE_MEDIA_PAUSE: - MediaController.getInstance().pauseAudio(MediaController.getInstance().getPlayingMessageObject()); + MediaController.getInstance().pauseMessage(MediaController.getInstance().getPlayingMessageObject()); break; case KeyEvent.KEYCODE_MEDIA_STOP: break; @@ -54,9 +54,9 @@ public class MusicPlayerReceiver extends BroadcastReceiver { } } else { if (intent.getAction().equals(MusicPlayerService.NOTIFY_PLAY)) { - MediaController.getInstance().playAudio(MediaController.getInstance().getPlayingMessageObject()); + MediaController.getInstance().playMessage(MediaController.getInstance().getPlayingMessageObject()); } else if (intent.getAction().equals(MusicPlayerService.NOTIFY_PAUSE) || intent.getAction().equals(android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { - MediaController.getInstance().pauseAudio(MediaController.getInstance().getPlayingMessageObject()); + MediaController.getInstance().pauseMessage(MediaController.getInstance().getPlayingMessageObject()); } else if (intent.getAction().equals(MusicPlayerService.NOTIFY_NEXT)) { MediaController.getInstance().playNextMessage(); } else if (intent.getAction().equals(MusicPlayerService.NOTIFY_CLOSE)) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java b/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java index 39176b364..6b273bc4a 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java @@ -49,8 +49,8 @@ public class MusicPlayerService extends Service implements NotificationCenter.No @Override public void onCreate() { audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioProgressDidChanged); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioPlayStateChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingPlayStateChanged); super.onCreate(); } @@ -159,7 +159,7 @@ public class MusicPlayerService extends Service implements NotificationCenter.No notification.bigContentView.setViewVisibility(R.id.player_progress_bar, View.GONE); } - if (MediaController.getInstance().isAudioPaused()) { + if (MediaController.getInstance().isMessagePaused()) { notification.contentView.setViewVisibility(R.id.player_pause, View.GONE); notification.contentView.setViewVisibility(R.id.player_play, View.VISIBLE); if (supportBigNotifications) { @@ -223,13 +223,13 @@ public class MusicPlayerService extends Service implements NotificationCenter.No metadataEditor.apply(); audioManager.unregisterRemoteControlClient(remoteControlClient); } - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioProgressDidChanged); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioPlayStateChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingPlayStateChanged); } @Override public void didReceivedNotification(int id, Object... args) { - if (id == NotificationCenter.audioPlayStateChanged) { + if (id == NotificationCenter.messagePlayingPlayStateChanged) { MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); if (messageObject != null) { createNotification(messageObject); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java index 59ae60747..0ba1575d8 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java @@ -23,7 +23,7 @@ import java.util.zip.ZipFile; public class NativeLoader { - private final static int LIB_VERSION = 26; + private final static int LIB_VERSION = 27; 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 c3fb03935..29168ea1b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java @@ -82,6 +82,10 @@ public class NotificationCenter { public static final int didSetNewWallpapper = totalEvents++; public static final int archivedStickersCountDidLoaded = totalEvents++; public static final int paymentFinished = totalEvents++; + public static final int reloadInterface = totalEvents++; + public static final int suggestedLangpack = totalEvents++; + public static final int channelRightsUpdated = totalEvents++; + public static final int proxySettingsChanged = totalEvents++; public static final int httpFileDidLoaded = totalEvents++; public static final int httpFileDidFailedLoad = totalEvents++; @@ -106,9 +110,10 @@ public class NotificationCenter { public static final int FileNewChunkAvailable = totalEvents++; public static final int FilePreparingFailed = totalEvents++; - public static final int audioProgressDidChanged = totalEvents++; - public static final int audioDidReset = totalEvents++; - public static final int audioPlayStateChanged = totalEvents++; + public static final int messagePlayingProgressDidChanged = totalEvents++; + public static final int messagePlayingDidReset = totalEvents++; + public static final int messagePlayingPlayStateChanged = totalEvents++; + public static final int messagePlayingDidStarted = totalEvents++; public static final int recordProgressChanged = totalEvents++; public static final int recordStarted = totalEvents++; public static final int recordStartError = totalEvents++; @@ -116,7 +121,6 @@ public class NotificationCenter { public static final int screenshotTook = totalEvents++; public static final int albumsDidLoaded = totalEvents++; public static final int audioDidSent = totalEvents++; - public static final int audioDidStarted = totalEvents++; public static final int audioRouteChanged = totalEvents++; public static final int didStartedCall = totalEvents++; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java index e2e6f1682..9d0fedfc0 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java @@ -928,6 +928,8 @@ public class NotificationsController { msg = LocaleController.formatString("NotificationMessageGame", R.string.NotificationMessageGame, name, messageObject.messageOwner.media.game.title); } else if (messageObject.isVoice()) { msg = LocaleController.formatString("NotificationMessageAudio", R.string.NotificationMessageAudio, name); + } else if (messageObject.isRoundVideo()) { + msg = LocaleController.formatString("NotificationMessageRound", R.string.NotificationMessageRound, name); } else if (messageObject.isMusic()) { msg = LocaleController.formatString("NotificationMessageMusic", R.string.NotificationMessageMusic, name); } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaContact) { @@ -1086,6 +1088,12 @@ public class NotificationsController { } else { msg = LocaleController.formatString("NotificationActionPinnedVoiceChannel", R.string.NotificationActionPinnedVoiceChannel, chat.title); } + } else if (object.isRoundVideo()) { + if (!ChatObject.isChannel(chat) || chat.megagroup) { + msg = LocaleController.formatString("NotificationActionPinnedRound", R.string.NotificationActionPinnedRound, name, chat.title); + } else { + msg = LocaleController.formatString("NotificationActionPinnedRoundChannel", R.string.NotificationActionPinnedRoundChannel, chat.title); + } } else if (object.isSticker()) { String emoji = messageObject.getStickerEmoji(); if (emoji != null) { @@ -1192,6 +1200,8 @@ public class NotificationsController { } } else if (messageObject.isVoice()) { msg = LocaleController.formatString("ChannelMessageAudio", R.string.ChannelMessageAudio, name); + } else if (messageObject.isRoundVideo()) { + msg = LocaleController.formatString("ChannelMessageRound", R.string.ChannelMessageRound, name); } else if (messageObject.isMusic()) { msg = LocaleController.formatString("ChannelMessageMusic", R.string.ChannelMessageMusic, name); } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaContact) { @@ -1241,6 +1251,8 @@ public class NotificationsController { } } else if (messageObject.isVoice()) { msg = LocaleController.formatString("ChannelMessageGroupAudio", R.string.ChannelMessageGroupAudio, name, chat.title); + } else if (messageObject.isRoundVideo()) { + msg = LocaleController.formatString("ChannelMessageGroupRound", R.string.ChannelMessageGroupRound, name, chat.title); } else if (messageObject.isMusic()) { msg = LocaleController.formatString("ChannelMessageGroupMusic", R.string.ChannelMessageGroupMusic, name, chat.title); } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaContact) { @@ -1291,6 +1303,8 @@ public class NotificationsController { } } else if (messageObject.isVoice()) { msg = LocaleController.formatString("NotificationMessageGroupAudio", R.string.NotificationMessageGroupAudio, name, chat.title); + } else if (messageObject.isRoundVideo()) { + msg = LocaleController.formatString("NotificationMessageGroupRound", R.string.NotificationMessageGroupRound, name, chat.title); } else if (messageObject.isMusic()) { msg = LocaleController.formatString("NotificationMessageGroupMusic", R.string.NotificationMessageGroupMusic, name, chat.title); } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaContact) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java index 6f8373ebf..86ec13634 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java @@ -65,7 +65,7 @@ public class SecretChatHelper { } } - public static final int CURRENT_SECRET_CHAT_LAYER = 46; + public static final int CURRENT_SECRET_CHAT_LAYER = 66; private ArrayList sendingNotifyLayer = new ArrayList<>(); private HashMap> secretHolesQueue = new HashMap<>(); @@ -192,13 +192,12 @@ public class SecretChatHelper { dialog.unread_count = 0; dialog.top_message = 0; dialog.last_message_date = update.date; - + MessagesController.getInstance().putEncryptedChat(newChat, false); AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { MessagesController.getInstance().dialogs_dict.put(dialog.id, dialog); MessagesController.getInstance().dialogs.add(dialog); - MessagesController.getInstance().putEncryptedChat(newChat, false); MessagesController.getInstance().sortDialogs(null); NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); } @@ -519,22 +518,9 @@ public class SecretChatHelper { } } - /*String fileName = audio.dc_id + "_" + audio.id + ".ogg"; TODO check - String fileName2 = newMsg.media.audio.dc_id + "_" + newMsg.media.audio.id + ".ogg"; - if (!fileName.equals(fileName2)) { - File cacheFile = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName); - File cacheFile2 = FileLoader.getPathToAttach(newMsg.media.audio); - if (cacheFile.renameTo(cacheFile2)) { - newMsg.attachPath = ""; - } - }*/ - ArrayList arr = new ArrayList<>(); arr.add(newMsg); MessagesStorage.getInstance().putMessages(arr, false, true, false, 0); - - //MessagesStorage.getInstance().putSentFile(originalPath, newMsg.media.document, 4); document - //MessagesStorage.getInstance().putSentFile(originalPath, newMsg.media.video, 5); video } } } @@ -569,13 +555,14 @@ public class SecretChatHelper { if (chat.seq_in == 0 && chat.seq_out == 0) { if (chat.admin_id == UserConfig.getClientUserId()) { chat.seq_out = 1; + chat.seq_in = -2; } else { - chat.seq_in = 1; + chat.seq_in = -1; } } if (newMsgObj.seq_in == 0 && newMsgObj.seq_out == 0) { - layer.in_seq_no = chat.seq_in; + layer.in_seq_no = chat.seq_in > 0 ? chat.seq_in : chat.seq_in + 2; layer.out_seq_no = chat.seq_out; chat.seq_out += 2; if (AndroidUtilities.getPeerLayerVersion(chat.layer) >= 20) { @@ -719,7 +706,7 @@ public class SecretChatHelper { newMsgObj.send_state = MessageObject.MESSAGE_SEND_STATE_SENT; NotificationCenter.getInstance().postNotificationName(NotificationCenter.messageReceivedByServer, newMsgObj.id, newMsgObj.id, newMsgObj, newMsgObj.dialog_id); SendMessagesHelper.getInstance().processSentMessage(newMsgObj.id); - if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj)) { + if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj) || MessageObject.isRoundVideoMessage(newMsgObj)) { SendMessagesHelper.getInstance().stopVideoService(attachPath); } SendMessagesHelper.getInstance().removeFromSendingMessages(newMsgObj.id); @@ -735,7 +722,7 @@ public class SecretChatHelper { newMsgObj.send_state = MessageObject.MESSAGE_SEND_STATE_SEND_ERROR; NotificationCenter.getInstance().postNotificationName(NotificationCenter.messageSendError, newMsgObj.id); SendMessagesHelper.getInstance().processSentMessage(newMsgObj.id); - if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj)) { + if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj) || MessageObject.isRoundVideoMessage(newMsgObj)) { SendMessagesHelper.getInstance().stopVideoService(newMsgObj.attachPath); } SendMessagesHelper.getInstance().removeFromSendingMessages(newMsgObj.id); @@ -964,7 +951,7 @@ public class SecretChatHelper { newMessage.media.document.thumb.type = "s"; } newMessage.media.document.dc_id = file.dc_id; - if (MessageObject.isVoiceMessage(newMessage)) { + if (MessageObject.isVoiceMessage(newMessage) || MessageObject.isRoundVideoMessage(newMessage)) { newMessage.media_unread = true; } } else if (decryptedMessage.media instanceof TLRPC.TL_decryptedMessageMediaExternalDocument) { @@ -1456,8 +1443,9 @@ public class SecretChatHelper { if (chat.seq_in == 0 && chat.seq_out == 0) { if (chat.admin_id == UserConfig.getClientUserId()) { chat.seq_out = 1; + chat.seq_in = -2; } else { - chat.seq_in = 1; + chat.seq_in = -1; } } if (layer.random_bytes.length < 15) { @@ -1466,7 +1454,7 @@ public class SecretChatHelper { } FileLog.e("current chat in_seq = " + chat.seq_in + " out_seq = " + chat.seq_out); FileLog.e("got message with in_seq = " + layer.in_seq_no + " out_seq = " + layer.out_seq_no); - if (layer.out_seq_no < chat.seq_in) { + if (layer.out_seq_no <= chat.seq_in) { return null; } if (chat.seq_in != layer.out_seq_no && chat.seq_in != layer.out_seq_no - 2) { @@ -1590,13 +1578,13 @@ public class SecretChatHelper { if (encryptedChat.key_fingerprint == fingerprint) { encryptedChat.auth_key = authKey; encryptedChat.key_create_date = ConnectionsManager.getInstance().getCurrentTime(); - encryptedChat.seq_in = 0; + encryptedChat.seq_in = -2; encryptedChat.seq_out = 1; MessagesStorage.getInstance().updateEncryptedChat(encryptedChat); + MessagesController.getInstance().putEncryptedChat(encryptedChat, false); AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - MessagesController.getInstance().putEncryptedChat(encryptedChat, false); NotificationCenter.getInstance().postNotificationName(NotificationCenter.encryptedChatUpdated, encryptedChat); sendNotifyLayerMessage(encryptedChat, null); } @@ -1665,7 +1653,7 @@ public class SecretChatHelper { salt[a] = (byte) ((byte) (Utilities.random.nextDouble() * 256) ^ res.random[a]); } encryptedChat.a_or_b = salt; - encryptedChat.seq_in = 1; + encryptedChat.seq_in = -1; encryptedChat.seq_out = 0; BigInteger p = new BigInteger(1, MessagesStorage.secretPBytes); BigInteger g_b = BigInteger.valueOf(MessagesStorage.secretG); @@ -1726,10 +1714,10 @@ public class SecretChatHelper { newChat.key_use_count_in = encryptedChat.key_use_count_in; newChat.key_use_count_out = encryptedChat.key_use_count_out; MessagesStorage.getInstance().updateEncryptedChat(newChat); + MessagesController.getInstance().putEncryptedChat(newChat, false); AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - MessagesController.getInstance().putEncryptedChat(newChat, false); NotificationCenter.getInstance().postNotificationName(NotificationCenter.encryptedChatUpdated, newChat); sendNotifyLayerMessage(newChat, null); } @@ -1817,7 +1805,7 @@ public class SecretChatHelper { } TLRPC.EncryptedChat chat = (TLRPC.EncryptedChat) response; chat.user_id = chat.participant_id; - chat.seq_in = 0; + chat.seq_in = -2; chat.seq_out = 1; chat.a_or_b = salt; MessagesController.getInstance().putEncryptedChat(chat, false); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java index 65c27c250..05b92e724 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java @@ -12,6 +12,7 @@ import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; @@ -645,7 +646,23 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { webPage = messageObject.messageOwner.media.webpage; } - sendMessage(messageObject.messageOwner.message, did, messageObject.replyMessageObject, webPage, true, messageObject.messageOwner.entities, null, null); + ArrayList entities; + if (messageObject.messageOwner.entities != null && !messageObject.messageOwner.entities.isEmpty()) { + entities = new ArrayList<>(); + for (int a = 0; a < messageObject.messageOwner.entities.size(); a++) { + TLRPC.MessageEntity entity = messageObject.messageOwner.entities.get(a); + if (entity instanceof TLRPC.TL_messageEntityBold || + entity instanceof TLRPC.TL_messageEntityItalic || + entity instanceof TLRPC.TL_messageEntityPre || + entity instanceof TLRPC.TL_messageEntityCode || + entity instanceof TLRPC.TL_messageEntityTextUrl) { + entities.add(entity); + } + } + } else { + entities = null; + } + sendMessage(messageObject.messageOwner.message, did, messageObject.replyMessageObject, webPage, true, entities, null, null); } else if ((int) did != 0) { ArrayList arrayList = new ArrayList<>(); arrayList.add(messageObject); @@ -704,25 +721,34 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter SendMessagesHelper.getInstance().sendMessage((TLRPC.TL_document) document, null, null, peer, replyingMessageObject, null, null); } - public void sendMessage(ArrayList messages, final long peer) { + public int sendMessage(ArrayList messages, final long peer) { if (messages == null || messages.isEmpty()) { - return; + return 0; } int lower_id = (int) peer; + int sendResult = 0; if (lower_id != 0) { final TLRPC.Peer to_id = MessagesController.getPeer((int) peer); boolean isMegagroup = false; boolean isSignature = false; + boolean canSendStickers = true; + boolean canSendMedia = true; + boolean canSendPreview = true; if (lower_id > 0) { TLRPC.User sendToUser = MessagesController.getInstance().getUser(lower_id); if (sendToUser == null) { - return; + return 0; } } else { TLRPC.Chat chat = MessagesController.getInstance().getChat(-lower_id); if (ChatObject.isChannel(chat)) { isMegagroup = chat.megagroup; isSignature = chat.signatures; + if (chat.banned_rights != null) { + canSendStickers = !chat.banned_rights.send_stickers; + canSendMedia = !chat.banned_rights.send_media; + canSendPreview = !chat.banned_rights.embed_links; + } } } @@ -733,17 +759,30 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter HashMap messagesByRandomIds = new HashMap<>(); TLRPC.InputPeer inputPeer = MessagesController.getInputPeer(lower_id); long lastDialogId = 0; - final boolean toMyself = peer == UserConfig.getClientUserId(); + int myId = UserConfig.getClientUserId(); + final boolean toMyself = peer == myId; for (int a = 0; a < messages.size(); a++) { MessageObject msgObj = messages.get(a); if (msgObj.getId() <= 0) { continue; } + if (!canSendStickers && (msgObj.isSticker() || msgObj.isGif() || msgObj.isGame())) { + if (sendResult == 0) { + sendResult = 1; + } + continue; + } else if (!canSendMedia && (msgObj.messageOwner.media instanceof TLRPC.TL_messageMediaPhoto || msgObj.messageOwner.media instanceof TLRPC.TL_messageMediaDocument)) { + if (sendResult == 0) { + sendResult = 2; + } + continue; + } final TLRPC.Message newMsg = new TLRPC.TL_message(); if (msgObj.isForwarded()) { newMsg.fwd_from = msgObj.messageOwner.fwd_from; - } else { + newMsg.flags = TLRPC.MESSAGE_FLAG_FWD; + } else if (msgObj.getDialogId() != myId) { newMsg.fwd_from = new TLRPC.TL_messageFwdHeader(); if (msgObj.isFromUser()) { newMsg.fwd_from.from_id = msgObj.messageOwner.from_id; @@ -761,9 +800,13 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } newMsg.date = msgObj.messageOwner.date; + newMsg.flags = TLRPC.MESSAGE_FLAG_FWD; + } + if (!canSendPreview && msgObj.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { + newMsg.media = new TLRPC.TL_messageMediaEmpty(); + } else { + newMsg.media = msgObj.messageOwner.media; } - newMsg.media = msgObj.messageOwner.media; - newMsg.flags = TLRPC.MESSAGE_FLAG_FWD; if (newMsg.media != null) { newMsg.flags |= TLRPC.MESSAGE_FLAG_HAS_MEDIA; } @@ -816,7 +859,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } newMsg.dialog_id = peer; newMsg.to_id = to_id; - if (MessageObject.isVoiceMessage(newMsg) && newMsg.to_id.channel_id == 0) { + if ((MessageObject.isVoiceMessage(newMsg) || MessageObject.isRoundVideoMessage(newMsg)) && newMsg.to_id.channel_id == 0) { newMsg.media_unread = true; } if (msgObj.messageOwner.to_id instanceof TLRPC.TL_peerChannel) { @@ -939,7 +982,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter removeFromSendingMessages(oldId); } }); - if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj)) { + if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isRoundVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj)) { stopVideoService(newMsgObj.attachPath); } } @@ -965,7 +1008,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter newMsgObj.send_state = MessageObject.MESSAGE_SEND_STATE_SEND_ERROR; NotificationCenter.getInstance().postNotificationName(NotificationCenter.messageSendError, newMsgObj.id); processSentMessage(newMsgObj.id); - if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj)) { + if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isRoundVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj)) { stopVideoService(newMsgObj.attachPath); } removeFromSendingMessages(newMsgObj.id); @@ -989,6 +1032,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter processForwardFromMyName(messages.get(a), peer); } } + return sendResult; } public int editMessage(MessageObject messageObject, String message, boolean searchLinks, final BaseFragment fragment, ArrayList entities, final Runnable callback) { @@ -1323,7 +1367,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } else if (retryMessageObject.type == 1) { photo = (TLRPC.TL_photo) newMsg.media.photo; type = 2; - } else if (retryMessageObject.type == 3 || videoEditedInfo != null) { + } else if (retryMessageObject.type == 3 || retryMessageObject.type == 5 || videoEditedInfo != null) { type = 3; document = (TLRPC.TL_document) newMsg.media.document; } else if (retryMessageObject.type == 12) { @@ -1452,7 +1496,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter newMsg.media.document = document; if (params != null && params.containsKey("query_id")) { type = 9; - } else if (MessageObject.isVideoDocument(document) || videoEditedInfo != null) { + } else if (MessageObject.isVideoDocument(document) || MessageObject.isRoundVideoDocument(document) || videoEditedInfo != null) { type = 3; } else if (MessageObject.isVoiceDocument(document)) { type = 8; @@ -1614,7 +1658,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } newMsg.ttl = Math.max(encryptedChat.ttl, duration + 1); - } else if (MessageObject.isVideoMessage(newMsg)) { + } else if (MessageObject.isVideoMessage(newMsg) || MessageObject.isRoundVideoMessage(newMsg)) { int duration = 0; for (int a = 0; a < newMsg.media.document.attributes.size(); a++) { TLRPC.DocumentAttribute attribute = newMsg.media.document.attributes.get(a); @@ -1627,14 +1671,14 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } } - if (high_id != 1 && MessageObject.isVoiceMessage(newMsg) && newMsg.to_id.channel_id == 0) { + if (high_id != 1 && (MessageObject.isVoiceMessage(newMsg) || MessageObject.isRoundVideoMessage(newMsg)) && newMsg.to_id.channel_id == 0) { newMsg.media_unread = true; } newMsg.send_state = MessageObject.MESSAGE_SEND_STATE_SENDING; newMsgObj = new MessageObject(newMsg, null, true); newMsgObj.replyMessageObject = reply_to_msg; - if (!newMsgObj.isForwarded() && (newMsgObj.type == 3 || videoEditedInfo != null) && !TextUtils.isEmpty(newMsg.attachPath)) { + if (!newMsgObj.isForwarded() && (newMsgObj.type == 3 || videoEditedInfo != null || newMsgObj.type == 2) && !TextUtils.isEmpty(newMsg.attachPath)) { newMsgObj.attachPathExists = true; } @@ -1988,7 +2032,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } else if (type == 3) { ImageLoader.fillPhotoSizeWithBytes(document.thumb); - if (MessageObject.isNewGifDocument(document)) { + if (MessageObject.isNewGifDocument(document) || MessageObject.isRoundVideoDocument(document)) { reqSend.media = new TLRPC.TL_decryptedMessageMediaDocument(); reqSend.media.attributes = document.attributes; if (document.thumb != null && document.thumb.bytes != null) { @@ -2210,7 +2254,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } } else if (message.type == 1) { - if (message.videoEditedInfo != null) { + if (message.videoEditedInfo != null && message.videoEditedInfo.needConvert()) { String location = message.obj.messageOwner.attachPath; if (location == null) { location = FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE) + "/" + message.documentLocation.id + ".mp4"; @@ -2218,6 +2262,25 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter putToDelayedMessages(location, message); MediaController.getInstance().scheduleVideoConvert(message.obj); } else { + if (message.videoEditedInfo != null) { + if (message.videoEditedInfo.file != null) { + TLRPC.InputMedia media; + if (message.sendRequest instanceof TLRPC.TL_messages_sendMedia) { + media = ((TLRPC.TL_messages_sendMedia) message.sendRequest).media; + } else { + media = ((TLRPC.TL_messages_sendBroadcast) message.sendRequest).media; + } + media.file = message.videoEditedInfo.file; + message.videoEditedInfo.file = null; + } else if (message.videoEditedInfo.encryptedFile != null) { + message.sendEncryptedRequest.media.size = (int) message.videoEditedInfo.estimatedSize; + message.sendEncryptedRequest.media.key = message.videoEditedInfo.key; + message.sendEncryptedRequest.media.iv = message.videoEditedInfo.iv; + SecretChatHelper.getInstance().performSendEncryptedRequest(message.sendEncryptedRequest, message.obj.messageOwner, message.encryptedChat, message.videoEditedInfo.encryptedFile, message.originalPath, message.obj); + message.videoEditedInfo.encryptedFile = null; + return; + } + } if (message.sendRequest != null) { TLRPC.InputMedia media; if (message.sendRequest instanceof TLRPC.TL_messages_sendMedia) { @@ -2231,7 +2294,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter location = FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE) + "/" + message.documentLocation.id + ".mp4"; } putToDelayedMessages(location, message); - if (message.obj.videoEditedInfo != null) { + if (message.obj.videoEditedInfo != null && message.obj.videoEditedInfo.needConvert()) { FileLoader.getInstance().uploadFile(location, false, false, message.documentLocation.size, ConnectionsManager.FileTypeVideo); } else { FileLoader.getInstance().uploadFile(location, false, false, ConnectionsManager.FileTypeVideo); @@ -2255,7 +2318,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } putToDelayedMessages(location, message); - if (message.obj.videoEditedInfo != null) { + if (message.obj.videoEditedInfo != null && message.obj.videoEditedInfo.needConvert()) { FileLoader.getInstance().uploadFile(location, true, false, message.documentLocation.size, ConnectionsManager.FileTypeVideo); } else { FileLoader.getInstance().uploadFile(location, true, false, ConnectionsManager.FileTypeVideo); @@ -2426,7 +2489,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter if (!isSentError) { StatsController.getInstance().incrementSentItemsCount(ConnectionsManager.getCurrentNetworkType(), StatsController.TYPE_MESSAGES, 1); newMsgObj.send_state = MessageObject.MESSAGE_SEND_STATE_SENT; - NotificationCenter.getInstance().postNotificationName(NotificationCenter.messageReceivedByServer, oldId, (isBroadcast ? oldId : newMsgObj.id), newMsgObj, newMsgObj.dialog_id); //TODO remove later? + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messageReceivedByServer, oldId, (isBroadcast ? oldId : newMsgObj.id), newMsgObj, newMsgObj.dialog_id); MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override public void run() { @@ -2456,7 +2519,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter removeFromSendingMessages(oldId); } }); - if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj)) { + if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isRoundVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj)) { stopVideoService(attachPath); } } @@ -2471,7 +2534,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter newMsgObj.send_state = MessageObject.MESSAGE_SEND_STATE_SEND_ERROR; NotificationCenter.getInstance().postNotificationName(NotificationCenter.messageSendError, newMsgObj.id); processSentMessage(newMsgObj.id); - if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj)) { + if (MessageObject.isVideoMessage(newMsgObj) || MessageObject.isRoundVideoMessage(newMsgObj) || MessageObject.isNewGifMessage(newMsgObj)) { stopVideoService(newMsgObj.attachPath); } removeFromSendingMessages(newMsgObj.id); @@ -2545,7 +2608,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter if (MessageObject.isVideoMessage(sentMessage)) { MessagesStorage.getInstance().putSentFile(originalPath, sentMessage.media.document, 2); sentMessage.attachPath = newMsg.attachPath; - } else if (!MessageObject.isVoiceMessage(sentMessage)) { + } else if (!MessageObject.isVoiceMessage(sentMessage) && !MessageObject.isRoundVideoMessage(sentMessage)) { MessagesStorage.getInstance().putSentFile(originalPath, sentMessage.media.document, 1); } @@ -2704,7 +2767,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } - private static boolean prepareSendingDocumentInternal(String path, String originalPath, Uri uri, String mime, final long dialog_id, final MessageObject reply_to_msg, String caption) { + private static boolean prepareSendingDocumentInternal(String path, String originalPath, Uri uri, String mime, final long dialog_id, final MessageObject reply_to_msg, CharSequence caption) { if ((path == null || path.length() == 0) && uri == null) { return false; } @@ -2750,16 +2813,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter if (ext.toLowerCase().equals("mp3") || ext.toLowerCase().equals("m4a")) { AudioInfo audioInfo = AudioInfo.getAudioInfo(f); if (audioInfo != null && audioInfo.getDuration() != 0) { - if (isEncrypted) { - int high_id = (int) (dialog_id >> 32); - TLRPC.EncryptedChat encryptedChat = MessagesController.getInstance().getEncryptedChat(high_id); - if (encryptedChat == null) { - return false; - } - attributeAudio = new TLRPC.TL_documentAttributeAudio(); - } else { - attributeAudio = new TLRPC.TL_documentAttributeAudio(); - } + attributeAudio = new TLRPC.TL_documentAttributeAudio(); attributeAudio.duration = (int) (audioInfo.getDuration() / 1000); attributeAudio.title = audioInfo.getTitle(); attributeAudio.performer = audioInfo.getArtist(); @@ -2856,7 +2910,11 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter document.thumb.type = "s"; } } - document.caption = caption; + if (caption != null) { + document.caption = caption.toString(); + } else { + document.caption = ""; + } final HashMap params = new HashMap<>(); if (originalPath != null) { @@ -3003,7 +3061,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter masks = new ArrayList<>(); masks.add(new ArrayList<>(stickers)); } - prepareSendingPhotos(paths, uris, dialog_id, reply_to_msg, captions, masks, inputContent); + prepareSendingPhotos(paths, uris, dialog_id, reply_to_msg, captions, masks, inputContent, false); } public static void prepareSendingBotContextResult(final TLRPC.BotInlineResult result, final HashMap params, final long dialog_id, final MessageObject reply_to_msg) { @@ -3459,7 +3517,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter }); } - public static void prepareSendingPhotos(ArrayList paths, ArrayList uris, final long dialog_id, final MessageObject reply_to_msg, final ArrayList captions, final ArrayList> masks, final InputContentInfoCompat inputContent) { + public static void prepareSendingPhotos(ArrayList paths, ArrayList uris, final long dialog_id, final MessageObject reply_to_msg, final ArrayList captions, final ArrayList> masks, final InputContentInfoCompat inputContent, final boolean forceDocument) { if (paths == null && uris == null || paths != null && paths.isEmpty() || uris != null && uris.isEmpty()) { return; } @@ -3498,7 +3556,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } boolean isDocument = false; - if (tempPath != null && (tempPath.endsWith(".gif") || tempPath.endsWith(".webp"))) { + if (forceDocument) { + isDocument = true; + extension = FileLoader.getFileExtension(new File(tempPath)); + } else if (tempPath != null && (tempPath.endsWith(".gif") || tempPath.endsWith(".webp"))) { if (tempPath.endsWith(".gif")) { extension = "gif"; } else { @@ -3519,6 +3580,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } + if (isDocument) { if (sendAsDocuments == null) { sendAsDocuments = new ArrayList<>(); @@ -3654,7 +3716,38 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } - public static void prepareSendingVideo(final String videoPath, final long estimatedSize, final long duration, final int width, final int height, final VideoEditedInfo videoEditedInfo, final long dialog_id, final MessageObject reply_to_msg, final String caption) { + private static Bitmap createVideoThumbnail(String filePath, long time) { + Bitmap bitmap = null; + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + retriever.setDataSource(filePath); + bitmap = retriever.getFrameAtTime(time, MediaMetadataRetriever.OPTION_NEXT_SYNC); + } catch (Exception ignore) { + // Assume this is a corrupt video file. + } finally { + try { + retriever.release(); + } catch (RuntimeException ex) { + // Ignore failures while cleaning up. + } + } + + if (bitmap == null) return null; + + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int max = Math.max(width, height); + if (max > 90) { + float scale = 90.0f / max; + int w = Math.round(scale * width); + int h = Math.round(scale * height); + bitmap = Bitmaps.createScaledBitmap(bitmap, w, h, true); + } + + return bitmap; + } + + public static void prepareSendingVideo(final String videoPath, final long estimatedSize, final long duration, final int width, final int height, final VideoEditedInfo videoEditedInfo, final long dialog_id, final MessageObject reply_to_msg, final CharSequence caption) { if (videoPath == null || videoPath.length() == 0) { return; } @@ -3664,24 +3757,49 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter boolean isEncrypted = (int) dialog_id == 0; - if (videoEditedInfo != null || videoPath.endsWith("mp4")) { + boolean isRound = videoEditedInfo != null && videoEditedInfo.roundVideo; + Bitmap thumb = null; + String thumbKey = null; + + if (videoEditedInfo != null || videoPath.endsWith("mp4") || isRound) { String path = videoPath; String originalPath = videoPath; File temp = new File(originalPath); + long startTime = 0; + originalPath += temp.length() + "_" + temp.lastModified(); if (videoEditedInfo != null) { - originalPath += duration + "_" + videoEditedInfo.startTime + "_" + videoEditedInfo.endTime; - if (videoEditedInfo.resultWidth == videoEditedInfo.originalWidth) { - originalPath += "_" + videoEditedInfo.resultWidth; + if (!isRound) { + originalPath += duration + "_" + videoEditedInfo.startTime + "_" + videoEditedInfo.endTime; + if (videoEditedInfo.resultWidth == videoEditedInfo.originalWidth) { + originalPath += "_" + videoEditedInfo.resultWidth; + } } + startTime = videoEditedInfo.startTime >= 0 ? videoEditedInfo.startTime : 0; } TLRPC.TL_document document = null; if (!isEncrypted) { - //document = (TLRPC.TL_document) MessagesStorage.getInstance().getSentFile(originalPath, !isEncrypted ? 2 : 5); + document = (TLRPC.TL_document) MessagesStorage.getInstance().getSentFile(originalPath, !isEncrypted ? 2 : 5); } if (document == null) { - Bitmap thumb = ThumbnailUtils.createVideoThumbnail(videoPath, MediaStore.Video.Thumbnails.MINI_KIND); + thumb = createVideoThumbnail(videoPath, startTime); + if (thumb == null) { + thumb = ThumbnailUtils.createVideoThumbnail(videoPath, MediaStore.Video.Thumbnails.MINI_KIND); + } TLRPC.PhotoSize size = ImageLoader.scaleAndSaveImage(thumb, 90, 90, 55, isEncrypted); + if (thumb != null && size !=null) { + if (isRound) { + if (isEncrypted) { + Utilities.blurBitmap(thumb, 7, Build.VERSION.SDK_INT < 21 ? 0 : 1, thumb.getWidth(), thumb.getHeight(), thumb.getRowBytes()); + thumbKey = String.format(size.location.volume_id + "_" + size.location.local_id + "@%d_%d_b2", (int) (AndroidUtilities.roundMessageSize / AndroidUtilities.density), (int) (AndroidUtilities.roundMessageSize / AndroidUtilities.density)); + } else { + Utilities.blurBitmap(thumb, 3, Build.VERSION.SDK_INT < 21 ? 0 : 1, thumb.getWidth(), thumb.getHeight(), thumb.getRowBytes()); + thumbKey = String.format(size.location.volume_id + "_" + size.location.local_id + "@%d_%d_b", (int) (AndroidUtilities.roundMessageSize / AndroidUtilities.density), (int) (AndroidUtilities.roundMessageSize / AndroidUtilities.density)); + } + } else { + thumb = null; + } + } document = new TLRPC.TL_document(); document.thumb = size; if (document.thumb == null) { @@ -3692,14 +3810,31 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } document.mime_type = "video/mp4"; UserConfig.saveConfig(false); - TLRPC.TL_documentAttributeVideo attributeVideo = new TLRPC.TL_documentAttributeVideo(); + TLRPC.TL_documentAttributeVideo attributeVideo; + if (isEncrypted) { + int high_id = (int) (dialog_id >> 32); + TLRPC.EncryptedChat encryptedChat = MessagesController.getInstance().getEncryptedChat(high_id); + if (encryptedChat == null) { + return; + } + if (AndroidUtilities.getPeerLayerVersion(encryptedChat.layer) >= 66) { + attributeVideo = new TLRPC.TL_documentAttributeVideo(); + } else { + attributeVideo = new TLRPC.TL_documentAttributeVideo_layer65(); + } + } else { + attributeVideo = new TLRPC.TL_documentAttributeVideo(); + } + attributeVideo.round_message = isRound; document.attributes.add(attributeVideo); - if (videoEditedInfo != null) { - if (videoEditedInfo.bitrate == -1) { + if (videoEditedInfo != null && videoEditedInfo.needConvert()) { + if (videoEditedInfo.muted) { document.attributes.add(new TLRPC.TL_documentAttributeAnimated()); fillVideoAttribute(videoPath, attributeVideo, videoEditedInfo); - videoEditedInfo.originalWidth = videoEditedInfo.resultWidth = attributeVideo.w; - videoEditedInfo.originalHeight = videoEditedInfo.resultHeight = attributeVideo.h; + videoEditedInfo.originalWidth = attributeVideo.w; + videoEditedInfo.originalHeight = attributeVideo.h; + attributeVideo.w = videoEditedInfo.resultWidth; + attributeVideo.h = videoEditedInfo.resultHeight; } else { attributeVideo.duration = (int) (duration / 1000); if (videoEditedInfo.rotationValue == 90 || videoEditedInfo.rotationValue == 270) { @@ -3727,13 +3862,22 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter final String originalPathFinal = originalPath; final String finalPath = path; final HashMap params = new HashMap<>(); - videoFinal.caption = caption; + final Bitmap thumbFinal = thumb; + final String thumbKeyFinal = thumbKey; + if (caption != null) { + videoFinal.caption = caption.toString(); + } else { + videoFinal.caption = ""; + } if (originalPath != null) { params.put("originalPath", originalPath); } AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { + if (thumbFinal != null && thumbKeyFinal != null) { + ImageLoader.getInstance().putImageToCache(new BitmapDrawable(thumbFinal), thumbKeyFinal); + } SendMessagesHelper.getInstance().sendMessage(videoFinal, videoEditedInfo, finalPath, dialog_id, reply_to_msg, null, params); } }); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java b/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java index 0b124f244..d91d2a6cf 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java @@ -38,6 +38,7 @@ public class UserConfig { public static int autoLockIn = 60 * 60; public static boolean allowScreenCapture; public static int lastPauseTime; + public static long lastAppPauseTime; public static boolean isWaitingForPasscodeEnter; public static boolean useFingerprint = true; public static String lastUpdateVersion; @@ -55,6 +56,14 @@ public class UserConfig { public static int migrateOffsetChannelId = -1; public static long migrateOffsetAccess = -1; + public static int totalDialogsLoadCount = 0; + public static int dialogsLoadOffsetId = 0; + public static int dialogsLoadOffsetDate = 0; + public static int dialogsLoadOffsetUserId = 0; + public static int dialogsLoadOffsetChatId = 0; + public static int dialogsLoadOffsetChannelId = 0; + public static long dialogsLoadOffsetAccess = 0; + public static int getNewMessageId() { int id; synchronized (sync) { @@ -87,6 +96,7 @@ public class UserConfig { editor.putInt("passcodeType", passcodeType); editor.putInt("autoLockIn", autoLockIn); editor.putInt("lastPauseTime", lastPauseTime); + editor.putLong("lastAppPauseTime", lastAppPauseTime); editor.putString("lastUpdateVersion2", lastUpdateVersion); editor.putInt("lastContactsSyncTime", lastContactsSyncTime); editor.putBoolean("useFingerprint", useFingerprint); @@ -96,14 +106,24 @@ public class UserConfig { editor.putBoolean("allowScreenCapture", allowScreenCapture); editor.putBoolean("pinnedDialogsLoaded", pinnedDialogsLoaded); - editor.putInt("migrateOffsetId", migrateOffsetId); + editor.putInt("3migrateOffsetId", migrateOffsetId); if (migrateOffsetId != -1) { - editor.putInt("migrateOffsetDate", migrateOffsetDate); - editor.putInt("migrateOffsetUserId", migrateOffsetUserId); - editor.putInt("migrateOffsetChatId", migrateOffsetChatId); - editor.putInt("migrateOffsetChannelId", migrateOffsetChannelId); - editor.putLong("migrateOffsetAccess", migrateOffsetAccess); + editor.putInt("3migrateOffsetDate", migrateOffsetDate); + editor.putInt("3migrateOffsetUserId", migrateOffsetUserId); + editor.putInt("3migrateOffsetChatId", migrateOffsetChatId); + editor.putInt("3migrateOffsetChannelId", migrateOffsetChannelId); + editor.putLong("3migrateOffsetAccess", migrateOffsetAccess); } + + + editor.putInt("2totalDialogsLoadCount", totalDialogsLoadCount); + editor.putInt("2dialogsLoadOffsetId", dialogsLoadOffsetId); + editor.putInt("2dialogsLoadOffsetDate", dialogsLoadOffsetDate); + editor.putInt("2dialogsLoadOffsetUserId", dialogsLoadOffsetUserId); + editor.putInt("2dialogsLoadOffsetChatId", dialogsLoadOffsetChatId); + editor.putInt("2dialogsLoadOffsetChannelId", dialogsLoadOffsetChannelId); + editor.putLong("2dialogsLoadOffsetAccess", dialogsLoadOffsetAccess); + if (tmpPassword != null) { SerializedData data = new SerializedData(); tmpPassword.serializeToStream(data); @@ -236,6 +256,7 @@ public class UserConfig { passcodeType = preferences.getInt("passcodeType", 0); autoLockIn = preferences.getInt("autoLockIn", 60 * 60); lastPauseTime = preferences.getInt("lastPauseTime", 0); + lastAppPauseTime = preferences.getLong("lastAppPauseTime", 0); useFingerprint = preferences.getBoolean("useFingerprint", true); lastUpdateVersion = preferences.getString("lastUpdateVersion2", "3.5"); lastContactsSyncTime = preferences.getInt("lastContactsSyncTime", (int) (System.currentTimeMillis() / 1000) - 23 * 60 * 60); @@ -249,14 +270,36 @@ public class UserConfig { lastPauseTime = (int) (System.currentTimeMillis() / 1000 - 60 * 10); } - migrateOffsetId = preferences.getInt("migrateOffsetId", 0); + migrateOffsetId = preferences.getInt("3migrateOffsetId", 0); if (migrateOffsetId != -1) { - migrateOffsetDate = preferences.getInt("migrateOffsetDate", 0); - migrateOffsetUserId = preferences.getInt("migrateOffsetUserId", 0); - migrateOffsetChatId = preferences.getInt("migrateOffsetChatId", 0); - migrateOffsetChannelId = preferences.getInt("migrateOffsetChannelId", 0); - migrateOffsetAccess = preferences.getLong("migrateOffsetAccess", 0); + migrateOffsetDate = preferences.getInt("3migrateOffsetDate", 0); + migrateOffsetUserId = preferences.getInt("3migrateOffsetUserId", 0); + migrateOffsetChatId = preferences.getInt("3migrateOffsetChatId", 0); + migrateOffsetChannelId = preferences.getInt("3migrateOffsetChannelId", 0); + migrateOffsetAccess = preferences.getLong("3migrateOffsetAccess", 0); } +// migrateOffsetId = 0; +// migrateOffsetDate = 0; +// migrateOffsetUserId = 0; +// migrateOffsetChatId = 0; +// migrateOffsetChannelId = 0; +// migrateOffsetAccess = 0; + + dialogsLoadOffsetId = preferences.getInt("2dialogsLoadOffsetId", -1); + totalDialogsLoadCount = preferences.getInt("2totalDialogsLoadCount", 0); + dialogsLoadOffsetDate = preferences.getInt("2dialogsLoadOffsetDate", -1); + dialogsLoadOffsetUserId = preferences.getInt("2dialogsLoadOffsetUserId", -1); + dialogsLoadOffsetChatId = preferences.getInt("2dialogsLoadOffsetChatId", -1); + dialogsLoadOffsetChannelId = preferences.getInt("2dialogsLoadOffsetChannelId", -1); + dialogsLoadOffsetAccess = preferences.getLong("2dialogsLoadOffsetAccess", -1); + +// dialogsLoadOffsetId = -1; +// totalDialogsLoadCount = 0; +// dialogsLoadOffsetDate = -1; +// dialogsLoadOffsetUserId = -1; +// dialogsLoadOffsetChatId = -1; +// dialogsLoadOffsetChannelId = -1; +// dialogsLoadOffsetAccess = -1; String string = preferences.getString("tmpPassword", null); if (string != null) { @@ -413,6 +456,13 @@ public class UserConfig { migrateOffsetChatId = -1; migrateOffsetChannelId = -1; migrateOffsetAccess = -1; + dialogsLoadOffsetId = 0; + totalDialogsLoadCount = 0; + dialogsLoadOffsetDate = 0; + dialogsLoadOffsetUserId = 0; + dialogsLoadOffsetChatId = 0; + dialogsLoadOffsetChannelId = 0; + dialogsLoadOffsetAccess = 0; appLocked = false; passcodeType = 0; passcodeHash = ""; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java b/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java index 0f52c7cff..64ddc3274 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java @@ -52,6 +52,7 @@ public class Utilities { public native static boolean loadWebpImage(Bitmap bitmap, ByteBuffer buffer, int len, BitmapFactory.Options options, boolean unpin); public native static int convertVideoFrame(ByteBuffer src, ByteBuffer dest, int destFormat, int width, int height, int padding, int swap); private native static void aesIgeEncryption(ByteBuffer buffer, byte[] key, byte[] iv, boolean encrypt, int offset, int length); + public native static void aesCtrDecryption(ByteBuffer buffer, byte[] key, byte[] iv, int offset, int length); public native static String readlink(String path); public static void aesIgeEncryption(ByteBuffer buffer, byte[] key, byte[] iv, boolean encrypt, boolean changeIv, int offset, int length) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java index df50bc923..2374b30fd 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java @@ -8,6 +8,8 @@ package org.telegram.messenger; +import org.telegram.tgnet.TLRPC; + import java.util.Locale; public class VideoEditedInfo { @@ -22,6 +24,12 @@ public class VideoEditedInfo { public String originalPath; public long estimatedSize; public long estimatedDuration; + public boolean roundVideo; + public boolean muted; + public TLRPC.InputFile file; + public TLRPC.InputEncryptedFile encryptedFile; + public byte[] key; + public byte[] iv; public String getString() { return String.format(Locale.US, "-1_%d_%d_%d_%d_%d_%d_%d_%d_%s", startTime, endTime, rotationValue, originalWidth, originalHeight, bitrate, resultWidth, resultHeight, originalPath); @@ -56,4 +64,8 @@ public class VideoEditedInfo { } return false; } + + public boolean needConvert() { + return !roundVideo || roundVideo && (startTime > 0 || endTime != -1 && endTime != estimatedDuration); + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/WearDataLayerListenerService.java b/TMessagesProj/src/main/java/org/telegram/messenger/WearDataLayerListenerService.java new file mode 100644 index 000000000..c8343cb0c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/WearDataLayerListenerService.java @@ -0,0 +1,164 @@ +package org.telegram.messenger; + +import android.text.TextUtils; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.wearable.Channel; +import com.google.android.gms.wearable.Wearable; +import com.google.android.gms.wearable.WearableListenerService; + +import org.json.JSONObject; +import org.telegram.tgnet.TLRPC; + +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by grishka on 16.06.17. + */ + +public class WearDataLayerListenerService extends WearableListenerService{ + + + @Override + public void onCreate(){ + super.onCreate(); + } + + @Override + public void onDestroy(){ + super.onDestroy(); + } + + @Override + public void onChannelOpened(final Channel ch){ + new Thread(new Runnable(){ + @Override + public void run(){ + GoogleApiClient apiClient=new GoogleApiClient.Builder(WearDataLayerListenerService.this).addApi(Wearable.API).build(); + if(!apiClient.blockingConnect().isSuccess()){ + FileLog.e("failed to connect google api client"); + return; + } + String path=ch.getPath(); + FileLog.d("wear channel path: "+path); + try{ + if("/getCurrentUser".equals(path)){ + DataOutputStream out=new DataOutputStream(new BufferedOutputStream(ch.getOutputStream(apiClient).await().getOutputStream())); + if(UserConfig.isClientActivated()){ + final TLRPC.User user=UserConfig.getCurrentUser(); + out.writeInt(user.id); + out.writeUTF(user.first_name); + out.writeUTF(user.last_name); + out.writeUTF(user.phone); + if(user.photo!=null){ + final File photo=FileLoader.getPathToAttach(user.photo.photo_small, true); + final CyclicBarrier barrier=new CyclicBarrier(2); + if(!photo.exists()){ + final NotificationCenter.NotificationCenterDelegate listener=new NotificationCenter.NotificationCenterDelegate(){ + @Override + public void didReceivedNotification(int id, Object... args){ + if(id==NotificationCenter.FileDidLoaded){ + FileLog.d("file loaded: "+args[0]+" "+args[0].getClass().getName()); + if(args[0].equals(photo.getName())){ + FileLog.e("LOADED USER PHOTO"); + try{barrier.await(10, TimeUnit.MILLISECONDS);}catch(Exception x){} + } + } + } + }; + AndroidUtilities.runOnUIThread(new Runnable(){ + @Override + public void run(){ + NotificationCenter.getInstance().addObserver(listener, NotificationCenter.FileDidLoaded); + FileLoader.getInstance().loadFile(user.photo.photo_small, null, 0, true); + } + }); + try{barrier.await(10, TimeUnit.SECONDS);}catch(Exception x){} + AndroidUtilities.runOnUIThread(new Runnable(){ + @Override + public void run(){ + NotificationCenter.getInstance().removeObserver(listener, NotificationCenter.FileDidLoaded); + } + }); + } + if(photo.exists() && photo.length()<=50*1024*1024){ + byte[] photoData=new byte[(int)photo.length()]; + FileInputStream photoIn=new FileInputStream(photo); + new DataInputStream(photoIn).readFully(photoData); + photoIn.close(); + out.writeInt(photoData.length); + out.write(photoData); + }else{ + out.writeInt(0); + } + }else{ + out.writeInt(0); + } + }else{ + out.writeInt(0); + } + out.flush(); + out.close(); + }else if("/waitForAuthCode".equals(path)){ + final String[] code={null}; + final CyclicBarrier barrier=new CyclicBarrier(2); + final NotificationCenter.NotificationCenterDelegate listener=new NotificationCenter.NotificationCenterDelegate(){ + @Override + public void didReceivedNotification(int id, Object... args){ + if(id==NotificationCenter.didReceivedNewMessages){ + long did = (Long) args[0]; + if(did==777000){ + ArrayList arr = (ArrayList) args[1]; + if(arr.size()>0){ + MessageObject msg=arr.get(0); + if(!TextUtils.isEmpty(msg.messageText)){ + Matcher matcher=Pattern.compile("[0-9]+").matcher(msg.messageText); + if(matcher.find()){ + code[0]=matcher.group(); + try{barrier.await(10, TimeUnit.MILLISECONDS);}catch(Exception x){} + } + } + } + } + } + } + }; + AndroidUtilities.runOnUIThread(new Runnable(){ + @Override + public void run(){ + NotificationCenter.getInstance().addObserver(listener, NotificationCenter.didReceivedNewMessages); + } + }); + try{barrier.await(10, TimeUnit.SECONDS);}catch(Exception x){} + AndroidUtilities.runOnUIThread(new Runnable(){ + @Override + public void run(){ + NotificationCenter.getInstance().removeObserver(listener, NotificationCenter.didReceivedNewMessages); + } + }); + DataOutputStream out=new DataOutputStream(ch.getOutputStream(apiClient).await().getOutputStream()); + if(code!=null) + out.writeUTF(code[0]); + else + out.writeUTF(""); + out.flush(); + out.close(); + } + }catch(Exception x){ + FileLog.e("error processing wear request", x); + } + ch.close(apiClient).await(); + apiClient.disconnect(); + } + }).start(); + } +} 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 ef3469b19..b98fc84ab 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java @@ -196,6 +196,14 @@ public class Browser { public static boolean isInternalUri(Uri uri) { String host = uri.getHost(); host = host != null ? host.toLowerCase() : ""; - return "tg".equals(uri.getScheme()) || "telegram.me".equals(host) || "t.me".equals(host) || "telegram.dog".equals(host); + if ("tg".equals(uri.getScheme())) { + return true; + } else if ("telegram.me".equals(host) || "t.me".equals(host) || "telegram.dog".equals(host) || "telesco.pe".equals(host)) { + String path = uri.getPath(); + if (path != null && path.length() > 1) { + return true; + } + } + return false; } } 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 0cb970d8a..4365545ae 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraController.java @@ -53,6 +53,7 @@ public class CameraController implements MediaRecorder.OnInfoListener { private VideoTakeCallback onVideoTakeCallback; private boolean recordingSmallVideo; private boolean cameraInitied; + private boolean loadingCameras; private static volatile CameraController Instance = null; @@ -78,9 +79,10 @@ public class CameraController implements MediaRecorder.OnInfoListener { } public void initCamera() { - if (cameraInitied) { + if (loadingCameras || cameraInitied) { return; } + loadingCameras = true; threadPool.execute(new Runnable() { @Override public void run() { @@ -102,6 +104,7 @@ public class CameraController implements MediaRecorder.OnInfoListener { Camera.Size size = list.get(a); if (size.height < 2160 && size.width < 2160) { cameraInfo.previewSizes.add(new Size(size.width, size.height)); + FileLog.e("preview size = " + size.width + " " + size.height); } } @@ -110,6 +113,7 @@ public class CameraController implements MediaRecorder.OnInfoListener { Camera.Size size = list.get(a); if (!"samsung".equals(Build.MANUFACTURER) || !"jflteuc".equals(Build.PRODUCT) || size.width < 2048) { cameraInfo.pictureSizes.add(new Size(size.width, size.height)); + FileLog.e("picture size = " + size.width + " " + size.height); } } @@ -121,11 +125,19 @@ public class CameraController implements MediaRecorder.OnInfoListener { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { + loadingCameras = false; cameraInitied = true; NotificationCenter.getInstance().postNotificationName(NotificationCenter.cameraInitied); } }); } catch (Exception e) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + loadingCameras = false; + cameraInitied = false; + } + }); FileLog.e(e); } } @@ -392,6 +404,44 @@ public class CameraController implements MediaRecorder.OnInfoListener { }); } + public void openRound(final CameraSession session, final SurfaceTexture texture, final Runnable callback, final Runnable configureCallback) { + if (session == null || texture == null) { + FileLog.e("failed to open round " + session + " tex = " + texture); + return; + } + threadPool.execute(new Runnable() { + @SuppressLint("NewApi") + @Override + public void run() { + Camera camera = session.cameraInfo.camera; + try { + FileLog.e("start creating round camera session"); + if (camera == null) { + camera = session.cameraInfo.camera = Camera.open(session.cameraInfo.cameraId); + } + Camera.Parameters params = camera.getParameters(); + + session.configureRoundCamera(); + if (configureCallback != null) { + configureCallback.run(); + } + camera.setPreviewTexture(texture); + camera.startPreview(); + if (callback != null) { + AndroidUtilities.runOnUIThread(callback); + } + FileLog.e("round camera session created"); + } catch (Exception e) { + session.cameraInfo.camera = null; + if (camera != null) { + camera.release(); + } + FileLog.e(e); + } + } + }); + } + public void open(final CameraSession session, final SurfaceTexture texture, final Runnable callback, final Runnable prestartCallback) { if (session == null || texture == null) { return; @@ -477,7 +527,9 @@ public class CameraController implements MediaRecorder.OnInfoListener { if (recordingSmallVideo) { pictureSize = new Size(4, 3); pictureSize = CameraController.chooseOptimalSize(info.getPictureSizes(), 640, 480, pictureSize); - recorder.setVideoEncodingBitRate(900000); + recorder.setVideoEncodingBitRate(900000 * 2); + recorder.setAudioEncodingBitRate(32000); + recorder.setAudioChannels(1); } else { pictureSize = new Size(16, 9); pictureSize = CameraController.chooseOptimalSize(info.getPictureSizes(), 720, 480, pictureSize); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraInfo.java index 7395b5e81..f98f8e3cb 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraInfo.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraInfo.java @@ -41,6 +41,10 @@ public class CameraInfo { return pictureSizes; } + public boolean isFrontface() { + return frontCamera != 0; + } + /*private int getScore(CameraSelectionCriteria criteria) { int score = 10; if (criteria != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraSession.java b/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraSession.java index 91b3910a6..faaa2dd7a 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraSession.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraSession.java @@ -24,6 +24,7 @@ import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLog; import java.util.ArrayList; +import java.util.List; public class CameraSession { @@ -39,6 +40,7 @@ public class CameraSession { private boolean initied; private boolean meteringAreaSupported; private int currentOrientation; + private int diffOrientation; private int jpegOrientation; private boolean sameTakePictureOrientation; @@ -143,11 +145,11 @@ public class CameraSession { return currentFlashMode; } - protected void setInitied() { + public void setInitied() { initied = true; } - protected boolean isInitied() { + public boolean isInitied() { return initied; } @@ -155,10 +157,113 @@ public class CameraSession { return currentOrientation; } + public int getWorldAngle() { + return diffOrientation; + } + public boolean isSameTakePictureOrientation() { return sameTakePictureOrientation; } + protected void configureRoundCamera() { + try { + isVideo = true; + Camera camera = cameraInfo.camera; + if (camera != null) { + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.Parameters params = null; + try { + params = camera.getParameters(); + } catch (Exception e) { + FileLog.e(e); + } + + Camera.getCameraInfo(cameraInfo.getCameraId(), info); + + int displayOrientation = getDisplayOrientation(info, true); + int cameraDisplayOrientation; + + if ("samsung".equals(Build.MANUFACTURER) && "sf2wifixx".equals(Build.PRODUCT)) { + cameraDisplayOrientation = 0; + } else { + int degrees = 0; + int temp = displayOrientation; + switch (temp) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + } + if (info.orientation % 90 != 0) { + info.orientation = 0; + } + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + temp = (info.orientation + degrees) % 360; + temp = (360 - temp) % 360; + } else { + temp = (info.orientation - degrees + 360) % 360; + } + cameraDisplayOrientation = temp; + } + camera.setDisplayOrientation(currentOrientation = cameraDisplayOrientation); + diffOrientation = currentOrientation - displayOrientation; + + if (params != null) { + FileLog.e("set preview size = " + previewSize.getWidth() + " " + previewSize.getHeight()); + params.setPreviewSize(previewSize.getWidth(), previewSize.getHeight()); + FileLog.e("set picture size = " + pictureSize.getWidth() + " " + pictureSize.getHeight()); + params.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); + params.setPictureFormat(pictureFormat); + params.setRecordingHint(true); + + String desiredMode = Camera.Parameters.FOCUS_MODE_AUTO; + if (params.getSupportedFocusModes().contains(desiredMode)) { + params.setFocusMode(desiredMode); + } + + int outputOrientation = 0; + if (jpegOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) { + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + outputOrientation = (info.orientation - jpegOrientation + 360) % 360; + } else { + outputOrientation = (info.orientation + jpegOrientation) % 360; + } + } + try { + params.setRotation(outputOrientation); + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + sameTakePictureOrientation = (360 - displayOrientation) % 360 == outputOrientation; + } else { + sameTakePictureOrientation = displayOrientation == outputOrientation; + } + } catch (Exception e) { + // + } + params.setFlashMode(currentFlashMode); + try { + camera.setParameters(params); + } catch (Exception e) { + // + } + + if (params.getMaxNumMeteringAreas() > 0) { + meteringAreaSupported = true; + } + } + } + } catch (Throwable e) { + FileLog.e(e); + } + } + protected void configurePhotoCamera() { try { Camera camera = cameraInfo.camera; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraView.java b/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraView.java index d302b2f0b..400ab1183 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraView.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraView.java @@ -51,6 +51,7 @@ public class CameraView extends FrameLayout implements TextureView.SurfaceTextur private float focusProgress = 1.0f; private float innerAlpha; private float outerAlpha; + private boolean initialFrontface; private int cx; private int cy; private Paint outerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -65,7 +66,7 @@ public class CameraView extends FrameLayout implements TextureView.SurfaceTextur public CameraView(Context context, boolean frontface) { super(context, null); - isFrontface = frontface; + initialFrontface = isFrontface = frontface; textureView = new TextureView(context); textureView.setSurfaceTextureListener(this); addView(textureView); @@ -132,14 +133,20 @@ public class CameraView extends FrameLayout implements TextureView.SurfaceTextur org.telegram.messenger.camera.Size aspectRatio; int wantedWidth; int wantedHeight; - if (Math.abs(screenSize - size4to3) < 0.1f) { - aspectRatio = new Size(4, 3); - wantedWidth = 1280; - wantedHeight = 960; - } else { + if (initialFrontface) { aspectRatio = new Size(16, 9); - wantedWidth = 1280; - wantedHeight = 720; + wantedWidth = 480; + wantedHeight = 270; + } else { + if (Math.abs(screenSize - size4to3) < 0.1f) { + aspectRatio = new Size(4, 3); + wantedWidth = 1280; + wantedHeight = 960; + } else { + aspectRatio = new Size(16, 9); + wantedWidth = 1280; + wantedHeight = 720; + } } if (textureView.getWidth() > 0 && textureView.getHeight() > 0) { int width = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/camera/Size.java b/TMessagesProj/src/main/java/org/telegram/messenger/camera/Size.java index 9805500c1..3cdc7e1c3 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/camera/Size.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/camera/Size.java @@ -67,6 +67,6 @@ public final class Size { return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2))); } - private final int mWidth; - private final int mHeight; + public final int mWidth; + public final int mHeight; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/BaseRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/BaseRenderer.java index b04a0d640..7d8aeed57 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/BaseRenderer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/BaseRenderer.java @@ -28,6 +28,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { private final int trackType; + private RendererConfiguration configuration; private int index; private int state; private SampleStream stream; @@ -70,9 +71,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } @Override - public final void enable(Format[] formats, SampleStream stream, long positionUs, - boolean joining, long offsetUs) throws ExoPlaybackException { + public final void enable(RendererConfiguration configuration, Format[] formats, + SampleStream stream, long positionUs, boolean joining, long offsetUs) + throws ExoPlaybackException { Assertions.checkState(state == STATE_DISABLED); + this.configuration = configuration; state = STATE_ENABLED; onEnabled(joining); replaceStream(formats, stream, offsetUs); @@ -107,10 +110,15 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } @Override - public final void setCurrentStreamIsFinal() { + public final void setCurrentStreamFinal() { streamIsFinal = true; } + @Override + public final boolean isCurrentStreamFinal() { + return streamIsFinal; + } + @Override public final void maybeThrowStreamError() throws IOException { stream.maybeThrowError(); @@ -119,6 +127,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @Override public final void resetPosition(long positionUs) throws ExoPlaybackException { streamIsFinal = false; + readEndOfStream = false; onPositionReset(positionUs, false); } @@ -194,8 +203,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * @param joining Whether this renderer is being enabled to join an ongoing playback. * @throws ExoPlaybackException If an error occurs. */ - protected void onPositionReset(long positionUs, boolean joining) - throws ExoPlaybackException { + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { // Do nothing. } @@ -232,10 +240,15 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { // Methods to be called by subclasses. + /** + * Returns the configuration set when the renderer was most recently enabled. + */ + protected final RendererConfiguration getConfiguration() { + return configuration; + } + /** * Returns the index of the renderer within the player. - * - * @return The index of the renderer within the player. */ protected final int getIndex() { return index; @@ -243,29 +256,48 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { /** * 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 #setCurrentStreamIsFinal()} has been + * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been * called. {@link C#RESULT_NOTHING_READ} is returned otherwise. * - * @see SampleStream#readData(FormatHolder, DecoderInputBuffer) * @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. + * @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) { - int result = stream.readData(formatHolder, buffer); + 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()) { readEndOfStream = true; return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ; } buffer.timeUs += streamOffsetUs; + } else if (result == C.RESULT_FORMAT_READ) { + Format format = formatHolder.format; + if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { + format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + streamOffsetUs); + formatHolder.format = format; + } } return result; } + /** + * Attempts to skip to the keyframe before the specified position, or to the end of the stream if + * {@code positionUs} is beyond it. + * + * @param positionUs The position in microseconds. + */ + protected void skipSource(long positionUs) { + stream.skipData(positionUs - streamOffsetUs); + } + /** * Returns whether the upstream source is ready. * @@ -275,13 +307,4 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return readEndOfStream ? streamIsFinal : stream.isReady(); } - /** - * Attempts to skip to the keyframe before the specified time. - * - * @param timeUs The specified time. - */ - protected void skipToKeyframeBefore(long timeUs) { - stream.skipToKeyframeBefore(timeUs); - } - } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/C.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/C.java index 6de53ea22..91236b59c 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/C.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/C.java @@ -15,9 +15,12 @@ */ package org.telegram.messenger.exoplayer2; +import android.annotation.TargetApi; +import android.content.Context; import android.media.AudioFormat; import android.media.AudioManager; import android.media.MediaCodec; +import android.media.MediaFormat; import android.support.annotation.IntDef; import android.view.Surface; import org.telegram.messenger.exoplayer2.util.Util; @@ -74,6 +77,21 @@ public final class C { */ public static final String UTF8_NAME = "UTF-8"; + /** + * The name of the UTF-16 charset. + */ + public static final String UTF16_NAME = "UTF-16"; + + /** + * * The name of the serif font family. + */ + public static final String SERIF_NAME = "serif"; + + /** + * * The name of the sans-serif font family. + */ + public static final String SANS_SERIF_NAME = "sans-serif"; + /** * Crypto modes for a codec. */ @@ -96,6 +114,13 @@ public final class C { @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC; + /** + * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to + * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}. + */ + @SuppressWarnings("InlinedApi") + public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE; + /** * Represents an audio encoding, or an invalid or unset value. */ @@ -434,9 +459,16 @@ public final class C { */ public static final UUID UUID_NIL = new UUID(0L, 0L); + /** + * UUID for the ClearKey DRM scheme. + *

+ * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up. + */ + public static final UUID CLEARKEY_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL); + /** * UUID for the Widevine DRM scheme. - *

+ *

* Widevine is supported on Android devices running Android 4.3 (API Level 18) and up. */ public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); @@ -463,15 +495,6 @@ public final class C { */ public static final int MSG_SET_VOLUME = 2; - /** - * A type of a message that can be passed to an audio {@link Renderer} via - * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object - * should be a {@link android.media.PlaybackParams}, or null, which will be used to configure the - * underlying {@link android.media.AudioTrack}. The message object should not be modified by the - * caller after it has been passed - */ - public static final int MSG_SET_PLAYBACK_PARAMS = 3; - /** * A type of a message that can be passed to an audio {@link Renderer} via * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object @@ -484,7 +507,7 @@ public final class C { * introduce a brief gap in audio output. Note also that tracks in the same audio session must * share the same routing, so a new audio session id will be generated. */ - public static final int MSG_SET_STREAM_TYPE = 4; + public static final int MSG_SET_STREAM_TYPE = 3; /** * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer} @@ -494,7 +517,7 @@ public final class C { * Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is * owned by a {@link android.view.SurfaceView}. */ - public static final int MSG_SET_SCALING_MODE = 5; + public static final int MSG_SET_SCALING_MODE = 4; /** * Applications or extensions may define custom {@code MSG_*} constants greater than or equal to @@ -506,7 +529,13 @@ public final class C { * The stereo mode for 360/3D/VR videos. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({Format.NO_VALUE, STEREO_MODE_MONO, STEREO_MODE_TOP_BOTTOM, STEREO_MODE_LEFT_RIGHT}) + @IntDef({ + Format.NO_VALUE, + STEREO_MODE_MONO, + STEREO_MODE_TOP_BOTTOM, + STEREO_MODE_LEFT_RIGHT, + STEREO_MODE_STEREO_MESH + }) public @interface StereoMode {} /** * Indicates Monoscopic stereo layout, used with 360/3D/VR videos. @@ -520,6 +549,86 @@ public final class C { * Indicates Left-Right stereo layout, used with 360/3D/VR videos. */ public static final int STEREO_MODE_LEFT_RIGHT = 2; + /** + * Indicates a stereo layout where the left and right eyes have separate meshes, + * used with 360/3D/VR videos. + */ + public static final int STEREO_MODE_STEREO_MESH = 3; + + /** + * Video colorspaces. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020}) + public @interface ColorSpace {} + /** + * @see MediaFormat#COLOR_STANDARD_BT709 + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709; + /** + * @see MediaFormat#COLOR_STANDARD_BT601_PAL + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL; + /** + * @see MediaFormat#COLOR_STANDARD_BT2020 + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020; + + /** + * Video color transfer characteristics. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({Format.NO_VALUE, COLOR_TRANSFER_SDR, COLOR_TRANSFER_ST2084, COLOR_TRANSFER_HLG}) + public @interface ColorTransfer {} + /** + * @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO; + /** + * @see MediaFormat#COLOR_TRANSFER_ST2084 + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084; + /** + * @see MediaFormat#COLOR_TRANSFER_HLG + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG; + + /** + * Video color range. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL}) + public @interface ColorRange {} + /** + * @see MediaFormat#COLOR_RANGE_LIMITED + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED; + /** + * @see MediaFormat#COLOR_RANGE_FULL + */ + @SuppressWarnings("InlinedApi") + public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL; + + /** + * Priority for media playback. + * + *

Larger values indicate higher priorities. + */ + public static final int PRIORITY_PLAYBACK = 0; + + /** + * Priority for media downloading. + * + *

Larger values indicate higher priorities. + */ + public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000; /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving @@ -543,4 +652,13 @@ public final class C { return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000); } + /** + * Returns a newly generated {@link android.media.AudioTrack} session identifier. + */ + @TargetApi(21) + public static int generateAudioSessionIdV21(Context context) { + return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)) + .generateAudioSessionId(); + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/DefaultLoadControl.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/DefaultLoadControl.java index 1293e95bb..17072445e 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/DefaultLoadControl.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/DefaultLoadControl.java @@ -19,6 +19,7 @@ import org.telegram.messenger.exoplayer2.source.TrackGroupArray; import org.telegram.messenger.exoplayer2.trackselection.TrackSelectionArray; import org.telegram.messenger.exoplayer2.upstream.Allocator; import org.telegram.messenger.exoplayer2.upstream.DefaultAllocator; +import org.telegram.messenger.exoplayer2.util.PriorityTaskManager; import org.telegram.messenger.exoplayer2.util.Util; /** @@ -60,6 +61,7 @@ public final class DefaultLoadControl implements LoadControl { private final long maxBufferUs; private final long bufferForPlaybackUs; private final long bufferForPlaybackAfterRebufferUs; + private final PriorityTaskManager priorityTaskManager; private int targetBufferSize; private boolean isBuffering; @@ -97,11 +99,36 @@ public final class DefaultLoadControl implements LoadControl { */ public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) { + this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, + null); + } + + /** + * Constructs a new instance. + * + * @param allocator The {@link DefaultAllocator} used by the loader. + * @param minBufferMs The minimum duration of media that the player will attempt to ensure is + * buffered at all times, in milliseconds. + * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in + * milliseconds. + * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or + * resume following a user action such as a seek, in milliseconds. + * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for + * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by + * buffer depletion rather than a user action. + * @param priorityTaskManager If not null, registers itself as a task with priority + * {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining + * periods. + */ + public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, + long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs, + PriorityTaskManager priorityTaskManager) { this.allocator = allocator; minBufferUs = minBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L; bufferForPlaybackUs = bufferForPlaybackMs * 1000L; bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; + this.priorityTaskManager = priorityTaskManager; } @Override @@ -146,8 +173,16 @@ public final class DefaultLoadControl implements LoadControl { public boolean shouldContinueLoading(long bufferedDurationUs) { int bufferTimeState = getBufferTimeState(bufferedDurationUs); boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; + boolean wasBuffering = isBuffering; isBuffering = bufferTimeState == BELOW_LOW_WATERMARK || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); + if (priorityTaskManager != null && isBuffering != wasBuffering) { + if (isBuffering) { + priorityTaskManager.add(C.PRIORITY_PLAYBACK); + } else { + priorityTaskManager.remove(C.PRIORITY_PLAYBACK); + } + } return isBuffering; } @@ -158,6 +193,9 @@ public final class DefaultLoadControl implements LoadControl { private void reset(boolean resetAllocator) { targetBufferSize = 0; + if (priorityTaskManager != null && isBuffering) { + priorityTaskManager.remove(C.PRIORITY_PLAYBACK); + } isBuffering = false; if (resetAllocator) { allocator.reset(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/DefaultRenderersFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/DefaultRenderersFactory.java new file mode 100755 index 000000000..0c09499e0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/DefaultRenderersFactory.java @@ -0,0 +1,327 @@ +/* + * 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 org.telegram.messenger.exoplayer2; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.IntDef; +import android.util.Log; +import org.telegram.messenger.exoplayer2.audio.AudioCapabilities; +import org.telegram.messenger.exoplayer2.audio.AudioProcessor; +import org.telegram.messenger.exoplayer2.audio.AudioRendererEventListener; +import org.telegram.messenger.exoplayer2.audio.MediaCodecAudioRenderer; +import org.telegram.messenger.exoplayer2.drm.DrmSessionManager; +import org.telegram.messenger.exoplayer2.drm.FrameworkMediaCrypto; +import org.telegram.messenger.exoplayer2.mediacodec.MediaCodecSelector; +import org.telegram.messenger.exoplayer2.metadata.MetadataRenderer; +import org.telegram.messenger.exoplayer2.text.TextRenderer; +import org.telegram.messenger.exoplayer2.trackselection.TrackSelector; +import org.telegram.messenger.exoplayer2.video.MediaCodecVideoRenderer; +import org.telegram.messenger.exoplayer2.video.VideoRendererEventListener; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Constructor; +import java.util.ArrayList; + +/** + * Default {@link RenderersFactory} implementation. + */ +public class DefaultRenderersFactory implements RenderersFactory { + + /** + * The default maximum duration for which a video renderer can attempt to seamlessly join an + * ongoing playback. + */ + public static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000; + + /** + * Modes for using extension renderers. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, + EXTENSION_RENDERER_MODE_PREFER}) + public @interface ExtensionRendererMode {} + /** + * Do not allow use of extension renderers. + */ + public static final int EXTENSION_RENDERER_MODE_OFF = 0; + /** + * Allow use of extension renderers. Extension renderers are indexed after core renderers of the + * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore + * prefer to use a core renderer to an extension renderer in the case that both are able to play + * a given track. + */ + public static final int EXTENSION_RENDERER_MODE_ON = 1; + /** + * Allow use of extension renderers. Extension renderers are indexed before core renderers of the + * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore + * prefer to use an extension renderer to a core renderer in the case that both are able to play + * a given track. + */ + public static final int EXTENSION_RENDERER_MODE_PREFER = 2; + + private static final String TAG = "DefaultRenderersFactory"; + + protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; + + private final Context context; + private final DrmSessionManager drmSessionManager; + private final @ExtensionRendererMode int extensionRendererMode; + private final long allowedVideoJoiningTimeMs; + + /** + * @param context A {@link Context}. + */ + public DefaultRenderersFactory(Context context) { + this(context, null); + } + + /** + * @param context A {@link Context}. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected + * playbacks are not required. + */ + public DefaultRenderersFactory(Context context, + DrmSessionManager drmSessionManager) { + this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF); + } + + /** + * @param context A {@link Context}. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected + * playbacks are not required.. + * @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. + */ + public DefaultRenderersFactory(Context context, + DrmSessionManager drmSessionManager, + @ExtensionRendererMode int extensionRendererMode) { + this(context, drmSessionManager, extensionRendererMode, + DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); + } + + /** + * @param context A {@link Context}. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected + * playbacks are not required.. + * @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 video renderers can attempt + * to seamlessly join an ongoing playback. + */ + public DefaultRenderersFactory(Context context, + DrmSessionManager drmSessionManager, + @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { + this.context = context; + this.drmSessionManager = drmSessionManager; + this.extensionRendererMode = extensionRendererMode; + this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; + } + + @Override + public Renderer[] createRenderers(Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput) { + ArrayList renderersList = new ArrayList<>(); + buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs, + eventHandler, videoRendererEventListener, extensionRendererMode, renderersList); + buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(), + eventHandler, audioRendererEventListener, extensionRendererMode, renderersList); + buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(), + extensionRendererMode, renderersList); + buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(), + extensionRendererMode, renderersList); + buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList); + return renderersList.toArray(new Renderer[renderersList.size()]); + } + + /** + * Builds video renderers for use by the player. + * + * @param context The {@link Context} associated with the player. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player + * will not be used for DRM protected playbacks. + * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video + * renderers can attempt to seamlessly join an ongoing playback. + * @param eventHandler A handler associated with the main thread's looper. + * @param eventListener An event listener. + * @param extensionRendererMode The extension renderer mode. + * @param out An array to which the built renderers should be appended. + */ + protected void buildVideoRenderers(Context context, + DrmSessionManager drmSessionManager, long allowedVideoJoiningTimeMs, + Handler eventHandler, VideoRendererEventListener eventListener, + @ExtensionRendererMode int extensionRendererMode, ArrayList out) { + out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT, + allowedVideoJoiningTimeMs, drmSessionManager, false, eventHandler, eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); + + if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { + return; + } + int extensionRendererIndex = out.size(); + if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) { + extensionRendererIndex--; + } + + try { + Class clazz = + Class.forName("org.telegram.messenger.exoplayer2.ext.vp9.LibvpxVideoRenderer"); + Constructor constructor = clazz.getConstructor(boolean.class, long.class, Handler.class, + VideoRendererEventListener.class, int.class); + Renderer renderer = (Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs, + eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); + out.add(extensionRendererIndex++, renderer); + Log.i(TAG, "Loaded LibvpxVideoRenderer."); + } catch (ClassNotFoundException e) { + // Expected if the app was built without the extension. + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Builds audio renderers for use by the player. + * + * @param context The {@link Context} associated with the player. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player + * will not be used for DRM protected playbacks. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio + * buffers before output. May be empty. + * @param eventHandler A handler to use when invoking event listeners and outputs. + * @param eventListener An event listener. + * @param extensionRendererMode The extension renderer mode. + * @param out An array to which the built renderers should be appended. + */ + protected void buildAudioRenderers(Context context, + DrmSessionManager drmSessionManager, + AudioProcessor[] audioProcessors, Handler eventHandler, + AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, + ArrayList out) { + out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, + eventHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors)); + + if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { + return; + } + int extensionRendererIndex = out.size(); + if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) { + extensionRendererIndex--; + } + + try { + Class clazz = + Class.forName("org.telegram.messenger.exoplayer2.ext.opus.LibopusAudioRenderer"); + Constructor constructor = clazz.getConstructor(Handler.class, + AudioRendererEventListener.class, AudioProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, + audioProcessors); + out.add(extensionRendererIndex++, renderer); + Log.i(TAG, "Loaded LibopusAudioRenderer."); + } catch (ClassNotFoundException e) { + // Expected if the app was built without the extension. + } catch (Exception e) { + throw new RuntimeException(e); + } + + try { + Class clazz = + Class.forName("org.telegram.messenger.exoplayer2.ext.flac.LibflacAudioRenderer"); + Constructor constructor = clazz.getConstructor(Handler.class, + AudioRendererEventListener.class, AudioProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, + audioProcessors); + out.add(extensionRendererIndex++, renderer); + Log.i(TAG, "Loaded LibflacAudioRenderer."); + } catch (ClassNotFoundException e) { + // Expected if the app was built without the extension. + } catch (Exception e) { + throw new RuntimeException(e); + } + + try { + Class clazz = + Class.forName("org.telegram.messenger.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); + Constructor constructor = clazz.getConstructor(Handler.class, + AudioRendererEventListener.class, AudioProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, + audioProcessors); + out.add(extensionRendererIndex++, renderer); + Log.i(TAG, "Loaded FfmpegAudioRenderer."); + } catch (ClassNotFoundException e) { + // Expected if the app was built without the extension. + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Builds text renderers for use by the player. + * + * @param context The {@link Context} associated with the player. + * @param output An output for the renderers. + * @param outputLooper The looper associated with the thread on which the output should be + * called. + * @param extensionRendererMode The extension renderer mode. + * @param out An array to which the built renderers should be appended. + */ + protected void buildTextRenderers(Context context, TextRenderer.Output output, + Looper outputLooper, @ExtensionRendererMode int extensionRendererMode, + ArrayList out) { + out.add(new TextRenderer(output, outputLooper)); + } + + /** + * Builds metadata renderers for use by the player. + * + * @param context The {@link Context} associated with the player. + * @param output An output for the renderers. + * @param outputLooper The looper associated with the thread on which the output should be + * called. + * @param extensionRendererMode The extension renderer mode. + * @param out An array to which the built renderers should be appended. + */ + protected void buildMetadataRenderers(Context context, MetadataRenderer.Output output, + Looper outputLooper, @ExtensionRendererMode int extensionRendererMode, + ArrayList out) { + out.add(new MetadataRenderer(output, outputLooper)); + } + + /** + * Builds any miscellaneous renderers used by the player. + * + * @param context The {@link Context} associated with the player. + * @param eventHandler A handler to use when invoking event listeners and outputs. + * @param extensionRendererMode The extension renderer mode. + * @param out An array to which the built renderers should be appended. + */ + protected void buildMiscellaneousRenderers(Context context, Handler eventHandler, + @ExtensionRendererMode int extensionRendererMode, ArrayList out) { + // Do nothing. + } + + /** + * Builds an array of {@link AudioProcessor}s that will process PCM audio before output. + */ + protected AudioProcessor[] buildAudioProcessors() { + return new AudioProcessor[0]; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlaybackException.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlaybackException.java index 391b6c415..4084984e4 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlaybackException.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlaybackException.java @@ -56,8 +56,7 @@ public final class ExoPlaybackException extends Exception { * The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and * {@link #TYPE_UNEXPECTED}. */ - @Type - public final int type; + @Type public final int type; /** * If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayer.java index d85ff79a2..2b3795e21 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayer.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2; +import android.support.annotation.Nullable; import org.telegram.messenger.exoplayer2.audio.MediaCodecAudioRenderer; import org.telegram.messenger.exoplayer2.metadata.MetadataRenderer; import org.telegram.messenger.exoplayer2.source.ConcatenatingMediaSource; @@ -23,9 +24,6 @@ import org.telegram.messenger.exoplayer2.source.MediaSource; import org.telegram.messenger.exoplayer2.source.MergingMediaSource; import org.telegram.messenger.exoplayer2.source.SingleSampleMediaSource; import org.telegram.messenger.exoplayer2.source.TrackGroupArray; -import org.telegram.messenger.exoplayer2.source.dash.DashMediaSource; -import org.telegram.messenger.exoplayer2.source.hls.HlsMediaSource; -import org.telegram.messenger.exoplayer2.source.smoothstreaming.SsMediaSource; import org.telegram.messenger.exoplayer2.text.TextRenderer; import org.telegram.messenger.exoplayer2.trackselection.DefaultTrackSelector; import org.telegram.messenger.exoplayer2.trackselection.TrackSelectionArray; @@ -47,12 +45,11 @@ import org.telegram.messenger.exoplayer2.video.MediaCodecVideoRenderer; *

    *
  • A {@link MediaSource} that defines the media to be played, loads the media, and from * which the loaded media can be read. A MediaSource is injected via {@link #prepare} at the start - * of playback. The library provides default implementations for regular media files - * ({@link ExtractorMediaSource}), DASH ({@link DashMediaSource}), SmoothStreaming - * ({@link SsMediaSource}) and HLS ({@link HlsMediaSource}), implementations for merging - * ({@link MergingMediaSource}) and concatenating ({@link ConcatenatingMediaSource}) other - * MediaSources, and an implementation for loading single samples - * ({@link SingleSampleMediaSource}) most often used for side-loaded subtitle and closed + * of playback. The library modules provide default implementations for regular media files + * ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS + * (HlsMediaSource), implementations for merging ({@link MergingMediaSource}) and concatenating + * ({@link ConcatenatingMediaSource}) other MediaSources, and an implementation for loading single + * samples ({@link SingleSampleMediaSource}) most often used for side-loaded subtitle and closed * caption files.
  • *
  • {@link Renderer}s that render individual components of the media. The library * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, @@ -120,8 +117,8 @@ public interface ExoPlayer { * removed from the timeline. The will not be reported via a separate call to * {@link #onPositionDiscontinuity()}. * - * @param timeline The latest timeline, or null if the timeline is being cleared. - * @param manifest The latest manifest, or null if the manifest is being cleared. + * @param timeline The latest timeline. Never null, but may be empty. + * @param manifest The latest manifest. May be null. */ void onTimelineChanged(Timeline timeline, Object manifest); @@ -172,6 +169,16 @@ public interface ExoPlayer { */ void onPositionDiscontinuity(); + /** + * Called when the current playback parameters change. The playback parameters may change due to + * a call to {@link ExoPlayer#setPlaybackParameters(PlaybackParameters)}, or the player itself + * may change them (for example, if audio playback switches to passthrough mode, where speed + * adjustment is no longer possible). + * + * @param playbackParameters The playback parameters. + */ + void onPlaybackParametersChanged(PlaybackParameters playbackParameters); + } /** @@ -330,17 +337,41 @@ public interface ExoPlayer { /** * Seeks to a position specified in milliseconds in the current window. * - * @param windowPositionMs The seek position in the current window. + * @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to + * the window's default position. */ - void seekTo(long windowPositionMs); + void seekTo(long positionMs); /** * Seeks to a position specified in milliseconds in the specified window. * * @param windowIndex The index of the window. - * @param windowPositionMs The seek position in the specified window. + * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to + * the window's default position. */ - void seekTo(int windowIndex, long windowPositionMs); + void seekTo(int windowIndex, long positionMs); + + /** + * 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. + * + * @param playbackParameters The playback parameters, or {@code null} to use the defaults. + */ + void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters); + + /** + * Returns the currently active playback parameters. + * + * @see EventListener#onPlaybackParametersChanged(PlaybackParameters) + */ + PlaybackParameters getPlaybackParameters(); /** * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention @@ -445,4 +476,20 @@ public interface ExoPlayer { */ int getBufferedPercentage(); + /** + * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is + * empty. + * + * @see Timeline.Window#isDynamic + */ + boolean isCurrentWindowDynamic(); + + /** + * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is + * empty. + * + * @see Timeline.Window#isSeekable + */ + boolean isCurrentWindowSeekable(); + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerFactory.java index 1817fbb69..7d4c1de19 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerFactory.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerFactory.java @@ -26,12 +26,6 @@ import org.telegram.messenger.exoplayer2.trackselection.TrackSelector; */ public final class ExoPlayerFactory { - /** - * The default maximum duration for which a video renderer can attempt to seamlessly join an - * ongoing playback. - */ - public static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000; - private ExoPlayerFactory() {} /** @@ -41,10 +35,13 @@ public final class ExoPlayerFactory { * @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 #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. */ + @Deprecated public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl) { - return newSimpleInstance(context, trackSelector, loadControl, null); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context); + return newSimpleInstance(renderersFactory, trackSelector, loadControl); } /** @@ -56,11 +53,13 @@ public final class ExoPlayerFactory { * @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 #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. */ + @Deprecated public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager) { - return newSimpleInstance(context, trackSelector, loadControl, - drmSessionManager, SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager); + return newSimpleInstance(renderersFactory, trackSelector, loadControl); } /** @@ -75,12 +74,15 @@ public final class ExoPlayerFactory { * @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(RenderersFactory, TrackSelector, LoadControl)}. */ + @Deprecated public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager, - @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode) { - return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, - extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); + @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) { + RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager, + extensionRendererMode); + return newSimpleInstance(renderersFactory, trackSelector, loadControl); } /** @@ -97,13 +99,52 @@ public final class ExoPlayerFactory { * 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(RenderersFactory, TrackSelector, LoadControl)}. */ + @Deprecated public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, LoadControl loadControl, DrmSessionManager drmSessionManager, - @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode, + @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { - return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager, + RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager, extensionRendererMode, allowedVideoJoiningTimeMs); + return newSimpleInstance(renderersFactory, trackSelector, loadControl); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated + * {@link Looper}. + * + * @param context A {@link Context}. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + */ + public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) { + return newSimpleInstance(new DefaultRenderersFactory(context), trackSelector); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated + * {@link Looper}. + * + * @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. + */ + public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory, + TrackSelector trackSelector) { + return newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl()); + } + + /** + * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated + * {@link Looper}. + * + * @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. + */ + public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory, + TrackSelector trackSelector, LoadControl loadControl) { + return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl); } /** diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerImpl.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerImpl.java index 2a1ebd7f4..ed7afd4c7 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerImpl.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerImpl.java @@ -19,15 +19,16 @@ import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.support.annotation.Nullable; import android.util.Log; import org.telegram.messenger.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; import org.telegram.messenger.exoplayer2.ExoPlayerImplInternal.SourceInfo; -import org.telegram.messenger.exoplayer2.ExoPlayerImplInternal.TrackInfo; import org.telegram.messenger.exoplayer2.source.MediaSource; import org.telegram.messenger.exoplayer2.source.TrackGroupArray; import org.telegram.messenger.exoplayer2.trackselection.TrackSelection; import org.telegram.messenger.exoplayer2.trackselection.TrackSelectionArray; import org.telegram.messenger.exoplayer2.trackselection.TrackSelector; +import org.telegram.messenger.exoplayer2.trackselection.TrackSelectorResult; import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.Util; import java.util.concurrent.CopyOnWriteArraySet; @@ -52,17 +53,20 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean playWhenReady; private int playbackState; private int pendingSeekAcks; + private int pendingPrepareAcks; private boolean isLoading; private Timeline timeline; private Object manifest; private TrackGroupArray trackGroups; private TrackSelectionArray trackSelections; + private PlaybackParameters playbackParameters; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; // Playback information when there is a pending seek/set source operation. private int maskingWindowIndex; + private int maskingPeriodIndex; private long maskingWindowPositionMs; /** @@ -74,7 +78,7 @@ import java.util.concurrent.CopyOnWriteArraySet; */ @SuppressLint("HandlerLeak") public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { - Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION + " [" + Util.DEVICE_DEBUG_INFO + "]"); + Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION_SLASHY + " [" + Util.DEVICE_DEBUG_INFO + "]"); Assertions.checkState(renderers.length > 0); this.renderers = Assertions.checkNotNull(renderers); this.trackSelector = Assertions.checkNotNull(trackSelector); @@ -87,6 +91,7 @@ import java.util.concurrent.CopyOnWriteArraySet; period = new Timeline.Period(); trackGroups = TrackGroupArray.EMPTY; trackSelections = emptyTrackSelections; + playbackParameters = PlaybackParameters.DEFAULT; eventHandler = new Handler() { @Override public void handleMessage(Message msg) { @@ -95,7 +100,7 @@ import java.util.concurrent.CopyOnWriteArraySet; }; playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0); internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, - eventHandler, playbackInfo); + eventHandler, playbackInfo, this); } @Override @@ -125,7 +130,7 @@ import java.util.concurrent.CopyOnWriteArraySet; timeline = Timeline.EMPTY; manifest = null; for (EventListener listener : listeners) { - listener.onTimelineChanged(null, null); + listener.onTimelineChanged(timeline, manifest); } } if (tracksSelected) { @@ -138,6 +143,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } } } + pendingPrepareAcks++; internalPlayer.prepare(mediaSource, resetPosition); } @@ -180,10 +186,26 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void seekTo(int windowIndex, long positionMs) { if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { - throw new IndexOutOfBoundsException(); + throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); } pendingSeekAcks++; maskingWindowIndex = windowIndex; + if (timeline.isEmpty()) { + maskingPeriodIndex = 0; + } else { + timeline.getWindow(windowIndex, window); + long resolvedPositionMs = + positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : positionMs; + int periodIndex = window.firstPeriodIndex; + long periodPositionUs = window.getPositionInFirstPeriodUs() + C.msToUs(resolvedPositionMs); + long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); + while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs + && periodIndex < window.lastPeriodIndex) { + periodPositionUs -= periodDurationUs; + periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs(); + } + maskingPeriodIndex = periodIndex; + } if (positionMs == C.TIME_UNSET) { maskingWindowPositionMs = 0; internalPlayer.seekTo(timeline, windowIndex, C.TIME_UNSET); @@ -196,6 +218,19 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + @Override + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { + if (playbackParameters == null) { + playbackParameters = PlaybackParameters.DEFAULT; + } + internalPlayer.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return playbackParameters; + } + @Override public void stop() { internalPlayer.stop(); @@ -219,7 +254,11 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public int getCurrentPeriodIndex() { - return playbackInfo.periodIndex; + if (timeline.isEmpty() || pendingSeekAcks > 0) { + return maskingPeriodIndex; + } else { + return playbackInfo.periodIndex; + } } @Override @@ -271,6 +310,16 @@ import java.util.concurrent.CopyOnWriteArraySet; : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration); } + @Override + public boolean isCurrentWindowDynamic() { + return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic; + } + + @Override + public boolean isCurrentWindowSeekable() { + return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable; + } + @Override public int getRendererCount() { return renderers.length; @@ -304,6 +353,10 @@ import java.util.concurrent.CopyOnWriteArraySet; // Not private so it can be called from an inner class without going through a thunk method. /* package */ void handleEvent(Message msg) { switch (msg.what) { + case ExoPlayerImplInternal.MSG_PREPARE_ACK: { + pendingPrepareAcks--; + break; + } case ExoPlayerImplInternal.MSG_STATE_CHANGED: { playbackState = msg.arg1; for (EventListener listener : listeners) { @@ -319,21 +372,25 @@ import java.util.concurrent.CopyOnWriteArraySet; break; } case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { - TrackInfo trackInfo = (TrackInfo) msg.obj; - tracksSelected = true; - trackGroups = trackInfo.groups; - trackSelections = trackInfo.selections; - trackSelector.onSelectionActivated(trackInfo.info); - for (EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); + if (pendingPrepareAcks == 0) { + TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; + tracksSelected = true; + trackGroups = trackSelectorResult.groups; + trackSelections = trackSelectorResult.selections; + trackSelector.onSelectionActivated(trackSelectorResult.info); + for (EventListener listener : listeners) { + listener.onTracksChanged(trackGroups, trackSelections); + } } break; } case ExoPlayerImplInternal.MSG_SEEK_ACK: { if (--pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(); + if (msg.arg1 != 0) { + for (EventListener listener : listeners) { + listener.onPositionDiscontinuity(); + } } } break; @@ -349,12 +406,24 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { SourceInfo sourceInfo = (SourceInfo) msg.obj; - timeline = sourceInfo.timeline; - manifest = sourceInfo.manifest; - playbackInfo = sourceInfo.playbackInfo; pendingSeekAcks -= sourceInfo.seekAcks; - for (EventListener listener : listeners) { - listener.onTimelineChanged(timeline, manifest); + if (pendingPrepareAcks == 0) { + timeline = sourceInfo.timeline; + manifest = sourceInfo.manifest; + playbackInfo = sourceInfo.playbackInfo; + for (EventListener listener : listeners) { + listener.onTimelineChanged(timeline, manifest); + } + } + break; + } + case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { + PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj; + if (!this.playbackParameters.equals(playbackParameters)) { + this.playbackParameters = playbackParameters; + for (EventListener listener : listeners) { + listener.onPlaybackParametersChanged(playbackParameters); + } } break; } @@ -365,6 +434,8 @@ import java.util.concurrent.CopyOnWriteArraySet; } break; } + default: + throw new IllegalStateException(); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerImplInternal.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerImplInternal.java index 3cc2c5a7e..db0c13557 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerImplInternal.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerImplInternal.java @@ -26,16 +26,14 @@ import org.telegram.messenger.exoplayer2.ExoPlayer.ExoPlayerMessage; import org.telegram.messenger.exoplayer2.source.MediaPeriod; import org.telegram.messenger.exoplayer2.source.MediaSource; import org.telegram.messenger.exoplayer2.source.SampleStream; -import org.telegram.messenger.exoplayer2.source.TrackGroupArray; import org.telegram.messenger.exoplayer2.trackselection.TrackSelection; import org.telegram.messenger.exoplayer2.trackselection.TrackSelectionArray; import org.telegram.messenger.exoplayer2.trackselection.TrackSelector; +import org.telegram.messenger.exoplayer2.trackselection.TrackSelectorResult; import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.MediaClock; -import org.telegram.messenger.exoplayer2.util.PriorityHandlerThread; import org.telegram.messenger.exoplayer2.util.StandaloneMediaClock; import org.telegram.messenger.exoplayer2.util.TraceUtil; -import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; /** @@ -72,20 +70,6 @@ import java.io.IOException; } - public static final class TrackInfo { - - public final TrackGroupArray groups; - public final TrackSelectionArray selections; - public final Object info; - - public TrackInfo(TrackGroupArray groups, TrackSelectionArray selections, Object info) { - this.groups = groups; - this.selections = selections; - this.info = info; - } - - } - public static final class SourceInfo { public final Timeline timeline; @@ -105,26 +89,29 @@ import java.io.IOException; private static final String TAG = "ExoPlayerImplInternal"; // External messages + public static final int MSG_PREPARE_ACK = 0; public static final int MSG_STATE_CHANGED = 1; public static final int MSG_LOADING_CHANGED = 2; public static final int MSG_TRACKS_CHANGED = 3; public static final int MSG_SEEK_ACK = 4; public static final int MSG_POSITION_DISCONTINUITY = 5; public static final int MSG_SOURCE_INFO_REFRESHED = 6; - public static final int MSG_ERROR = 7; + public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 7; + public static final int MSG_ERROR = 8; // Internal messages private static final int MSG_PREPARE = 0; private static final int MSG_SET_PLAY_WHEN_READY = 1; private static final int MSG_DO_SOME_WORK = 2; private static final int MSG_SEEK_TO = 3; - private static final int MSG_STOP = 4; - private static final int MSG_RELEASE = 5; - private static final int MSG_REFRESH_SOURCE_INFO = 6; - private static final int MSG_PERIOD_PREPARED = 7; - private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 8; - private static final int MSG_TRACK_SELECTION_INVALIDATED = 9; - private static final int MSG_CUSTOM = 10; + private static final int MSG_SET_PLAYBACK_PARAMETERS = 4; + private static final int MSG_STOP = 5; + private static final int MSG_RELEASE = 6; + private static final int MSG_REFRESH_SOURCE_INFO = 7; + private static final int MSG_PERIOD_PREPARED = 8; + private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; + private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; + private static final int MSG_CUSTOM = 11; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -137,6 +124,14 @@ import java.io.IOException; */ private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100; + /** + * Offset added to all sample timestamps read by renderers to make them non-negative. This is + * provided for convenience of sources that may return negative timestamps due to prerolling + * samples from a keyframe before their first sample with timestamp zero, so it must be set to a + * value greater than or equal to the maximum key-frame interval in seekable periods. + */ + private static final int RENDERER_TIMESTAMP_OFFSET_US = 60000000; + private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; @@ -145,10 +140,12 @@ import java.io.IOException; private final Handler handler; private final HandlerThread internalPlaybackThread; private final Handler eventHandler; + private final ExoPlayer player; private final Timeline.Window window; private final Timeline.Period period; private PlaybackInfo playbackInfo; + private PlaybackParameters playbackParameters; private Renderer rendererMediaClockSource; private MediaClock rendererMediaClock; private MediaSource mediaSource; @@ -174,7 +171,7 @@ import java.io.IOException; public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, boolean playWhenReady, Handler eventHandler, - PlaybackInfo playbackInfo) { + PlaybackInfo playbackInfo, ExoPlayer player) { this.renderers = renderers; this.trackSelector = trackSelector; this.loadControl = loadControl; @@ -182,6 +179,7 @@ import java.io.IOException; this.eventHandler = eventHandler; this.state = ExoPlayer.STATE_IDLE; this.playbackInfo = playbackInfo; + this.player = player; rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { @@ -193,10 +191,11 @@ import java.io.IOException; window = new Timeline.Window(); period = new Timeline.Period(); trackSelector.init(this); + playbackParameters = PlaybackParameters.DEFAULT; // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can // not normally change to this priority" is incorrect. - internalPlaybackThread = new PriorityHandlerThread("ExoPlayerImplInternal:Handler", + internalPlaybackThread = new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); handler = new Handler(internalPlaybackThread.getLooper(), this); @@ -216,6 +215,10 @@ import java.io.IOException; .sendToTarget(); } + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget(); + } + public void stop() { handler.sendEmptyMessage(MSG_STOP); } @@ -309,6 +312,10 @@ import java.io.IOException; seekToInternal((SeekPosition) msg.obj); return true; } + case MSG_SET_PLAYBACK_PARAMETERS: { + setPlaybackParametersInternal((PlaybackParameters) msg.obj); + return true; + } case MSG_STOP: { stopInternal(); return true; @@ -376,13 +383,14 @@ import java.io.IOException; } private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { - resetInternal(); + eventHandler.sendEmptyMessage(MSG_PREPARE_ACK); + resetInternal(true); loadControl.onPrepared(); if (resetPosition) { playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); } this.mediaSource = mediaSource; - mediaSource.prepareSource(this); + mediaSource.prepareSource(player, true, this); setState(ExoPlayer.STATE_BUFFERING); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -460,6 +468,8 @@ import java.io.IOException; TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); + playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs); + boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; for (Renderer renderer : enabledRenderers) { @@ -481,6 +491,19 @@ import java.io.IOException; maybeThrowPeriodPrepareError(); } + // The standalone media clock never changes playback parameters, so just check the renderer. + if (rendererMediaClock != null) { + PlaybackParameters playbackParameters = rendererMediaClock.getPlaybackParameters(); + if (!playbackParameters.equals(this.playbackParameters)) { + // TODO: Make LoadControl, period transition position projection, adaptive track selection + // and potentially any time-related code in renderers take into account the playback speed. + this.playbackParameters = playbackParameters; + standaloneMediaClock.synchronize(rendererMediaClock); + eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters) + .sendToTarget(); + } + } + long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period) .getDurationUs(); if (allRenderersEnded @@ -546,12 +569,20 @@ import java.io.IOException; Pair periodPosition = resolveSeekPosition(seekPosition); if (periodPosition == null) { - // TODO: We should probably propagate an error here. - // We failed to resolve the seek position. Stop the player. - stopInternal(); + // The seek position was valid for the timeline that it was performed into, but the + // timeline has changed and a suitable seek position could not be resolved in the new one. + playbackInfo = new PlaybackInfo(0, 0); + eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget(); + // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't + // ignored. + playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); + setState(ExoPlayer.STATE_ENDED); + // Reset, but retain the source so that it can still be used should a seek occur. + resetInternal(false); return; } + boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; int periodIndex = periodPosition.first; long periodPositionUs = periodPosition.second; @@ -561,10 +592,13 @@ import java.io.IOException; // Seek position equals the current position. Do nothing. return; } - periodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); + long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); + seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; + periodPositionUs = newPeriodPositionUs; } finally { playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs); - eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) + .sendToTarget(); } } @@ -603,6 +637,7 @@ import java.io.IOException; enabledRenderers = new Renderer[0]; rendererMediaClock = null; rendererMediaClockSource = null; + playingPeriodHolder = null; } // Update the holders. @@ -628,7 +663,8 @@ import java.io.IOException; } private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { - rendererPositionUs = playingPeriodHolder == null ? periodPositionUs + rendererPositionUs = playingPeriodHolder == null + ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US : playingPeriodHolder.toRendererTime(periodPositionUs); standaloneMediaClock.setPositionUs(rendererPositionUs); for (Renderer renderer : enabledRenderers) { @@ -636,14 +672,22 @@ import java.io.IOException; } } + private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { + playbackParameters = rendererMediaClock != null + ? rendererMediaClock.setPlaybackParameters(playbackParameters) + : standaloneMediaClock.setPlaybackParameters(playbackParameters); + this.playbackParameters = playbackParameters; + eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + } + private void stopInternal() { - resetInternal(); + resetInternal(true); loadControl.onStopped(); setState(ExoPlayer.STATE_IDLE); } private void releaseInternal() { - resetInternal(); + resetInternal(true); loadControl.onReleased(); setState(ExoPlayer.STATE_IDLE); synchronized (this) { @@ -652,12 +696,13 @@ import java.io.IOException; } } - private void resetInternal() { + private void resetInternal(boolean releaseMediaSource) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; standaloneMediaClock.stop(); rendererMediaClock = null; rendererMediaClockSource = null; + rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US; for (Renderer renderer : enabledRenderers) { try { ensureStopped(renderer); @@ -670,15 +715,17 @@ import java.io.IOException; enabledRenderers = new Renderer[0]; releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder); - if (mediaSource != null) { - mediaSource.releaseSource(); - mediaSource = null; - } loadingPeriodHolder = null; readingPeriodHolder = null; playingPeriodHolder = null; - timeline = null; setIsLoading(false); + if (releaseMediaSource) { + if (mediaSource != null) { + mediaSource.releaseSource(); + mediaSource = null; + } + timeline = null; + } } private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { @@ -761,7 +808,7 @@ import java.io.IOException; if (sampleStream == null) { // The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take // over timing responsibilities. - standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs()); + standaloneMediaClock.synchronize(rendererMediaClock); } rendererMediaClock = null; rendererMediaClockSource = null; @@ -774,7 +821,8 @@ import java.io.IOException; } } } - eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget(); + eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult) + .sendToTarget(); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } else { // Release and re-prepare/buffer periods after the one whose selection changed. @@ -803,9 +851,6 @@ import java.io.IOException; } private boolean haveSufficientBuffer(boolean rebuffering) { - if (loadingPeriodHolder == null) { - return false; - } long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared ? loadingPeriodHolder.startPositionUs : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs(); @@ -843,18 +888,21 @@ import java.io.IOException; if (oldTimeline == null) { if (pendingInitialSeekCount > 0) { Pair periodPosition = resolveSeekPosition(pendingSeekPosition); - if (periodPosition == null) { - // We failed to resolve the seek position. Stop the player. - notifySourceInfoRefresh(manifest, 0); - // TODO: We should probably propagate an error here. - stopInternal(); - return; - } - playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second); processedInitialSeekCount = pendingInitialSeekCount; pendingInitialSeekCount = 0; pendingSeekPosition = null; + if (periodPosition == null) { + // The seek position was valid for the timeline that it was performed into, but the + // timeline has changed and a suitable seek position could not be resolved in the new one. + handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); + return; + } + playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second); } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { + if (timeline.isEmpty()) { + handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); + return; + } Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second); } @@ -874,10 +922,8 @@ import java.io.IOException; // period whose window we can restart from. int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline); if (newPeriodIndex == C.INDEX_UNSET) { - // We failed to resolve a subsequent period. Stop the player. - notifySourceInfoRefresh(manifest, processedInitialSeekCount); - // TODO: We should probably propagate an error here. - stopInternal(); + // We failed to resolve a suitable restart position. + handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); return; } // We resolved a subsequent period. Seek to the default position in the corresponding window. @@ -947,6 +993,18 @@ import java.io.IOException; notifySourceInfoRefresh(manifest, processedInitialSeekCount); } + private void handleSourceInfoRefreshEndedPlayback(Object manifest, + int processedInitialSeekCount) { + // Set the playback position to (0,0) for notifying the eventHandler. + playbackInfo = new PlaybackInfo(0, 0); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); + // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored. + playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); + setState(ExoPlayer.STATE_ENDED); + // Reset, but retain the source so that it can still be used should a seek occur. + resetInternal(false); + } + private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) { eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget(); @@ -978,6 +1036,8 @@ import java.io.IOException; * * @param seekPosition The position to resolve. * @return The resolved position, or null if resolution was not successful. + * @throws IllegalSeekPositionException If the window index of the seek position is outside the + * bounds of the timeline. */ private Pair resolveSeekPosition(SeekPosition seekPosition) { Timeline seekTimeline = seekPosition.timeline; @@ -985,11 +1045,17 @@ import java.io.IOException; // The application performed a blind seek without a non-empty timeline (most likely based on // knowledge of what the future timeline will be). Use the internal timeline. seekTimeline = timeline; - Assertions.checkIndex(seekPosition.windowIndex, 0, timeline.getWindowCount()); } // Map the SeekPosition to a position in the corresponding timeline. - Pair periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex, - seekPosition.windowPositionUs); + Pair periodPosition; + try { + periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex, + seekPosition.windowPositionUs); + } catch (IndexOutOfBoundsException e) { + // The window index of the seek position was outside the bounds of the timeline. + throw new IllegalSeekPositionException(timeline, seekPosition.windowIndex, + seekPosition.windowPositionUs); + } if (timeline == seekTimeline) { // Our internal timeline is the seek timeline, so the mapped position is correct. return periodPosition; @@ -1042,6 +1108,7 @@ import java.io.IOException; */ private Pair getPeriodPosition(Timeline timeline, int windowIndex, long windowPositionUs, long defaultPositionProjectionUs) { + Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount()); timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs); if (windowPositionUs == C.TIME_UNSET) { windowPositionUs = window.getDefaultPositionUs(); @@ -1067,66 +1134,8 @@ import java.io.IOException; return; } - if (loadingPeriodHolder == null - || (loadingPeriodHolder.isFullyBuffered() && !loadingPeriodHolder.isLast - && (playingPeriodHolder == null - || loadingPeriodHolder.index - playingPeriodHolder.index < MAXIMUM_BUFFER_AHEAD_PERIODS))) { - // We don't have a loading period or it's fully loaded, so try and create the next one. - int newLoadingPeriodIndex = loadingPeriodHolder == null ? playbackInfo.periodIndex - : loadingPeriodHolder.index + 1; - if (newLoadingPeriodIndex >= timeline.getPeriodCount()) { - // The period is not available yet. - mediaSource.maybeThrowSourceInfoRefreshError(); - } else { - int windowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex; - boolean isFirstPeriodInWindow = newLoadingPeriodIndex - == timeline.getWindow(windowIndex, window).firstPeriodIndex; - long periodStartPositionUs; - if (loadingPeriodHolder == null) { - periodStartPositionUs = playbackInfo.startPositionUs; - } else if (!isFirstPeriodInWindow) { - // We're starting to buffer a new period in the current window. Always start from the - // beginning of the period. - periodStartPositionUs = 0; - } else { - // We're starting to buffer a new window. When playback transitions to this window we'll - // want it to be from its default start position. The expected delay until playback - // transitions is equal the duration of media that's currently buffered (assuming no - // interruptions). Hence we project the default start position forward by the duration of - // the buffer, and start buffering from this point. - long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset() - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() - - rendererPositionUs; - Pair defaultPosition = getPeriodPosition(timeline, windowIndex, - C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); - if (defaultPosition == null) { - newLoadingPeriodIndex = C.INDEX_UNSET; - periodStartPositionUs = C.TIME_UNSET; - } else { - newLoadingPeriodIndex = defaultPosition.first; - periodStartPositionUs = defaultPosition.second; - } - } - if (newLoadingPeriodIndex != C.INDEX_UNSET) { - long rendererPositionOffsetUs = loadingPeriodHolder == null ? periodStartPositionUs - : (loadingPeriodHolder.getRendererOffset() - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()); - timeline.getPeriod(newLoadingPeriodIndex, period, true); - boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; - MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, - rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid, - newLoadingPeriodIndex, isLastPeriod, periodStartPositionUs); - if (loadingPeriodHolder != null) { - loadingPeriodHolder.next = newPeriodHolder; - } - loadingPeriodHolder = newPeriodHolder; - loadingPeriodHolder.mediaPeriod.prepare(this); - setIsLoading(true); - } - } - } - + // Update the loading period if required. + maybeUpdateLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); } else if (loadingPeriodHolder != null && loadingPeriodHolder.needsContinueLoading) { @@ -1152,28 +1161,49 @@ import java.io.IOException; } if (readingPeriodHolder.isLast) { - // The renderers have their final SampleStreams. - for (Renderer renderer : enabledRenderers) { - renderer.setCurrentStreamIsFinal(); + 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; } - for (Renderer renderer : enabledRenderers) { - if (!renderer.hasReadStreamToEnd()) { + 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())) { return; } } + if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) { - TrackSelectionArray oldTrackSelections = readingPeriodHolder.trackSelections; + TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult; readingPeriodHolder = readingPeriodHolder.next; - TrackSelectionArray newTrackSelections = readingPeriodHolder.trackSelections; + TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult; + + boolean initialDiscontinuity = + readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; - TrackSelection oldSelection = oldTrackSelections.get(i); - TrackSelection newSelection = newTrackSelections.get(i); - if (oldSelection != null) { - if (newSelection != null) { + TrackSelection oldSelection = oldTrackSelectorResult.selections.get(i); + if (oldSelection == null) { + // The renderer has no current stream 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()) { + TrackSelection newSelection = newTrackSelectorResult.selections.get(i); + RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i]; + RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i]; + if (newSelection != null && newConfig.equals(oldConfig)) { // Replace the renderer's SampleStream so the transition to playing the next period can // be seamless. Format[] formats = new Format[newSelection.length()]; @@ -1183,15 +1213,90 @@ import java.io.IOException; renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], readingPeriodHolder.getRendererOffset()); } else { - // The renderer will be disabled when transitioning to playing the next period. Mark the - // SampleStream as final to play out any remaining data. - renderer.setCurrentStreamIsFinal(); + // The renderer will be disabled when transitioning to playing the next period, either + // because there's no new selection or because a configuration change is required. Mark + // the SampleStream as final to play out any remaining data. + renderer.setCurrentStreamFinal(); } } } } } + private void maybeUpdateLoadingPeriod() throws IOException { + int newLoadingPeriodIndex; + if (loadingPeriodHolder == null) { + newLoadingPeriodIndex = playbackInfo.periodIndex; + } else { + int loadingPeriodIndex = loadingPeriodHolder.index; + if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered() + || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) { + // Either the existing loading period is the last period, or we are not ready to advance to + // loading the next period because it hasn't been fully buffered or its duration is unknown. + return; + } + if (playingPeriodHolder != null + && loadingPeriodIndex - playingPeriodHolder.index == MAXIMUM_BUFFER_AHEAD_PERIODS) { + // We are already buffering the maximum number of periods ahead. + return; + } + newLoadingPeriodIndex = loadingPeriodHolder.index + 1; + } + + if (newLoadingPeriodIndex >= timeline.getPeriodCount()) { + // The next period is not available yet. + mediaSource.maybeThrowSourceInfoRefreshError(); + return; + } + + long newLoadingPeriodStartPositionUs; + if (loadingPeriodHolder == null) { + newLoadingPeriodStartPositionUs = playbackInfo.positionUs; + } else { + int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex; + if (newLoadingPeriodIndex + != timeline.getWindow(newLoadingWindowIndex, window).firstPeriodIndex) { + // We're starting to buffer a new period in the current window. Always start from the + // beginning of the period. + newLoadingPeriodStartPositionUs = 0; + } else { + // We're starting to buffer a new window. When playback transitions to this window we'll + // want it to be from its default start position. The expected delay until playback + // transitions is equal the duration of media that's currently buffered (assuming no + // interruptions). Hence we project the default start position forward by the duration of + // the buffer, and start buffering from this point. + long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset() + + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() + - rendererPositionUs; + Pair defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex, + C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); + if (defaultPosition == null) { + return; + } + + newLoadingPeriodIndex = defaultPosition.first; + newLoadingPeriodStartPositionUs = defaultPosition.second; + } + } + + long rendererPositionOffsetUs = loadingPeriodHolder == null + ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US + : (loadingPeriodHolder.getRendererOffset() + + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()); + timeline.getPeriod(newLoadingPeriodIndex, period, true); + boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1 + && !timeline.getWindow(period.windowIndex, window).isDynamic; + MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, + rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid, + newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs); + if (loadingPeriodHolder != null) { + loadingPeriodHolder.next = newPeriodHolder; + } + loadingPeriodHolder = newPeriodHolder; + loadingPeriodHolder.mediaPeriod.prepare(this); + setIsLoading(true); + } + private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException { if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { // Stale event. @@ -1216,7 +1321,8 @@ import java.io.IOException; } private void maybeContinueLoading() { - long nextLoadPositionUs = loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs(); + long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0 + : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { setIsLoading(false); } else { @@ -1241,21 +1347,28 @@ import java.io.IOException; } private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException { - playingPeriodHolder = periodHolder; + if (playingPeriodHolder == periodHolder) { + return; + } + int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; - TrackSelection newSelection = periodHolder.trackSelections.get(i); + TrackSelection newSelection = periodHolder.trackSelectorResult.selections.get(i); if (newSelection != null) { - // The renderer should be enabled when playing the new period. enabledRendererCount++; - } else if (rendererWasEnabledFlags[i]) { - // The renderer should be disabled when playing the new period. + } + if (rendererWasEnabledFlags[i] && (newSelection == null + || (renderer.isCurrentStreamFinal() + && renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) { + // The renderer should be disabled before playing the next period, either because it's not + // needed to play the next period, or because we need to re-enable it as its current stream + // is final and it's not reading ahead. if (renderer == rendererMediaClockSource) { // Sync standaloneMediaClock so that it can take over timing responsibilities. - standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs()); + standaloneMediaClock.synchronize(rendererMediaClock); rendererMediaClock = null; rendererMediaClockSource = null; } @@ -1264,7 +1377,8 @@ import java.io.IOException; } } - eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget(); + playingPeriodHolder = periodHolder; + eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult).sendToTarget(); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } @@ -1274,10 +1388,12 @@ import java.io.IOException; enabledRendererCount = 0; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; - TrackSelection newSelection = playingPeriodHolder.trackSelections.get(i); + TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get(i); if (newSelection != null) { enabledRenderers[enabledRendererCount++] = renderer; if (renderer.getState() == Renderer.STATE_DISABLED) { + RendererConfiguration rendererConfiguration = + playingPeriodHolder.trackSelectorResult.rendererConfigurations[i]; // The renderer needs enabling with its new track selection. boolean playing = playWhenReady && state == ExoPlayer.STATE_READY; // Consider as joining only if the renderer was previously disabled. @@ -1288,8 +1404,8 @@ import java.io.IOException; formats[j] = newSelection.getFormat(j); } // Enable the renderer. - renderer.enable(formats, playingPeriodHolder.sampleStreams[i], rendererPositionUs, - joining, playingPeriodHolder.getRendererOffset()); + renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[i], + rendererPositionUs, joining, playingPeriodHolder.getRendererOffset()); MediaClock mediaClock = renderer.getMediaClock(); if (mediaClock != null) { if (rendererMediaClock != null) { @@ -1298,6 +1414,7 @@ import java.io.IOException; } rendererMediaClock = mediaClock; rendererMediaClockSource = renderer; + rendererMediaClock.setPlaybackParameters(playbackParameters); } // Start the renderer if playing. if (playing) { @@ -1326,6 +1443,7 @@ import java.io.IOException; public boolean hasEnabledTracks; public MediaPeriodHolder next; public boolean needsContinueLoading; + public TrackSelectorResult trackSelectorResult; private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; @@ -1333,10 +1451,7 @@ import java.io.IOException; private final LoadControl loadControl; private final MediaSource mediaSource; - private Object trackSelectionsInfo; - private TrackGroupArray trackGroups; - private TrackSelectionArray trackSelections; - private TrackSelectionArray periodTrackSelections; + private TrackSelectorResult periodTrackSelectorResult; public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl, @@ -1382,20 +1497,17 @@ import java.io.IOException; public void handlePrepared() throws ExoPlaybackException { prepared = true; - trackGroups = mediaPeriod.getTrackGroups(); selectTracks(); startPositionUs = updatePeriodTrackSelection(startPositionUs, false); } public boolean selectTracks() throws ExoPlaybackException { - Pair selectorResult = trackSelector.selectTracks( - rendererCapabilities, trackGroups); - TrackSelectionArray newTrackSelections = selectorResult.first; - if (newTrackSelections.equals(periodTrackSelections)) { + TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities, + mediaPeriod.getTrackGroups()); + if (selectorResult.isEquivalent(periodTrackSelectorResult)) { return false; } - trackSelections = newTrackSelections; - trackSelectionsInfo = selectorResult.second; + trackSelectorResult = selectorResult; return true; } @@ -1406,16 +1518,16 @@ import java.io.IOException; public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams, boolean[] streamResetFlags) { + TrackSelectionArray trackSelections = trackSelectorResult.selections; for (int i = 0; i < trackSelections.length; i++) { mayRetainStreamFlags[i] = !forceRecreateStreams - && Util.areEqual(periodTrackSelections == null ? null : periodTrackSelections.get(i), - trackSelections.get(i)); + && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i); } // Disable streams on the period and get new streams for updated/newly-enabled tracks. positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags, sampleStreams, streamResetFlags, positionUs); - periodTrackSelections = trackSelections; + periodTrackSelectorResult = trackSelectorResult; // Update whether we have enabled tracks and sanity check the expected streams are non-null. hasEnabledTracks = false; @@ -1429,14 +1541,10 @@ import java.io.IOException; } // The track selection has changed. - loadControl.onTracksSelected(renderers, trackGroups, trackSelections); + loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections); return positionUs; } - public TrackInfo getTrackInfo() { - return new TrackInfo(trackGroups, trackSelections, trackSelectionsInfo); - } - public void release() { try { mediaSource.releasePeriod(mediaPeriod); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerLibraryInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerLibraryInfo.java index 302c7562a..0e25366cb 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerLibraryInfo.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ExoPlayerLibraryInfo.java @@ -21,18 +21,26 @@ package org.telegram.messenger.exoplayer2; public interface ExoPlayerLibraryInfo { /** - * The version of the library, expressed as a string. + * The version of the library expressed as a string, for example "1.2.3". */ - String VERSION = "2.0.4"; + // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. + String VERSION = "2.4.0"; /** - * The version of the library, expressed as an integer. + * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. + */ + // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. + String VERSION_SLASHY = "ExoPlayerLib/2.4.0"; + + /** + * The version of the library expressed as an integer, for example 1002003. *

    * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * integer version 123045006 (123-045-006). */ - int VERSION_INT = 2000004; + // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. + int VERSION_INT = 2004000; /** * Whether the library was compiled with {@link org.telegram.messenger.exoplayer2.util.Assertions} @@ -45,5 +53,5 @@ public interface ExoPlayerLibraryInfo { * trace enabled. */ boolean TRACE_ENABLED = true; - + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Format.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Format.java index 2868758d9..dda02f3d9 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Format.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Format.java @@ -24,6 +24,7 @@ import org.telegram.messenger.exoplayer2.drm.DrmInitData; import org.telegram.messenger.exoplayer2.metadata.Metadata; import org.telegram.messenger.exoplayer2.util.MimeTypes; import org.telegram.messenger.exoplayer2.util.Util; +import org.telegram.messenger.exoplayer2.video.ColorInfo; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -120,7 +121,7 @@ public final class Format implements Parcelable { /** * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link - * C#STEREO_MODE_LEFT_RIGHT}. + * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}. */ @C.StereoMode public final int stereoMode; @@ -128,6 +129,10 @@ public final class Format implements Parcelable { * The projection data for 360/VR video, or null if not applicable. */ public final byte[] projectionData; + /** + * The color metadata associated with the video, helps with accurate color reproduction. + */ + public final ColorInfo colorInfo; // Audio specific. @@ -183,20 +188,18 @@ public final class Format implements Parcelable { */ public final int accessibilityChannel; - // Lazily initialized hashcode and framework media format. - + // Lazily initialized hashcode. private int hashCode; - private MediaFormat frameworkMediaFormat; // Video. public static Format createVideoContainerFormat(String id, String containerMimeType, String sampleMimeType, String codecs, int bitrate, int width, int height, - float frameRate, List initializationData) { + float frameRate, List initializationData, @C.SelectionFlags int selectionFlags) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width, - height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null, - null); + height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, + initializationData, null, null); } public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, @@ -212,17 +215,18 @@ public final class Format implements Parcelable { DrmInitData drmInitData) { return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, initializationData, rotationDegrees, pixelWidthHeightRatio, null, - NO_VALUE, drmInitData); + NO_VALUE, null, drmInitData); } public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, int maxInputSize, int width, int height, float frameRate, List initializationData, int rotationDegrees, float pixelWidthHeightRatio, - byte[] projectionData, @C.StereoMode int stereoMode, DrmInitData drmInitData) { + byte[] projectionData, @C.StereoMode int stereoMode, ColorInfo colorInfo, + DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height, - frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, - initializationData, drmInitData, null); + frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, + colorInfo, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, + OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null); } // Audio. @@ -231,8 +235,8 @@ public final class Format implements Parcelable { String sampleMimeType, String codecs, int bitrate, int channelCount, int sampleRate, List initializationData, @C.SelectionFlags int selectionFlags, String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, + NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null, null); } @@ -259,7 +263,7 @@ public final class Format implements Parcelable { List initializationData, DrmInitData drmInitData, @C.SelectionFlags int selectionFlags, String language, Metadata metadata) { return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding, + NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, metadata); } @@ -277,38 +281,39 @@ public final class Format implements Parcelable { String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel, OFFSET_SAMPLE_RELATIVE, null, null, null); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) { return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE); + NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList()); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, @C.SelectionFlags int selectionFlags, String language, - int accessibilityChannel, DrmInitData drmInitData) { + int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel, + DrmInitData drmInitData) { return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE); + accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList()); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData, long subsampleOffsetUs) { return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - NO_VALUE, drmInitData, subsampleOffsetUs); + NO_VALUE, drmInitData, subsampleOffsetUs, Collections.emptyList()); } public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, String language, - int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs) { + int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs, + List initializationData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, null, - drmInitData, null); + NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, null); } // Image. @@ -316,32 +321,41 @@ public final class Format implements Parcelable { public static Format createImageSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, List initializationData, String language, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null); } // Generic. - public static Format createContainerFormat(String id, String containerMimeType, String codecs, - String sampleMimeType, int bitrate) { + public static Format createContainerFormat(String id, String containerMimeType, + String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, + String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, null); + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, + null); + } + + public static Format createSampleFormat(String id, String sampleMimeType, + long subsampleOffsetUs) { + return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null); } public static Format createSampleFormat(String id, String sampleMimeType, String codecs, int bitrate, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null); } /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs, int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees, float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode, - int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int encoderDelay, - int encoderPadding, @C.SelectionFlags int selectionFlags, String language, + ColorInfo colorInfo, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, + int encoderDelay, int encoderPadding, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel, long subsampleOffsetUs, List initializationData, DrmInitData drmInitData, Metadata metadata) { this.id = id; @@ -357,6 +371,7 @@ public final class Format implements Parcelable { this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.projectionData = projectionData; this.stereoMode = stereoMode; + this.colorInfo = colorInfo; this.channelCount = channelCount; this.sampleRate = sampleRate; this.pcmEncoding = pcmEncoding; @@ -388,6 +403,7 @@ public final class Format implements Parcelable { boolean hasProjectionData = in.readInt() != 0; projectionData = hasProjectionData ? in.createByteArray() : null; stereoMode = in.readInt(); + colorInfo = in.readParcelable(ColorInfo.class.getClassLoader()); channelCount = in.readInt(); sampleRate = in.readInt(); pcmEncoding = in.readInt(); @@ -409,67 +425,71 @@ public final class Format implements Parcelable { public Format copyWithMaxInputSize(int maxInputSize) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height, @C.SelectionFlags int selectionFlags, String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } - public Format copyWithManifestFormatInfo(Format manifestFormat, - boolean preferManifestDrmInitData) { + @SuppressWarnings("ReferenceEquality") + public Format copyWithManifestFormatInfo(Format manifestFormat) { + if (this == manifestFormat) { + // No need to copy from ourselves. + return this; + } String id = manifestFormat.id; String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs; int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate; float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate; @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; String language = this.language == null ? manifestFormat.language : this.language; - DrmInitData drmInitData = (preferManifestDrmInitData && manifestFormat.drmInitData != null) - || this.drmInitData == null ? manifestFormat.drmInitData : this.drmInitData; + DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData + : this.drmInitData; return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, - language, accessibilityChannel, subsampleOffsetUs, initializationData, drmInitData, - metadata); + colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, + selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, + drmInitData, metadata); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } public Format copyWithDrmInitData(DrmInitData drmInitData) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } public Format copyWithMetadata(Metadata metadata) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, + encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, + initializationData, drmInitData, metadata); } /** @@ -486,31 +506,29 @@ public final class Format implements Parcelable { @SuppressLint("InlinedApi") @TargetApi(16) public final MediaFormat getFrameworkMediaFormatV16() { - if (frameworkMediaFormat == null) { - MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, sampleMimeType); - maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language); - maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize); - maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width); - maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height); - maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate); - maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees); - maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount); - maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate); - maybeSetIntegerV16(format, "encoder-delay", encoderDelay); - maybeSetIntegerV16(format, "encoder-padding", encoderPadding); - for (int i = 0; i < initializationData.size(); i++) { - format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); - } - frameworkMediaFormat = format; + MediaFormat format = new MediaFormat(); + format.setString(MediaFormat.KEY_MIME, sampleMimeType); + maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language); + maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize); + maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width); + maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height); + maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate); + maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees); + maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount); + maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate); + maybeSetIntegerV16(format, "encoder-delay", encoderDelay); + maybeSetIntegerV16(format, "encoder-padding", encoderPadding); + for (int i = 0; i < initializationData.size(); i++) { + format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); } - return frameworkMediaFormat; + maybeSetColorInfoV24(format, colorInfo); + return format; } @Override public String toString() { return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", " - + ", " + language + ", [" + width + ", " + height + ", " + frameRate + "]" + + language + ", [" + width + ", " + height + ", " + frameRate + "]" + ", [" + channelCount + ", " + sampleRate + "])"; } @@ -560,6 +578,7 @@ public final class Format implements Parcelable { || !Util.areEqual(codecs, other.codecs) || !Util.areEqual(drmInitData, other.drmInitData) || !Util.areEqual(metadata, other.metadata) + || !Util.areEqual(colorInfo, other.colorInfo) || !Arrays.equals(projectionData, other.projectionData) || initializationData.size() != other.initializationData.size()) { return false; @@ -572,6 +591,17 @@ public final class Format implements Parcelable { return true; } + @TargetApi(24) + private static void maybeSetColorInfoV24(MediaFormat format, ColorInfo colorInfo) { + if (colorInfo == null) { + return; + } + maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer); + maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace); + maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange); + maybeSetByteBufferV16(format, MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo); + } + @TargetApi(16) private static void maybeSetStringV16(MediaFormat format, String key, String value) { if (value != null) { @@ -593,6 +623,45 @@ public final class Format implements Parcelable { } } + @TargetApi(16) + private static void maybeSetByteBufferV16(MediaFormat format, String key, byte[] value) { + if (value != null) { + format.setByteBuffer(key, ByteBuffer.wrap(value)); + } + } + + // Utility methods + + /** + * Returns a prettier {@link String} than {@link #toString()}, intended for logging. + */ + public static String toLogString(Format format) { + if (format == null) { + return "null"; + } + StringBuilder builder = new StringBuilder(); + builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType); + if (format.bitrate != Format.NO_VALUE) { + builder.append(", bitrate=").append(format.bitrate); + } + if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) { + builder.append(", res=").append(format.width).append("x").append(format.height); + } + if (format.frameRate != Format.NO_VALUE) { + builder.append(", fps=").append(format.frameRate); + } + if (format.channelCount != Format.NO_VALUE) { + builder.append(", channels=").append(format.channelCount); + } + if (format.sampleRate != Format.NO_VALUE) { + builder.append(", sample_rate=").append(format.sampleRate); + } + if (format.language != null) { + builder.append(", language=").append(format.language); + } + return builder.toString(); + } + // Parcelable implementation. @Override @@ -618,6 +687,7 @@ public final class Format implements Parcelable { dest.writeByteArray(projectionData); } dest.writeInt(stereoMode); + dest.writeParcelable(colorInfo, flags); dest.writeInt(channelCount); dest.writeInt(sampleRate); dest.writeInt(pcmEncoding); @@ -636,9 +706,6 @@ public final class Format implements Parcelable { dest.writeParcelable(metadata, 0); } - /** - * {@link Creator} implementation. - */ public static final Creator CREATOR = new Creator() { @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/IllegalSeekPositionException.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/IllegalSeekPositionException.java new file mode 100755 index 000000000..b6de1d66b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/IllegalSeekPositionException.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2; + +/** + * Thrown when an attempt is made to seek to a position that does not exist in the player's + * {@link Timeline}. + */ +public final class IllegalSeekPositionException extends IllegalStateException { + + /** + * The {@link Timeline} in which the seek was attempted. + */ + public final Timeline timeline; + /** + * The index of the window being seeked to. + */ + public final int windowIndex; + /** + * The seek position in the specified window. + */ + public final long positionMs; + + /** + * @param timeline The {@link Timeline} in which the seek was attempted. + * @param windowIndex The index of the window being seeked to. + * @param positionMs The seek position in the specified window. + */ + public IllegalSeekPositionException(Timeline timeline, int windowIndex, long positionMs) { + this.timeline = timeline; + this.windowIndex = windowIndex; + this.positionMs = positionMs; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/PlaybackParameters.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/PlaybackParameters.java new file mode 100755 index 000000000..511820a97 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/PlaybackParameters.java @@ -0,0 +1,83 @@ +/* + * 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 org.telegram.messenger.exoplayer2; + +/** + * The parameters that apply to playback. + */ +public final class PlaybackParameters { + + /** + * The default playback parameters: real-time playback with no pitch modification. + */ + public static final PlaybackParameters DEFAULT = new PlaybackParameters(1f, 1f); + + /** + * The factor by which playback will be sped up. + */ + public final float speed; + + /** + * The factor by which the audio pitch will be scaled. + */ + public final float pitch; + + private final int scaledUsPerMs; + + /** + * Creates new playback parameters. + * + * @param speed The factor by which playback will be sped up. + * @param pitch The factor by which the audio pitch will be scaled. + */ + public PlaybackParameters(float speed, float pitch) { + this.speed = speed; + this.pitch = pitch; + scaledUsPerMs = Math.round(speed * 1000f); + } + + /** + * Scales the millisecond duration {@code timeMs} by the playback speed, returning the result in + * microseconds. + * + * @param timeMs The time to scale, in milliseconds. + * @return The scaled time, in microseconds. + */ + public long getSpeedAdjustedDurationUs(long timeMs) { + return timeMs * scaledUsPerMs; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PlaybackParameters other = (PlaybackParameters) obj; + return this.speed == other.speed && this.pitch == other.pitch; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + Float.floatToRawIntBits(speed); + result = 31 * result + Float.floatToRawIntBits(pitch); + return result; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Renderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Renderer.java index 2c244451b..6b1fc261c 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Renderer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Renderer.java @@ -92,6 +92,7 @@ public interface Renderer extends ExoPlayerComponent { * This method may be called when the renderer is in the following states: * {@link #STATE_DISABLED}. * + * @param configuration The renderer configuration. * @param formats The enabled formats. * @param stream The {@link SampleStream} from which the renderer should consume. * @param positionUs The player's current position. @@ -100,8 +101,8 @@ public interface Renderer extends ExoPlayerComponent { * before they are rendered. * @throws ExoPlaybackException If an error occurs. */ - void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining, - long offsetUs) throws ExoPlaybackException; + void enable(RendererConfiguration configuration, Format[] formats, SampleStream stream, + long positionUs, boolean joining, long offsetUs) throws ExoPlaybackException; /** * Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be @@ -149,7 +150,13 @@ public interface Renderer extends ExoPlayerComponent { * This method may be called when the renderer is in the following states: * {@link #STATE_ENABLED}, {@link #STATE_STARTED}. */ - void setCurrentStreamIsFinal(); + void setCurrentStreamFinal(); + + /** + * Returns whether the current {@link SampleStream} will be the final one supplied before the + * renderer is next disabled or reset. + */ + boolean isCurrentStreamFinal(); /** * Throws an error that's preventing the renderer from reading from its {@link SampleStream}. Does diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/RendererCapabilities.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/RendererCapabilities.java index 572968c2f..b4f25651d 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/RendererCapabilities.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/RendererCapabilities.java @@ -79,6 +79,20 @@ public interface RendererCapabilities { */ int ADAPTIVE_NOT_SUPPORTED = 0b0000; + /** + * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of + * {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + */ + int TUNNELING_SUPPORT_MASK = 0b10000; + /** + * The {@link Renderer} supports tunneled output. + */ + int TUNNELING_SUPPORTED = 0b10000; + /** + * The {@link Renderer} does not support tunneled output. + */ + int TUNNELING_NOT_SUPPORTED = 0b00000; + /** * 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 @@ -91,7 +105,7 @@ public interface RendererCapabilities { /** * Returns the extent to which the {@link Renderer} supports a given format. The returned value is - * the bitwise OR of two properties: + * 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_SUBTYPE} and @@ -99,9 +113,12 @@ public interface RendererCapabilities { *
    • 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}.
    • + *
    • The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and + * {@link #TUNNELING_NOT_SUPPORTED}.
    • *
    * The individual properties can be retrieved by performing a bitwise AND with - * {@link #FORMAT_SUPPORT_MASK} and {@link #ADAPTIVE_SUPPORT_MASK} respectively. + * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and + * {@link #TUNNELING_SUPPORT_MASK} respectively. * * @param format The format. * @return The extent to which the renderer is capable of supporting the given format. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/RendererConfiguration.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/RendererConfiguration.java new file mode 100755 index 000000000..2c46a8031 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/RendererConfiguration.java @@ -0,0 +1,60 @@ +/* + * 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 org.telegram.messenger.exoplayer2; + +/** + * The configuration of a {@link Renderer}. + */ +public final class RendererConfiguration { + + /** + * The default configuration. + */ + public static final RendererConfiguration DEFAULT = + new RendererConfiguration(C.AUDIO_SESSION_ID_UNSET); + + /** + * The audio session id to use for tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling + * should not be enabled. + */ + public final int tunnelingAudioSessionId; + + /** + * @param tunnelingAudioSessionId The audio session id to use for tunneling, or + * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. + */ + public RendererConfiguration(int tunnelingAudioSessionId) { + this.tunnelingAudioSessionId = tunnelingAudioSessionId; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + RendererConfiguration other = (RendererConfiguration) obj; + return tunnelingAudioSessionId == other.tunnelingAudioSessionId; + } + + @Override + public int hashCode() { + return tunnelingAudioSessionId; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/RenderersFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/RenderersFactory.java new file mode 100755 index 000000000..b134d7eab --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/RenderersFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2; + +import android.os.Handler; +import org.telegram.messenger.exoplayer2.audio.AudioRendererEventListener; +import org.telegram.messenger.exoplayer2.metadata.MetadataRenderer; +import org.telegram.messenger.exoplayer2.text.TextRenderer; +import org.telegram.messenger.exoplayer2.video.VideoRendererEventListener; + +/** + * Builds {@link Renderer} instances for use by a {@link SimpleExoPlayer}. + */ +public interface RenderersFactory { + + /** + * Builds the {@link Renderer} instances for a {@link SimpleExoPlayer}. + * + * @param eventHandler A handler to use when invoking event listeners and outputs. + * @param videoRendererEventListener An event listener for video renderers. + * @param videoRendererEventListener An event listener for audio renderers. + * @param textRendererOutput An output for text renderers. + * @param metadataRendererOutput An output for metadata renderers. + * @return The {@link Renderer instances}. + */ + Renderer[] createRenderers(Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/SimpleExoPlayer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/SimpleExoPlayer.java index e8d9163b4..7755a54c3 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/SimpleExoPlayer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/SimpleExoPlayer.java @@ -16,40 +16,27 @@ package org.telegram.messenger.exoplayer2; import android.annotation.TargetApi; -import android.content.Context; import android.graphics.SurfaceTexture; import android.media.MediaCodec; import android.media.PlaybackParams; import android.os.Handler; -import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; -import org.telegram.messenger.exoplayer2.audio.AudioCapabilities; import org.telegram.messenger.exoplayer2.audio.AudioRendererEventListener; -import org.telegram.messenger.exoplayer2.audio.AudioTrack; -import org.telegram.messenger.exoplayer2.audio.MediaCodecAudioRenderer; import org.telegram.messenger.exoplayer2.decoder.DecoderCounters; -import org.telegram.messenger.exoplayer2.drm.DrmSessionManager; -import org.telegram.messenger.exoplayer2.drm.FrameworkMediaCrypto; -import org.telegram.messenger.exoplayer2.mediacodec.MediaCodecSelector; import org.telegram.messenger.exoplayer2.metadata.Metadata; import org.telegram.messenger.exoplayer2.metadata.MetadataRenderer; -import org.telegram.messenger.exoplayer2.metadata.id3.Id3Decoder; import org.telegram.messenger.exoplayer2.source.MediaSource; import org.telegram.messenger.exoplayer2.source.TrackGroupArray; import org.telegram.messenger.exoplayer2.text.Cue; import org.telegram.messenger.exoplayer2.text.TextRenderer; import org.telegram.messenger.exoplayer2.trackselection.TrackSelectionArray; import org.telegram.messenger.exoplayer2.trackselection.TrackSelector; -import org.telegram.messenger.exoplayer2.video.MediaCodecVideoRenderer; import org.telegram.messenger.exoplayer2.video.VideoRendererEventListener; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.reflect.Constructor; -import java.util.ArrayList; import java.util.List; /** @@ -93,38 +80,12 @@ public class SimpleExoPlayer implements ExoPlayer { void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture); } - /** - * Modes for using extension renderers. - */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, EXTENSION_RENDERER_MODE_PREFER}) - public @interface ExtensionRendererMode {} - /** - * Do not allow use of extension renderers. - */ - public static final int EXTENSION_RENDERER_MODE_OFF = 0; - /** - * Allow use of extension renderers. Extension renderers are indexed after core renderers of the - * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore - * prefer to use a core renderer to an extension renderer in the case that both are able to play - * a given track. - */ - public static final int EXTENSION_RENDERER_MODE_ON = 1; - /** - * Allow use of extension renderers. Extension renderers are indexed before core renderers of the - * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore - * prefer to use an extension renderer to a core renderer in the case that both are able to play - * a given track. - */ - public static final int EXTENSION_RENDERER_MODE_PREFER = 2; - private static final String TAG = "SimpleExoPlayer"; - protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; + + protected final Renderer[] renderers; private final ExoPlayer player; - private final Renderer[] renderers; private final ComponentListener componentListener; - private final Handler mainHandler; private final int videoRendererCount; private final int audioRendererCount; @@ -150,19 +111,12 @@ public class SimpleExoPlayer implements ExoPlayer { @C.StreamType private int audioStreamType; private float audioVolume; - private PlaybackParamsHolder playbackParamsHolder; - protected SimpleExoPlayer(Context context, TrackSelector trackSelector, LoadControl loadControl, - DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { - mainHandler = new Handler(); + protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, + LoadControl loadControl) { componentListener = new ComponentListener(); - - // Build the renderers. - ArrayList renderersList = new ArrayList<>(); - buildRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - allowedVideoJoiningTimeMs, renderersList); - renderers = renderersList.toArray(new Renderer[renderersList.size()]); + renderers = renderersFactory.createRenderers(new Handler(), componentListener, + componentListener, componentListener, componentListener); // Obtain counts of video and audio renderers. int videoRendererCount = 0; @@ -182,7 +136,7 @@ public class SimpleExoPlayer implements ExoPlayer { // Set initial values. audioVolume = 1; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioStreamType = C.STREAM_TYPE_DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; @@ -244,6 +198,18 @@ public class SimpleExoPlayer implements ExoPlayer { setVideoSurfaceInternal(surface, false); } + /** + * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. + * Else does nothing. + * + * @param surface The surface to clear. + */ + public void clearVideoSurface(Surface surface) { + if (surface != null && surface == this.surface) { + setVideoSurface(null); + } + } + /** * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be * rendered. The player will track the lifecycle of the surface automatically. @@ -261,6 +227,18 @@ public class SimpleExoPlayer implements ExoPlayer { } } + /** + * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being + * rendered if it matches the one passed. Else does nothing. + * + * @param surfaceHolder The surface holder to clear. + */ + public void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) { + setVideoSurfaceHolder(null); + } + } + /** * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the * lifecycle of the surface automatically. @@ -268,7 +246,17 @@ public class SimpleExoPlayer implements ExoPlayer { * @param surfaceView The surface view. */ public void setVideoSurfaceView(SurfaceView surfaceView) { - setVideoSurfaceHolder(surfaceView.getHolder()); + setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); + } + + /** + * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one passed. + * Else does nothing. + * + * @param surfaceView The texture view to clear. + */ + public void clearVideoSurfaceView(SurfaceView surfaceView) { + clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } /** @@ -277,9 +265,13 @@ public class SimpleExoPlayer implements ExoPlayer { * * @param textureView The texture view. */ - public ComponentListener setVideoTextureView(TextureView textureView) { + public void setVideoTextureView(TextureView textureView) { + if (this.textureView == textureView) { + return; + } removeSurfaceCallbacks(); this.textureView = textureView; + needSetSurface = true; if (textureView == null) { setVideoSurfaceInternal(null, true); } else { @@ -288,13 +280,20 @@ public class SimpleExoPlayer implements ExoPlayer { } SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true); - if (surfaceTexture != null) { - needSetSurface = false; - } - textureView.setSurfaceTextureListener(componentListener); } - return componentListener; + } + + /** + * Clears the {@link TextureView} onto which video is being rendered if it matches the one passed. + * Else does nothing. + * + * @param textureView The texture view to clear. + */ + public void clearVideoTextureView(TextureView textureView) { + if (textureView != null && textureView == this.textureView) { + setVideoTextureView(null); + } } /** @@ -354,37 +353,20 @@ public class SimpleExoPlayer implements ExoPlayer { /** * Sets the {@link PlaybackParams} governing audio playback. * + * @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}. * @param params The {@link PlaybackParams}, or null to clear any previously set parameters. */ + @Deprecated @TargetApi(23) - public void setPlaybackParams(PlaybackParams params) { + public void setPlaybackParams(@Nullable PlaybackParams params) { + PlaybackParameters playbackParameters; if (params != null) { - // The audio renderers will call this on the playback thread to ensure they can query - // parameters without failure. We do the same up front, which is redundant except that it - // ensures an immediate call to getPlaybackParams will retrieve the instance with defaults - // allowed, rather than this change becoming visible sometime later once the audio renderers - // receive the parameters. params.allowDefaults(); - playbackParamsHolder = new PlaybackParamsHolder(params); + playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch()); } else { - playbackParamsHolder = null; + playbackParameters = null; } - ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; - int count = 0; - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_PLAYBACK_PARAMS, params); - } - } - player.sendMessages(messages); - } - - /** - * Returns the {@link PlaybackParams} governing audio playback, or null if not set. - */ - @TargetApi(23) - public PlaybackParams getPlaybackParams() { - return playbackParamsHolder == null ? null : playbackParamsHolder.params; + setPlaybackParameters(playbackParameters); } /** @@ -402,7 +384,7 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Returns the audio session identifier, or {@code AudioTrack.SESSION_ID_NOT_SET} if not set. + * Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set. */ public int getAudioSessionId() { return audioSessionId; @@ -431,6 +413,57 @@ public class SimpleExoPlayer implements ExoPlayer { videoListener = listener; } + /** + * Clears the listener receiving video events if it matches the one passed. Else does nothing. + * + * @param listener The listener to clear. + */ + public void clearVideoListener(VideoListener listener) { + if (videoListener == listener) { + videoListener = null; + } + } + + /** + * Sets an output to receive text events. + * + * @param output The output. + */ + public void setTextOutput(TextRenderer.Output output) { + textOutput = output; + } + + /** + * Clears the output receiving text events if it matches the one passed. Else does nothing. + * + * @param output The output to clear. + */ + public void clearTextOutput(TextRenderer.Output output) { + if (textOutput == output) { + textOutput = null; + } + } + + /** + * Sets a listener to receive metadata events. + * + * @param output The output. + */ + public void setMetadataOutput(MetadataRenderer.Output output) { + metadataOutput = output; + } + + /** + * Clears the output receiving metadata events if it matches the one passed. Else does nothing. + * + * @param output The output to clear. + */ + public void clearMetadataOutput(MetadataRenderer.Output output) { + if (metadataOutput == output) { + metadataOutput = null; + } + } + /** * Sets a listener to receive debug events from the video renderer. * @@ -449,33 +482,6 @@ public class SimpleExoPlayer implements ExoPlayer { audioDebugListener = listener; } - /** - * Sets an output to receive text events. - * - * @param output The output. - */ - public void setTextOutput(TextRenderer.Output output) { - textOutput = output; - } - - /** - * @deprecated Use {@link #setMetadataOutput(MetadataRenderer.Output)} instead. - * @param output The output. - */ - @Deprecated - public void setId3Output(MetadataRenderer.Output output) { - setMetadataOutput(output); - } - - /** - * Sets a listener to receive metadata events. - * - * @param output The output. - */ - public void setMetadataOutput(MetadataRenderer.Output output) { - metadataOutput = output; - } - // ExoPlayer implementation @Override @@ -499,8 +505,8 @@ public class SimpleExoPlayer implements ExoPlayer { } @Override - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline) { - player.prepare(mediaSource, resetPosition, resetTimeline); + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + player.prepare(mediaSource, resetPosition, resetState); } @Override @@ -538,6 +544,16 @@ public class SimpleExoPlayer implements ExoPlayer { player.seekTo(windowIndex, positionMs); } + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + player.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return player.getPlaybackParameters(); + } + @Override public void stop() { player.stop(); @@ -565,6 +581,36 @@ public class SimpleExoPlayer implements ExoPlayer { player.blockingSendMessages(messages); } + @Override + public int getRendererCount() { + return player.getRendererCount(); + } + + @Override + public int getRendererType(int index) { + return player.getRendererType(index); + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + return player.getCurrentTrackGroups(); + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + return player.getCurrentTrackSelections(); + } + + @Override + public Timeline getCurrentTimeline() { + return player.getCurrentTimeline(); + } + + @Override + public Object getCurrentManifest() { + return player.getCurrentManifest(); + } + @Override public int getCurrentPeriodIndex() { return player.getCurrentPeriodIndex(); @@ -596,205 +642,13 @@ public class SimpleExoPlayer implements ExoPlayer { } @Override - public int getRendererCount() { - return player.getRendererCount(); + public boolean isCurrentWindowDynamic() { + return player.isCurrentWindowDynamic(); } @Override - public int getRendererType(int index) { - return player.getRendererType(index); - } - - @Override - public TrackGroupArray getCurrentTrackGroups() { - return player.getCurrentTrackGroups(); - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - return player.getCurrentTrackSelections(); - } - - @Override - public Timeline getCurrentTimeline() { - return player.getCurrentTimeline(); - } - - @Override - public Object getCurrentManifest() { - return player.getCurrentManifest(); - } - - // Renderer building. - - private void buildRenderers(Context context, Handler mainHandler, - DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs, - ArrayList out) { - buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - componentListener, allowedVideoJoiningTimeMs, out); - buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - componentListener, out); - buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out); - buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out); - buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out); - } - - /** - * Builds video renderers for use by the player. - * - * @param context The {@link Context} associated with the player. - * @param mainHandler A handler associated with the main thread's looper. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. - * @param extensionRendererMode The extension renderer mode. - * @param eventListener An event listener. - * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers - * can attempt to seamlessly join an ongoing playback. - * @param out An array to which the built renderers should be appended. - */ - protected void buildVideoRenderers(Context context, Handler mainHandler, - DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, VideoRendererEventListener eventListener, - long allowedVideoJoiningTimeMs, ArrayList out) { - out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT, - allowedVideoJoiningTimeMs, drmSessionManager, false, mainHandler, eventListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); - - if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { - return; - } - int extensionRendererIndex = out.size(); - if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) { - extensionRendererIndex--; - } - - try { - Class clazz = - Class.forName("org.telegram.messenger.exoplayer2.ext.vp9.LibvpxVideoRenderer"); - Constructor constructor = clazz.getConstructor(boolean.class, long.class, Handler.class, - VideoRendererEventListener.class, int.class); - Renderer renderer = (Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs, - mainHandler, componentListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); - out.add(extensionRendererIndex++, renderer); - Log.i(TAG, "Loaded LibvpxVideoRenderer."); - } catch (ClassNotFoundException e) { - // Expected if the app was built without the extension. - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Builds audio renderers for use by the player. - * - * @param context The {@link Context} associated with the player. - * @param mainHandler A handler associated with the main thread's looper. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. - * @param extensionRendererMode The extension renderer mode. - * @param eventListener An event listener. - * @param out An array to which the built renderers should be appended. - */ - protected void buildAudioRenderers(Context context, Handler mainHandler, - DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener, - ArrayList out) { - out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, - mainHandler, eventListener, AudioCapabilities.getCapabilities(context))); - - if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { - return; - } - int extensionRendererIndex = out.size(); - if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) { - extensionRendererIndex--; - } - - try { - Class clazz = - Class.forName("org.telegram.messenger.exoplayer2.ext.opus.LibopusAudioRenderer"); - Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); - out.add(extensionRendererIndex++, renderer); - Log.i(TAG, "Loaded LibopusAudioRenderer."); - } catch (ClassNotFoundException e) { - // Expected if the app was built without the extension. - } catch (Exception e) { - throw new RuntimeException(e); - } - - try { - Class clazz = - Class.forName("org.telegram.messenger.exoplayer2.ext.flac.LibflacAudioRenderer"); - Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); - out.add(extensionRendererIndex++, renderer); - Log.i(TAG, "Loaded LibflacAudioRenderer."); - } catch (ClassNotFoundException e) { - // Expected if the app was built without the extension. - } catch (Exception e) { - throw new RuntimeException(e); - } - - try { - Class clazz = - Class.forName("org.telegram.messenger.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); - Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); - out.add(extensionRendererIndex++, renderer); - Log.i(TAG, "Loaded FfmpegAudioRenderer."); - } catch (ClassNotFoundException e) { - // Expected if the app was built without the extension. - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Builds text renderers for use by the player. - * - * @param context The {@link Context} associated with the player. - * @param mainHandler A handler associated with the main thread's looper. - * @param extensionRendererMode The extension renderer mode. - * @param output An output for the renderers. - * @param out An array to which the built renderers should be appended. - */ - protected void buildTextRenderers(Context context, Handler mainHandler, - @ExtensionRendererMode int extensionRendererMode, TextRenderer.Output output, - ArrayList out) { - out.add(new TextRenderer(output, mainHandler.getLooper())); - } - - /** - * Builds metadata renderers for use by the player. - * - * @param context The {@link Context} associated with the player. - * @param mainHandler A handler associated with the main thread's looper. - * @param extensionRendererMode The extension renderer mode. - * @param output An output for the renderers. - * @param out An array to which the built renderers should be appended. - */ - protected void buildMetadataRenderers(Context context, Handler mainHandler, - @ExtensionRendererMode int extensionRendererMode, MetadataRenderer.Output output, - ArrayList out) { - out.add(new MetadataRenderer(output, mainHandler.getLooper(), new Id3Decoder())); - } - - /** - * Builds any miscellaneous renderers used by the player. - * - * @param context The {@link Context} associated with the player. - * @param mainHandler A handler associated with the main thread's looper. - * @param extensionRendererMode The extension renderer mode. - * @param out An array to which the built renderers should be appended. - */ - protected void buildMiscellaneousRenderers(Context context, Handler mainHandler, - @ExtensionRendererMode int extensionRendererMode, ArrayList out) { - // Do nothing. + public boolean isCurrentWindowSeekable() { + return player.isCurrentWindowSeekable(); } // Internal methods. @@ -838,11 +692,7 @@ public class SimpleExoPlayer implements ExoPlayer { this.ownsSurface = ownsSurface; } - public ComponentListener getComponentListener() { - return componentListener; - } - - public final class ComponentListener implements VideoRendererEventListener, + private final class ComponentListener implements VideoRendererEventListener, AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output, SurfaceHolder.Callback, TextureView.SurfaceTextureListener { @@ -962,7 +812,7 @@ public class SimpleExoPlayer implements ExoPlayer { } audioFormat = null; audioDecoderCounters = null; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; } // TextRenderer.Output implementation @@ -1032,15 +882,4 @@ public class SimpleExoPlayer implements ExoPlayer { } - @TargetApi(23) - private static final class PlaybackParamsHolder { - - public final PlaybackParams params; - - public PlaybackParamsHolder(PlaybackParams params) { - this.params = params; - } - - } - } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Timeline.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Timeline.java index a99c92452..3adbaaf55 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Timeline.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/Timeline.java @@ -262,9 +262,24 @@ public abstract class Timeline { */ public int lastPeriodIndex; - private long defaultPositionUs; - private long durationUs; - private long positionInFirstPeriodUs; + /** + * The default position relative to the start of the window at which to begin playback, in + * microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a + * non-zero default position projection, and if the specified projection cannot be performed + * whilst remaining within the bounds of the window. + */ + public long defaultPositionUs; + + /** + * The duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown. + */ + public long durationUs; + + /** + * The position of the start of this window relative to the start of the first period belonging + * to it, in microseconds. + */ + public long positionInFirstPeriodUs; /** * Sets the data held by this window. @@ -363,19 +378,29 @@ public abstract class Timeline { */ public int windowIndex; - private long durationUs; + /** + * The duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown. + */ + public long durationUs; + + /** + * Whether this period contains an ad. + */ + public boolean isAd; + private long positionInWindowUs; /** * Sets the data held by this period. */ public Period set(Object id, Object uid, int windowIndex, long durationUs, - long positionInWindowUs) { + long positionInWindowUs, boolean isAd) { this.id = id; this.uid = uid; this.windowIndex = windowIndex; this.durationUs = durationUs; this.positionInWindowUs = positionInWindowUs; + this.isAd = isAd; return this; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/Ac3Util.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/Ac3Util.java index 73269614c..98ec9e081 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/Ac3Util.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/Ac3Util.java @@ -28,6 +28,44 @@ import java.nio.ByteBuffer; */ public final class Ac3Util { + /** + * Holds sample format information as presented by a syncframe header. + */ + public static final class Ac3SyncFrameInfo { + + /** + * The sample mime type of the bitstream. One of {@link MimeTypes#AUDIO_AC3} and + * {@link MimeTypes#AUDIO_E_AC3}. + */ + public final String mimeType; + /** + * The audio sampling rate in Hz. + */ + public final int sampleRate; + /** + * The number of audio channels + */ + public final int channelCount; + /** + * The size of the frame. + */ + public final int frameSize; + /** + * Number of audio samples in the frame. + */ + public final int sampleCount; + + private Ac3SyncFrameInfo(String mimeType, int channelCount, int sampleRate, int frameSize, + int sampleCount) { + this.mimeType = mimeType; + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.frameSize = frameSize; + this.sampleCount = sampleCount; + } + + } + /** * The number of new samples per (E-)AC-3 audio block. */ @@ -114,62 +152,61 @@ public final class Ac3Util { } /** - * Returns the AC-3 format given {@code data} containing a syncframe. The reading position of - * {@code data} will be modified. + * Returns (E-)AC-3 format information given {@code data} containing a syncframe. The reading + * position of {@code data} will be modified. * * @param data The data to parse, positioned at the start of the syncframe. - * @param trackId The track identifier to set on the format, or null. - * @param language The language to set on the format. - * @param drmInitData {@link DrmInitData} to be included in the format. - * @return The AC-3 format parsed from data in the header. + * @return The (E-)AC-3 format data parsed from the header. */ - public static Format parseAc3SyncframeFormat(ParsableBitArray data, String trackId, - String language, DrmInitData drmInitData) { - data.skipBits(16 + 16); // syncword, crc1 - int fscod = data.readBits(2); - data.skipBits(6 + 5 + 3); // frmsizecod, bsid, bsmod - int acmod = data.readBits(3); - if ((acmod & 0x01) != 0 && acmod != 1) { - data.skipBits(2); // cmixlev - } - if ((acmod & 0x04) != 0) { - data.skipBits(2); // surmixlev - } - if (acmod == 2) { - data.skipBits(2); // dsurmod - } - boolean lfeon = data.readBit(); - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_AC3, null, Format.NO_VALUE, - Format.NO_VALUE, CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0), - SAMPLE_RATE_BY_FSCOD[fscod], null, drmInitData, 0, language); - } - - /** - * Returns the E-AC-3 format given {@code data} containing a syncframe. The reading position of - * {@code data} will be modified. - * - * @param data The data to parse, positioned at the start of the syncframe. - * @param trackId The track identifier to set on the format, or null. - * @param language The language to set on the format. - * @param drmInitData {@link DrmInitData} to be included in the format. - * @return The E-AC-3 format parsed from data in the header. - */ - public static Format parseEac3SyncframeFormat(ParsableBitArray data, String trackId, - String language, DrmInitData drmInitData) { - data.skipBits(16 + 2 + 3 + 11); // syncword, strmtype, substreamid, frmsiz + public static Ac3SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { + int initialPosition = data.getPosition(); + data.skipBits(40); + boolean isEac3 = data.readBits(5) == 16; + data.setPosition(initialPosition); + String mimeType; int sampleRate; - int fscod = data.readBits(2); - if (fscod == 3) { - sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)]; - } else { - data.skipBits(2); // numblkscod + int acmod; + int frameSize; + int sampleCount; + if (isEac3) { + mimeType = MimeTypes.AUDIO_E_AC3; + data.skipBits(16 + 2 + 3); // syncword, strmtype, substreamid + frameSize = (data.readBits(11) + 1) * 2; + int fscod = data.readBits(2); + int audioBlocks; + if (fscod == 3) { + sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)]; + audioBlocks = 6; + } else { + int numblkscod = data.readBits(2); + audioBlocks = BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod]; + sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; + } + sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks; + acmod = data.readBits(3); + } else /* is AC-3 */ { + mimeType = MimeTypes.AUDIO_AC3; + data.skipBits(16 + 16); // syncword, crc1 + int fscod = data.readBits(2); + int frmsizecod = data.readBits(6); + frameSize = getAc3SyncframeSize(fscod, frmsizecod); + data.skipBits(5 + 3); // bsid, bsmod + acmod = data.readBits(3); + if ((acmod & 0x01) != 0 && acmod != 1) { + data.skipBits(2); // cmixlev + } + if ((acmod & 0x04) != 0) { + data.skipBits(2); // surmixlev + } + if (acmod == 2) { + data.skipBits(2); // dsurmod + } sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; + sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; } - int acmod = data.readBits(3); boolean lfeon = data.readBit(); - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE, - Format.NO_VALUE, CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0), sampleRate, null, - drmInitData, 0, language); + int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); + return new Ac3SyncFrameInfo(mimeType, channelCount, sampleRate, frameSize, sampleCount); } /** @@ -187,16 +224,6 @@ public final class Ac3Util { return getAc3SyncframeSize(fscod, frmsizecod); } - /** - * Returns the size in bytes of the given E-AC-3 syncframe. - * - * @param data The syncframe to parse. - * @return The syncframe size in bytes. - */ - public static int parseEAc3SyncframeSize(byte[] data) { - return 2 * (((data[2] & 0x07) << 8) + (data[3] & 0xFF) + 1); // frmsiz - } - /** * Returns the number of audio samples in an AC-3 syncframe. */ @@ -205,22 +232,10 @@ public final class Ac3Util { } /** - * Returns the number of audio samples represented by the given E-AC-3 syncframe. + * Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's + * position is not modified. * - * @param data The syncframe to parse. - * @return The number of audio samples represented by the syncframe. - */ - public static int parseEAc3SyncframeAudioSampleCount(byte[] data) { - // See ETSI TS 102 366 subsection E.1.2.2. - return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (((data[4] & 0xC0) >> 6) == 0x03 ? 6 // fscod - : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(data[4] & 0x30) >> 4]); - } - - /** - * Like {@link #parseEAc3SyncframeAudioSampleCount(byte[])} but reads from a {@link ByteBuffer}. - * The buffer's position is not modified. - * - * @param buffer The {@link ByteBuffer} from which to read. + * @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) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/AudioProcessor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/AudioProcessor.java new file mode 100755 index 000000000..c1aa67522 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/AudioProcessor.java @@ -0,0 +1,123 @@ +/* + * 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 org.telegram.messenger.exoplayer2.audio; + +import org.telegram.messenger.exoplayer2.C; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Interface for audio processors. + */ +public interface AudioProcessor { + + /** + * Exception thrown when a processor can't be configured for a given input audio format. + */ + final class UnhandledFormatException extends Exception { + + public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) { + super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding " + + encoding); + } + + } + + /** + * An empty, direct {@link ByteBuffer}. + */ + ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()); + + /** + * Configures the processor to process input audio with the specified format. After calling this + * method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the + * processor will not accept any buffers until it is reconfigured. Returns {@code true} if the + * processor must be flushed, or if the value returned by {@link #isActive()} has changed as a + * result of the call. If it's active, {@link #getOutputChannelCount()} and + * {@link #getOutputEncoding()} return the processor's output format. + * + * @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 {@code true} if the processor must be flushed or the value returned by + * {@link #isActive()} has changed as a result of the call. + * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. + */ + boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException; + + /** + * Returns whether the processor is configured and active. + */ + boolean isActive(); + + /** + * Returns the number of audio channels in the data output by the processor. + */ + int getOutputChannelCount(); + + /** + * Returns the audio encoding used in the data output by the processor. + */ + @C.Encoding + int getOutputEncoding(); + + /** + * 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 + * read-only. Its position will be advanced by the number of bytes consumed (which may be zero). + * The caller retains ownership of the provided buffer. Calling this method invalidates any + * previous buffer returned by {@link #getOutput()}. + * + * @param buffer The input buffer to process. + */ + void queueInput(ByteBuffer buffer); + + /** + * Queues an end of stream signal. After this method has been called, + * {@link #queueInput(ByteBuffer)} may not be called until after the next call to + * {@link #flush()}. Calling {@link #getOutput()} will return any remaining output data. Multiple + * calls may be required to read all of the remaining output data. {@link #isEnded()} will return + * {@code true} once all remaining output data has been read. + */ + void queueEndOfStream(); + + /** + * Returns a buffer containing processed output data between its position and limit. The buffer + * will always be a direct byte buffer with native byte order. Calling this method invalidates any + * previously returned buffer. The buffer will be empty if no output is available. + * + * @return A buffer containing processed output data between its position and limit. + */ + ByteBuffer getOutput(); + + /** + * Returns whether this processor will return no more output from {@link #getOutput()} until it + * has been {@link #flush()}ed and more input has been queued. + */ + boolean isEnded(); + + /** + * Clears any state in preparation for receiving a new stream of input buffers. + */ + void flush(); + + /** + * Resets the processor to its initial state. + */ + void reset(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/AudioTrack.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/AudioTrack.java index 4b54ddef4..b08aa001e 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/AudioTrack.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/AudioTrack.java @@ -17,41 +17,47 @@ package org.telegram.messenger.exoplayer2.audio; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioTimestamp; -import android.media.PlaybackParams; import android.os.ConditionVariable; import android.os.SystemClock; import android.util.Log; import org.telegram.messenger.exoplayer2.C; -import org.telegram.messenger.exoplayer2.Format; +import org.telegram.messenger.exoplayer2.PlaybackParameters; import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.MimeTypes; import org.telegram.messenger.exoplayer2.util.Util; import java.lang.reflect.Method; import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.LinkedList; /** * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles * playback position smoothing, non-blocking writes and reconfiguration. *

    * Before starting playback, specify the input format by calling - * {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)}, optionally - * specifying an audio session. + * {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)}, + * {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()} + * to configure audio playback. These methods may be called after writing data to the track, in + * which case it will be reinitialized as required. *

    * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. *

    - * Call {@link #configure(String, int, int, int, int)} whenever the input format changes. If - * {@link #isInitialized()} returns {@code false} after the call, it is necessary to call - * {@link #initialize(int)} before writing more data. + * Call {@link #configure(String, int, int, int, int)} whenever the input format changes. The track + * will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}. *

    - * The underlying {@link android.media.AudioTrack} is created by {@link #initialize(int)} and - * released by {@link #reset()} (and {@link #configure(String, int, int, int, int)} unless the input - * format is unchanged). It is safe to call {@link #initialize(int)} after calling {@link #reset()} - * without reconfiguration. + * Calling {@link #reset()} releases the underlying {@link android.media.AudioTrack} (and so does + * calling {@link #configure(String, int, int, int, int)} unless the format is unchanged). It is + * safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling + * {@link #configure(String, int, int, int, int)}. *

    - * Call {@link #release()} when the instance is no longer required. + * Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers will + * be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call + * {@link #release()} when the instance is no longer required. */ public final class AudioTrack { @@ -60,6 +66,19 @@ public final class AudioTrack { */ public interface Listener { + /** + * Called when the audio track has been initialized with a newly generated audio session id. + * + * @param audioSessionId The newly generated audio session id. + */ + void onAudioSessionId(int audioSessionId); + + /** + * Called when the audio track handles a buffer whose timestamp is discontinuous with the last + * buffer handled since it was reset. + */ + void onPositionDiscontinuity(); + /** * Called when the audio track underruns. * @@ -73,6 +92,21 @@ public final class AudioTrack { } + /** + * Thrown when a failure occurs configuring the track. + */ + public static final class ConfigurationException extends Exception { + + public ConfigurationException(Throwable cause) { + super(cause); + } + + public ConfigurationException(String message) { + super(message); + } + + } + /** * Thrown when a failure occurs initializing an {@link android.media.AudioTrack}. */ @@ -104,13 +138,15 @@ public final class AudioTrack { public static final class WriteException extends Exception { /** - * An error value returned from {@link android.media.AudioTrack#write(byte[], int, int)}. + * The error value returned from {@link android.media.AudioTrack#write(byte[], int, int)} or + * {@link android.media.AudioTrack#write(ByteBuffer, int, int)}. */ public final int errorCode; /** - * @param errorCode An error value returned from - * {@link android.media.AudioTrack#write(byte[], int, int)}. + * @param errorCode The error value returned from + * {@link android.media.AudioTrack#write(byte[], int, int)} or + * {@link android.media.AudioTrack#write(ByteBuffer, int, int)}. */ public WriteException(int errorCode) { super("AudioTrack write failed: " + errorCode); @@ -135,21 +171,7 @@ public final class AudioTrack { } /** - * Returned in the result of {@link #handleBuffer} if the buffer was discontinuous. - */ - public static final int RESULT_POSITION_DISCONTINUITY = 1; - /** - * Returned in the result of {@link #handleBuffer} if the buffer can be released. - */ - public static final int RESULT_BUFFER_CONSUMED = 2; - - /** - * Represents an unset {@link android.media.AudioTrack} session identifier. - */ - public static final int SESSION_ID_NOT_SET = 0; - - /** - * Returned by {@link #getCurrentPositionUs} when the position is not set. + * Returned by {@link #getCurrentPositionUs(boolean)} when the position is not set. */ public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE; @@ -210,15 +232,15 @@ public final class AudioTrack { /** * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more * than this amount. - * - *

    This is a fail safe that should not be required on correctly functioning devices. + *

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

    This is a fail safe that should not be required on correctly functioning devices. + *

    + * 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; @@ -230,6 +252,13 @@ public final class AudioTrack { private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; + /** + * The minimum number of output bytes from {@link #sonicAudioProcessor} at which the speedup is + * calculated using the input/output byte counts from the processor, rather than using the + * current playback parameters speed. + */ + private static final int SONIC_MIN_BYTES_FOR_SPEEDUP = 1024; + /** * Whether to enable a workaround for an issue where an audio effect does not keep its session * active across releasing/initializing a new audio track, on platform builds where @@ -249,30 +278,41 @@ public final class AudioTrack { public static boolean failOnSpuriousAudioTimestamp = false; private final AudioCapabilities audioCapabilities; + private final ChannelMappingAudioProcessor channelMappingAudioProcessor; + private final SonicAudioProcessor sonicAudioProcessor; + private final AudioProcessor[] availableAudioProcessors; private final Listener listener; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; private final AudioTrackUtil audioTrackUtil; + private final LinkedList playbackParametersCheckpoints; /** - * Used to keep the audio session active on pre-V21 builds (see {@link #initialize(int)}). + * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). */ private android.media.AudioTrack keepSessionIdAudioTrack; private android.media.AudioTrack audioTrack; private int sampleRate; private int channelConfig; + @C.Encoding + private int encoding; + @C.Encoding + private int outputEncoding; @C.StreamType private int streamType; - @C.Encoding - private int sourceEncoding; - @C.Encoding - private int targetEncoding; private boolean passthrough; - private int pcmFrameSize; private int bufferSize; private long bufferSizeUs; + private PlaybackParameters drainingPlaybackParameters; + private PlaybackParameters playbackParameters; + private long playbackParametersOffsetUs; + private long playbackParametersPositionUs; + + private ByteBuffer avSyncHeader; + private int bytesUntilNextAvSync; + private int nextPlayheadOffsetIndex; private int playheadOffsetCount; private long smoothedPlayheadOffsetUs; @@ -281,8 +321,12 @@ public final class AudioTrack { private long lastTimestampSampleTimeUs; private Method getLatencyMethod; + private int pcmFrameSize; private long submittedPcmBytes; private long submittedEncodedFrames; + private int outputPcmFrameSize; + private long writtenPcmBytes; + private long writtenEncodedFrames; private int framesPerEncodedSample; private int startMediaTimeState; private long startMediaTimeUs; @@ -290,21 +334,30 @@ public final class AudioTrack { private long latencyUs; private float volume; - private byte[] temporaryBuffer; - private int temporaryBufferOffset; - private ByteBuffer currentSourceBuffer; - - private ByteBuffer resampledBuffer; - private boolean useResampledBuffer; + private AudioProcessor[] audioProcessors; + private ByteBuffer[] outputBuffers; + private ByteBuffer inputBuffer; + private ByteBuffer outputBuffer; + private byte[] preV21OutputBuffer; + private int preV21OutputBufferOffset; + private int drainingAudioProcessorIndex; + private boolean handledEndOfStream; + private boolean playing; + private int audioSessionId; + private boolean tunneling; private boolean hasData; private long lastFeedElapsedRealtimeMs; /** - * @param audioCapabilities The current audio capabilities. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before + * output. May be empty. * @param listener Listener for audio track events. */ - public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) { + public AudioTrack(AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, + Listener listener) { this.audioCapabilities = audioCapabilities; this.listener = listener; releasingConditionVariable = new ConditionVariable(true); @@ -316,17 +369,28 @@ public final class AudioTrack { // There's no guarantee this method exists. Do nothing. } } - if (Util.SDK_INT >= 23) { - audioTrackUtil = new AudioTrackUtilV23(); - } else if (Util.SDK_INT >= 19) { + if (Util.SDK_INT >= 19) { audioTrackUtil = new AudioTrackUtilV19(); } else { audioTrackUtil = new AudioTrackUtil(); } + channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); + sonicAudioProcessor = new SonicAudioProcessor(); + availableAudioProcessors = new AudioProcessor[3 + audioProcessors.length]; + availableAudioProcessors[0] = new ResamplingAudioProcessor(); + availableAudioProcessors[1] = channelMappingAudioProcessor; + System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length); + availableAudioProcessors[2 + audioProcessors.length] = sonicAudioProcessor; playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; streamType = C.STREAM_TYPE_DEFAULT; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + playbackParameters = PlaybackParameters.DEFAULT; + drainingAudioProcessorIndex = C.INDEX_UNSET; + this.audioProcessors = new AudioProcessor[0]; + outputBuffers = new ByteBuffer[0]; + playbackParametersCheckpoints = new LinkedList<>(); } /** @@ -340,14 +404,6 @@ public final class AudioTrack { && audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType)); } - /** - * Returns whether the audio track has been successfully initialized via {@link #initialize} and - * not yet {@link #reset}. - */ - public boolean isInitialized() { - return audioTrack != null; - } - /** * Returns the playback position in the stream starting at zero, in microseconds, or * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available. @@ -369,33 +425,29 @@ public final class AudioTrack { } long systemClockUs = System.nanoTime() / 1000; - long currentPositionUs; + long positionUs; if (audioTimestampSet) { - // How long ago in the past the audio timestamp is (negative if it's in the future). - long presentationDiff = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000); - // Fixes such difference if the playback speed is not real time speed. - long actualSpeedPresentationDiff = (long) (presentationDiff - * audioTrackUtil.getPlaybackSpeed()); - long framesDiff = durationUsToFrames(actualSpeedPresentationDiff); - // The position of the frame that's currently being presented. - long currentFramePosition = audioTrackUtil.getTimestampFramePosition() + framesDiff; - currentPositionUs = framesToDurationUs(currentFramePosition) + startMediaTimeUs; + // Calculate the speed-adjusted position using the timestamp (which may be in the future). + long elapsedSinceTimestampUs = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000); + long elapsedSinceTimestampFrames = durationUsToFrames(elapsedSinceTimestampUs); + long elapsedFrames = audioTrackUtil.getTimestampFramePosition() + elapsedSinceTimestampFrames; + positionUs = framesToDurationUs(elapsedFrames); } else { if (playheadOffsetCount == 0) { // The AudioTrack has started, but we don't have any samples to compute a smoothed position. - currentPositionUs = audioTrackUtil.getPlaybackHeadPositionUs() + startMediaTimeUs; + positionUs = audioTrackUtil.getPositionUs(); } else { - // getPlayheadPositionUs() only has a granularity of ~20ms, so we base the position off the + // getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the // system clock (and a smoothed offset between it and the playhead position) so as to // prevent jitter in the reported positions. - currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs; + positionUs = systemClockUs + smoothedPlayheadOffsetUs; } if (!sourceEnded) { - currentPositionUs -= latencyUs; + positionUs -= latencyUs; } } - return currentPositionUs; + return startMediaTimeUs + applySpeedup(positionUs); } /** @@ -409,9 +461,56 @@ public final class AudioTrack { * {@link C#ENCODING_PCM_32BIT}. * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a * suitable buffer size automatically. + * @throws ConfigurationException If an error occurs configuring the track. */ public void configure(String mimeType, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) { + @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException { + configure(mimeType, channelCount, sampleRate, pcmEncoding, specifiedBufferSize, null); + } + + /** + * Configures (or reconfigures) the audio track. + * + * @param mimeType The mime type. + * @param channelCount The number of channels. + * @param sampleRate The sample rate in Hz. + * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, + * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and + * {@link C#ENCODING_PCM_32BIT}. + * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a + * suitable buffer size automatically. + * @param outputChannels A mapping from input to output channels that is applied to this track's + * input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the + * input unchanged. Otherwise, the element at index {@code i} specifies index of the input + * channel to map to output channel {@code i} when preprocessing input buffers. After the + * map is applied the audio data will have {@code outputChannels.length} channels. + * @throws ConfigurationException If an error occurs configuring the track. + */ + public void configure(String mimeType, int channelCount, int sampleRate, + @C.PcmEncoding int pcmEncoding, int specifiedBufferSize, int[] outputChannels) + throws ConfigurationException { + boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); + @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; + boolean flush = false; + if (!passthrough) { + pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount); + channelMappingAudioProcessor.setChannelMap(outputChannels); + for (AudioProcessor audioProcessor : availableAudioProcessors) { + try { + flush |= audioProcessor.configure(sampleRate, channelCount, encoding); + } catch (AudioProcessor.UnhandledFormatException e) { + throw new ConfigurationException(e); + } + if (audioProcessor.isActive()) { + channelCount = audioProcessor.getOutputChannelCount(); + encoding = audioProcessor.getOutputEncoding(); + } + } + if (flush) { + resetAudioProcessors(); + } + } + int channelConfig; switch (channelCount) { case 1: @@ -439,21 +538,31 @@ public final class AudioTrack { channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; break; default: - throw new IllegalArgumentException("Unsupported channel count: " + channelCount); + throw new ConfigurationException("Unsupported channel count: " + channelCount); } - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); - @C.Encoding int sourceEncoding; - if (passthrough) { - sourceEncoding = getEncodingForMimeType(mimeType); - } else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT - || pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) { - sourceEncoding = pcmEncoding; - } else { - throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding); + // Workaround for overly strict channel configuration checks on nVidia Shield. + if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) { + switch (channelCount) { + case 7: + channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; + break; + case 3: + case 5: + channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; + break; + default: + break; + } } - if (isInitialized() && this.sourceEncoding == sourceEncoding && this.sampleRate == sampleRate + // Workaround for Nexus Player not reporting support for mono passthrough. + // (See [Internal: b/34268671].) + if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) { + channelConfig = AudioFormat.CHANNEL_OUT_STEREO; + } + + if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -461,48 +570,63 @@ public final class AudioTrack { reset(); - this.sourceEncoding = sourceEncoding; + this.encoding = encoding; this.passthrough = passthrough; this.sampleRate = sampleRate; this.channelConfig = channelConfig; - targetEncoding = passthrough ? sourceEncoding : C.ENCODING_PCM_16BIT; - pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels. + outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; + outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount); if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; } else if (passthrough) { // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into // account. [Internal: b/25181305] - if (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3) { + if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { // AC-3 allows bitrates up to 640 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); - } else /* (targetEncoding == C.ENCODING_DTS || targetEncoding == C.ENCODING_DTS_HD */ { + } else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); } } else { int minBufferSize = - android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding); + android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; - int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize; + int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; int maxAppBufferSize = (int) Math.max(minBufferSize, - durationUsToFrames(MAX_BUFFER_DURATION_US) * pcmFrameSize); + durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize : multipliedBufferSize; } - bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize)); + bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize); + + // The old playback parameters may no longer be applicable so try to reset them now. + setPlaybackParameters(playbackParameters); } - /** - * Initializes the audio track for writing new buffers using {@link #handleBuffer}. - * - * @param sessionId Audio track session identifier to re-use, or {@link #SESSION_ID_NOT_SET} to - * create a new one. - * @return The new (or re-used) session identifier. - */ - public int initialize(int sessionId) throws InitializationException { + private void resetAudioProcessors() { + ArrayList newAudioProcessors = new ArrayList<>(); + for (AudioProcessor audioProcessor : availableAudioProcessors) { + if (audioProcessor.isActive()) { + newAudioProcessors.add(audioProcessor); + } else { + audioProcessor.flush(); + } + } + int count = newAudioProcessors.size(); + audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]); + outputBuffers = new ByteBuffer[count]; + for (int i = 0; i < count; i++) { + AudioProcessor audioProcessor = audioProcessors[i]; + audioProcessor.flush(); + outputBuffers[i] = audioProcessor.getOutput(); + } + } + + private void initialize() throws InitializationException { // If we're asynchronously releasing a previous audio track then we block until it has been // released. This guarantees that we cannot end up in a state where we have multiple audio // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust @@ -510,23 +634,26 @@ public final class AudioTrack { // initialization of the audio track to fail. releasingConditionVariable.block(); - if (sessionId == SESSION_ID_NOT_SET) { + if (tunneling) { + audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding, + bufferSize, audioSessionId); + } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, MODE_STREAM); + outputEncoding, bufferSize, MODE_STREAM); } else { // Re-attach to the same audio session. audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, MODE_STREAM, sessionId); + outputEncoding, bufferSize, MODE_STREAM, audioSessionId); } checkAudioTrackInitialized(); - sessionId = audioTrack.getAudioSessionId(); + int audioSessionId = audioTrack.getAudioSessionId(); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { // The workaround creates an audio track with a two byte buffer on the same session, and // does not release it until this object is released, which keeps the session active. if (keepSessionIdAudioTrack != null - && sessionId != keepSessionIdAudioTrack.getAudioSessionId()) { + && audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) { releaseKeepSessionIdAudioTrack(); } if (keepSessionIdAudioTrack == null) { @@ -535,21 +662,25 @@ public final class AudioTrack { @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate, - channelConfig, encoding, bufferSize, MODE_STATIC, sessionId); + channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId); } } } + if (this.audioSessionId != audioSessionId) { + this.audioSessionId = audioSessionId; + listener.onAudioSessionId(audioSessionId); + } audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds()); - setAudioTrackVolume(); + setVolumeInternal(); hasData = false; - return sessionId; } /** * Starts or resumes playing audio if the audio track has been initialized. */ public void play() { + playing = true; if (isInitialized()) { resumeSystemTimeUs = System.nanoTime() / 1000; audioTrack.play(); @@ -567,47 +698,40 @@ public final class AudioTrack { } /** - * Attempts to write data from a {@link ByteBuffer} to the audio track, starting from its current - * position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is - * advanced by the number of bytes that were successfully written. + * Attempts to process data from a {@link ByteBuffer}, starting from its current position and + * ending at its limit (exclusive). The position of the {@link ByteBuffer} is advanced by the + * number of bytes that were handled. {@link Listener#onPositionDiscontinuity()} will be called if + * {@code presentationTimeUs} is discontinuous with the last buffer handled since the last reset. *

    - * Returns a bit field containing {@link #RESULT_BUFFER_CONSUMED} if the data was written in full, - * and {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was discontinuous with previously - * written data. - *

    - * If the data was not written in full then the same {@link ByteBuffer} must be provided to - * subsequent calls until it has been fully consumed, except in the case of an interleaving call - * to {@link #configure} or {@link #reset}. + * Returns whether the data was handled in full. If the data was not handled in full then the same + * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed, + * except in the case of an interleaving call to {@link #reset()} (or an interleaving call to + * {@link #configure(String, int, int, int, int)} that caused the track to be reset). * - * @param buffer The buffer containing audio data to play back. - * @param presentationTimeUs Presentation timestamp of the next buffer in microseconds. - * @return A bit field with {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released, and - * {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was not contiguous with previously - * written data. + * @param buffer The buffer containing audio data. + * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. + * @return Whether the buffer was handled fully. + * @throws InitializationException If an error occurs initializing the track. * @throws WriteException If an error occurs writing the audio data. */ - public int handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { - boolean hadData = hasData; - hasData = hasPendingData(); - if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { - long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; - listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); + @SuppressWarnings("ReferenceEquality") + public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) + throws InitializationException, WriteException { + Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer); + if (!isInitialized()) { + initialize(); + if (playing) { + play(); + } } - int result = writeBuffer(buffer, presentationTimeUs); - lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); - return result; - } - - private int writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { - boolean isNewSourceBuffer = currentSourceBuffer == null; - Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer); - currentSourceBuffer = buffer; if (needsPassthroughWorkarounds()) { // An AC-3 audio track continues to play data written while it is paused. Stop writing so its // buffer empties. See [Internal: b/18899620]. if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) { - return 0; + // We force an underrun to pause the track, so don't notify the listener in this case. + hasData = false; + return false; } // A new AC-3 audio track's playback position continues to increase from the old track's @@ -615,32 +739,44 @@ public final class AudioTrack { // head position actually returns to zero. if (audioTrack.getPlayState() == PLAYSTATE_STOPPED && audioTrackUtil.getPlaybackHeadPosition() != 0) { - return 0; + return false; } } - int result = 0; - if (isNewSourceBuffer) { - // We're seeing this buffer for the first time. + boolean hadData = hasData; + hasData = hasPendingData(); + if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { + long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; + listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); + } - if (!currentSourceBuffer.hasRemaining()) { + if (inputBuffer == null) { + // We are seeing this buffer for the first time. + if (!buffer.hasRemaining()) { // The buffer is empty. - currentSourceBuffer = null; - return RESULT_BUFFER_CONSUMED; - } - - useResampledBuffer = targetEncoding != sourceEncoding; - if (useResampledBuffer) { - Assertions.checkState(targetEncoding == C.ENCODING_PCM_16BIT); - // Resample the buffer to get the data in the target encoding. - resampledBuffer = resampleTo16BitPcm(currentSourceBuffer, sourceEncoding, resampledBuffer); - buffer = resampledBuffer; + return true; } if (passthrough && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. - framesPerEncodedSample = getFramesPerEncodedSample(targetEncoding, buffer); + framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); } + + if (drainingPlaybackParameters != null) { + if (!drainAudioProcessorsToEndOfStream()) { + // Don't process any more input until draining completes. + return false; + } + // Store the position and corresponding media time from which the parameters will apply. + playbackParametersCheckpoints.add(new PlaybackParametersCheckpoint( + drainingPlaybackParameters, Math.max(0, presentationTimeUs), + framesToDurationUs(getWrittenFrames()))); + drainingPlaybackParameters = null; + // The audio processors have drained, so flush them. This will cause any active speed + // adjustment audio processor to start producing audio with the new parameters. + resetAudioProcessors(); + } + if (startMediaTimeState == START_NOT_SET) { startMediaTimeUs = Math.max(0, presentationTimeUs); startMediaTimeState = START_IN_SYNC; @@ -659,66 +795,180 @@ public final class AudioTrack { // number of bytes submitted. startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs); startMediaTimeState = START_IN_SYNC; - result |= RESULT_POSITION_DISCONTINUITY; + listener.onPositionDiscontinuity(); } } - if (Util.SDK_INT < 21) { - // Copy {@code buffer} into {@code temporaryBuffer}. - int bytesRemaining = buffer.remaining(); - if (temporaryBuffer == null || temporaryBuffer.length < bytesRemaining) { - temporaryBuffer = new byte[bytesRemaining]; - } - int originalPosition = buffer.position(); - buffer.get(temporaryBuffer, 0, bytesRemaining); - buffer.position(originalPosition); - temporaryBufferOffset = 0; + + if (passthrough) { + submittedEncodedFrames += framesPerEncodedSample; + } else { + submittedPcmBytes += buffer.remaining(); } + + inputBuffer = buffer; } - buffer = useResampledBuffer ? resampledBuffer : buffer; + if (passthrough) { + // Passthrough buffers are not processed. + writeBuffer(inputBuffer, presentationTimeUs); + } else { + processBuffers(presentationTimeUs); + } + + if (!inputBuffer.hasRemaining()) { + inputBuffer = null; + return true; + } + return false; + } + + private void processBuffers(long avSyncPresentationTimeUs) throws WriteException { + int count = audioProcessors.length; + int index = count; + while (index >= 0) { + ByteBuffer input = index > 0 ? outputBuffers[index - 1] + : (inputBuffer != null ? inputBuffer : AudioProcessor.EMPTY_BUFFER); + if (index == count) { + writeBuffer(input, avSyncPresentationTimeUs); + } else { + AudioProcessor audioProcessor = audioProcessors[index]; + audioProcessor.queueInput(input); + ByteBuffer output = audioProcessor.getOutput(); + outputBuffers[index] = output; + if (output.hasRemaining()) { + // Handle the output as input to the next audio processor or the AudioTrack. + index++; + continue; + } + } + + if (input.hasRemaining()) { + // The input wasn't consumed and no output was produced, so give up for now. + return; + } + + // Get more input from upstream. + index--; + } + } + + @SuppressWarnings("ReferenceEquality") + private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) + throws WriteException { + if (!buffer.hasRemaining()) { + return true; + } + if (outputBuffer != null) { + Assertions.checkArgument(outputBuffer == buffer); + } else { + outputBuffer = buffer; + if (Util.SDK_INT < 21) { + int bytesRemaining = buffer.remaining(); + if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { + preV21OutputBuffer = new byte[bytesRemaining]; + } + int originalPosition = buffer.position(); + buffer.get(preV21OutputBuffer, 0, bytesRemaining); + buffer.position(originalPosition); + preV21OutputBufferOffset = 0; + } + } int bytesRemaining = buffer.remaining(); int bytesWritten = 0; if (Util.SDK_INT < 21) { // passthrough == false // Work out how many bytes we can write without the risk of blocking. int bytesPending = - (int) (submittedPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * pcmFrameSize)); + (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize)); int bytesToWrite = bufferSize - bytesPending; if (bytesToWrite > 0) { bytesToWrite = Math.min(bytesRemaining, bytesToWrite); - bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); - if (bytesWritten >= 0) { - temporaryBufferOffset += bytesWritten; + bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); + if (bytesWritten > 0) { + preV21OutputBufferOffset += bytesWritten; + buffer.position(buffer.position() + bytesWritten); } - buffer.position(buffer.position() + bytesWritten); } + } else if (tunneling) { + Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET); + bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, + avSyncPresentationTimeUs); } else { bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); } + lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); + if (bytesWritten < 0) { throw new WriteException(bytesWritten); } if (!passthrough) { - submittedPcmBytes += bytesWritten; + writtenPcmBytes += bytesWritten; } if (bytesWritten == bytesRemaining) { if (passthrough) { - submittedEncodedFrames += framesPerEncodedSample; + writtenEncodedFrames += framesPerEncodedSample; } - currentSourceBuffer = null; - result |= RESULT_BUFFER_CONSUMED; + outputBuffer = null; + return true; } - return result; + return false; } /** - * Ensures that the last data passed to {@link #handleBuffer(ByteBuffer, long)} is played in full. + * Plays out remaining audio. {@link #isEnded()} will return {@code true} when playback has ended. + * + * @throws WriteException If an error occurs draining data to the track. */ - public void handleEndOfStream() { - if (isInitialized()) { - audioTrackUtil.handleEndOfStream(getSubmittedFrames()); + public void playToEndOfStream() throws WriteException { + if (handledEndOfStream || !isInitialized()) { + return; } + + if (drainAudioProcessorsToEndOfStream()) { + // The audio processors have drained, so drain the underlying audio track. + audioTrackUtil.handleEndOfStream(getWrittenFrames()); + bytesUntilNextAvSync = 0; + handledEndOfStream = true; + } + } + + private boolean drainAudioProcessorsToEndOfStream() throws WriteException { + boolean audioProcessorNeedsEndOfStream = false; + if (drainingAudioProcessorIndex == C.INDEX_UNSET) { + drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0; + audioProcessorNeedsEndOfStream = true; + } + while (drainingAudioProcessorIndex < audioProcessors.length) { + AudioProcessor audioProcessor = audioProcessors[drainingAudioProcessorIndex]; + if (audioProcessorNeedsEndOfStream) { + audioProcessor.queueEndOfStream(); + } + processBuffers(C.TIME_UNSET); + if (!audioProcessor.isEnded()) { + return false; + } + audioProcessorNeedsEndOfStream = true; + drainingAudioProcessorIndex++; + } + + // Finish writing any remaining output to the track. + if (outputBuffer != null) { + writeBuffer(outputBuffer, C.TIME_UNSET); + if (outputBuffer != null) { + return false; + } + } + drainingAudioProcessorIndex = C.INDEX_UNSET; + return true; + } + + /** + * Returns whether all buffers passed to {@link #handleBuffer(ByteBuffer, long)} have been + * completely processed and played. + */ + public boolean isEnded() { + return !isInitialized() || (handledEndOfStream && !hasPendingData()); } /** @@ -726,38 +976,115 @@ public final class AudioTrack { */ public boolean hasPendingData() { return isInitialized() - && (getSubmittedFrames() > audioTrackUtil.getPlaybackHeadPosition() + && (getWrittenFrames() > audioTrackUtil.getPlaybackHeadPosition() || overrideHasPendingData()); } /** - * Sets the playback parameters. Only available for {@link Util#SDK_INT} >= 23 + * Attempts to set the playback parameters and returns the active playback parameters, which may + * differ from those passed in. * - * @param playbackParams The playback parameters to be used by the - * {@link android.media.AudioTrack}. - * @throws UnsupportedOperationException if the Playback Parameters are not supported. That is, - * {@link Util#SDK_INT} < 23. + * @param playbackParameters The new playback parameters to attempt to set. + * @return The active playback parameters. */ - public void setPlaybackParams(PlaybackParams playbackParams) { - audioTrackUtil.setPlaybackParams(playbackParams); + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + if (passthrough) { + // The playback parameters are always the default in passthrough mode. + this.playbackParameters = PlaybackParameters.DEFAULT; + return this.playbackParameters; + } + playbackParameters = new PlaybackParameters( + sonicAudioProcessor.setSpeed(playbackParameters.speed), + sonicAudioProcessor.setPitch(playbackParameters.pitch)); + PlaybackParameters lastSetPlaybackParameters = + drainingPlaybackParameters != null ? drainingPlaybackParameters + : !playbackParametersCheckpoints.isEmpty() + ? playbackParametersCheckpoints.getLast().playbackParameters + : this.playbackParameters; + if (!playbackParameters.equals(lastSetPlaybackParameters)) { + if (isInitialized()) { + // Drain the audio processors so we can determine the frame position at which the new + // parameters apply. + drainingPlaybackParameters = playbackParameters; + } else { + this.playbackParameters = playbackParameters; + } + } + return this.playbackParameters; } /** - * Sets the stream type for audio track. If the stream type has changed, {@link #isInitialized()} - * will return {@code false} and the caller must re-{@link #initialize(int)} the audio track - * before writing more data. The caller must not reuse the audio session identifier when - * re-initializing with a new stream type. + * Gets the {@link PlaybackParameters}. + */ + public PlaybackParameters getPlaybackParameters() { + return playbackParameters; + } + + /** + * Sets the stream type for audio track. If the stream type has changed and if the audio track + * is not configured for use with tunneling, then the audio track is reset and the audio session + * id is cleared. + *

    + * If the audio track is configured for use with tunneling then the stream type is ignored, the + * audio track is not reset and the audio session id is not cleared. The passed stream type will + * be used if the audio track is later re-configured into non-tunneled mode. * * @param streamType The {@link C.StreamType} to use for audio output. - * @return Whether the stream type changed. */ - public boolean setStreamType(@C.StreamType int streamType) { + public void setStreamType(@C.StreamType int streamType) { if (this.streamType == streamType) { - return false; + return; } this.streamType = streamType; + if (tunneling) { + // The stream type is ignored in tunneling mode, so no need to reset. + return; + } reset(); - return true; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + } + + /** + * Sets the audio session id. The audio track is reset if the audio session id has changed. + */ + public void setAudioSessionId(int audioSessionId) { + if (this.audioSessionId != audioSessionId) { + this.audioSessionId = audioSessionId; + reset(); + } + } + + /** + * Enables tunneling. The audio track is reset if tunneling was previously disabled or if the + * audio session id has changed. Enabling tunneling requires platform API version 21 onwards. + *

    + * If this instance has {@link AudioProcessor}s and tunneling is enabled, care must be taken that + * audio processors do not output buffers with a different duration than their input, and buffer + * processors must produce output corresponding to their last input immediately after that input + * is queued. + * + * @param tunnelingAudioSessionId The audio session id to use. + * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. + */ + public void enableTunnelingV21(int tunnelingAudioSessionId) { + Assertions.checkState(Util.SDK_INT >= 21); + if (!tunneling || audioSessionId != tunnelingAudioSessionId) { + tunneling = true; + audioSessionId = tunnelingAudioSessionId; + reset(); + } + } + + /** + * Disables tunneling. If tunneling was previously enabled then the audio track is reset and the + * audio session id is cleared. + */ + public void disableTunneling() { + if (tunneling) { + tunneling = false; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + reset(); + } } /** @@ -768,17 +1095,17 @@ public final class AudioTrack { public void setVolume(float volume) { if (this.volume != volume) { this.volume = volume; - setAudioTrackVolume(); + setVolumeInternal(); } } - private void setAudioTrackVolume() { + private void setVolumeInternal() { if (!isInitialized()) { // Do nothing. } else if (Util.SDK_INT >= 21) { - setAudioTrackVolumeV21(audioTrack, volume); + setVolumeInternalV21(audioTrack, volume); } else { - setAudioTrackVolumeV3(audioTrack, volume); + setVolumeInternalV3(audioTrack, volume); } } @@ -786,6 +1113,7 @@ public final class AudioTrack { * Pauses playback. */ public void pause() { + playing = false; if (isInitialized()) { resetSyncParams(); audioTrackUtil.pause(); @@ -795,16 +1123,37 @@ public final class AudioTrack { /** * Releases the underlying audio track asynchronously. *

    - * Calling {@link #initialize(int)} will block until the audio track has been released, so it is - * safe to initialize immediately after a reset. The audio session may remain active until - * {@link #release()} is called. + * Calling {@link #handleBuffer(ByteBuffer, long)} will block until the audio track has been + * released, so it is safe to use the audio track immediately after a reset. The audio session may + * remain active until {@link #release()} is called. */ public void reset() { if (isInitialized()) { submittedPcmBytes = 0; submittedEncodedFrames = 0; + writtenPcmBytes = 0; + writtenEncodedFrames = 0; framesPerEncodedSample = 0; - currentSourceBuffer = null; + if (drainingPlaybackParameters != null) { + playbackParameters = drainingPlaybackParameters; + drainingPlaybackParameters = null; + } else if (!playbackParametersCheckpoints.isEmpty()) { + playbackParameters = playbackParametersCheckpoints.getLast().playbackParameters; + } + playbackParametersCheckpoints.clear(); + playbackParametersOffsetUs = 0; + playbackParametersPositionUs = 0; + inputBuffer = null; + outputBuffer = null; + for (int i = 0; i < audioProcessors.length; i++) { + AudioProcessor audioProcessor = audioProcessors[i]; + audioProcessor.flush(); + outputBuffers[i] = audioProcessor.getOutput(); + } + handledEndOfStream = false; + drainingAudioProcessorIndex = C.INDEX_UNSET; + avSyncHeader = null; + bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; latencyUs = 0; resetSyncParams(); @@ -837,6 +1186,11 @@ public final class AudioTrack { public void release() { reset(); releaseKeepSessionIdAudioTrack(); + for (AudioProcessor audioProcessor : availableAudioProcessors) { + audioProcessor.reset(); + } + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + playing = false; } /** @@ -865,11 +1219,41 @@ public final class AudioTrack { return isInitialized() && startMediaTimeState != START_NOT_SET; } + /** + * Returns the underlying audio track {@code positionUs} with any applicable speedup applied. + */ + private long applySpeedup(long positionUs) { + while (!playbackParametersCheckpoints.isEmpty() + && positionUs >= playbackParametersCheckpoints.getFirst().positionUs) { + // We are playing (or about to play) media with the new playback parameters, so update them. + PlaybackParametersCheckpoint checkpoint = playbackParametersCheckpoints.remove(); + playbackParameters = checkpoint.playbackParameters; + playbackParametersPositionUs = checkpoint.positionUs; + playbackParametersOffsetUs = checkpoint.mediaTimeUs - startMediaTimeUs; + } + + if (playbackParameters.speed == 1f) { + return positionUs + playbackParametersOffsetUs - playbackParametersPositionUs; + } + + if (playbackParametersCheckpoints.isEmpty() + && sonicAudioProcessor.getOutputByteCount() >= SONIC_MIN_BYTES_FOR_SPEEDUP) { + return playbackParametersOffsetUs + + Util.scaleLargeTimestamp(positionUs - playbackParametersPositionUs, + sonicAudioProcessor.getInputByteCount(), sonicAudioProcessor.getOutputByteCount()); + } + + // We are playing drained data at a previous playback speed, or don't have enough bytes to + // calculate an accurate speedup, so fall back to multiplying by the speed. + return playbackParametersOffsetUs + + (long) ((double) playbackParameters.speed * (positionUs - playbackParametersPositionUs)); + } + /** * Updates the audio track latency and playback position parameters. */ private void maybeSampleSyncParams() { - long playbackPositionUs = audioTrackUtil.getPlaybackHeadPositionUs(); + long playbackPositionUs = audioTrackUtil.getPositionUs(); if (playbackPositionUs == 0) { // The AudioTrack hasn't output anything yet. return; @@ -974,8 +1358,8 @@ public final class AudioTrack { throw new InitializationException(state, sampleRate, channelConfig, bufferSize); } - private long pcmBytesToFrames(long byteCount) { - return byteCount / pcmFrameSize; + private boolean isInitialized() { + return audioTrack != null; } private long framesToDurationUs(long frameCount) { @@ -987,7 +1371,11 @@ public final class AudioTrack { } private long getSubmittedFrames() { - return passthrough ? submittedEncodedFrames : pcmBytesToFrames(submittedPcmBytes); + return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize); + } + + private long getWrittenFrames() { + return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize); } private void resetSyncParams() { @@ -1005,7 +1393,7 @@ public final class AudioTrack { */ private boolean needsPassthroughWorkarounds() { return Util.SDK_INT < 23 - && (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3); + && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3); } /** @@ -1021,79 +1409,23 @@ public final class AudioTrack { } /** - * Converts the provided buffer into 16-bit PCM. - * - * @param buffer The buffer containing the data to convert. - * @param sourceEncoding The data encoding. - * @param out A buffer into which the output should be written, if its capacity is sufficient. - * @return The 16-bit PCM output. Different to the out parameter if null was passed, or if the - * capacity was insufficient for the output. + * Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback. */ - private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, @C.PcmEncoding int sourceEncoding, - ByteBuffer out) { - int offset = buffer.position(); - int limit = buffer.limit(); - int size = limit - offset; - - int resampledSize; - switch (sourceEncoding) { - case C.ENCODING_PCM_8BIT: - resampledSize = size * 2; - break; - case C.ENCODING_PCM_24BIT: - resampledSize = (size / 3) * 2; - break; - case C.ENCODING_PCM_32BIT: - resampledSize = size / 2; - break; - case C.ENCODING_PCM_16BIT: - case C.ENCODING_INVALID: - case Format.NO_VALUE: - default: - // Never happens. - throw new IllegalStateException(); - } - - ByteBuffer resampledBuffer = out; - if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { - resampledBuffer = ByteBuffer.allocateDirect(resampledSize); - } - resampledBuffer.position(0); - resampledBuffer.limit(resampledSize); - - // Samples are little endian. - switch (sourceEncoding) { - case C.ENCODING_PCM_8BIT: - // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. - for (int i = offset; i < limit; i++) { - resampledBuffer.put((byte) 0); - resampledBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); - } - break; - case C.ENCODING_PCM_24BIT: - // 24->16 bit resampling. Drop the least significant byte. - for (int i = offset; i < limit; i += 3) { - resampledBuffer.put(buffer.get(i + 1)); - resampledBuffer.put(buffer.get(i + 2)); - } - break; - case C.ENCODING_PCM_32BIT: - // 32->16 bit resampling. Drop the two least significant bytes. - for (int i = offset; i < limit; i += 4) { - resampledBuffer.put(buffer.get(i + 2)); - resampledBuffer.put(buffer.get(i + 3)); - } - break; - case C.ENCODING_PCM_16BIT: - case C.ENCODING_INVALID: - case Format.NO_VALUE: - default: - // Never happens. - throw new IllegalStateException(); - } - - resampledBuffer.position(0); - return resampledBuffer; + @TargetApi(21) + private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate, + int channelConfig, int encoding, int bufferSize, int sessionId) { + AudioAttributes attributesBuilder = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) + .setFlags(AudioAttributes.FLAG_HW_AV_SYNC) + .build(); + AudioFormat format = new AudioFormat.Builder() + .setChannelMask(channelConfig) + .setEncoding(encoding) + .setSampleRate(sampleRate) + .build(); + return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM, + sessionId); } @C.Encoding @@ -1125,18 +1457,57 @@ public final class AudioTrack { } @TargetApi(21) - private static int writeNonBlockingV21( - android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) { + private static int writeNonBlockingV21(android.media.AudioTrack audioTrack, ByteBuffer buffer, + int size) { return audioTrack.write(buffer, size, WRITE_NON_BLOCKING); } @TargetApi(21) - private static void setAudioTrackVolumeV21(android.media.AudioTrack audioTrack, float volume) { + private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack, + ByteBuffer buffer, int size, long presentationTimeUs) { + // TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed. + // if (Util.SDK_INT >= 23) { + // // The underlying platform AudioTrack writes AV sync headers directly. + // return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000); + // } + if (avSyncHeader == null) { + avSyncHeader = ByteBuffer.allocate(16); + avSyncHeader.order(ByteOrder.BIG_ENDIAN); + avSyncHeader.putInt(0x55550001); + } + if (bytesUntilNextAvSync == 0) { + avSyncHeader.putInt(4, size); + avSyncHeader.putLong(8, presentationTimeUs * 1000); + avSyncHeader.position(0); + bytesUntilNextAvSync = size; + } + int avSyncHeaderBytesRemaining = avSyncHeader.remaining(); + if (avSyncHeaderBytesRemaining > 0) { + int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING); + if (result < 0) { + bytesUntilNextAvSync = 0; + return result; + } + if (result < avSyncHeaderBytesRemaining) { + return 0; + } + } + int result = writeNonBlockingV21(audioTrack, buffer, size); + if (result < 0) { + bytesUntilNextAvSync = 0; + return result; + } + bytesUntilNextAvSync -= result; + return result; + } + + @TargetApi(21) + private static void setVolumeInternalV21(android.media.AudioTrack audioTrack, float volume) { audioTrack.setVolume(volume); } @SuppressWarnings("deprecation") - private static void setAudioTrackVolumeV3(android.media.AudioTrack audioTrack, float volume) { + private static void setVolumeInternalV3(android.media.AudioTrack audioTrack, float volume) { audioTrack.setStereoVolume(volume, volume); } @@ -1178,15 +1549,15 @@ public final class AudioTrack { /** * Stops the audio track in a way that ensures media written to it is played out in full, and - * that {@link #getPlaybackHeadPosition()} and {@link #getPlaybackHeadPositionUs()} continue to - * increment as the remaining media is played out. + * that {@link #getPlaybackHeadPosition()} and {@link #getPositionUs()} continue to increment as + * the remaining media is played out. * - * @param submittedFrames The total number of frames that have been submitted. + * @param writtenFrames The total number of frames that have been written. */ - public void handleEndOfStream(long submittedFrames) { + public void handleEndOfStream(long writtenFrames) { stopPlaybackHeadPosition = getPlaybackHeadPosition(); stopTimestampUs = SystemClock.elapsedRealtime() * 1000; - endPlaybackHeadPosition = submittedFrames; + endPlaybackHeadPosition = writtenFrames; audioTrack.stop(); } @@ -1208,8 +1579,7 @@ public final class AudioTrack { * returns the playback head position as a long that will only wrap around if the value exceeds * {@link Long#MAX_VALUE} (which in practice will never happen). * - * @return {@link android.media.AudioTrack#getPlaybackHeadPosition()} of {@link #audioTrack} - * expressed as a long. + * @return The playback head position, in frames. */ public long getPlaybackHeadPosition() { if (stopTimestampUs != C.TIME_UNSET) { @@ -1244,9 +1614,9 @@ public final class AudioTrack { } /** - * Returns {@link #getPlaybackHeadPosition()} expressed as microseconds. + * Returns the duration of played media since reconfiguration, in microseconds. */ - public long getPlaybackHeadPositionUs() { + public long getPositionUs() { return (getPlaybackHeadPosition() * C.MICROS_PER_SECOND) / sampleRate; } @@ -1290,28 +1660,6 @@ public final class AudioTrack { throw new UnsupportedOperationException(); } - /** - * Sets the Playback Parameters to be used by the underlying {@link android.media.AudioTrack}. - * - * @param playbackParams The playback parameters to be used by the - * {@link android.media.AudioTrack}. - * @throws UnsupportedOperationException If Playback Parameters are not supported - * (i.e. {@link Util#SDK_INT} < 23). - */ - public void setPlaybackParams(PlaybackParams playbackParams) { - throw new UnsupportedOperationException(); - } - - /** - * Returns the configured playback speed according to the used Playback Parameters. If these are - * not supported, 1.0f(normal speed) is returned. - * - * @return The speed factor used by the underlying {@link android.media.AudioTrack}. - */ - public float getPlaybackSpeed() { - return 1.0f; - } - } @TargetApi(19) @@ -1363,41 +1711,20 @@ public final class AudioTrack { } - @TargetApi(23) - private static class AudioTrackUtilV23 extends AudioTrackUtilV19 { + /** + * Stores playback parameters with the position and media time at which they apply. + */ + private static final class PlaybackParametersCheckpoint { - private PlaybackParams playbackParams; - private float playbackSpeed; + private final PlaybackParameters playbackParameters; + private final long mediaTimeUs; + private final long positionUs; - public AudioTrackUtilV23() { - playbackSpeed = 1.0f; - } - - @Override - public void reconfigure(android.media.AudioTrack audioTrack, - boolean needsPassthroughWorkaround) { - super.reconfigure(audioTrack, needsPassthroughWorkaround); - maybeApplyPlaybackParams(); - } - - @Override - public void setPlaybackParams(PlaybackParams playbackParams) { - playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams()) - .allowDefaults(); - this.playbackParams = playbackParams; - this.playbackSpeed = playbackParams.getSpeed(); - maybeApplyPlaybackParams(); - } - - @Override - public float getPlaybackSpeed() { - return playbackSpeed; - } - - private void maybeApplyPlaybackParams() { - if (audioTrack != null && playbackParams != null) { - audioTrack.setPlaybackParams(playbackParams); - } + private PlaybackParametersCheckpoint(PlaybackParameters playbackParameters, long mediaTimeUs, + long positionUs) { + this.playbackParameters = playbackParameters; + this.mediaTimeUs = mediaTimeUs; + this.positionUs = positionUs; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/ChannelMappingAudioProcessor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/ChannelMappingAudioProcessor.java new file mode 100755 index 000000000..ca4af2036 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -0,0 +1,162 @@ +/* + * 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 org.telegram.messenger.exoplayer2.audio; + +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.C.Encoding; +import org.telegram.messenger.exoplayer2.Format; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +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. + */ +/* package */ final class ChannelMappingAudioProcessor implements AudioProcessor { + + private int channelCount; + private int sampleRateHz; + private int[] pendingOutputChannels; + + private boolean active; + private int[] outputChannels; + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + /** + * Creates a new processor that applies a channel mapping. + */ + public ChannelMappingAudioProcessor() { + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; + } + + /** + * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} + * to start using the new channel map. + * + * @see AudioTrack#configure(String, int, int, int, int, int[]) + */ + public void setChannelMap(int[] outputChannels) { + pendingOutputChannels = outputChannels; + } + + @Override + public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding) + throws UnhandledFormatException { + boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels); + outputChannels = pendingOutputChannels; + if (outputChannels == null) { + active = false; + return outputChannelsChanged; + } + if (encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (!outputChannelsChanged && this.sampleRateHz == sampleRateHz + && this.channelCount == channelCount) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + + active = channelCount != outputChannels.length; + for (int i = 0; i < outputChannels.length; i++) { + int channelIndex = outputChannels[i]; + if (channelIndex >= channelCount) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + active |= (channelIndex != i); + } + return true; + } + + @Override + public boolean isActive() { + return active; + } + + @Override + public int getOutputChannelCount() { + return outputChannels == null ? channelCount : outputChannels.length; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + int position = inputBuffer.position(); + int limit = inputBuffer.limit(); + int frameCount = (limit - position) / (2 * channelCount); + int outputSize = frameCount * outputChannels.length * 2; + if (buffer.capacity() < outputSize) { + buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + while (position < limit) { + for (int channelIndex : outputChannels) { + buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex)); + } + position += channelCount * 2; + } + inputBuffer.position(limit); + buffer.flip(); + outputBuffer = buffer; + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + + @Override + public void flush() { + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + } + + @Override + public void reset() { + flush(); + buffer = EMPTY_BUFFER; + channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; + outputChannels = null; + active = false; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/MediaCodecAudioRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/MediaCodecAudioRenderer.java index af04048fa..ffc84a918 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -19,12 +19,12 @@ import android.annotation.TargetApi; import android.media.MediaCodec; import android.media.MediaCrypto; import android.media.MediaFormat; -import android.media.PlaybackParams; import android.media.audiofx.Virtualizer; import android.os.Handler; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.ExoPlaybackException; import org.telegram.messenger.exoplayer2.Format; +import org.telegram.messenger.exoplayer2.PlaybackParameters; import org.telegram.messenger.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import org.telegram.messenger.exoplayer2.drm.DrmSessionManager; import org.telegram.messenger.exoplayer2.drm.FrameworkMediaCrypto; @@ -41,16 +41,16 @@ import java.nio.ByteBuffer; * Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}. */ @TargetApi(16) -public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock, - AudioTrack.Listener { +public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock { private final EventDispatcher eventDispatcher; private final AudioTrack audioTrack; private boolean passthroughEnabled; + private boolean codecNeedsDiscardChannelsWorkaround; private android.media.MediaFormat passthroughMediaFormat; private int pcmEncoding; - private int audioSessionId; + private int channelCount; private long currentPositionUs; private boolean allowPositionDiscontinuity; @@ -123,14 +123,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before + * output. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { + AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, + AudioProcessor... audioProcessors) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; - audioTrack = new AudioTrack(audioCapabilities, this); + audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -141,8 +143,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media if (!MimeTypes.isAudio(mimeType)) { return FORMAT_UNSUPPORTED_TYPE; } + int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { - return ADAPTIVE_NOT_SEAMLESS | FORMAT_HANDLED; + return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; } MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false); if (decoderInfo == null) { @@ -155,7 +158,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media && (format.channelCount == Format.NO_VALUE || decoderInfo.isAudioChannelCountSupportedV21(format.channelCount))); int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return ADAPTIVE_NOT_SEAMLESS | formatSupport; + return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport; } @Override @@ -185,7 +188,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto) { + protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, + MediaCrypto crypto) { + codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); if (passthroughEnabled) { // Override the MIME type used to configure the codec if we are using a passthrough decoder. passthroughMediaFormat = format.getFrameworkMediaFormatV16(); @@ -217,39 +222,72 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // output 16-bit PCM. pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding : C.ENCODING_PCM_16BIT; + channelCount = newFormat.channelCount; } @Override - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { boolean passthrough = passthroughMediaFormat != null; String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) : MimeTypes.AUDIO_RAW; MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); - audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + int[] channelMap; + if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) { + channelMap = new int[this.channelCount]; + for (int i = 0; i < this.channelCount; i++) { + channelMap[i] = i; + } + } else { + channelMap = null; + } + + try { + audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap); + } catch (AudioTrack.ConfigurationException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } } /** - * Called when the audio session id becomes known. Once the id is known it will not change (and - * hence this method will not be called again) unless the renderer is disabled and then - * subsequently re-enabled. - *

    - * The default implementation is a no-op. One reason for overriding this method would be to - * instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For - * this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()} - * (if not before). + * Called when the audio session id becomes known. The default implementation is a no-op. One + * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in + * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances + * should be released in {@link #onDisabled()} (if not before). * - * @param audioSessionId The audio session id. + * @see AudioTrack.Listener#onAudioSessionId(int) */ protected void onAudioSessionId(int audioSessionId) { // Do nothing. } + /** + * @see AudioTrack.Listener#onPositionDiscontinuity() + */ + protected void onAudioTrackPositionDiscontinuity() { + // Do nothing. + } + + /** + * @see AudioTrack.Listener#onUnderrun(int, long, long) + */ + protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, + long elapsedSinceLastFeedMs) { + // Do nothing. + } + @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { super.onEnabled(joining); eventDispatcher.enabled(decoderCounters); + int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; + if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { + audioTrack.enableTunnelingV21(tunnelingAudioSessionId); + } else { + audioTrack.disableTunneling(); + } } @Override @@ -274,7 +312,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onDisabled() { - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; try { audioTrack.release(); } finally { @@ -289,7 +326,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override public boolean isEnded() { - return super.isEnded() && !audioTrack.hasPendingData(); + return super.isEnded() && audioTrack.isEnded(); } @Override @@ -308,6 +345,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return currentPositionUs; } + @Override + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + return audioTrack.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return audioTrack.getPlaybackParameters(); + } + @Override protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, @@ -325,54 +372,25 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return true; } - if (!audioTrack.isInitialized()) { - // Initialize the AudioTrack now. - try { - if (audioSessionId == AudioTrack.SESSION_ID_NOT_SET) { - audioSessionId = audioTrack.initialize(AudioTrack.SESSION_ID_NOT_SET); - eventDispatcher.audioSessionId(audioSessionId); - onAudioSessionId(audioSessionId); - } else { - audioTrack.initialize(audioSessionId); - } - } catch (AudioTrack.InitializationException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); - } - if (getState() == STATE_STARTED) { - audioTrack.play(); - } - } - - int handleBufferResult; try { - handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs); - } catch (AudioTrack.WriteException e) { + if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) { + codec.releaseOutputBuffer(bufferIndex, false); + decoderCounters.renderedOutputBufferCount++; + return true; + } + } catch (AudioTrack.InitializationException | AudioTrack.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } - - // If we are out of sync, allow currentPositionUs to jump backwards. - if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { - handleAudioTrackDiscontinuity(); - allowPositionDiscontinuity = true; - } - - // Release the buffer if it was consumed. - if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { - codec.releaseOutputBuffer(bufferIndex, false); - decoderCounters.renderedOutputBufferCount++; - return true; - } - return false; } @Override - protected void onOutputStreamEnded() { - audioTrack.handleEndOfStream(); - } - - protected void handleAudioTrackDiscontinuity() { - // Do nothing + protected void renderToEndOfStream() throws ExoPlaybackException { + try { + audioTrack.playToEndOfStream(); + } catch (AudioTrack.WriteException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } } @Override @@ -381,14 +399,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media case C.MSG_SET_VOLUME: audioTrack.setVolume((Float) message); break; - case C.MSG_SET_PLAYBACK_PARAMS: - audioTrack.setPlaybackParams((PlaybackParams) message); - break; case C.MSG_SET_STREAM_TYPE: @C.StreamType int streamType = (Integer) message; - if (audioTrack.setStreamType(streamType)) { - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; - } + audioTrack.setStreamType(streamType); break; default: super.handleMessage(messageType, message); @@ -396,11 +409,41 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } - // AudioTrack.Listener implementation. + /** + * Returns whether the decoder is known to output six audio channels when provided with input with + * fewer than six channels. + *

    + * See [Internal: b/35655036]. + */ + private static boolean codecNeedsDiscardChannelsWorkaround(String codecName) { + // The workaround applies to Samsung Galaxy S6 and Samsung Galaxy S7. + return Util.SDK_INT < 24 && "OMX.SEC.aac.dec".equals(codecName) + && "samsung".equals(Util.MANUFACTURER) + && (Util.DEVICE.startsWith("zeroflte") || Util.DEVICE.startsWith("herolte") + || Util.DEVICE.startsWith("heroqlte")); + } + + private final class AudioTrackListener implements AudioTrack.Listener { + + @Override + public void onAudioSessionId(int audioSessionId) { + eventDispatcher.audioSessionId(audioSessionId); + MediaCodecAudioRenderer.this.onAudioSessionId(audioSessionId); + } + + @Override + public void onPositionDiscontinuity() { + onAudioTrackPositionDiscontinuity(); + // We are out of sync so allow currentPositionUs to jump backwards. + MediaCodecAudioRenderer.this.allowPositionDiscontinuity = true; + } + + @Override + public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } - @Override - public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/ResamplingAudioProcessor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/ResamplingAudioProcessor.java new file mode 100755 index 000000000..afa3c217a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/ResamplingAudioProcessor.java @@ -0,0 +1,179 @@ +/* + * 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 org.telegram.messenger.exoplayer2.audio; + +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.Format; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}. + */ +/* package */ final class ResamplingAudioProcessor implements AudioProcessor { + + private int sampleRateHz; + private int channelCount; + @C.PcmEncoding + private int encoding; + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + /** + * Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. + */ + public ResamplingAudioProcessor() { + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + encoding = C.ENCODING_INVALID; + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + } + + @Override + public boolean configure(int sampleRateHz, int channelCount, @C.Encoding 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); + } + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount + && this.encoding == encoding) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + this.encoding = encoding; + if (encoding == C.ENCODING_PCM_16BIT) { + buffer = EMPTY_BUFFER; + } + return true; + } + + @Override + public boolean isActive() { + return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT; + } + + @Override + public int getOutputChannelCount() { + return channelCount; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + // Prepare the output buffer. + int position = inputBuffer.position(); + int limit = inputBuffer.limit(); + int size = limit - position; + int resampledSize; + switch (encoding) { + case C.ENCODING_PCM_8BIT: + resampledSize = size * 2; + break; + case C.ENCODING_PCM_24BIT: + resampledSize = (size / 3) * 2; + break; + case C.ENCODING_PCM_32BIT: + resampledSize = size / 2; + break; + case C.ENCODING_PCM_16BIT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + throw new IllegalStateException(); + } + if (buffer.capacity() < resampledSize) { + buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + + // Resample the little endian input and update the input/output buffers. + switch (encoding) { + case C.ENCODING_PCM_8BIT: + // 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_24BIT: + // 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. + 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_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + inputBuffer.position(inputBuffer.limit()); + buffer.flip(); + outputBuffer = buffer; + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + + @Override + public void flush() { + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + } + + @Override + public void reset() { + flush(); + buffer = EMPTY_BUFFER; + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + encoding = C.ENCODING_INVALID; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/SimpleDecoderAudioRenderer.java index e62afc2b8..e4fe25306 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -15,15 +15,17 @@ */ package org.telegram.messenger.exoplayer2.audio; -import android.media.PlaybackParams; +import android.media.audiofx.Virtualizer; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.support.annotation.IntDef; import org.telegram.messenger.exoplayer2.BaseRenderer; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.ExoPlaybackException; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.FormatHolder; +import org.telegram.messenger.exoplayer2.PlaybackParameters; import org.telegram.messenger.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import org.telegram.messenger.exoplayer2.decoder.DecoderCounters; import org.telegram.messenger.exoplayer2.decoder.DecoderInputBuffer; @@ -32,23 +34,46 @@ import org.telegram.messenger.exoplayer2.decoder.SimpleOutputBuffer; import org.telegram.messenger.exoplayer2.drm.DrmSession; import org.telegram.messenger.exoplayer2.drm.DrmSessionManager; import org.telegram.messenger.exoplayer2.drm.ExoMediaCrypto; +import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.MediaClock; import org.telegram.messenger.exoplayer2.util.MimeTypes; import org.telegram.messenger.exoplayer2.util.TraceUtil; import org.telegram.messenger.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Decodes and renders audio using a {@link SimpleDecoder}. */ -public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock, - AudioTrack.Listener { +public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock { + @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 DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; - private final EventDispatcher eventDispatcher; private final AudioTrack audioTrack; - private final DrmSessionManager drmSessionManager; private final FormatHolder formatHolder; + private final DecoderInputBuffer flagsOnlyBuffer; private DecoderCounters decoderCounters; private Format inputFormat; @@ -59,14 +84,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private DrmSession drmSession; private DrmSession pendingDrmSession; + @ReinitializationState private int decoderReinitializationState; + private boolean decoderReceivedBuffers; + private boolean audioTrackNeedsConfigure; + private long currentPositionUs; private boolean allowPositionDiscontinuity; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; - private int audioSessionId; - public SimpleDecoderAudioRenderer() { this(null, null); } @@ -75,10 +102,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener) { - this(eventHandler, eventListener, null); + AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { + this(eventHandler, eventListener, null, null, false, audioProcessors); } /** @@ -106,17 +134,21 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + AudioProcessor... audioProcessors) { super(C.TRACK_TYPE_AUDIO); - eventDispatcher = new EventDispatcher(eventHandler, eventListener); - audioTrack = new AudioTrack(audioCapabilities, this); this.drmSessionManager = drmSessionManager; - formatHolder = new FormatHolder(); this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + eventDispatcher = new EventDispatcher(eventHandler, eventListener); + audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); + formatHolder = new FormatHolder(); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); + decoderReinitializationState = REINITIALIZATION_STATE_NONE; + audioTrackNeedsConfigure = true; } @Override @@ -124,59 +156,98 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return this; } + @Override + public final int supportsFormat(Format format) { + int formatSupport = supportsFormatInternal(format); + if (formatSupport == FORMAT_UNSUPPORTED_TYPE || formatSupport == FORMAT_UNSUPPORTED_SUBTYPE) { + return formatSupport; + } + int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; + return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport; + } + + /** + * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for + * {@link #supportsFormat(Format)}. + * + * @param format The format. + * @return The extent to which the renderer supports the format itself. + */ + protected abstract int supportsFormatInternal(Format format); + @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { + try { + audioTrack.playToEndOfStream(); + } catch (AudioTrack.WriteException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } return; } // Try and read a format if we don't have one already. - if (inputFormat == null && !readFormat()) { - // We can't make progress without one. - return; - } - - drmSession = pendingDrmSession; - ExoMediaCrypto mediaCrypto = null; - if (drmSession != null) { - @DrmSession.State int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } else if (drmSessionState == DrmSession.STATE_OPENED - || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { - mediaCrypto = drmSession.getMediaCrypto(); + if (inputFormat == null) { + // We don't have a format yet, so try and read one. + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, true); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + // End of stream read having not read a format. + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + processEndOfStream(); + return; } else { - // The drm session isn't open yet. + // 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. - if (decoder == null) { + maybeInitDecoder(); + + if (decoder != null) { try { - long codecInitializingTimestamp = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("createAudioDecoder"); - decoder = createDecoder(inputFormat, mediaCrypto); + // Rendering loop. + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer()) {} + while (feedInputBuffer()) {} TraceUtil.endSection(); - long codecInitializedTimestamp = SystemClock.elapsedRealtime(); - eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, - codecInitializedTimestamp - codecInitializingTimestamp); - decoderCounters.decoderInitCount++; - } catch (AudioDecoderException e) { + } catch (AudioDecoderException | AudioTrack.ConfigurationException + | AudioTrack.InitializationException | AudioTrack.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } + decoderCounters.ensureUpdated(); } + } - // Rendering loop. - try { - TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer()) {} - while (feedInputBuffer()) {} - TraceUtil.endSection(); - } catch (AudioTrack.InitializationException | AudioTrack.WriteException - | AudioDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); - } - decoderCounters.ensureUpdated(); + /** + * Called when the audio session id becomes known. The default implementation is a no-op. One + * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in + * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances + * should be released in {@link #onDisabled()} (if not before). + * + * @see AudioTrack.Listener#onAudioSessionId(int) + */ + protected void onAudioSessionId(int audioSessionId) { + // Do nothing. + } + + /** + * @see AudioTrack.Listener#onPositionDiscontinuity() + */ + protected void onAudioTrackPositionDiscontinuity() { + // Do nothing. + } + + /** + * @see AudioTrack.Listener#onUnderrun(int, long, long) + */ + protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, + long elapsedSinceLastFeedMs) { + // Do nothing. } /** @@ -205,12 +276,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements null, null, 0, null); } - private boolean drainOutputBuffer() throws AudioDecoderException, - AudioTrack.InitializationException, AudioTrack.WriteException { - if (outputStreamEnded) { - return false; - } - + private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException, + AudioTrack.ConfigurationException, AudioTrack.InitializationException, + AudioTrack.WriteException { if (outputBuffer == null) { outputBuffer = decoder.dequeueOutputBuffer(); if (outputBuffer == null) { @@ -220,38 +288,28 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } if (outputBuffer.isEndOfStream()) { - outputStreamEnded = true; - audioTrack.handleEndOfStream(); - outputBuffer.release(); - outputBuffer = null; + 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(); + // The audio track may need to be recreated once the new output format is known. + audioTrackNeedsConfigure = true; + } else { + outputBuffer.release(); + outputBuffer = null; + processEndOfStream(); + } return false; } - if (!audioTrack.isInitialized()) { + if (audioTrackNeedsConfigure) { Format outputFormat = getOutputFormat(); audioTrack.configure(outputFormat.sampleMimeType, outputFormat.channelCount, outputFormat.sampleRate, outputFormat.pcmEncoding, 0); - if (audioSessionId == AudioTrack.SESSION_ID_NOT_SET) { - audioSessionId = audioTrack.initialize(AudioTrack.SESSION_ID_NOT_SET); - eventDispatcher.audioSessionId(audioSessionId); - onAudioSessionId(audioSessionId); - } else { - audioTrack.initialize(audioSessionId); - } - if (getState() == STATE_STARTED) { - audioTrack.play(); - } + audioTrackNeedsConfigure = false; } - int handleBufferResult = audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs); - - // If we are out of sync, allow currentPositionUs to jump backwards. - if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { - allowPositionDiscontinuity = true; - } - - // Release the buffer if it was consumed. - if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { + if (audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) { decoderCounters.renderedOutputBufferCount++; outputBuffer.release(); outputBuffer = null; @@ -262,7 +320,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean feedInputBuffer() throws AudioDecoderException, ExoPlaybackException { - if (inputStreamEnded) { + 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; } @@ -273,12 +333,20 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } } + 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; 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); + result = readSource(formatHolder, inputBuffer, false); } if (result == C.RESULT_NOTHING_READ) { @@ -301,6 +369,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } inputBuffer.flip(); decoder.queueInputBuffer(inputBuffer); + decoderReceivedBuffers = true; decoderCounters.inputBufferCount++; inputBuffer = null; return true; @@ -318,19 +387,34 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements && (bufferEncrypted || !playClearSamplesWithoutKeys); } - private void flushDecoder() { - inputBuffer = null; - waitingForKeys = false; - if (outputBuffer != null) { - outputBuffer.release(); - outputBuffer = null; + private void processEndOfStream() throws ExoPlaybackException { + outputStreamEnded = true; + try { + audioTrack.playToEndOfStream(); + } catch (AudioTrack.WriteException e) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } + } + + private void flushDecoder() throws ExoPlaybackException { + waitingForKeys = false; + if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { + releaseDecoder(); + maybeInitDecoder(); + } else { + inputBuffer = null; + if (outputBuffer != null) { + outputBuffer.release(); + outputBuffer = null; + } + decoder.flush(); + decoderReceivedBuffers = false; } - decoder.flush(); } @Override public boolean isEnded() { - return outputStreamEnded && !audioTrack.hasPendingData(); + return outputStreamEnded && audioTrack.isEnded(); } @Override @@ -350,27 +434,30 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return currentPositionUs; } - /** - * Called when the audio session id becomes known. Once the id is known it will not change (and - * hence this method will not be called again) unless the renderer is disabled and then - * subsequently re-enabled. - *

    - * The default implementation is a no-op. - * - * @param audioSessionId The audio session id. - */ - protected void onAudioSessionId(int audioSessionId) { - // Do nothing. + @Override + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + return audioTrack.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return audioTrack.getPlaybackParameters(); } @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { decoderCounters = new DecoderCounters(); eventDispatcher.enabled(decoderCounters); + int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; + if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { + audioTrack.enableTunnelingV21(tunnelingAudioSessionId); + } else { + audioTrack.disableTunneling(); + } } @Override - protected void onPositionReset(long positionUs, boolean joining) { + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { audioTrack.reset(); currentPositionUs = positionUs; allowPositionDiscontinuity = true; @@ -393,17 +480,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override protected void onDisabled() { - inputBuffer = null; - outputBuffer = null; inputFormat = null; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioTrackNeedsConfigure = true; waitingForKeys = false; try { - if (decoder != null) { - decoder.release(); - decoder = null; - decoderCounters.decoderReleaseCount++; - } + releaseDecoder(); audioTrack.release(); } finally { try { @@ -425,13 +506,52 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } } - private boolean readFormat() throws ExoPlaybackException { - int result = readSource(formatHolder, null); - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - return true; + private void maybeInitDecoder() throws ExoPlaybackException { + if (decoder != null) { + return; } - return false; + + drmSession = pendingDrmSession; + ExoMediaCrypto mediaCrypto = null; + if (drmSession != null) { + @DrmSession.State int drmSessionState = drmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } else if (drmSessionState == DrmSession.STATE_OPENED + || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { + mediaCrypto = drmSession.getMediaCrypto(); + } else { + // The drm session isn't open yet. + return; + } + } + + try { + long codecInitializingTimestamp = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("createAudioDecoder"); + decoder = createDecoder(inputFormat, mediaCrypto); + TraceUtil.endSection(); + long codecInitializedTimestamp = SystemClock.elapsedRealtime(); + eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, + codecInitializedTimestamp - codecInitializingTimestamp); + decoderCounters.decoderInitCount++; + } catch (AudioDecoderException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + } + + private void releaseDecoder() { + if (decoder == null) { + return; + } + + inputBuffer = null; + outputBuffer = null; + decoder.release(); + decoder = null; + decoderCounters.decoderReleaseCount++; + decoderReinitializationState = REINITIALIZATION_STATE_NONE; + decoderReceivedBuffers = false; } private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { @@ -456,6 +576,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } } + 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; + } + eventDispatcher.inputFormatChanged(newFormat); } @@ -465,14 +595,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements case C.MSG_SET_VOLUME: audioTrack.setVolume((Float) message); break; - case C.MSG_SET_PLAYBACK_PARAMS: - audioTrack.setPlaybackParams((PlaybackParams) message); - break; case C.MSG_SET_STREAM_TYPE: @C.StreamType int streamType = (Integer) message; - if (audioTrack.setStreamType(streamType)) { - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; - } + audioTrack.setStreamType(streamType); break; default: super.handleMessage(messageType, message); @@ -480,11 +605,27 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } } - // AudioTrack.Listener implementation. + private final class AudioTrackListener implements AudioTrack.Listener { + + @Override + public void onAudioSessionId(int audioSessionId) { + eventDispatcher.audioSessionId(audioSessionId); + SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId); + } + + @Override + public void onPositionDiscontinuity() { + onAudioTrackPositionDiscontinuity(); + // We are out of sync so allow currentPositionUs to jump backwards. + SimpleDecoderAudioRenderer.this.allowPositionDiscontinuity = true; + } + + @Override + public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } - @Override - public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/Sonic.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/Sonic.java new file mode 100755 index 000000000..8182d95bc --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/Sonic.java @@ -0,0 +1,534 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2010 Bill Cox, Sonic Library + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2.audio; + +import org.telegram.messenger.exoplayer2.util.Assertions; +import java.nio.ShortBuffer; +import java.util.Arrays; + +/** + * Sonic audio stream processor for time/pitch stretching. + *

    + * Based on https://github.com/waywardgeek/sonic. + */ +/* package */ final class Sonic { + + private static final boolean USE_CHORD_PITCH = false; + private static final int MINIMUM_PITCH = 65; + private static final int MAXIMUM_PITCH = 400; + private static final int AMDF_FREQUENCY = 4000; + + private final int sampleRate; + private final int numChannels; + private final int minPeriod; + private final int maxPeriod; + private final int maxRequired; + private final short[] downSampleBuffer; + + private int inputBufferSize; + private short[] inputBuffer; + private int outputBufferSize; + private short[] outputBuffer; + private int pitchBufferSize; + private short[] pitchBuffer; + private int oldRatePosition; + private int newRatePosition; + private float speed; + private float pitch; + private int numInputSamples; + private int numOutputSamples; + private int numPitchSamples; + private int remainingInputToCopy; + private int prevPeriod; + private int prevMinDiff; + private int minDiff; + private int maxDiff; + + /** + * Creates a new Sonic audio stream processor. + * + * @param sampleRate The sample rate of input audio. + * @param numChannels The number of channels in the input audio. + */ + public Sonic(int sampleRate, int numChannels) { + this.sampleRate = sampleRate; + this.numChannels = numChannels; + minPeriod = sampleRate / MAXIMUM_PITCH; + maxPeriod = sampleRate / MINIMUM_PITCH; + maxRequired = 2 * maxPeriod; + downSampleBuffer = new short[maxRequired]; + inputBufferSize = maxRequired; + inputBuffer = new short[maxRequired * numChannels]; + outputBufferSize = maxRequired; + outputBuffer = new short[maxRequired * numChannels]; + pitchBufferSize = maxRequired; + pitchBuffer = new short[maxRequired * numChannels]; + oldRatePosition = 0; + newRatePosition = 0; + prevPeriod = 0; + speed = 1.0f; + pitch = 1.0f; + } + + /** + * Sets the output speed. + */ + public void setSpeed(float speed) { + this.speed = speed; + } + + /** + * Gets the output speed. + */ + public float getSpeed() { + return speed; + } + + /** + * Sets the output pitch. + */ + public void setPitch(float pitch) { + this.pitch = pitch; + } + + /** + * Gets the output pitch. + */ + public float getPitch() { + return pitch; + } + + /** + * Queues remaining data from {@code buffer}, and advances its position by the number of bytes + * consumed. + * + * @param buffer A {@link ShortBuffer} containing input data between its position and limit. + */ + public void queueInput(ShortBuffer buffer) { + int samplesToWrite = buffer.remaining() / numChannels; + int bytesToWrite = samplesToWrite * numChannels * 2; + enlargeInputBufferIfNeeded(samplesToWrite); + buffer.get(inputBuffer, numInputSamples * numChannels, bytesToWrite / 2); + numInputSamples += samplesToWrite; + processStreamInput(); + } + + /** + * Gets available output, outputting to the start of {@code buffer}. The buffer's position will be + * advanced by the number of bytes written. + * + * @param buffer A {@link ShortBuffer} into which output will be written. + */ + public void getOutput(ShortBuffer buffer) { + int samplesToRead = Math.min(buffer.remaining() / numChannels, numOutputSamples); + buffer.put(outputBuffer, 0, samplesToRead * numChannels); + numOutputSamples -= samplesToRead; + System.arraycopy(outputBuffer, samplesToRead * numChannels, outputBuffer, 0, + numOutputSamples * numChannels); + } + + /** + * Forces generating output using whatever data has been queued already. No extra delay will be + * added to the output, but flushing in the middle of words could introduce distortion. + */ + public void queueEndOfStream() { + int remainingSamples = numInputSamples; + float s = speed / pitch; + int expectedOutputSamples = + numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / pitch + 0.5f); + + // Add enough silence to flush both input and pitch buffers. + enlargeInputBufferIfNeeded(remainingSamples + 2 * maxRequired); + for (int xSample = 0; xSample < 2 * maxRequired * numChannels; xSample++) { + inputBuffer[remainingSamples * numChannels + xSample] = 0; + } + numInputSamples += 2 * maxRequired; + processStreamInput(); + // Throw away any extra samples we generated due to the silence we added. + if (numOutputSamples > expectedOutputSamples) { + numOutputSamples = expectedOutputSamples; + } + // Empty input and pitch buffers. + numInputSamples = 0; + remainingInputToCopy = 0; + numPitchSamples = 0; + } + + /** + * Returns the number of output samples that can be read with {@link #getOutput(ShortBuffer)}. + */ + public int getSamplesAvailable() { + return numOutputSamples; + } + + // Internal methods. + + private void enlargeOutputBufferIfNeeded(int numSamples) { + if (numOutputSamples + numSamples > outputBufferSize) { + outputBufferSize += (outputBufferSize / 2) + numSamples; + outputBuffer = Arrays.copyOf(outputBuffer, outputBufferSize * numChannels); + } + } + + private void enlargeInputBufferIfNeeded(int numSamples) { + if (numInputSamples + numSamples > inputBufferSize) { + inputBufferSize += (inputBufferSize / 2) + numSamples; + inputBuffer = Arrays.copyOf(inputBuffer, inputBufferSize * numChannels); + } + } + + private void removeProcessedInputSamples(int position) { + int remainingSamples = numInputSamples - position; + System.arraycopy(inputBuffer, position * numChannels, inputBuffer, 0, + remainingSamples * numChannels); + numInputSamples = remainingSamples; + } + + private void copyToOutput(short[] samples, int position, int numSamples) { + enlargeOutputBufferIfNeeded(numSamples); + System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels, + numSamples * numChannels); + numOutputSamples += numSamples; + } + + private int copyInputToOutput(int position) { + int numSamples = Math.min(maxRequired, remainingInputToCopy); + copyToOutput(inputBuffer, position, numSamples); + remainingInputToCopy -= numSamples; + return numSamples; + } + + private void downSampleInput(short[] samples, int position, int skip) { + // If skip is greater than one, average skip samples together and write them to the down-sample + // buffer. If numChannels is greater than one, mix the channels together as we down sample. + int numSamples = maxRequired / skip; + int samplesPerValue = numChannels * skip; + position *= numChannels; + for (int i = 0; i < numSamples; i++) { + int value = 0; + for (int j = 0; j < samplesPerValue; j++) { + value += samples[position + i * samplesPerValue + j]; + } + value /= samplesPerValue; + downSampleBuffer[i] = (short) value; + } + } + + private int findPitchPeriodInRange(short[] samples, int position, int minPeriod, int maxPeriod) { + // Find the best frequency match in the range, and given a sample skip multiple. For now, just + // find the pitch of the first channel. + int bestPeriod = 0; + int worstPeriod = 255; + int minDiff = 1; + int maxDiff = 0; + position *= numChannels; + for (int period = minPeriod; period <= maxPeriod; period++) { + int diff = 0; + for (int i = 0; i < period; i++) { + short sVal = samples[position + i]; + short pVal = samples[position + period + i]; + diff += sVal >= pVal ? sVal - pVal : pVal - sVal; + } + // Note that the highest number of samples we add into diff will be less than 256, since we + // skip samples. Thus, diff is a 24 bit number, and we can safely multiply by numSamples + // without overflow. + if (diff * bestPeriod < minDiff * period) { + minDiff = diff; + bestPeriod = period; + } + if (diff * worstPeriod > maxDiff * period) { + maxDiff = diff; + worstPeriod = period; + } + } + this.minDiff = minDiff / bestPeriod; + this.maxDiff = maxDiff / worstPeriod; + return bestPeriod; + } + + /** + * Returns whether the previous pitch period estimate is a better approximation, which can occur + * at the abrupt end of voiced words. + */ + private boolean previousPeriodBetter(int minDiff, int maxDiff, boolean preferNewPeriod) { + if (minDiff == 0 || prevPeriod == 0) { + return false; + } + if (preferNewPeriod) { + if (maxDiff > minDiff * 3) { + // Got a reasonable match this period + return false; + } + if (minDiff * 2 <= prevMinDiff * 3) { + // Mismatch is not that much greater this period + return false; + } + } else { + if (minDiff <= prevMinDiff) { + return false; + } + } + return true; + } + + private int findPitchPeriod(short[] samples, int position, boolean preferNewPeriod) { + // Find the pitch period. This is a critical step, and we may have to try multiple ways to get a + // good answer. This version uses AMDF. To improve speed, we down sample by an integer factor + // get in the 11 kHz range, and then do it again with a narrower frequency range without down + // sampling. + int period; + int retPeriod; + int skip = sampleRate > AMDF_FREQUENCY ? sampleRate / AMDF_FREQUENCY : 1; + if (numChannels == 1 && skip == 1) { + period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod); + } else { + downSampleInput(samples, position, skip); + period = findPitchPeriodInRange(downSampleBuffer, 0, minPeriod / skip, maxPeriod / skip); + if (skip != 1) { + period *= skip; + int minP = period - (skip * 4); + int maxP = period + (skip * 4); + if (minP < minPeriod) { + minP = minPeriod; + } + if (maxP > maxPeriod) { + maxP = maxPeriod; + } + if (numChannels == 1) { + period = findPitchPeriodInRange(samples, position, minP, maxP); + } else { + downSampleInput(samples, position, 1); + period = findPitchPeriodInRange(downSampleBuffer, 0, minP, maxP); + } + } + } + if (previousPeriodBetter(minDiff, maxDiff, preferNewPeriod)) { + retPeriod = prevPeriod; + } else { + retPeriod = period; + } + prevMinDiff = minDiff; + prevPeriod = period; + return retPeriod; + } + + private void moveNewSamplesToPitchBuffer(int originalNumOutputSamples) { + int numSamples = numOutputSamples - originalNumOutputSamples; + if (numPitchSamples + numSamples > pitchBufferSize) { + pitchBufferSize += (pitchBufferSize / 2) + numSamples; + pitchBuffer = Arrays.copyOf(pitchBuffer, pitchBufferSize * numChannels); + } + System.arraycopy(outputBuffer, originalNumOutputSamples * numChannels, pitchBuffer, + numPitchSamples * numChannels, numSamples * numChannels); + numOutputSamples = originalNumOutputSamples; + numPitchSamples += numSamples; + } + + private void removePitchSamples(int numSamples) { + if (numSamples == 0) { + return; + } + System.arraycopy(pitchBuffer, numSamples * numChannels, pitchBuffer, 0, + (numPitchSamples - numSamples) * numChannels); + numPitchSamples -= numSamples; + } + + private void adjustPitch(int originalNumOutputSamples) { + // Latency due to pitch changes could be reduced by looking at past samples to determine pitch, + // rather than future. + if (numOutputSamples == originalNumOutputSamples) { + return; + } + moveNewSamplesToPitchBuffer(originalNumOutputSamples); + int position = 0; + while (numPitchSamples - position >= maxRequired) { + int period = findPitchPeriod(pitchBuffer, position, false); + int newPeriod = (int) (period / pitch); + enlargeOutputBufferIfNeeded(newPeriod); + if (pitch >= 1.0f) { + overlapAdd(newPeriod, numChannels, outputBuffer, numOutputSamples, pitchBuffer, position, + pitchBuffer, position + period - newPeriod); + } else { + int separation = newPeriod - period; + overlapAddWithSeparation(period, numChannels, separation, outputBuffer, numOutputSamples, + pitchBuffer, position, pitchBuffer, position); + } + numOutputSamples += newPeriod; + position += period; + } + removePitchSamples(position); + } + + private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) { + short left = in[inPos * numChannels]; + short right = in[inPos * numChannels + numChannels]; + int position = newRatePosition * oldSampleRate; + int leftPosition = oldRatePosition * newSampleRate; + int rightPosition = (oldRatePosition + 1) * newSampleRate; + int ratio = rightPosition - position; + int width = rightPosition - leftPosition; + return (short) ((ratio * left + (width - ratio) * right) / width); + } + + private void adjustRate(float rate, int originalNumOutputSamples) { + if (numOutputSamples == originalNumOutputSamples) { + return; + } + int newSampleRate = (int) (sampleRate / rate); + int oldSampleRate = sampleRate; + // Set these values to help with the integer math. + while (newSampleRate > (1 << 14) || oldSampleRate > (1 << 14)) { + newSampleRate /= 2; + oldSampleRate /= 2; + } + moveNewSamplesToPitchBuffer(originalNumOutputSamples); + // Leave at least one pitch sample in the buffer. + for (int position = 0; position < numPitchSamples - 1; position++) { + while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) { + enlargeOutputBufferIfNeeded(1); + for (int i = 0; i < numChannels; i++) { + outputBuffer[numOutputSamples * numChannels + i] = + interpolate(pitchBuffer, position + i, oldSampleRate, newSampleRate); + } + newRatePosition++; + numOutputSamples++; + } + oldRatePosition++; + if (oldRatePosition == oldSampleRate) { + oldRatePosition = 0; + Assertions.checkState(newRatePosition == newSampleRate); + newRatePosition = 0; + } + } + removePitchSamples(numPitchSamples - 1); + } + + private int skipPitchPeriod(short[] samples, int position, float speed, int period) { + // Skip over a pitch period, and copy period/speed samples to the output. + int newSamples; + if (speed >= 2.0f) { + newSamples = (int) (period / (speed - 1.0f)); + } else { + newSamples = period; + remainingInputToCopy = (int) (period * (2.0f - speed) / (speed - 1.0f)); + } + enlargeOutputBufferIfNeeded(newSamples); + overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples, samples, position, samples, + position + period); + numOutputSamples += newSamples; + return newSamples; + } + + private int insertPitchPeriod(short[] samples, int position, float speed, int period) { + // Insert a pitch period, and determine how much input to copy directly. + int newSamples; + if (speed < 0.5f) { + newSamples = (int) (period * speed / (1.0f - speed)); + } else { + newSamples = period; + remainingInputToCopy = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed)); + } + enlargeOutputBufferIfNeeded(period + newSamples); + System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels, + period * numChannels); + overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples + period, samples, + position + period, samples, position); + numOutputSamples += period + newSamples; + return newSamples; + } + + private void changeSpeed(float speed) { + if (numInputSamples < maxRequired) { + return; + } + int numSamples = numInputSamples; + int position = 0; + do { + if (remainingInputToCopy > 0) { + position += copyInputToOutput(position); + } else { + int period = findPitchPeriod(inputBuffer, position, true); + if (speed > 1.0) { + position += period + skipPitchPeriod(inputBuffer, position, speed, period); + } else { + position += insertPitchPeriod(inputBuffer, position, speed, period); + } + } + } while (position + maxRequired <= numSamples); + removeProcessedInputSamples(position); + } + + private void processStreamInput() { + // Resample as many pitch periods as we have buffered on the input. + int originalNumOutputSamples = numOutputSamples; + float s = speed / pitch; + if (s > 1.00001 || s < 0.99999) { + changeSpeed(s); + } else { + copyToOutput(inputBuffer, 0, numInputSamples); + numInputSamples = 0; + } + if (USE_CHORD_PITCH) { + if (pitch != 1.0f) { + adjustPitch(originalNumOutputSamples); + } + } else if (!USE_CHORD_PITCH && pitch != 1.0f) { + adjustRate(pitch, originalNumOutputSamples); + } + } + + private static void overlapAdd(int numSamples, int numChannels, short[] out, int outPos, + short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) { + for (int i = 0; i < numChannels; i++) { + int o = outPos * numChannels + i; + int u = rampUpPos * numChannels + i; + int d = rampDownPos * numChannels + i; + for (int t = 0; t < numSamples; t++) { + out[o] = (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * t) / numSamples); + o += numChannels; + d += numChannels; + u += numChannels; + } + } + } + + private static void overlapAddWithSeparation(int numSamples, int numChannels, int separation, + short[] out, int outPos, short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) { + for (int i = 0; i < numChannels; i++) { + int o = outPos * numChannels + i; + int u = rampUpPos * numChannels + i; + int d = rampDownPos * numChannels + i; + for (int t = 0; t < numSamples + separation; t++) { + if (t < separation) { + out[o] = (short) (rampDown[d] * (numSamples - t) / numSamples); + d += numChannels; + } else if (t < numSamples) { + out[o] = + (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * (t - separation)) + / numSamples); + d += numChannels; + u += numChannels; + } else { + out[o] = (short) (rampUp[u] * (t - separation) / numSamples); + u += numChannels; + } + o += numChannels; + } + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/SonicAudioProcessor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/SonicAudioProcessor.java new file mode 100755 index 000000000..42990b401 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/audio/SonicAudioProcessor.java @@ -0,0 +1,212 @@ +/* + * 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 org.telegram.messenger.exoplayer2.audio; + +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.C.Encoding; +import org.telegram.messenger.exoplayer2.Format; +import org.telegram.messenger.exoplayer2.util.Util; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; + +/** + * An {@link AudioProcessor} that uses the Sonic library to modify the speed/pitch of audio. + */ +public final class SonicAudioProcessor implements AudioProcessor { + + /** + * The maximum allowed playback speed in {@link #setSpeed(float)}. + */ + public static final float MAXIMUM_SPEED = 8.0f; + /** + * The minimum allowed playback speed in {@link #setSpeed(float)}. + */ + public static final float MINIMUM_SPEED = 0.1f; + /** + * The maximum allowed pitch in {@link #setPitch(float)}. + */ + public static final float MAXIMUM_PITCH = 8.0f; + /** + * The minimum allowed pitch in {@link #setPitch(float)}. + */ + public static final float MINIMUM_PITCH = 0.1f; + + /** + * The threshold below which the difference between two pitch/speed factors is negligible. + */ + private static final float CLOSE_THRESHOLD = 0.01f; + + private int channelCount; + private int sampleRateHz; + + private Sonic sonic; + private float speed; + private float pitch; + + private ByteBuffer buffer; + private ShortBuffer shortBuffer; + private ByteBuffer outputBuffer; + private long inputBytes; + private long outputBytes; + private boolean inputEnded; + + /** + * Creates a new Sonic audio processor. + */ + public SonicAudioProcessor() { + speed = 1f; + pitch = 1f; + channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; + buffer = EMPTY_BUFFER; + shortBuffer = buffer.asShortBuffer(); + outputBuffer = EMPTY_BUFFER; + } + + /** + * Sets the playback speed. The new speed will take effect after a call to {@link #flush()}. + * + * @param speed The requested new playback speed. + * @return The actual new playback speed. + */ + public float setSpeed(float speed) { + this.speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED); + return this.speed; + } + + /** + * Sets the playback pitch. The new pitch will take effect after a call to {@link #flush()}. + * + * @param pitch The requested new pitch. + * @return The actual new pitch. + */ + public float setPitch(float pitch) { + this.pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH); + return pitch; + } + + /** + * Returns the number of bytes of input queued since the last call to {@link #flush()}. + */ + public long getInputByteCount() { + return inputBytes; + } + + /** + * Returns the number of bytes of output dequeued since the last call to {@link #flush()}. + */ + public long getOutputByteCount() { + return outputBytes; + } + + @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); + } + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + return true; + } + + @Override + public boolean isActive() { + return Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD; + } + + @Override + public int getOutputChannelCount() { + return channelCount; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + if (inputBuffer.hasRemaining()) { + ShortBuffer shortBuffer = inputBuffer.asShortBuffer(); + int inputSize = inputBuffer.remaining(); + inputBytes += inputSize; + sonic.queueInput(shortBuffer); + inputBuffer.position(inputBuffer.position() + inputSize); + } + int outputSize = sonic.getSamplesAvailable() * channelCount * 2; + if (outputSize > 0) { + if (buffer.capacity() < outputSize) { + buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); + shortBuffer = buffer.asShortBuffer(); + } else { + buffer.clear(); + shortBuffer.clear(); + } + sonic.getOutput(shortBuffer); + outputBytes += outputSize; + buffer.limit(outputSize); + outputBuffer = buffer; + } + } + + @Override + public void queueEndOfStream() { + sonic.queueEndOfStream(); + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @Override + public boolean isEnded() { + return inputEnded && (sonic == null || sonic.getSamplesAvailable() == 0); + } + + @Override + public void flush() { + sonic = new Sonic(sampleRateHz, channelCount); + sonic.setSpeed(speed); + sonic.setPitch(pitch); + outputBuffer = EMPTY_BUFFER; + inputBytes = 0; + outputBytes = 0; + inputEnded = false; + } + + @Override + public void reset() { + sonic = null; + buffer = EMPTY_BUFFER; + shortBuffer = buffer.asShortBuffer(); + outputBuffer = EMPTY_BUFFER; + channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; + inputBytes = 0; + outputBytes = 0; + inputEnded = false; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/decoder/CryptoInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/decoder/CryptoInfo.java index a01adc9de..eeb357a3e 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/decoder/CryptoInfo.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/decoder/CryptoInfo.java @@ -49,11 +49,21 @@ public final class CryptoInfo { * @see android.media.MediaCodec.CryptoInfo#numSubSamples */ public int numSubSamples; + /** + * @see android.media.MediaCodec.CryptoInfo.Pattern + */ + public int patternBlocksToEncrypt; + /** + * @see android.media.MediaCodec.CryptoInfo.Pattern + */ + public int patternBlocksToSkip; private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo; + private final PatternHolderV24 patternHolder; public CryptoInfo() { frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null; + patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null; } /** @@ -67,11 +77,21 @@ public final class CryptoInfo { this.key = key; this.iv = iv; this.mode = mode; + patternBlocksToEncrypt = 0; + patternBlocksToSkip = 0; if (Util.SDK_INT >= 16) { updateFrameworkCryptoInfoV16(); } } + public void setPattern(int patternBlocksToEncrypt, int patternBlocksToSkip) { + this.patternBlocksToEncrypt = patternBlocksToEncrypt; + this.patternBlocksToSkip = patternBlocksToSkip; + if (Util.SDK_INT >= 24) { + patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip); + } + } + /** * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance. *

    @@ -93,8 +113,35 @@ public final class CryptoInfo { @TargetApi(16) private void updateFrameworkCryptoInfoV16() { - frameworkCryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, key, iv, - mode); + // Update fields directly because the framework's CryptoInfo.set performs an unnecessary object + // allocation on Android N. + frameworkCryptoInfo.numSubSamples = numSubSamples; + frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData; + frameworkCryptoInfo.numBytesOfEncryptedData = numBytesOfEncryptedData; + frameworkCryptoInfo.key = key; + frameworkCryptoInfo.iv = iv; + frameworkCryptoInfo.mode = mode; + if (Util.SDK_INT >= 24) { + patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip); + } + } + + @TargetApi(24) + private static final class PatternHolderV24 { + + private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo; + private final android.media.MediaCodec.CryptoInfo.Pattern pattern; + + private PatternHolderV24(android.media.MediaCodec.CryptoInfo frameworkCryptoInfo) { + this.frameworkCryptoInfo = frameworkCryptoInfo; + pattern = new android.media.MediaCodec.CryptoInfo.Pattern(0, 0); + } + + private void set(int blocksToEncrypt, int blocksToSkip) { + pattern.set(blocksToEncrypt, blocksToSkip); + frameworkCryptoInfo.setPattern(pattern); + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/decoder/DecoderInputBuffer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/decoder/DecoderInputBuffer.java index 8855a6a07..e4b756b45 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/decoder/DecoderInputBuffer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/decoder/DecoderInputBuffer.java @@ -61,8 +61,16 @@ public class DecoderInputBuffer extends Buffer { */ public long timeUs; - @BufferReplacementMode - private final int bufferReplacementMode; + @BufferReplacementMode private final int bufferReplacementMode; + + /** + * Creates a new instance for which {@link #isFlagsOnly()} will return true. + * + * @return A new flags only input buffer. + */ + public static DecoderInputBuffer newFlagsOnlyInstance() { + return new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); + } /** * @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One @@ -110,6 +118,14 @@ public class DecoderInputBuffer extends Buffer { data = newData; } + /** + * Returns whether the buffer is only able to hold flags, meaning {@link #data} is null and + * its replacement mode is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}. + */ + public final boolean isFlagsOnly() { + return data == null && bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DISABLED; + } + /** * Returns whether the {@link C#BUFFER_FLAG_ENCRYPTED} flag is set. */ diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/StreamingDrmSessionManager.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/DefaultDrmSessionManager.java similarity index 61% rename from TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/StreamingDrmSessionManager.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/DefaultDrmSessionManager.java index 8f85b9ae7..5fad98182 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/StreamingDrmSessionManager.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/DefaultDrmSessionManager.java @@ -24,7 +24,10 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.support.annotation.IntDef; import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.drm.DrmInitData.SchemeData; import org.telegram.messenger.exoplayer2.drm.ExoMediaDrm.KeyRequest; @@ -32,19 +35,23 @@ import org.telegram.messenger.exoplayer2.drm.ExoMediaDrm.OnEventListener; import org.telegram.messenger.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import org.telegram.messenger.exoplayer2.extractor.mp4.PsshAtomUtil; import org.telegram.messenger.exoplayer2.util.Assertions; +import org.telegram.messenger.exoplayer2.util.MimeTypes; import org.telegram.messenger.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.HashMap; +import java.util.Map; import java.util.UUID; /** - * A {@link DrmSessionManager} that supports streaming playbacks using {@link MediaDrm}. + * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}. */ @TargetApi(18) -public class StreamingDrmSessionManager implements DrmSessionManager, +public class DefaultDrmSessionManager implements DrmSessionManager, DrmSession { /** - * Listener of {@link StreamingDrmSessionManager} events. + * Listener of {@link DefaultDrmSessionManager} events. */ public interface EventListener { @@ -60,6 +67,16 @@ public class StreamingDrmSessionManager implements Drm */ void onDrmSessionManagerError(Exception e); + /** + * Called each time offline keys are restored. + */ + void onDrmKeysRestored(); + + /** + * Called each time offline keys are removed. + */ + void onDrmKeysRemoved(); + } /** @@ -67,9 +84,33 @@ public class StreamingDrmSessionManager implements Drm */ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; + /** Determines the action to be done after a session acquired. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE}) + public @interface Mode {} + /** + * Loads and refreshes (if necessary) a license for playback. Supports streaming and offline + * licenses. + */ + public static final int MODE_PLAYBACK = 0; + /** + * Restores an offline license to allow its status to be queried. If the offline license is + * expired sets state to {@link #STATE_ERROR}. + */ + public static final int MODE_QUERY = 1; + /** Downloads an offline license or renews an existing one. */ + public static final int MODE_DOWNLOAD = 2; + /** Releases an existing offline license. */ + public static final int MODE_RELEASE = 3; + + private static final String TAG = "OfflineDrmSessionMngr"; + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + 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 final Handler eventHandler; private final EventListener eventListener; private final ExoMediaDrm mediaDrm; @@ -85,14 +126,17 @@ public class StreamingDrmSessionManager implements Drm private HandlerThread requestHandlerThread; private Handler postRequestHandler; + private int mode; private int openCount; private boolean provisioningInProgress; @DrmSession.State private int state; private T mediaCrypto; - private Exception lastException; - private SchemeData schemeData; + private DrmSessionException lastException; + private byte[] schemeInitData; + private String schemeMimeType; private byte[] sessionId; + private byte[] offlineLicenseKeySetId; /** * Instantiates a new instance using the Widevine scheme. @@ -105,7 +149,7 @@ public class StreamingDrmSessionManager implements Drm * @param eventListener A listener of events. May be null if delivery of events is not required. * @throws UnsupportedDrmException If the specified DRM scheme is not supported. */ - public static StreamingDrmSessionManager newWidevineInstance( + public static DefaultDrmSessionManager newWidevineInstance( MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters, @@ -125,7 +169,7 @@ public class StreamingDrmSessionManager implements Drm * @param eventListener A listener of events. May be null if delivery of events is not required. * @throws UnsupportedDrmException If the specified DRM scheme is not supported. */ - public static StreamingDrmSessionManager newPlayReadyInstance( + public static DefaultDrmSessionManager newPlayReadyInstance( MediaDrmCallback callback, String customData, Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { HashMap optionalKeyRequestParameters; @@ -151,10 +195,10 @@ public class StreamingDrmSessionManager implements Drm * @param eventListener A listener of events. May be null if delivery of events is not required. * @throws UnsupportedDrmException If the specified DRM scheme is not supported. */ - public static StreamingDrmSessionManager newFrameworkInstance( + public static DefaultDrmSessionManager newFrameworkInstance( UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { - return new StreamingDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, + return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, optionalKeyRequestParameters, eventHandler, eventListener); } @@ -168,7 +212,7 @@ public class StreamingDrmSessionManager implements Drm * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public StreamingDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, + public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) { this.uuid = uuid; @@ -179,6 +223,7 @@ public class StreamingDrmSessionManager implements Drm this.eventListener = eventListener; mediaDrm.setOnEventListener(new MediaDrmEventListener()); state = STATE_CLOSED; + mode = MODE_PLAYBACK; } /** @@ -229,6 +274,37 @@ public class StreamingDrmSessionManager implements Drm 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. + * + *

    By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when + * required. + * + *

    {@code mode} must be one of these: + *

      + *
    • {@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is + * requested otherwise the offline license is restored. + *
    • {@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license + * is restored. + *
    • {@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is + * requested otherwise the offline license is renewed. + *
    • {@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license + * is released. + *
    + * + * @param mode The mode to be set. + * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. + */ + public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) { + Assertions.checkState(openCount == 0); + if (mode == MODE_QUERY || mode == MODE_RELEASE) { + Assertions.checkNotNull(offlineLicenseKeySetId); + } + this.mode = mode; + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + } + // DrmSessionManager implementation. @Override @@ -248,18 +324,28 @@ public class StreamingDrmSessionManager implements Drm requestHandlerThread.start(); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - schemeData = drmInitData.get(uuid); - if (schemeData == null) { - onError(new IllegalStateException("Media does not support uuid: " + uuid)); - return this; - } - if (Util.SDK_INT < 21) { - // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeData.data, C.WIDEVINE_UUID); - if (psshData == null) { - // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. - } else { - schemeData = new SchemeData(C.WIDEVINE_UUID, schemeData.mimeType, psshData); + if (offlineLicenseKeySetId == null) { + SchemeData schemeData = drmInitData.get(uuid); + if (schemeData == null) { + onError(new IllegalStateException("Media does not support uuid: " + uuid)); + return this; + } + schemeInitData = schemeData.data; + schemeMimeType = schemeData.mimeType; + if (Util.SDK_INT < 21) { + // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, C.WIDEVINE_UUID); + if (psshData == null) { + // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. + } else { + schemeInitData = psshData; + } + } + if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) + && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) + || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { + // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. + schemeMimeType = CENC_SCHEME_MIME_TYPE; } } state = STATE_OPENING; @@ -280,7 +366,8 @@ public class StreamingDrmSessionManager implements Drm postRequestHandler = null; requestHandlerThread.quit(); requestHandlerThread = null; - schemeData = null; + schemeInitData = null; + schemeMimeType = null; mediaCrypto = null; lastException = null; if (sessionId != null) { @@ -314,10 +401,25 @@ public class StreamingDrmSessionManager implements Drm } @Override - public final Exception getError() { + public final DrmSessionException getError() { return state == STATE_ERROR ? lastException : null; } + @Override + public Map queryKeyStatus() { + // User may call this method rightfully even if state == STATE_ERROR. So only check if there is + // a sessionId + if (sessionId == null) { + throw new IllegalStateException(); + } + return mediaDrm.queryKeyStatus(sessionId); + } + + @Override + public byte[] getOfflineLicenseKeySetId() { + return offlineLicenseKeySetId; + } + // Internal methods. private void openInternal(boolean allowProvisioning) { @@ -325,7 +427,7 @@ public class StreamingDrmSessionManager implements Drm sessionId = mediaDrm.openSession(); mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId); state = STATE_OPENED; - postKeyRequest(); + doLicense(); } catch (NotProvisionedException e) { if (allowProvisioning) { postProvisionRequest(); @@ -363,20 +465,86 @@ public class StreamingDrmSessionManager implements Drm if (state == STATE_OPENING) { openInternal(false); } else { - postKeyRequest(); + doLicense(); } } catch (DeniedByServerException e) { onError(e); } } - private void postKeyRequest() { - KeyRequest keyRequest; + private void doLicense() { + switch (mode) { + case MODE_PLAYBACK: + case MODE_QUERY: + if (offlineLicenseKeySetId == null) { + postKeyRequest(sessionId, MediaDrm.KEY_TYPE_STREAMING); + } else { + if (restoreKeys()) { + long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); + if (mode == MODE_PLAYBACK + && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { + Log.d(TAG, "Offline license has expired or will expire soon. " + + "Remaining seconds: " + licenseDurationRemainingSec); + postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); + } else if (licenseDurationRemainingSec <= 0) { + onError(new KeysExpiredException()); + } else { + state = STATE_OPENED_WITH_KEYS; + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmKeysRestored(); + } + }); + } + } + } + } + break; + case MODE_DOWNLOAD: + if (offlineLicenseKeySetId == null) { + postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); + } else { + // Renew + if (restoreKeys()) { + postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); + } + } + break; + case MODE_RELEASE: + if (restoreKeys()) { + postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE); + } + break; + } + } + + private boolean restoreKeys() { try { - keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType, - MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters); + mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId); + return true; + } catch (Exception e) { + Log.e(TAG, "Error trying to restore Widevine keys.", e); + onError(e); + } + return false; + } + + private long getLicenseDurationRemainingSec() { + if (!C.WIDEVINE_UUID.equals(uuid)) { + return Long.MAX_VALUE; + } + Pair pair = WidevineUtil.getLicenseDurationRemainingSec(this); + return Math.min(pair.first, pair.second); + } + + private void postKeyRequest(byte[] scope, int keyType) { + try { + KeyRequest keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType, + optionalKeyRequestParameters); postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); - } catch (NotProvisionedException e) { + } catch (Exception e) { onKeysError(e); } } @@ -393,15 +561,31 @@ public class StreamingDrmSessionManager implements Drm } try { - mediaDrm.provideKeyResponse(sessionId, (byte[]) response); - state = STATE_OPENED_WITH_KEYS; - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysLoaded(); - } - }); + if (mode == MODE_RELEASE) { + mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response); + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmKeysRemoved(); + } + }); + } + } else { + byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response); + if ((mode == MODE_DOWNLOAD || (mode == MODE_PLAYBACK && offlineLicenseKeySetId != null)) + && keySetId != null && keySetId.length != 0) { + offlineLicenseKeySetId = keySetId; + } + state = STATE_OPENED_WITH_KEYS; + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmKeysLoaded(); + } + }); + } } } catch (Exception e) { onKeysError(e); @@ -417,7 +601,7 @@ public class StreamingDrmSessionManager implements Drm } private void onError(final Exception e) { - lastException = e; + lastException = new DrmSessionException(e); if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override @@ -446,11 +630,16 @@ public class StreamingDrmSessionManager implements Drm } switch (msg.what) { case MediaDrm.EVENT_KEY_REQUIRED: - postKeyRequest(); + doLicense(); break; case MediaDrm.EVENT_KEY_EXPIRED: - state = STATE_OPENED; - onError(new KeysExpiredException()); + // When an already expired key is loaded MediaDrm sends this event immediately. Ignore + // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still + // waiting for key response. + if (state == STATE_OPENED_WITH_KEYS) { + state = STATE_OPENED; + onError(new KeysExpiredException()); + } break; case MediaDrm.EVENT_PROVISION_REQUIRED: state = STATE_OPENED; @@ -466,7 +655,9 @@ public class StreamingDrmSessionManager implements Drm @Override public void onEvent(ExoMediaDrm md, byte[] sessionId, int event, int extra, byte[] data) { - mediaDrmHandler.sendEmptyMessage(event); + if (mode == MODE_PLAYBACK) { + mediaDrmHandler.sendEmptyMessage(event); + } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/DrmSession.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/DrmSession.java index bd833e001..1064ef553 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/DrmSession.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/DrmSession.java @@ -16,9 +16,11 @@ package org.telegram.messenger.exoplayer2.drm; import android.annotation.TargetApi; +import android.media.MediaDrm; import android.support.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Map; /** * A DRM session. @@ -26,6 +28,15 @@ import java.lang.annotation.RetentionPolicy; @TargetApi(16) public interface DrmSession { + /** Wraps the exception which is the cause of the error state. */ + class DrmSessionException extends Exception { + + public DrmSessionException(Exception e) { + super(e); + } + + } + /** * The state of the DRM session. */ @@ -59,8 +70,7 @@ public interface DrmSession { * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING}, * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}. */ - @State - int getState(); + @State int getState(); /** * Returns a {@link ExoMediaCrypto} for the open session. @@ -96,6 +106,26 @@ public interface DrmSession { * * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise. */ - Exception getError(); + DrmSessionException getError(); + + /** + * Returns an informative description of the key status for the session. The status is in the form + * of {name, value} pairs. + * + *

    Since DRM license policies vary by vendor, the specific status field names are determined by + * each DRM vendor. Refer to your DRM provider documentation for definitions of the field names + * for a particular DRM engine plugin. + * + * @return A map of key status. + * @throws IllegalStateException If called when the session isn't opened. + * @see MediaDrm#queryKeyStatus(byte[]) + */ + Map queryKeyStatus(); + + /** + * Returns the key set id of the offline license loaded into this session, if there is one. Null + * otherwise. + */ + byte[] getOfflineLicenseKeySetId(); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/FrameworkMediaDrm.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/FrameworkMediaDrm.java index f3b4deb4f..82827819f 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/FrameworkMediaDrm.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/FrameworkMediaDrm.java @@ -23,6 +23,7 @@ import android.media.MediaDrm; import android.media.NotProvisionedException; import android.media.ResourceBusyException; import android.media.UnsupportedSchemeException; +import android.support.annotation.NonNull; import org.telegram.messenger.exoplayer2.util.Assertions; import java.util.HashMap; import java.util.Map; @@ -62,7 +63,8 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() { @Override - public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) { + public void onEvent(@NonNull MediaDrm md, byte[] sessionId, int event, int extra, + byte[] data) { listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data); } }); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/HttpMediaDrmCallback.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/HttpMediaDrmCallback.java index be3bdd691..7b6051ae9 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/HttpMediaDrmCallback.java @@ -24,6 +24,8 @@ import org.telegram.messenger.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import org.telegram.messenger.exoplayer2.upstream.DataSourceInputStream; import org.telegram.messenger.exoplayer2.upstream.DataSpec; import org.telegram.messenger.exoplayer2.upstream.HttpDataSource; +import org.telegram.messenger.exoplayer2.upstream.HttpDataSource.Factory; +import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; import java.util.HashMap; @@ -57,21 +59,62 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { } /** + * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request + * properties can be set by calling {@link #setKeyRequestProperty(String, String)}. * @param defaultUrl The default license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @param keyRequestProperties Request properties to set when making key requests, or null. */ + @Deprecated public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory, Map keyRequestProperties) { this.dataSourceFactory = dataSourceFactory; this.defaultUrl = defaultUrl; - this.keyRequestProperties = keyRequestProperties; + this.keyRequestProperties = new HashMap<>(); + if (keyRequestProperties != null) { + this.keyRequestProperties.putAll(keyRequestProperties); + } + } + + /** + * Sets a header for key requests made by the callback. + * + * @param name The name of the header field. + * @param value The value of the field. + */ + public void setKeyRequestProperty(String name, String value) { + Assertions.checkNotNull(name); + Assertions.checkNotNull(value); + synchronized (keyRequestProperties) { + keyRequestProperties.put(name, value); + } + } + + /** + * Clears a header for key requests made by the callback. + * + * @param name The name of the header field. + */ + public void clearKeyRequestProperty(String name) { + Assertions.checkNotNull(name); + synchronized (keyRequestProperties) { + keyRequestProperties.remove(name); + } + } + + /** + * Clears all headers for key requests made by the callback. + */ + public void clearAllKeyRequestProperties() { + synchronized (keyRequestProperties) { + keyRequestProperties.clear(); + } } @Override public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); - return executePost(url, new byte[0], null); + return executePost(dataSourceFactory, url, new byte[0], null); } @Override @@ -85,14 +128,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { if (C.PLAYREADY_UUID.equals(uuid)) { requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); } - if (keyRequestProperties != null) { + synchronized (keyRequestProperties) { requestProperties.putAll(keyRequestProperties); } - return executePost(url, request.getData(), requestProperties); + return executePost(dataSourceFactory, url, request.getData(), requestProperties); } - private byte[] executePost(String url, byte[] data, Map requestProperties) - throws IOException { + private static byte[] executePost(HttpDataSource.Factory dataSourceFactory, String url, + byte[] data, Map requestProperties) throws IOException { HttpDataSource dataSource = dataSourceFactory.createDataSource(); if (requestProperties != null) { for (Map.Entry requestProperty : requestProperties.entrySet()) { @@ -105,7 +148,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { try { return Util.toByteArray(inputStream); } finally { - inputStream.close(); + Util.closeQuietly(inputStream); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/OfflineLicenseHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/OfflineLicenseHelper.java new file mode 100755 index 000000000..1ed3d7c72 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/OfflineLicenseHelper.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.telegram.messenger.exoplayer2.drm; + +import android.media.MediaDrm; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Pair; +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.drm.DefaultDrmSessionManager.EventListener; +import org.telegram.messenger.exoplayer2.drm.DefaultDrmSessionManager.Mode; +import org.telegram.messenger.exoplayer2.drm.DrmSession.DrmSessionException; +import org.telegram.messenger.exoplayer2.upstream.HttpDataSource; +import org.telegram.messenger.exoplayer2.upstream.HttpDataSource.Factory; +import org.telegram.messenger.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.HashMap; + +/** + * Helper class to download, renew and release offline licenses. + */ +public final class OfflineLicenseHelper { + + private final ConditionVariable conditionVariable; + private final DefaultDrmSessionManager drmSessionManager; + private final HandlerThread handlerThread; + + /** + * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance + * is no longer required. + * + * @param licenseUrl The default license URL. + * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @return A new instance which uses Widevine CDM. + * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be + * instantiated. + */ + public static OfflineLicenseHelper newWidevineInstance( + String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { + return newWidevineInstance( + new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null); + } + + /** + * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance + * is no longer required. + * + * @param callback Performs key and provisioning requests. + * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument + * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * @return A new instance which uses Widevine CDM. + * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be + * instantiated. + * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, + * MediaDrmCallback, HashMap, Handler, EventListener) + */ + public static OfflineLicenseHelper newWidevineInstance( + MediaDrmCallback callback, HashMap optionalKeyRequestParameters) + throws UnsupportedDrmException { + return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback, + optionalKeyRequestParameters); + } + + /** + * Constructs an instance. Call {@link #release()} when the instance is no longer required. + * + * @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 MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, + * MediaDrmCallback, HashMap, Handler, EventListener) + */ + public OfflineLicenseHelper(ExoMediaDrm mediaDrm, MediaDrmCallback callback, + HashMap optionalKeyRequestParameters) { + handlerThread = new HandlerThread("OfflineLicenseHelper"); + handlerThread.start(); + conditionVariable = new ConditionVariable(); + EventListener eventListener = new EventListener() { + @Override + public void onDrmKeysLoaded() { + conditionVariable.open(); + } + + @Override + public void onDrmSessionManagerError(Exception e) { + conditionVariable.open(); + } + + @Override + public void onDrmKeysRestored() { + conditionVariable.open(); + } + + @Override + public void onDrmKeysRemoved() { + conditionVariable.open(); + } + }; + drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, mediaDrm, callback, + optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener); + } + + /** Releases the helper. Should be called when the helper is no longer required. */ + public void release() { + handlerThread.quit(); + } + + /** + * Downloads an offline license. + * + * @param drmInitData The {@link DrmInitData} for the content whose license is to be downloaded. + * @return The key set id for the downloaded license. + * @throws IOException If an error occurs reading data from the stream. + * @throws InterruptedException If the thread has been interrupted. + * @throws DrmSessionException Thrown when a DRM session error occurs. + */ + public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws IOException, + InterruptedException, DrmSessionException { + Assertions.checkArgument(drmInitData != null); + return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData); + } + + /** + * Renews an offline license. + * + * @param offlineLicenseKeySetId The key set id of the license to be renewed. + * @return The renewed offline license key set id. + * @throws DrmSessionException Thrown when a DRM session error occurs. + */ + public synchronized byte[] renewLicense(byte[] offlineLicenseKeySetId) + throws DrmSessionException { + Assertions.checkNotNull(offlineLicenseKeySetId); + return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, null); + } + + /** + * Releases an offline license. + * + * @param offlineLicenseKeySetId The key set id of the license to be released. + * @throws DrmSessionException Thrown when a DRM session error occurs. + */ + public synchronized void releaseLicense(byte[] offlineLicenseKeySetId) + throws DrmSessionException { + Assertions.checkNotNull(offlineLicenseKeySetId); + blockingKeyRequest(DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, null); + } + + /** + * Returns the remaining license and playback durations in seconds, for an offline license. + * + * @param offlineLicenseKeySetId The key set id of the license. + * @return The remaining license and playback durations, in seconds. + * @throws DrmSessionException Thrown when a DRM session error occurs. + */ + public synchronized Pair getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId) + throws DrmSessionException { + Assertions.checkNotNull(offlineLicenseKeySetId); + DrmSession drmSession = openBlockingKeyRequest(DefaultDrmSessionManager.MODE_QUERY, + offlineLicenseKeySetId, null); + DrmSessionException error = drmSession.getError(); + Pair licenseDurationRemainingSec = + WidevineUtil.getLicenseDurationRemainingSec(drmSession); + drmSessionManager.releaseSession(drmSession); + if (error != null) { + if (error.getCause() instanceof KeysExpiredException) { + return Pair.create(0L, 0L); + } + throw error; + } + return licenseDurationRemainingSec; + } + + private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, + DrmInitData drmInitData) throws DrmSessionException { + DrmSession drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, + drmInitData); + DrmSessionException error = drmSession.getError(); + byte[] keySetId = drmSession.getOfflineLicenseKeySetId(); + drmSessionManager.releaseSession(drmSession); + if (error != null) { + throw error; + } + return keySetId; + } + + private DrmSession openBlockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, + DrmInitData drmInitData) { + drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId); + conditionVariable.close(); + DrmSession drmSession = drmSessionManager.acquireSession(handlerThread.getLooper(), + drmInitData); + // Block current thread until key loading is finished + conditionVariable.block(); + return drmSession; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/UnsupportedDrmException.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/UnsupportedDrmException.java index 4c20d7fdc..6c8c66fad 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/UnsupportedDrmException.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/UnsupportedDrmException.java @@ -43,8 +43,7 @@ public final class UnsupportedDrmException extends Exception { /** * Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}. */ - @Reason - public final int reason; + @Reason public final int reason; /** * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/WidevineUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/WidevineUtil.java new file mode 100755 index 000000000..b1e11dbb0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/drm/WidevineUtil.java @@ -0,0 +1,62 @@ +/* + * 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 org.telegram.messenger.exoplayer2.drm; + +import android.util.Pair; +import org.telegram.messenger.exoplayer2.C; +import java.util.Map; + +/** + * Utility methods for Widevine. + */ +public final class WidevineUtil { + + /** Widevine specific key status field name for the remaining license duration, in seconds. */ + public static final String PROPERTY_LICENSE_DURATION_REMAINING = "LicenseDurationRemaining"; + /** Widevine specific key status field name for the remaining playback duration, in seconds. */ + public static final String PROPERTY_PLAYBACK_DURATION_REMAINING = "PlaybackDurationRemaining"; + + private WidevineUtil() {} + + /** + * Returns license and playback durations remaining in seconds. + * + * @return A {@link Pair} consisting of the remaining license and playback durations in seconds. + * @throws IllegalStateException If called when a session isn't opened. + * @param drmSession + */ + public static Pair getLicenseDurationRemainingSec(DrmSession drmSession) { + Map keyStatus = drmSession.queryKeyStatus(); + return new Pair<>( + getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING), + getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING)); + } + + private static long getDurationRemainingSec(Map keyStatus, String property) { + if (keyStatus != null) { + try { + String value = keyStatus.get(property); + if (value != null) { + return Long.parseLong(value); + } + } catch (NumberFormatException e) { + // do nothing. + } + } + return C.TIME_UNSET; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ChunkIndex.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ChunkIndex.java index 6502a5fd5..3d9cef3bf 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ChunkIndex.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ChunkIndex.java @@ -61,7 +61,11 @@ public final class ChunkIndex implements SeekMap { this.durationsUs = durationsUs; this.timesUs = timesUs; length = sizes.length; - durationUs = durationsUs[length - 1] + timesUs[length - 1]; + if (length > 0) { + durationUs = durationsUs[length - 1] + timesUs[length - 1]; + } else { + durationUs = 0; + } } /** diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultExtractorInput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultExtractorInput.java index d495cde17..34b640c80 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultExtractorInput.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultExtractorInput.java @@ -18,6 +18,7 @@ package org.telegram.messenger.exoplayer2.extractor; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.upstream.DataSource; import org.telegram.messenger.exoplayer2.util.Assertions; +import org.telegram.messenger.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.util.Arrays; @@ -27,6 +28,8 @@ import java.util.Arrays; */ public final class DefaultExtractorInput implements ExtractorInput { + private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024; + private static final int PEEK_MAX_FREE_SPACE = 512 * 1024; private static final byte[] SCRATCH_SPACE = new byte[4096]; private final DataSource dataSource; @@ -46,7 +49,7 @@ public final class DefaultExtractorInput implements ExtractorInput { this.dataSource = dataSource; this.position = position; this.streamLength = length; - peekBuffer = new byte[8 * 1024]; + peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE]; } @Override @@ -176,7 +179,9 @@ public final class DefaultExtractorInput implements ExtractorInput { private void ensureSpaceForPeek(int length) { int requiredLength = peekBufferPosition + length; if (requiredLength > peekBuffer.length) { - peekBuffer = Arrays.copyOf(peekBuffer, Math.max(peekBuffer.length * 2, requiredLength)); + int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2, + requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE); + peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity); } } @@ -218,7 +223,12 @@ public final class DefaultExtractorInput implements ExtractorInput { private void updatePeekBuffer(int bytesConsumed) { peekBufferLength -= bytesConsumed; peekBufferPosition = 0; - System.arraycopy(peekBuffer, bytesConsumed, peekBuffer, 0, peekBufferLength); + byte[] newPeekBuffer = peekBuffer; + if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) { + newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE]; + } + System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength); + peekBuffer = newPeekBuffer; } /** diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultExtractorsFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultExtractorsFactory.java index 5709efb9e..162c2d57e 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -15,141 +15,132 @@ */ package org.telegram.messenger.exoplayer2.extractor; -import java.util.ArrayList; -import java.util.List; +import org.telegram.messenger.exoplayer2.extractor.flv.FlvExtractor; +import org.telegram.messenger.exoplayer2.extractor.mkv.MatroskaExtractor; +import org.telegram.messenger.exoplayer2.extractor.mp3.Mp3Extractor; +import org.telegram.messenger.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import org.telegram.messenger.exoplayer2.extractor.mp4.Mp4Extractor; +import org.telegram.messenger.exoplayer2.extractor.ogg.OggExtractor; +import org.telegram.messenger.exoplayer2.extractor.ts.Ac3Extractor; +import org.telegram.messenger.exoplayer2.extractor.ts.AdtsExtractor; +import org.telegram.messenger.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; +import org.telegram.messenger.exoplayer2.extractor.ts.PsExtractor; +import org.telegram.messenger.exoplayer2.extractor.ts.TsExtractor; +import org.telegram.messenger.exoplayer2.extractor.wav.WavExtractor; +import java.lang.reflect.Constructor; /** * An {@link ExtractorsFactory} that provides an array of extractors for the following formats: * *

      - *
    • MP4, including M4A ({@link org.telegram.messenger.exoplayer2.extractor.mp4.Mp4Extractor})
    • - *
    • fMP4 ({@link org.telegram.messenger.exoplayer2.extractor.mp4.FragmentedMp4Extractor})
    • - *
    • Matroska and WebM ({@link org.telegram.messenger.exoplayer2.extractor.mkv.MatroskaExtractor}) - *
    • - *
    • Ogg Vorbis/FLAC ({@link org.telegram.messenger.exoplayer2.extractor.ogg.OggExtractor}
    • - *
    • MP3 ({@link org.telegram.messenger.exoplayer2.extractor.mp3.Mp3Extractor})
    • - *
    • AAC ({@link org.telegram.messenger.exoplayer2.extractor.ts.AdtsExtractor})
    • - *
    • MPEG TS ({@link org.telegram.messenger.exoplayer2.extractor.ts.TsExtractor})
    • - *
    • MPEG PS ({@link org.telegram.messenger.exoplayer2.extractor.ts.PsExtractor})
    • - *
    • FLV ({@link org.telegram.messenger.exoplayer2.extractor.flv.FlvExtractor})
    • - *
    • WAV ({@link org.telegram.messenger.exoplayer2.extractor.wav.WavExtractor})
    • + *
    • MP4, including M4A ({@link Mp4Extractor})
    • + *
    • fMP4 ({@link FragmentedMp4Extractor})
    • + *
    • Matroska and WebM ({@link MatroskaExtractor})
    • + *
    • Ogg Vorbis/FLAC ({@link OggExtractor}
    • + *
    • MP3 ({@link Mp3Extractor})
    • + *
    • AAC ({@link AdtsExtractor})
    • + *
    • MPEG TS ({@link TsExtractor})
    • + *
    • MPEG PS ({@link PsExtractor})
    • + *
    • FLV ({@link FlvExtractor})
    • + *
    • WAV ({@link WavExtractor})
    • + *
    • AC3 ({@link Ac3Extractor})
    • *
    • FLAC (only available if the FLAC extension is built and included)
    • *
    */ public final class DefaultExtractorsFactory implements ExtractorsFactory { - // Lazily initialized default extractor classes in priority order. - private static List> defaultExtractorClasses; + private static final Constructor FLAC_EXTRACTOR_CONSTRUCTOR; + static { + Constructor flacExtractorConstructor = null; + try { + flacExtractorConstructor = + Class.forName("org.telegram.messenger.exoplayer2.ext.flac.FlacExtractor") + .asSubclass(Extractor.class).getConstructor(); + } catch (ClassNotFoundException e) { + // Extractor not found. + } catch (NoSuchMethodException e) { + // Constructor not found. + } + FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor; + } + + private @MatroskaExtractor.Flags int matroskaFlags; + private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags; + private @Mp3Extractor.Flags int mp3Flags; + private @DefaultTsPayloadReaderFactory.Flags int tsFlags; /** - * Creates a new factory for the default extractors. + * Sets flags for {@link MatroskaExtractor} instances created by the factory. + * + * @see MatroskaExtractor#MatroskaExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. */ - public DefaultExtractorsFactory() { - synchronized (DefaultExtractorsFactory.class) { - if (defaultExtractorClasses == null) { - // Lazily initialize defaultExtractorClasses. - List> extractorClasses = new ArrayList<>(); - // We reference extractors using reflection so that they can be deleted cleanly. - // Class.forName is used so that automated tools like proguard can detect the use of - // reflection (see http://proguard.sourceforge.net/FAQ.html#forname). - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.extractor.mkv.MatroskaExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.extractor.mp4.FragmentedMp4Extractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.extractor.mp4.Mp4Extractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.extractor.mp3.Mp3Extractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.extractor.ts.AdtsExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.extractor.ts.Ac3Extractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.extractor.ts.TsExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.extractor.flv.FlvExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.extractor.ogg.OggExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.extractor.ts.PsExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.extractor.wav.WavExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - try { - extractorClasses.add( - Class.forName("org.telegram.messenger.exoplayer2.ext.flac.FlacExtractor") - .asSubclass(Extractor.class)); - } catch (ClassNotFoundException e) { - // Extractor not found. - } - defaultExtractorClasses = extractorClasses; - } - } + public synchronized DefaultExtractorsFactory setMatroskaExtractorFlags( + @MatroskaExtractor.Flags int flags) { + this.matroskaFlags = flags; + return this; + } + + /** + * Sets flags for {@link FragmentedMp4Extractor} instances created by the factory. + * + * @see FragmentedMp4Extractor#FragmentedMp4Extractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setFragmentedMp4ExtractorFlags( + @FragmentedMp4Extractor.Flags int flags) { + this.fragmentedMp4Flags = flags; + return this; + } + + /** + * Sets flags for {@link Mp3Extractor} instances created by the factory. + * + * @see Mp3Extractor#Mp3Extractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setMp3ExtractorFlags(@Mp3Extractor.Flags int flags) { + mp3Flags = flags; + return this; + } + + /** + * Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances + * created by the factory. + * + * @see TsExtractor#TsExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setTsExtractorFlags( + @DefaultTsPayloadReaderFactory.Flags int flags) { + tsFlags = flags; + return this; } @Override - public Extractor[] createExtractors() { - Extractor[] extractors = new Extractor[defaultExtractorClasses.size()]; - for (int i = 0; i < extractors.length; i++) { + public synchronized Extractor[] createExtractors() { + Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 11 : 12]; + extractors[0] = new MatroskaExtractor(matroskaFlags); + extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags); + extractors[2] = new Mp4Extractor(); + extractors[3] = new Mp3Extractor(mp3Flags); + extractors[4] = new AdtsExtractor(); + extractors[5] = new Ac3Extractor(); + extractors[6] = new TsExtractor(tsFlags); + extractors[7] = new FlvExtractor(); + extractors[8] = new OggExtractor(); + extractors[9] = new PsExtractor(); + extractors[10] = new WavExtractor(); + if (FLAC_EXTRACTOR_CONSTRUCTOR != null) { try { - extractors[i] = defaultExtractorClasses.get(i).getConstructor().newInstance(); + extractors[11] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance(); } catch (Exception e) { // Should never happen. - throw new IllegalStateException("Unexpected error creating default extractor", e); + throw new IllegalStateException("Unexpected error creating FLAC extractor", e); } } return extractors; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultTrackOutput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultTrackOutput.java index ec2eb222c..1359ce4da 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultTrackOutput.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/DefaultTrackOutput.java @@ -70,11 +70,12 @@ public final class DefaultTrackOutput implements TrackOutput { 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 Allocation lastAllocation; private int lastAllocationOffset; - private boolean needKeyframe; private boolean pendingSplice; private UpstreamFormatChangedListener upstreamFormatChangeListener; @@ -90,7 +91,6 @@ public final class DefaultTrackOutput implements TrackOutput { scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); state = new AtomicInteger(); lastAllocationOffset = allocationLength; - needKeyframe = true; } // Called by the consuming thread, but only when there is no loading thread. @@ -226,13 +226,28 @@ public final class DefaultTrackOutput implements TrackOutput { } /** - * Attempts to skip to the keyframe before the specified time, if it's present in the buffer. + * Skips all samples currently in the buffer. + */ + public void skipAll() { + long nextOffset = infoQueue.skipAll(); + if (nextOffset != C.POSITION_UNSET) { + dropDownstreamTo(nextOffset); + } + } + + /** + * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer + * contains a keyframe with a timestamp of {@code timeUs} or earlier. If + * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} + * falls within the buffer. * * @param timeUs The seek time. + * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end + * of the buffer. * @return Whether the skip was successful. */ - public boolean skipToKeyframeBefore(long timeUs) { - long nextOffset = infoQueue.skipToKeyframeBefore(timeUs); + public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { + long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer); if (nextOffset == C.POSITION_UNSET) { return false; } @@ -247,38 +262,41 @@ public final class DefaultTrackOutput implements TrackOutput { * @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. + * @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 decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will * be set if the buffer's timestamp is less than this value. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean loadingFinished, - long decodeOnlyUntilUs) { - switch (infoQueue.readData(formatHolder, buffer, downstreamFormat, extrasHolder)) { - case C.RESULT_NOTHING_READ: - if (loadingFinished) { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } - return C.RESULT_NOTHING_READ; + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, + boolean loadingFinished, long decodeOnlyUntilUs) { + int result = infoQueue.readData(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.timeUs < decodeOnlyUntilUs) { - buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + if (!buffer.isEndOfStream()) { + if (buffer.timeUs < decodeOnlyUntilUs) { + buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + } + // 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); + // Advance the read head. + dropDownstreamTo(extrasHolder.nextOffset); } - // 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); - // Advance the read head. - dropDownstreamTo(extrasHolder.nextOffset); return C.RESULT_BUFFER_READ; + case C.RESULT_NOTHING_READ: + return C.RESULT_NOTHING_READ; default: throw new IllegalStateException(); } @@ -425,23 +443,24 @@ public final class DefaultTrackOutput implements TrackOutput { } /** - * Like {@link #format(Format)}, but with an offset that will be added to the timestamps of - * samples subsequently queued to the buffer. The offset is also used to adjust - * {@link Format#subsampleOffsetUs} for both the {@link Format} passed and those subsequently - * passed to {@link #format(Format)}. + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples + * subsequently queued to the buffer. * - * @param format The format. * @param sampleOffsetUs The timestamp offset in microseconds. */ - public void formatWithOffset(Format format, long sampleOffsetUs) { - this.sampleOffsetUs = sampleOffsetUs; - format(format); + public void setSampleOffsetUs(long sampleOffsetUs) { + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + pendingFormatAdjustment = true; + } } @Override public void format(Format format) { Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); boolean formatChanged = infoQueue.format(adjustedFormat); + lastUnadjustedFormat = format; + pendingFormatAdjustment = false; if (upstreamFormatChangeListener != null && formatChanged) { upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); } @@ -498,6 +517,9 @@ public final class DefaultTrackOutput implements TrackOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, byte[] encryptionKey) { + if (pendingFormatAdjustment) { + format(lastUnadjustedFormat); + } if (!startWriteOperation()) { infoQueue.commitSampleTimestamp(timeUs); return; @@ -509,12 +531,6 @@ public final class DefaultTrackOutput implements TrackOutput { } pendingSplice = false; } - if (needKeyframe) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) { - return; - } - needKeyframe = false; - } timeUs += sampleOffsetUs; long absoluteOffset = totalBytesWritten - size - offset; infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey); @@ -544,7 +560,6 @@ public final class DefaultTrackOutput implements TrackOutput { totalBytesWritten = 0; lastAllocation = null; lastAllocationOffset = allocationLength; - needKeyframe = true; } /** @@ -601,6 +616,7 @@ public final class DefaultTrackOutput implements TrackOutput { private long largestDequeuedTimestampUs; private long largestQueuedTimestampUs; + private boolean upstreamKeyframeRequired; private boolean upstreamFormatRequired; private Format upstreamFormat; private int upstreamSourceId; @@ -617,6 +633,7 @@ public final class DefaultTrackOutput implements TrackOutput { largestDequeuedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; upstreamFormatRequired = true; + upstreamKeyframeRequired = true; } public void clearSampleData() { @@ -624,6 +641,7 @@ public final class DefaultTrackOutput implements TrackOutput { relativeReadIndex = 0; relativeWriteIndex = 0; queueSize = 0; + upstreamKeyframeRequired = true; } // Called by the consuming thread, but only when there is no loading thread. @@ -732,28 +750,44 @@ public final class DefaultTrackOutput implements TrackOutput { * 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. + * sample has been read. 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 readData(FormatHolder formatHolder, DecoderInputBuffer buffer, - Format downstreamFormat, BufferExtrasHolder extrasHolder) { + boolean formatRequired, boolean loadingFinished, Format downstreamFormat, + BufferExtrasHolder extrasHolder) { if (queueSize == 0) { - if (upstreamFormat != null && upstreamFormat != downstreamFormat) { + if (loadingFinished) { + 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; } - return C.RESULT_NOTHING_READ; } - if (formats[relativeReadIndex] != downstreamFormat) { + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { formatHolder.format = formats[relativeReadIndex]; return C.RESULT_FORMAT_READ; } + if (buffer.isFlagsOnly()) { + return C.RESULT_NOTHING_READ; + } + buffer.timeUs = timesUs[relativeReadIndex]; buffer.setFlags(flags[relativeReadIndex]); extrasHolder.size = sizes[relativeReadIndex]; @@ -775,20 +809,40 @@ public final class DefaultTrackOutput implements TrackOutput { } /** - * Attempts to locate the keyframe before the specified time, if it's present in the buffer. + * Skips all samples in the buffer. + * + * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no + * dropping of data is required. + */ + public synchronized long skipAll() { + if (queueSize == 0) { + return C.POSITION_UNSET; + } + + int lastSampleIndex = (relativeReadIndex + queueSize - 1) % capacity; + relativeReadIndex = (relativeReadIndex + queueSize) % capacity; + absoluteReadIndex += queueSize; + queueSize = 0; + return offsets[lastSampleIndex] + sizes[lastSampleIndex]; + } + + /** + * Attempts to locate the keyframe before or at the specified time. If + * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} + * falls within the buffer. * * @param timeUs The seek time. + * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end + * of the buffer. * @return The offset of the keyframe's data if the keyframe was present. * {@link C#POSITION_UNSET} otherwise. */ - public synchronized long skipToKeyframeBefore(long timeUs) { + public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) { return C.POSITION_UNSET; } - int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1; - long lastTimeUs = timesUs[lastWriteIndex]; - if (timeUs > lastTimeUs) { + if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) { return C.POSITION_UNSET; } @@ -814,9 +868,9 @@ public final class DefaultTrackOutput implements TrackOutput { return C.POSITION_UNSET; } - queueSize -= sampleCountToKeyframe; relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity; absoluteReadIndex += sampleCountToKeyframe; + queueSize -= sampleCountToKeyframe; return offsets[relativeReadIndex]; } @@ -839,6 +893,12 @@ public final class DefaultTrackOutput implements TrackOutput { public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, int size, byte[] encryptionKey) { + if (upstreamKeyframeRequired) { + if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + return; + } + upstreamKeyframeRequired = false; + } Assertions.checkState(!upstreamFormatRequired); commitSampleTimestamp(timeUs); timesUs[relativeWriteIndex] = timeUs; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/Extractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/Extractor.java index 615e4d0aa..8e5c6bd06 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/Extractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/Extractor.java @@ -93,12 +93,14 @@ public interface Extractor { * position} in the stream. Valid random access positions are the start of the stream and * positions that can be obtained from any {@link SeekMap} passed to the {@link ExtractorOutput}. * - * @param position The seek position. + * @param position The byte offset in the stream from which data will be provided. + * @param timeUs The seek time in microseconds. */ - void seek(long position); + void seek(long position, long timeUs); /** * Releases all kept resources. */ void release(); + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ExtractorOutput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ExtractorOutput.java index 89f935dbd..c266fa239 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ExtractorOutput.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ExtractorOutput.java @@ -23,17 +23,18 @@ public interface ExtractorOutput { /** * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track. *

    - * The same {@link TrackOutput} is returned if multiple calls are made with the same - * {@code trackId}. + * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. * - * @param trackId A track identifier. + * @param id A track identifier. + * @param type The type of the track. Typically one of the {@link org.telegram.messenger.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. * @return The {@link TrackOutput} for the given track identifier. */ - TrackOutput track(int trackId); + TrackOutput track(int id, int type); /** * Called when all tracks have been identified, meaning no new {@code trackId} values will be - * passed to {@link #track(int)}. + * passed to {@link #track(int, int)}. */ void endTracks(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/GaplessInfoHolder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/GaplessInfoHolder.java index dd6dead24..7a0fa45b7 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/GaplessInfoHolder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/GaplessInfoHolder.java @@ -18,6 +18,7 @@ package org.telegram.messenger.exoplayer2.extractor; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.metadata.Metadata; import org.telegram.messenger.exoplayer2.metadata.id3.CommentFrame; +import org.telegram.messenger.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,6 +27,18 @@ import java.util.regex.Pattern; */ public final class GaplessInfoHolder { + /** + * A {@link FramePredicate} suitable for use when decoding {@link Metadata} that will be passed + * to {@link #setFromMetadata(Metadata)}. Only frames that might contain gapless playback + * information are decoded. + */ + public static final FramePredicate GAPLESS_INFO_ID3_FRAME_PREDICATE = new FramePredicate() { + @Override + public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) { + return id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2); + } + }; + private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/AudioTagPayloadReader.java index 8f764a7db..0637c9f24 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -29,21 +29,20 @@ import java.util.Collections; */ /* package */ final class AudioTagPayloadReader extends TagPayloadReader { - // Audio format + private static final int AUDIO_FORMAT_MP3 = 2; + private static final int AUDIO_FORMAT_ALAW = 7; + private static final int AUDIO_FORMAT_ULAW = 8; private static final int AUDIO_FORMAT_AAC = 10; - // AAC PACKET TYPE private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0; private static final int AAC_PACKET_TYPE_AAC_RAW = 1; - // SAMPLING RATES - private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] { - 5500, 11000, 22000, 44000 - }; + private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] {5512, 11025, 22050, 44100}; // State variables private boolean hasParsedAudioDataHeader; private boolean hasOutputFormat; + private int audioFormat; public AudioTagPayloadReader(TrackOutput output) { super(output); @@ -58,13 +57,23 @@ import java.util.Collections; protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException { if (!hasParsedAudioDataHeader) { int header = data.readUnsignedByte(); - int audioFormat = (header >> 4) & 0x0F; - int sampleRateIndex = (header >> 2) & 0x03; - if (sampleRateIndex < 0 || sampleRateIndex >= AUDIO_SAMPLING_RATE_TABLE.length) { - throw new UnsupportedFormatException("Invalid sample rate index: " + sampleRateIndex); - } - // TODO: Add support for MP3 and PCM. - if (audioFormat != AUDIO_FORMAT_AAC) { + audioFormat = (header >> 4) & 0x0F; + if (audioFormat == AUDIO_FORMAT_MP3) { + int sampleRateIndex = (header >> 2) & 0x03; + int sampleRate = AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex]; + Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_MPEG, null, + Format.NO_VALUE, Format.NO_VALUE, 1, sampleRate, null, null, 0, null); + output.format(format); + hasOutputFormat = true; + } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { + String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW + : MimeTypes.AUDIO_ULAW; + 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); + output.format(format); + hasOutputFormat = true; + } else if (audioFormat != AUDIO_FORMAT_AAC) { throw new UnsupportedFormatException("Audio format not supported: " + audioFormat); } hasParsedAudioDataHeader = true; @@ -77,23 +86,28 @@ import java.util.Collections; @Override protected void parsePayload(ParsableByteArray data, long timeUs) { - int packetType = data.readUnsignedByte(); - // Parse sequence header just in case it was not done before. - if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { - byte[] audioSpecifiConfig = new byte[data.bytesLeft()]; - data.readBytes(audioSpecifiConfig, 0, audioSpecifiConfig.length); - Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( - audioSpecifiConfig); - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, - Collections.singletonList(audioSpecifiConfig), null, 0, null); - output.format(format); - hasOutputFormat = true; - } else if (packetType == AAC_PACKET_TYPE_AAC_RAW) { - // Sample audio AAC frames - int bytesToWrite = data.bytesLeft(); - output.sampleData(data, bytesToWrite); - output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, bytesToWrite, 0, null); + if (audioFormat == AUDIO_FORMAT_MP3) { + int sampleSize = data.bytesLeft(); + output.sampleData(data, sampleSize); + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + } else { + int packetType = data.readUnsignedByte(); + if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { + // Parse the sequence header. + byte[] audioSpecificConfig = new byte[data.bytesLeft()]; + data.readBytes(audioSpecificConfig, 0, audioSpecificConfig.length); + Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( + audioSpecificConfig); + Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, + Collections.singletonList(audioSpecificConfig), null, 0, null); + output.format(format); + hasOutputFormat = true; + } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) { + int sampleSize = data.bytesLeft(); + output.sampleData(data, sampleSize); + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/FlvExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/FlvExtractor.java index 0884b324c..142e57f21 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/FlvExtractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/FlvExtractor.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.extractor.flv; +import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.extractor.Extractor; import org.telegram.messenger.exoplayer2.extractor.ExtractorInput; import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; @@ -126,7 +127,7 @@ public final class FlvExtractor implements Extractor, SeekMap { } @Override - public void seek(long position) { + public void seek(long position, long timeUs) { parserState = STATE_READING_FLV_HEADER; bytesToNextTagHeader = 0; } @@ -183,10 +184,12 @@ public final class FlvExtractor implements Extractor, SeekMap { boolean hasAudio = (flags & 0x04) != 0; boolean hasVideo = (flags & 0x01) != 0; if (hasAudio && audioReader == null) { - audioReader = new AudioTagPayloadReader(extractorOutput.track(TAG_TYPE_AUDIO)); + audioReader = new AudioTagPayloadReader( + extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO)); } if (hasVideo && videoReader == null) { - videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO)); + videoReader = new VideoTagPayloadReader( + extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); } if (metadataReader == null) { metadataReader = new ScriptTagPayloadReader(null); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index 6cb9c1307..b2fcf3391 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -80,8 +80,8 @@ import java.util.Map; } int type = readAmfType(data); if (type != AMF_TYPE_ECMA_ARRAY) { - // Should never happen. - throw new ParserException(); + // We're not interested in this metadata. + return; } // Set the duration to the value contained in the metadata, if present. Map metadata = readAmfEcmaArray(data); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/VideoTagPayloadReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/VideoTagPayloadReader.java index da931de90..f6c0835b5 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/VideoTagPayloadReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/flv/VideoTagPayloadReader.java @@ -93,7 +93,7 @@ import org.telegram.messenger.exoplayer2.video.AvcConfig; avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null); output.format(format); hasOutputFormat = true; - } else if (packetType == AVC_PACKET_TYPE_AVC_NALU) { + } else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) { // TODO: Deduplicate with Mp4Extractor. // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mkv/MatroskaExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mkv/MatroskaExtractor.java index 3e096d673..7ee1194b3 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.extractor.mkv; +import android.support.annotation.IntDef; import android.util.SparseArray; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; @@ -26,6 +27,7 @@ import org.telegram.messenger.exoplayer2.extractor.Extractor; import org.telegram.messenger.exoplayer2.extractor.ExtractorInput; import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; import org.telegram.messenger.exoplayer2.extractor.ExtractorsFactory; +import org.telegram.messenger.exoplayer2.extractor.MpegAudioHeader; import org.telegram.messenger.exoplayer2.extractor.PositionHolder; import org.telegram.messenger.exoplayer2.extractor.SeekMap; import org.telegram.messenger.exoplayer2.extractor.TrackOutput; @@ -35,8 +37,11 @@ import org.telegram.messenger.exoplayer2.util.NalUnitUtil; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; import org.telegram.messenger.exoplayer2.util.Util; import org.telegram.messenger.exoplayer2.video.AvcConfig; +import org.telegram.messenger.exoplayer2.video.ColorInfo; import org.telegram.messenger.exoplayer2.video.HevcConfig; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; @@ -63,6 +68,22 @@ public final class MatroskaExtractor implements Extractor { }; + /** + * Flags controlling the behavior of the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {FLAG_DISABLE_SEEK_FOR_CUES}) + public @interface Flags {} + /** + * Flag to disable seeking for cues. + *

    + * Normally (i.e. when this flag is not set) the extractor will seek to the cues element if its + * position is specified in the seek head and if it's after the first cluster. Setting this flag + * disables seeking to the cues element. If the cues element is after the first cluster then the + * media is treated as being unseekable. + */ + public static final int FLAG_DISABLE_SEEK_FOR_CUES = 1; + private static final int UNSET_ENTRY_ID = -1; private static final int BLOCK_STATE_START = 0; @@ -84,6 +105,7 @@ public final class MatroskaExtractor implements Extractor { private static final String CODEC_ID_VORBIS = "A_VORBIS"; private static final String CODEC_ID_OPUS = "A_OPUS"; private static final String CODEC_ID_AAC = "A_AAC"; + private static final String CODEC_ID_MP2 = "A_MPEG/L2"; private static final String CODEC_ID_MP3 = "A_MPEG/L3"; private static final String CODEC_ID_AC3 = "A_AC3"; private static final String CODEC_ID_E_AC3 = "A_EAC3"; @@ -97,10 +119,10 @@ public final class MatroskaExtractor implements Extractor { private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8"; private static final String CODEC_ID_VOBSUB = "S_VOBSUB"; private static final String CODEC_ID_PGS = "S_HDMV/PGS"; + private static final String CODEC_ID_DVBSUB = "S_DVBSUB"; private static final int VORBIS_MAX_INPUT_SIZE = 8192; private static final int OPUS_MAX_INPUT_SIZE = 5760; - private static final int MP3_MAX_INPUT_SIZE = 4096; private static final int ENCRYPTION_IV_SIZE = 8; private static final int TRACK_TYPE_AUDIO = 2; @@ -166,6 +188,23 @@ public final class MatroskaExtractor implements Extractor { private static final int ID_PROJECTION = 0x7670; private static final int ID_PROJECTION_PRIVATE = 0x7672; private static final int ID_STEREO_MODE = 0x53B8; + private static final int ID_COLOUR = 0x55B0; + private static final int ID_COLOUR_RANGE = 0x55B9; + private static final int ID_COLOUR_TRANSFER = 0x55BA; + private static final int ID_COLOUR_PRIMARIES = 0x55BB; + private static final int ID_MAX_CLL = 0x55BC; + private static final int ID_MAX_FALL = 0x55BD; + private static final int ID_MASTERING_METADATA = 0x55D0; + private static final int ID_PRIMARY_R_CHROMATICITY_X = 0x55D1; + private static final int ID_PRIMARY_R_CHROMATICITY_Y = 0x55D2; + private static final int ID_PRIMARY_G_CHROMATICITY_X = 0x55D3; + private static final int ID_PRIMARY_G_CHROMATICITY_Y = 0x55D4; + private static final int ID_PRIMARY_B_CHROMATICITY_X = 0x55D5; + private static final int ID_PRIMARY_B_CHROMATICITY_Y = 0x55D6; + private static final int ID_WHITE_POINT_CHROMATICITY_X = 0x55D7; + private static final int ID_WHITE_POINT_CHROMATICITY_Y = 0x55D8; + private static final int ID_LUMNINANCE_MAX = 0x55D9; + private static final int ID_LUMNINANCE_MIN = 0x55DA; private static final int LACING_NONE = 0; private static final int LACING_XIPH = 1; @@ -220,6 +259,7 @@ public final class MatroskaExtractor implements Extractor { private final EbmlReader reader; private final VarintReader varintReader; private final SparseArray tracks; + private final boolean seekForCuesEnabled; // Temporary arrays. private final ParsableByteArray nalStartCode; @@ -287,12 +327,17 @@ public final class MatroskaExtractor implements Extractor { private ExtractorOutput extractorOutput; public MatroskaExtractor() { - this(new DefaultEbmlReader()); + this(0); } - /* package */ MatroskaExtractor(EbmlReader reader) { + public MatroskaExtractor(@Flags int flags) { + this(new DefaultEbmlReader(), flags); + } + + /* package */ MatroskaExtractor(EbmlReader reader, @Flags int flags) { this.reader = reader; this.reader.init(new InnerEbmlReaderOutput()); + seekForCuesEnabled = (flags & FLAG_DISABLE_SEEK_FOR_CUES) == 0; varintReader = new VarintReader(); tracks = new SparseArray<>(); scratch = new ParsableByteArray(4); @@ -317,7 +362,7 @@ public final class MatroskaExtractor implements Extractor { } @Override - public void seek(long position) { + public void seek(long position, long timeUs) { clusterTimecodeUs = C.TIME_UNSET; blockState = BLOCK_STATE_START; reader.reset(); @@ -366,6 +411,8 @@ public final class MatroskaExtractor implements Extractor { case ID_CUE_TRACK_POSITIONS: case ID_BLOCK_GROUP: case ID_PROJECTION: + case ID_COLOUR: + case ID_MASTERING_METADATA: return EbmlReader.TYPE_MASTER; case ID_EBML_READ_VERSION: case ID_DOC_TYPE_READ_VERSION: @@ -396,6 +443,11 @@ public final class MatroskaExtractor implements Extractor { case ID_CUE_CLUSTER_POSITION: case ID_REFERENCE_BLOCK: case ID_STEREO_MODE: + case ID_COLOUR_RANGE: + case ID_COLOUR_TRANSFER: + case ID_COLOUR_PRIMARIES: + case ID_MAX_CLL: + case ID_MAX_FALL: return EbmlReader.TYPE_UNSIGNED_INT; case ID_DOC_TYPE: case ID_CODEC_ID: @@ -411,6 +463,16 @@ public final class MatroskaExtractor implements Extractor { return EbmlReader.TYPE_BINARY; case ID_DURATION: case ID_SAMPLING_FREQUENCY: + case ID_PRIMARY_R_CHROMATICITY_X: + case ID_PRIMARY_R_CHROMATICITY_Y: + case ID_PRIMARY_G_CHROMATICITY_X: + case ID_PRIMARY_G_CHROMATICITY_Y: + case ID_PRIMARY_B_CHROMATICITY_X: + case ID_PRIMARY_B_CHROMATICITY_Y: + case ID_WHITE_POINT_CHROMATICITY_X: + case ID_WHITE_POINT_CHROMATICITY_Y: + case ID_LUMNINANCE_MAX: + case ID_LUMNINANCE_MIN: return EbmlReader.TYPE_FLOAT; default: return EbmlReader.TYPE_UNKNOWN; @@ -446,7 +508,7 @@ public final class MatroskaExtractor implements Extractor { case ID_CLUSTER: if (!sentSeekMap) { // We need to build cues before parsing the cluster. - if (cuesContentPosition != C.POSITION_UNSET) { + if (seekForCuesEnabled && cuesContentPosition != C.POSITION_UNSET) { // We know where the Cues element is located. Seek to request it. seekForCues = true; } else { @@ -469,6 +531,9 @@ public final class MatroskaExtractor implements Extractor { case ID_TRACK_ENTRY: currentTrack = new Track(); break; + case ID_MASTERING_METADATA: + currentTrack.hasColorInfo = true; + break; default: break; } @@ -528,11 +593,9 @@ public final class MatroskaExtractor implements Extractor { } break; case ID_TRACK_ENTRY: - if (tracks.get(currentTrack.number) == null && isCodecSupported(currentTrack.codecId)) { + if (isCodecSupported(currentTrack.codecId)) { currentTrack.initializeOutput(extractorOutput, currentTrack.number); tracks.put(currentTrack.number, currentTrack); - } else { - // We've seen this track entry before, or the codec is unsupported. Do nothing. } currentTrack = null; break; @@ -674,10 +737,67 @@ public final class MatroskaExtractor implements Extractor { case 3: currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM; break; + case 15: + currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH; + break; default: break; } break; + case ID_COLOUR_PRIMARIES: + currentTrack.hasColorInfo = true; + switch ((int) value) { + case 1: + currentTrack.colorSpace = C.COLOR_SPACE_BT709; + break; + case 4: // BT.470M. + case 5: // BT.470BG. + case 6: // SMPTE 170M. + case 7: // SMPTE 240M. + currentTrack.colorSpace = C.COLOR_SPACE_BT601; + break; + case 9: + currentTrack.colorSpace = C.COLOR_SPACE_BT2020; + break; + default: + break; + } + break; + case ID_COLOUR_TRANSFER: + switch ((int) value) { + case 1: // BT.709. + case 6: // SMPTE 170M. + case 7: // SMPTE 240M. + currentTrack.colorTransfer = C.COLOR_TRANSFER_SDR; + break; + case 16: + currentTrack.colorTransfer = C.COLOR_TRANSFER_ST2084; + break; + case 18: + currentTrack.colorTransfer = C.COLOR_TRANSFER_HLG; + break; + default: + break; + } + break; + case ID_COLOUR_RANGE: + switch((int) value) { + case 1: // Broadcast range. + currentTrack.colorRange = C.COLOR_RANGE_LIMITED; + break; + case 2: + currentTrack.colorRange = C.COLOR_RANGE_FULL; + break; + default: + break; + } + break; + case ID_MAX_CLL: + currentTrack.maxContentLuminance = (int) value; + break; + case ID_MAX_FALL: + currentTrack.maxFrameAverageLuminance = (int) value; + break; default: break; } @@ -691,6 +811,36 @@ public final class MatroskaExtractor implements Extractor { case ID_SAMPLING_FREQUENCY: currentTrack.sampleRate = (int) value; break; + case ID_PRIMARY_R_CHROMATICITY_X: + currentTrack.primaryRChromaticityX = (float) value; + break; + case ID_PRIMARY_R_CHROMATICITY_Y: + currentTrack.primaryRChromaticityY = (float) value; + break; + case ID_PRIMARY_G_CHROMATICITY_X: + currentTrack.primaryGChromaticityX = (float) value; + break; + case ID_PRIMARY_G_CHROMATICITY_Y: + currentTrack.primaryGChromaticityY = (float) value; + break; + case ID_PRIMARY_B_CHROMATICITY_X: + currentTrack.primaryBChromaticityX = (float) value; + break; + case ID_PRIMARY_B_CHROMATICITY_Y: + currentTrack.primaryBChromaticityY = (float) value; + break; + case ID_WHITE_POINT_CHROMATICITY_X: + currentTrack.whitePointChromaticityX = (float) value; + break; + case ID_WHITE_POINT_CHROMATICITY_Y: + currentTrack.whitePointChromaticityY = (float) value; + break; + case ID_LUMNINANCE_MAX: + currentTrack.maxMasteringLuminance = (float) value; + break; + case ID_LUMNINANCE_MIN: + currentTrack.minMasteringLuminance = (float) value; + break; default: break; } @@ -1218,6 +1368,7 @@ public final class MatroskaExtractor implements Extractor { || CODEC_ID_OPUS.equals(codecId) || CODEC_ID_VORBIS.equals(codecId) || CODEC_ID_AAC.equals(codecId) + || CODEC_ID_MP2.equals(codecId) || CODEC_ID_MP3.equals(codecId) || CODEC_ID_AC3.equals(codecId) || CODEC_ID_E_AC3.equals(codecId) @@ -1230,7 +1381,8 @@ public final class MatroskaExtractor implements Extractor { || CODEC_ID_PCM_INT_LIT.equals(codecId) || CODEC_ID_SUBRIP.equals(codecId) || CODEC_ID_VOBSUB.equals(codecId) - || CODEC_ID_PGS.equals(codecId); + || CODEC_ID_PGS.equals(codecId) + || CODEC_ID_DVBSUB.equals(codecId); } /** @@ -1300,6 +1452,16 @@ public final class MatroskaExtractor implements Extractor { private static final class Track { private static final int DISPLAY_UNIT_PIXELS = 0; + private static final int MAX_CHROMATICITY = 50000; // Defined in CTA-861.3. + /** + * Default max content light level (CLL) that should be encoded into hdrStaticInfo. + */ + private static final int DEFAULT_MAX_CLL = 1000; // nits. + + /** + * Default frame-average light level (FALL) that should be encoded into hdrStaticInfo. + */ + private static final int DEFAULT_MAX_FALL = 200; // nits. // Common elements. public String codecId; @@ -1321,6 +1483,25 @@ public final class MatroskaExtractor implements Extractor { public byte[] projectionData = null; @C.StereoMode public int stereoMode = Format.NO_VALUE; + public boolean hasColorInfo = false; + @C.ColorSpace + public int colorSpace = Format.NO_VALUE; + @C.ColorTransfer + public int colorTransfer = Format.NO_VALUE; + @C.ColorRange + public int colorRange = Format.NO_VALUE; + public int maxContentLuminance = DEFAULT_MAX_CLL; + public int maxFrameAverageLuminance = DEFAULT_MAX_FALL; + public float primaryRChromaticityX = Format.NO_VALUE; + public float primaryRChromaticityY = Format.NO_VALUE; + public float primaryGChromaticityX = Format.NO_VALUE; + public float primaryGChromaticityY = Format.NO_VALUE; + public float primaryBChromaticityX = Format.NO_VALUE; + public float primaryBChromaticityY = Format.NO_VALUE; + public float whitePointChromaticityX = Format.NO_VALUE; + public float whitePointChromaticityY = Format.NO_VALUE; + public float maxMasteringLuminance = Format.NO_VALUE; + public float minMasteringLuminance = Format.NO_VALUE; // Audio elements. Initially set to their default values. public int channelCount = 1; @@ -1403,9 +1584,13 @@ public final class MatroskaExtractor implements Extractor { mimeType = MimeTypes.AUDIO_AAC; initializationData = Collections.singletonList(codecPrivate); break; + case CODEC_ID_MP2: + mimeType = MimeTypes.AUDIO_MPEG_L2; + maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; + break; case CODEC_ID_MP3: mimeType = MimeTypes.AUDIO_MPEG; - maxInputSize = MP3_MAX_INPUT_SIZE; + maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; break; case CODEC_ID_AC3: mimeType = MimeTypes.AUDIO_AC3; @@ -1454,10 +1639,17 @@ public final class MatroskaExtractor implements Extractor { case CODEC_ID_PGS: mimeType = MimeTypes.APPLICATION_PGS; break; + case CODEC_ID_DVBSUB: + mimeType = MimeTypes.APPLICATION_DVBSUBS; + // Init data: composition_page (2), ancillary_page (2) + initializationData = Collections.singletonList(new byte[] {codecPrivate[0], + codecPrivate[1], codecPrivate[2], codecPrivate[3]}); + break; default: throw new ParserException("Unrecognized codec identifier."); } + int type; Format format; @C.SelectionFlags int selectionFlags = 0; selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0; @@ -1465,10 +1657,12 @@ public final class MatroskaExtractor implements Extractor { // TODO: Consider reading the name elements of the tracks and, if present, incorporating them // into the trackId passed when creating the formats. if (MimeTypes.isAudio(mimeType)) { + type = C.TRACK_TYPE_AUDIO; format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding, initializationData, drmInitData, selectionFlags, language); } else if (MimeTypes.isVideo(mimeType)) { + type = C.TRACK_TYPE_VIDEO; if (displayUnit == Track.DISPLAY_UNIT_PIXELS) { displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth; displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight; @@ -1477,24 +1671,65 @@ public final class MatroskaExtractor implements Extractor { if (displayWidth != Format.NO_VALUE && displayHeight != Format.NO_VALUE) { pixelWidthHeightRatio = ((float) (height * displayWidth)) / (width * displayHeight); } + ColorInfo colorInfo = null; + if (hasColorInfo) { + byte[] hdrStaticInfo = getHdrStaticInfo(); + colorInfo = new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo); + } format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData, - Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData); + Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo, + drmInitData); } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, selectionFlags, language, drmInitData); } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) - || MimeTypes.APPLICATION_PGS.equals(mimeType)) { + || MimeTypes.APPLICATION_PGS.equals(mimeType) + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, initializationData, language, drmInitData); } else { throw new ParserException("Unexpected MIME type."); } - this.output = output.track(number); + this.output = output.track(number, type); this.output.format(format); } + /** + * Returns the HDR Static Info as defined in CTA-861.3. + */ + private byte[] getHdrStaticInfo() { + // Are all fields present. + if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE + || primaryGChromaticityX == Format.NO_VALUE || primaryGChromaticityY == Format.NO_VALUE + || primaryBChromaticityX == Format.NO_VALUE || primaryBChromaticityY == Format.NO_VALUE + || whitePointChromaticityX == Format.NO_VALUE + || whitePointChromaticityY == Format.NO_VALUE || maxMasteringLuminance == Format.NO_VALUE + || minMasteringLuminance == Format.NO_VALUE) { + return null; + } + + byte[] hdrStaticInfoData = new byte[25]; + ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData); + hdrStaticInfo.put((byte) 0); // Type. + hdrStaticInfo.putShort((short) ((primaryRChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryRChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryGChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryGChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryBChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((primaryBChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((whitePointChromaticityX * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) ((whitePointChromaticityY * MAX_CHROMATICITY) + 0.5f)); + hdrStaticInfo.putShort((short) (maxMasteringLuminance + 0.5f)); + hdrStaticInfo.putShort((short) (minMasteringLuminance + 0.5f)); + hdrStaticInfo.putShort((short) maxContentLuminance); + hdrStaticInfo.putShort((short) maxFrameAverageLuminance); + return hdrStaticInfoData; + } + /** * Builds initialization data for a {@link Format} from FourCC codec private data. *

    diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp3/Mp3Extractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp3/Mp3Extractor.java index faa4a3f04..0f86b612b 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.extractor.mp3; +import android.support.annotation.IntDef; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.ParserException; @@ -33,6 +34,8 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; import org.telegram.messenger.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Extracts data from an MP3 file. @@ -51,6 +54,23 @@ public final class Mp3Extractor implements Extractor { }; + /** + * Flags controlling the behavior of the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA}) + public @interface Flags {} + /** + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + */ + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + /** + * 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 = 2; + /** * The maximum number of bytes to search when synchronizing, before giving up. */ @@ -72,6 +92,7 @@ public final class Mp3Extractor implements Extractor { private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); + @Flags private final int flags; private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; @@ -93,16 +114,27 @@ public final class Mp3Extractor implements Extractor { * Constructs a new {@link Mp3Extractor}. */ public Mp3Extractor() { - this(C.TIME_UNSET); + this(0); } /** * Constructs a new {@link Mp3Extractor}. * + * @param flags Flags that control the extractor's behavior. + */ + public Mp3Extractor(@Flags int flags) { + this(flags, C.TIME_UNSET); + } + + /** + * Constructs a new {@link Mp3Extractor}. + * + * @param flags Flags that control the extractor's behavior. * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or * {@link C#TIME_UNSET} if forcing is not required. */ - public Mp3Extractor(long forcedFirstSampleTimestampUs) { + public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) { + this.flags = flags; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(SCRATCH_LENGTH); synchronizedHeader = new MpegAudioHeader(); @@ -118,12 +150,12 @@ public final class Mp3Extractor implements Extractor { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = extractorOutput.track(0); + trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); extractorOutput.endTracks(); } @Override - public void seek(long position) { + public void seek(long position, long timeUs) { synchronizedHeaderData = 0; basisTimeUs = C.TIME_UNSET; samplesRead = 0; @@ -151,7 +183,8 @@ public final class Mp3Extractor implements Extractor { trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata)); + gaplessInfoHolder.encoderPadding, null, null, 0, null, + (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); } return readSample(input); } @@ -284,7 +317,11 @@ public final class Mp3Extractor implements Extractor { byte[] id3Data = new byte[tagLength]; System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); - metadata = new Id3Decoder().decode(id3Data, tagLength); + // We need to parse enough ID3 metadata to retrieve any gapless playback information even + // if ID3 metadata parsing is disabled. + Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0 + ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null; + metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength); if (metadata != null) { gaplessInfoHolder.setFromMetadata(metadata); } @@ -350,7 +387,8 @@ public final class Mp3Extractor implements Extractor { } } - if (seeker == null) { + if (seeker == null || (!seeker.isSeekable() + && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { // Repopulate the synchronized header in case we had to skip an invalid seeking header, which // would give an invalid CBR bitrate. input.resetPeekPosition(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Atom.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Atom.java index a30a709e0..34d679bea 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Atom.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Atom.java @@ -127,6 +127,7 @@ import java.util.List; public static final int TYPE_mean = Util.getIntegerCodeForString("mean"); public static final int TYPE_name = Util.getIntegerCodeForString("name"); public static final int TYPE_data = Util.getIntegerCodeForString("data"); + public static final int TYPE_emsg = Util.getIntegerCodeForString("emsg"); public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d"); public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d"); public static final int TYPE_proj = Util.getIntegerCodeForString("proj"); @@ -134,6 +135,7 @@ import java.util.List; public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); public static final int TYPE_camm = Util.getIntegerCodeForString("camm"); + public static final int TYPE_alac = Util.getIntegerCodeForString("alac"); public final int type; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/AtomParsers.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/AtomParsers.java index 7e1236304..8d08c6895 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/AtomParsers.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/AtomParsers.java @@ -332,6 +332,9 @@ import java.util.List; return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } + // Omit any sample at the end point of an edit for audio tracks. + boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO; + // Count the number of samples after applying edits. int editedSampleCount = 0; int nextSampleIndex = 0; @@ -342,7 +345,8 @@ import java.util.List; long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, track.movieTimescale); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, true, false); + int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample, + false); editedSampleCount += endIndex - startIndex; copyMetadata |= nextSampleIndex != startIndex; nextSampleIndex = endIndex; @@ -365,7 +369,7 @@ import java.util.List; long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale, track.movieTimescale); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, true, false); + int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false); if (copyMetadata) { int count = endIndex - startIndex; System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count); @@ -604,26 +608,14 @@ import java.util.List; || childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl || childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt - || childAtomType == Atom.TYPE__mp3) { + || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac) { parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, isQuickTime, drmInitData, out, i); - } else if (childAtomType == Atom.TYPE_TTML) { - out.format = Format.createTextSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData); - } else if (childAtomType == Atom.TYPE_tx3g) { - out.format = Format.createTextSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_TX3G, null, Format.NO_VALUE, 0, language, drmInitData); - } else if (childAtomType == Atom.TYPE_wvtt) { - out.format = Format.createTextSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_MP4VTT, null, Format.NO_VALUE, 0, language, drmInitData); - } else if (childAtomType == Atom.TYPE_stpp) { - out.format = Format.createTextSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData, - 0 /* subsample timing is absolute */); - } else if (childAtomType == Atom.TYPE_c608) { - out.format = Format.createTextSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, language, drmInitData); - out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT; + } else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g + || childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp + || childAtomType == Atom.TYPE_c608) { + parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, + language, drmInitData, out); } else if (childAtomType == Atom.TYPE_camm) { out.format = Format.createSampleFormat(Integer.toString(trackId), MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData); @@ -633,12 +625,49 @@ import java.util.List; return out; } + private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position, + int atomSize, int trackId, String language, DrmInitData drmInitData, StsdData out) + throws ParserException { + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); + + // Default values. + List initializationData = null; + long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE; + + String mimeType; + if (atomType == Atom.TYPE_TTML) { + mimeType = MimeTypes.APPLICATION_TTML; + } else if (atomType == Atom.TYPE_tx3g) { + mimeType = MimeTypes.APPLICATION_TX3G; + int sampleDescriptionLength = atomSize - Atom.HEADER_SIZE - 8; + byte[] sampleDescriptionData = new byte[sampleDescriptionLength]; + parent.readBytes(sampleDescriptionData, 0, sampleDescriptionLength); + initializationData = Collections.singletonList(sampleDescriptionData); + } else if (atomType == Atom.TYPE_wvtt) { + mimeType = MimeTypes.APPLICATION_MP4VTT; + } else if (atomType == Atom.TYPE_stpp) { + mimeType = MimeTypes.APPLICATION_TTML; + subsampleOffsetUs = 0; // Subsample timing is absolute. + } else if (atomType == Atom.TYPE_c608) { + // Defined by the QuickTime File Format specification. + mimeType = MimeTypes.APPLICATION_MP4CEA608; + out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT; + } else { + // Never happens. + throw new IllegalStateException(); + } + + out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, + Format.NO_VALUE, 0, language, Format.NO_VALUE, drmInitData, subsampleOffsetUs, + initializationData); + } + private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position, int size, int trackId, int rotationDegrees, DrmInitData drmInitData, StsdData out, int entryIndex) throws ParserException { - parent.setPosition(position + Atom.HEADER_SIZE); + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); - parent.skipBytes(24); + parent.skipBytes(16); int width = parent.readUnsignedShort(); int height = parent.readUnsignedShort(); boolean pixelWidthHeightRatioFromPasp = false; @@ -715,6 +744,9 @@ import java.util.List; case 2: stereoMode = C.STEREO_MODE_LEFT_RIGHT; break; + case 3: + stereoMode = C.STEREO_MODE_STEREO_MESH; + break; default: break; } @@ -730,7 +762,7 @@ import java.util.List; out.format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, initializationData, - rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData); + rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, null, drmInitData); } /** @@ -776,15 +808,14 @@ import java.util.List; private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position, int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData, StsdData out, int entryIndex) { - parent.setPosition(position + Atom.HEADER_SIZE); + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); int quickTimeSoundDescriptionVersion = 0; if (isQuickTime) { - parent.skipBytes(8); quickTimeSoundDescriptionVersion = parent.readUnsignedShort(); parent.skipBytes(6); } else { - parent.skipBytes(16); + parent.skipBytes(8); } int channelCount; @@ -838,6 +869,8 @@ import java.util.List; mimeType = MimeTypes.AUDIO_RAW; } else if (atomType == Atom.TYPE__mp3) { mimeType = MimeTypes.AUDIO_MPEG; + } else if (atomType == Atom.TYPE_alac) { + mimeType = MimeTypes.AUDIO_ALAC; } byte[] initializationData = null; @@ -875,6 +908,10 @@ import java.util.List; out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); + } else if (childAtomType == Atom.TYPE_alac) { + initializationData = new byte[childAtomSize]; + parent.setPosition(childPosition); + parent.readBytes(initializationData, 0, childAtomSize); } childPosition += childAtomSize; } @@ -1163,6 +1200,8 @@ import java.util.List; */ private static final class StsdData { + public static final int STSD_HEADER_SIZE = 8; + public final TrackEncryptionBox[] trackEncryptionBoxes; public Format format; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 575337f3f..ff0b671fc 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -20,6 +20,7 @@ import android.util.Log; import android.util.Pair; import android.util.SparseArray; import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.ParserException; import org.telegram.messenger.exoplayer2.drm.DrmInitData; import org.telegram.messenger.exoplayer2.drm.DrmInitData.SchemeData; @@ -30,20 +31,22 @@ import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; import org.telegram.messenger.exoplayer2.extractor.ExtractorsFactory; import org.telegram.messenger.exoplayer2.extractor.PositionHolder; import org.telegram.messenger.exoplayer2.extractor.SeekMap; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; import org.telegram.messenger.exoplayer2.extractor.TrackOutput; import org.telegram.messenger.exoplayer2.extractor.mp4.Atom.ContainerAtom; import org.telegram.messenger.exoplayer2.extractor.mp4.Atom.LeafAtom; +import org.telegram.messenger.exoplayer2.text.cea.CeaUtil; import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.MimeTypes; import org.telegram.messenger.exoplayer2.util.NalUnitUtil; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedList; import java.util.List; import java.util.Stack; import java.util.UUID; @@ -65,15 +68,13 @@ public final class FragmentedMp4Extractor implements Extractor { }; - private static final String TAG = "FragmentedMp4Extractor"; - private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); - /** * Flags controlling the behavior of the extractor. */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, - FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_SIDELOADED}) + FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK, + FLAG_SIDELOADED}) public @interface Flags {} /** * Flag to work around an issue in some video streams where every frame is marked as a sync frame. @@ -87,12 +88,24 @@ public final class FragmentedMp4Extractor implements Extractor { * Flag to ignore any tfdt boxes in the stream. */ public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2; + /** + * Flag to indicate that the extractor should output an event message metadata track. Any event + * messages in the stream will be delivered as samples to this track. + */ + public static final int FLAG_ENABLE_EMSG_TRACK = 4; + /** + * Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages + * contained within SEI NAL units in the stream will be delivered as samples to this track. + */ + public static final int FLAG_ENABLE_CEA608_TRACK = 8; /** * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 * container. */ - private static final int FLAG_SIDELOADED = 4; + private static final int FLAG_SIDELOADED = 16; + private static final String TAG = "FragmentedMp4Extractor"; + private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); 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}; @@ -104,8 +117,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static final int STATE_READING_SAMPLE_CONTINUE = 4; // Workarounds. - @Flags - private final int flags; + @Flags private final int flags; private final Track sideloadedTrack; // Track-linked data bundle, accessible as a whole through trackID. @@ -113,7 +125,8 @@ public final class FragmentedMp4Extractor implements Extractor { // Temporary arrays. private final ParsableByteArray nalStartCode; - private final ParsableByteArray nalLength; + private final ParsableByteArray nalPrefix; + private final ParsableByteArray nalBuffer; private final ParsableByteArray encryptionSignalByte; // Adjusts sample timestamps. @@ -123,6 +136,7 @@ public final class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray atomHeader; private final byte[] extendedTypeScratch; private final Stack containerAtoms; + private final LinkedList pendingMetadataSampleInfos; private int parserState; private int atomType; @@ -130,21 +144,33 @@ public final class FragmentedMp4Extractor implements Extractor { private int atomHeaderBytesRead; private ParsableByteArray atomData; private long endOfMdatPosition; + private int pendingMetadataSampleBytes; private long durationUs; + private long segmentIndexEarliestPresentationTimeUs; private TrackBundle currentTrackBundle; private int sampleSize; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; + private boolean processSeiNalUnitPayload; // Extractor output. private ExtractorOutput extractorOutput; + private TrackOutput eventMessageTrackOutput; + private TrackOutput[] cea608TrackOutputs; // Whether extractorOutput.seekMap has been called. private boolean haveOutputSeekMap; public FragmentedMp4Extractor() { - this(0, null); + this(0); + } + + /** + * @param flags Flags that control the extractor's behavior. + */ + public FragmentedMp4Extractor(@Flags int flags) { + this(flags, null); } /** @@ -152,28 +178,31 @@ public final class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) { - this(flags, null, timestampAdjuster); + this(flags, timestampAdjuster, null); } /** * @param flags Flags that control the extractor's behavior. + * @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. - * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ - public FragmentedMp4Extractor(@Flags int flags, Track sideloadedTrack, - TimestampAdjuster timestampAdjuster) { - this.sideloadedTrack = sideloadedTrack; + public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, + Track sideloadedTrack) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; + this.sideloadedTrack = sideloadedTrack; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); - nalLength = new ParsableByteArray(4); + nalPrefix = new ParsableByteArray(5); + nalBuffer = new ParsableByteArray(); encryptionSignalByte = new ParsableByteArray(1); extendedTypeScratch = new byte[16]; containerAtoms = new Stack<>(); + pendingMetadataSampleInfos = new LinkedList<>(); trackBundles = new SparseArray<>(); durationUs = C.TIME_UNSET; + segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET; enterReadingAtomHeaderState(); } @@ -186,19 +215,22 @@ public final class FragmentedMp4Extractor implements Extractor { public void init(ExtractorOutput output) { extractorOutput = output; if (sideloadedTrack != null) { - TrackBundle bundle = new TrackBundle(output.track(0)); + TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type)); bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); trackBundles.put(0, bundle); + maybeInitExtraTracks(); extractorOutput.endTracks(); } } @Override - public void seek(long position) { + public void seek(long position, long timeUs) { int trackCount = trackBundles.size(); for (int i = 0; i < trackCount; i++) { trackBundles.valueAt(i).reset(); } + pendingMetadataSampleInfos.clear(); + pendingMetadataSampleBytes = 0; containerAtoms.clear(); enterReadingAtomHeaderState(); } @@ -257,6 +289,10 @@ public final class FragmentedMp4Extractor implements Extractor { atomSize = atomHeader.readUnsignedLongToLong(); } + if (atomSize < atomHeaderBytesRead) { + throw new ParserException("Atom size less than header length (unsupported)."); + } + long atomPosition = input.getPosition() - atomHeaderBytesRead; if (atomType == Atom.TYPE_moof) { // The data positions may be updated when parsing the tfhd/trun. @@ -332,9 +368,12 @@ public final class FragmentedMp4Extractor implements Extractor { if (!containerAtoms.isEmpty()) { containerAtoms.peek().add(leaf); } else if (leaf.type == Atom.TYPE_sidx) { - ChunkIndex segmentIndex = parseSidx(leaf.data, inputPosition); - extractorOutput.seekMap(segmentIndex); + Pair result = parseSidx(leaf.data, inputPosition); + segmentIndexEarliestPresentationTimeUs = result.first; + extractorOutput.seekMap(result.second); haveOutputSeekMap = true; + } else if (leaf.type == Atom.TYPE_emsg) { + onEmsgLeafAtomRead(leaf.data); } } @@ -387,18 +426,19 @@ public final class FragmentedMp4Extractor implements Extractor { // We need to create the track bundles. for (int i = 0; i < trackCount; i++) { Track track = tracks.valueAt(i); - trackBundles.put(track.id, new TrackBundle(extractorOutput.track(i))); + TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); + trackBundle.init(track, defaultSampleValuesArray.get(track.id)); + trackBundles.put(track.id, trackBundle); durationUs = Math.max(durationUs, track.durationUs); } + maybeInitExtraTracks(); extractorOutput.endTracks(); } else { Assertions.checkState(trackBundles.size() == trackCount); - } - - // Initialization of tracks and default sample values. - for (int i = 0; i < trackCount; i++) { - Track track = tracks.valueAt(i); - trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id)); + for (int i = 0; i < trackCount; i++) { + Track track = tracks.valueAt(i); + trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id)); + } } } @@ -413,6 +453,53 @@ public final class FragmentedMp4Extractor implements Extractor { } } + private void maybeInitExtraTracks() { + if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) { + eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); + eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, + Format.OFFSET_SAMPLE_RELATIVE)); + } + if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) { + TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, + C.TRACK_TYPE_TEXT); + cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, + null, Format.NO_VALUE, 0, null, null)); + cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput}; + } + } + + /** + * Handles an emsg atom (defined in 23009-1). + */ + private void onEmsgLeafAtomRead(ParsableByteArray atom) { + if (eventMessageTrackOutput == null) { + return; + } + // Parse the event's presentation time delta. + atom.setPosition(Atom.FULL_HEADER_SIZE); + atom.readNullTerminatedString(); // schemeIdUri + atom.readNullTerminatedString(); // value + long timescale = atom.readUnsignedInt(); + long presentationTimeDeltaUs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); + // Output the sample data. + atom.setPosition(Atom.FULL_HEADER_SIZE); + int sampleSize = atom.bytesLeft(); + eventMessageTrackOutput.sampleData(atom, sampleSize); + // Output the sample metadata. + if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { + // We can output the sample metadata immediately. + eventMessageTrackOutput.sampleMetadata( + segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs, + C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null); + } else { + // We need the first sample timestamp in the segment before we can output the metadata. + pendingMetadataSampleInfos.addLast( + new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); + pendingMetadataSampleBytes += sampleSize; + } + } + /** * Parses a trex atom (defined in 14496-12). */ @@ -624,7 +711,7 @@ public final class FragmentedMp4Extractor implements Extractor { DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; int defaultSampleDescriptionIndex = ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0) - ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; + ? 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) @@ -828,8 +915,13 @@ public final class FragmentedMp4Extractor implements Extractor { /** * Parses a sidx atom (defined in 14496-12). + * + * @param atom The atom data. + * @param inputPosition The input position of the first byte after the atom. + * @return A pair consisting of the earliest presentation time in microseconds, and the parsed + * {@link ChunkIndex}. */ - private static ChunkIndex parseSidx(ParsableByteArray atom, long inputPosition) + private static Pair parseSidx(ParsableByteArray atom, long inputPosition) throws ParserException { atom.setPosition(Atom.HEADER_SIZE); int fullAtom = atom.readInt(); @@ -846,6 +938,8 @@ public final class FragmentedMp4Extractor implements Extractor { earliestPresentationTime = atom.readUnsignedLongToLong(); offset += atom.readUnsignedLongToLong(); } + long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime, + C.MICROS_PER_SECOND, timescale); atom.skipBytes(2); @@ -856,7 +950,7 @@ public final class FragmentedMp4Extractor implements Extractor { long[] timesUs = new long[referenceCount]; long time = earliestPresentationTime; - long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale); + long timeUs = earliestPresentationTimeUs; for (int i = 0; i < referenceCount; i++) { int firstInt = atom.readInt(); @@ -880,7 +974,8 @@ public final class FragmentedMp4Extractor implements Extractor { offset += sizes[i]; } - return new ChunkIndex(sizes, offsets, durationsUs, timesUs); + return Pair.create(earliestPresentationTimeUs, + new ChunkIndex(sizes, offsets, durationsUs, timesUs)); } private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException { @@ -942,13 +1037,9 @@ public final class FragmentedMp4Extractor implements Extractor { // We skip bytes preceding the next sample to read. int bytesToSkip = (int) (nextDataPosition - input.getPosition()); if (bytesToSkip < 0) { - if (nextDataPosition == currentTrackBundle.fragment.atomPosition) { - // Assume the sample data must be contiguous in the mdat with no preceeding data. - Log.w(TAG, "Offset to sample data was missing."); - bytesToSkip = 0; - } else { - throw new ParserException("Offset to sample data was negative."); - } + // Assume the sample data must be contiguous in the mdat with no preceding data. + Log.w(TAG, "Ignoring negative offset to sample data."); + bytesToSkip = 0; } input.skipFully(bytesToSkip); this.currentTrackBundle = currentTrackBundle; @@ -976,29 +1067,49 @@ public final class FragmentedMp4Extractor implements Extractor { if (track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. - byte[] nalLengthData = nalLength.data; - nalLengthData[0] = 0; - nalLengthData[1] = 0; - nalLengthData[2] = 0; - int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength; + byte[] nalPrefixData = nalPrefix.data; + nalPrefixData[0] = 0; + nalPrefixData[1] = 0; + nalPrefixData[2] = 0; + int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1; int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength; // NAL units are length delimited, but the decoder requires start code delimited units. // Loop until we've written the sample to the track output, replacing length delimiters with // start codes as we encounter them. while (sampleBytesWritten < sampleSize) { if (sampleCurrentNalBytesRemaining == 0) { - // Read the NAL length so that we know where we find the next one. - input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); - nalLength.setPosition(0); - sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); + // Read the NAL length so that we know where we find the next one, and its type. + input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength); + nalPrefix.setPosition(0); + sampleCurrentNalBytesRemaining = nalPrefix.readUnsignedIntToInt() - 1; // Write a start code for the current NAL unit. nalStartCode.setPosition(0); output.sampleData(nalStartCode, 4); - sampleBytesWritten += 4; + // Write the NAL unit type byte. + output.sampleData(nalPrefix, 1); + processSeiNalUnitPayload = cea608TrackOutputs != null + && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); + sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; } else { - // Write the payload of the NAL unit. - int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + int writtenBytes; + if (processSeiNalUnitPayload) { + // Read and write the payload of the SEI NAL unit. + nalBuffer.reset(sampleCurrentNalBytesRemaining); + input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining); + output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining); + writtenBytes = sampleCurrentNalBytesRemaining; + // Unescape and process the SEI NAL unit. + int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit()); + // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte. + nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); + nalBuffer.setLimit(unescapedLength); + CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer, + cea608TrackOutputs); + } else { + // Write the payload of the NAL unit. + writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + } sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } @@ -1025,6 +1136,14 @@ public final class FragmentedMp4Extractor implements Extractor { } output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey); + while (!pendingMetadataSampleInfos.isEmpty()) { + MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); + pendingMetadataSampleBytes -= sampleInfo.size; + eventMessageTrackOutput.sampleMetadata( + sampleTimeUs + sampleInfo.presentationTimeDeltaUs, + C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null); + } + currentTrackBundle.currentSampleIndex++; currentTrackBundle.currentSampleInTrackRun++; if (currentTrackBundle.currentSampleInTrackRun @@ -1130,7 +1249,7 @@ public final class FragmentedMp4Extractor implements Extractor { || atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz || atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid || atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst - || atom == Atom.TYPE_mehd; + || atom == Atom.TYPE_mehd || atom == Atom.TYPE_emsg; } /** Returns whether the extractor should decode a container atom with type {@code atom}. */ @@ -1140,6 +1259,21 @@ public final class FragmentedMp4Extractor implements Extractor { || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts; } + /** + * Holds data corresponding to a metadata sample. + */ + private static final class MetadataSampleInfo { + + public final long presentationTimeDeltaUs; + public final int size; + + public MetadataSampleInfo(long presentationTimeDeltaUs, int size) { + this.presentationTimeDeltaUs = presentationTimeDeltaUs; + this.size = size; + } + + } + /** * Holds data corresponding to a single track. */ diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/MetadataUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/MetadataUtil.java index bf5f5bb22..0be95907e 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/MetadataUtil.java @@ -188,7 +188,7 @@ import org.telegram.messenger.exoplayer2.util.Util; if (atomType == Atom.TYPE_data) { data.skipBytes(8); // version (1), flags (3), empty (4) String value = data.readNullTerminatedString(atomSize - 16); - return new TextInformationFrame(id, value); + return new TextInformationFrame(id, null, value); } Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type)); return null; @@ -213,7 +213,7 @@ import org.telegram.messenger.exoplayer2.util.Util; value = Math.min(1, value); } if (value >= 0) { - return isTextInformationFrame ? new TextInformationFrame(id, Integer.toString(value)) + return isTextInformationFrame ? new TextInformationFrame(id, null, Integer.toString(value)) : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value)); } Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); @@ -228,12 +228,12 @@ import org.telegram.messenger.exoplayer2.util.Util; data.skipBytes(10); // version (1), flags (3), empty (4), empty (2) int index = data.readUnsignedShort(); if (index > 0) { - String description = "" + index; + String value = "" + index; int count = data.readUnsignedShort(); if (count > 0) { - description += "/" + count; + value += "/" + count; } - return new TextInformationFrame(attributeName, description); + return new TextInformationFrame(attributeName, null, value); } } Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); @@ -245,7 +245,7 @@ import org.telegram.messenger.exoplayer2.util.Util; String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) ? STANDARD_GENRES[genreCode - 1] : null; if (genreString != null) { - return new TextInformationFrame("TCON", genreString); + return new TextInformationFrame("TCON", null, genreString); } Log.w(TAG, "Failed to parse standard genre code"); return null; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Mp4Extractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Mp4Extractor.java index 92a3c0b4b..9593479c8 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.extractor.mp4; +import android.support.annotation.IntDef; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.ParserException; @@ -33,6 +34,8 @@ import org.telegram.messenger.exoplayer2.util.NalUnitUtil; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Stack; @@ -54,11 +57,15 @@ public final class Mp4Extractor implements Extractor, SeekMap { }; - // Parser states. - private static final int STATE_AFTER_SEEK = 0; - private static final int STATE_READING_ATOM_HEADER = 1; - private static final int STATE_READING_ATOM_PAYLOAD = 2; - private static final int STATE_READING_SAMPLE = 3; + /** + * Parser states. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_READING_ATOM_HEADER, STATE_READING_ATOM_PAYLOAD, STATE_READING_SAMPLE}) + private @interface State {} + private static final int STATE_READING_ATOM_HEADER = 0; + private static final int STATE_READING_ATOM_PAYLOAD = 1; + 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 "); @@ -76,7 +83,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private final ParsableByteArray atomHeader; private final Stack containerAtoms; - private int parserState; + @State private int parserState; private int atomType; private long atomSize; private int atomHeaderBytesRead; @@ -96,7 +103,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { containerAtoms = new Stack<>(); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); - enterReadingAtomHeaderState(); } @Override @@ -110,12 +116,16 @@ public final class Mp4Extractor implements Extractor, SeekMap { } @Override - public void seek(long position) { + public void seek(long position, long timeUs) { containerAtoms.clear(); atomHeaderBytesRead = 0; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; - parserState = STATE_AFTER_SEEK; + if (position == 0) { + enterReadingAtomHeaderState(); + } else if (tracks != null) { + updateSampleIndices(timeUs); + } } @Override @@ -128,13 +138,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { throws IOException, InterruptedException { while (true) { switch (parserState) { - case STATE_AFTER_SEEK: - if (input.getPosition() == 0) { - enterReadingAtomHeaderState(); - } else { - parserState = STATE_READING_SAMPLE; - } - break; case STATE_READING_ATOM_HEADER: if (!readAtomHeader(input)) { return RESULT_END_OF_INPUT; @@ -145,8 +148,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { return RESULT_SEEK; } break; - default: + case STATE_READING_SAMPLE: return readSample(input, seekPosition); + default: + throw new IllegalStateException(); } } } @@ -173,8 +178,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Handle the case where the requested time is before the first synchronization sample. sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); } - track.sampleIndex = sampleIndex; - long offset = sampleTable.offsets[sampleIndex]; if (offset < earliestSamplePosition) { earliestSamplePosition = offset; @@ -340,7 +343,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { continue; } - Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i)); + Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, + extractorOutput.track(i, track.type)); // Each sample has up to three bytes of overhead for the start code that replaces its length. // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; @@ -478,6 +482,21 @@ public final class Mp4Extractor implements Extractor, SeekMap { return earliestSampleTrackIndex; } + /** + * Updates every track's sample index to point its latest sync sample before/at {@code timeUs}. + */ + private void updateSampleIndices(long timeUs) { + for (Mp4Track track : tracks) { + TrackSampleTable sampleTable = track.sampleTable; + int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + if (sampleIndex == C.INDEX_UNSET) { + // Handle the case where the requested time is before the first synchronization sample. + sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + } + track.sampleIndex = sampleIndex; + } + } + /** * Returns whether the extractor should decode a leaf atom with type {@code atom}. */ diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Track.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Track.java index e784faaca..fcf9ab3c6 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Track.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/mp4/Track.java @@ -75,8 +75,7 @@ public final class Track { * One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each * sample. */ - @Transformation - public final int sampleTransformation; + @Transformation public final int sampleTransformation; /** * Track encryption boxes for the different track sample descriptions. Entries may be null. @@ -94,7 +93,7 @@ public final class Track { public final long[] editListMediaTimes; /** - * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. -1 for + * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for * other track types. */ public final int nalUnitLengthFieldLength; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/DefaultOggSeeker.java index c6680c6e8..cca90bc12 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -45,7 +45,6 @@ import java.io.IOException; private int state; private long totalGranules; - private volatile long queriedGranule; private long positionBeforeSeekToEnd; private long targetGranule; @@ -114,9 +113,9 @@ import java.io.IOException; } @Override - public long startSeek() { + public long startSeek(long timeUs) { Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); - targetGranule = queriedGranule; + targetGranule = timeUs == 0 ? 0 : streamReader.convertTimeToGranule(timeUs); state = STATE_SEEK; resetSeeking(); return targetGranule; @@ -222,11 +221,10 @@ import java.io.IOException; @Override public long getPosition(long timeUs) { if (timeUs == 0) { - queriedGranule = 0; return startPosition; } - queriedGranule = streamReader.convertTimeToGranule(timeUs); - return getEstimatedPosition(startPosition, queriedGranule, DEFAULT_OFFSET); + long granule = streamReader.convertTimeToGranule(timeUs); + return getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/FlacReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/FlacReader.java index a0a474bce..60c10dd23 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/FlacReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/FlacReader.java @@ -127,12 +127,15 @@ import java.util.List; private static final int METADATA_LENGTH_OFFSET = 1; private static final int SEEK_POINT_SIZE = 18; - private long[] sampleNumbers; - private long[] offsets; - private long firstFrameOffset = -1; - private volatile long queriedGranule; - private volatile long seekedGranule; - private long currentGranule = -1; + private long[] seekPointGranules; + private long[] seekPointOffsets; + private long firstFrameOffset; + private long pendingSeekGranule; + + public FlacOggSeeker() { + firstFrameOffset = -1; + pendingSeekGranule = -1; + } public void setFirstFrameOffset(long firstFrameOffset) { this.firstFrameOffset = firstFrameOffset; @@ -141,40 +144,40 @@ import java.util.List; /** * Parses a FLAC file seek table metadata structure and initializes internal fields. * - * @param data - * A ParsableByteArray including whole seek table metadata block. Its position should be set - * to the beginning of the block. + * @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 + * METADATA_BLOCK_SEEKTABLE */ public void parseSeekTable(ParsableByteArray data) { data.skipBytes(METADATA_LENGTH_OFFSET); int length = data.readUnsignedInt24(); int numberOfSeekPoints = length / SEEK_POINT_SIZE; - - sampleNumbers = new long[numberOfSeekPoints]; - offsets = new long[numberOfSeekPoints]; - + seekPointGranules = new long[numberOfSeekPoints]; + seekPointOffsets = new long[numberOfSeekPoints]; for (int i = 0; i < numberOfSeekPoints; i++) { - sampleNumbers[i] = data.readLong(); - offsets[i] = data.readLong(); + 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 (currentGranule >= 0) { - currentGranule = -currentGranule - 2; - return currentGranule; + if (pendingSeekGranule >= 0) { + long result = -(pendingSeekGranule + 2); + pendingSeekGranule = -1; + return result; } return -1; } @Override - public synchronized long startSeek() { - currentGranule = seekedGranule; - return queriedGranule; + public long startSeek(long timeUs) { + long granule = convertTimeToGranule(timeUs); + int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); + pendingSeekGranule = seekPointGranules[index]; + return granule; } @Override @@ -188,11 +191,10 @@ import java.util.List; } @Override - public synchronized long getPosition(long timeUs) { - queriedGranule = convertTimeToGranule(timeUs); - int index = Util.binarySearchFloor(sampleNumbers, queriedGranule, true, true); - seekedGranule = sampleNumbers[index]; - return firstFrameOffset + offsets[index]; + public long getPosition(long timeUs) { + long granule = convertTimeToGranule(timeUs); + int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); + return firstFrameOffset + seekPointOffsets[index]; } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/OggExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/OggExtractor.java index 3677b097a..3693f453e 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/OggExtractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/OggExtractor.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.extractor.ogg; +import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.ParserException; import org.telegram.messenger.exoplayer2.extractor.Extractor; import org.telegram.messenger.exoplayer2.extractor.ExtractorInput; @@ -75,15 +76,15 @@ public class OggExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - TrackOutput trackOutput = output.track(0); + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); output.endTracks(); // TODO: fix the case if sniff() isn't called streamReader.init(output, trackOutput); } @Override - public void seek(long position) { - streamReader.seek(position); + public void seek(long position, long timeUs) { + streamReader.seek(position, timeUs); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/OggSeeker.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/OggSeeker.java index 1140501a6..454146f56 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/OggSeeker.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/OggSeeker.java @@ -35,9 +35,10 @@ import java.io.IOException; /** * Initializes a seek operation. * + * @param timeUs The seek position in microseconds. * @return The granule position targeted by the seek. */ - long startSeek(); + long startSeek(long timeUs); /** * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/StreamReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/StreamReader.java index f1fedcb65..c541a94f3 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/StreamReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ogg/StreamReader.java @@ -81,15 +81,15 @@ import java.io.IOException; } /** - * @see Extractor#seek(long) + * @see Extractor#seek(long, long) */ - final void seek(long position) { + final void seek(long position, long timeUs) { oggPacket.reset(); if (position == 0) { reset(!seekMapSet); } else { if (state != STATE_READ_HEADERS) { - targetGranule = oggSeeker.startSeek(); + targetGranule = oggSeeker.startSeek(timeUs); state = STATE_READ_PAYLOAD; } } @@ -162,7 +162,7 @@ import java.io.IOException; seekPosition.position = position; return Extractor.RESULT_SEEK; } else if (position < -1) { - onSeekEnd(-position - 2); + onSeekEnd(-(position + 2)); } if (!seekMapSet) { SeekMap seekMap = oggSeeker.createSeekMap(); @@ -232,7 +232,7 @@ import java.io.IOException; /** * Called on end of seeking. * - * @param currentGranule Current granule at the current position of input. + * @param currentGranule The granule at the current input position. */ protected void onSeekEnd(long currentGranule) { this.currentGranule = currentGranule; @@ -246,7 +246,7 @@ import java.io.IOException; } @Override - public long startSeek() { + public long startSeek(long timeUs) { return 0; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/rawcc/RawCcExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/rawcc/RawCcExtractor.java index 18034da4e..815e633e5 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -65,7 +65,7 @@ public final class RawCcExtractor implements Extractor { @Override public void init(ExtractorOutput output) { output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); - trackOutput = output.track(0); + trackOutput = output.track(0, C.TRACK_TYPE_TEXT); output.endTracks(); trackOutput.format(format); } @@ -83,8 +83,11 @@ public final class RawCcExtractor implements Extractor { while (true) { switch (parserState) { case STATE_READING_HEADER: - parseHeader(input); - parserState = STATE_READING_TIMESTAMP_AND_COUNT; + if (parseHeader(input)) { + parserState = STATE_READING_TIMESTAMP_AND_COUNT; + } else { + return RESULT_END_OF_INPUT; + } break; case STATE_READING_TIMESTAMP_AND_COUNT: if (parseTimestampAndSampleCount(input)) { @@ -105,7 +108,7 @@ public final class RawCcExtractor implements Extractor { } @Override - public void seek(long position) { + public void seek(long position, long timeUs) { parserState = STATE_READING_HEADER; } @@ -114,14 +117,18 @@ public final class RawCcExtractor implements Extractor { // Do nothing } - private void parseHeader(ExtractorInput input) throws IOException, InterruptedException { + private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException { dataScratch.reset(); - input.readFully(dataScratch.data, 0, HEADER_SIZE); - if (dataScratch.readInt() != HEADER_ID) { - throw new IOException("Input not RawCC"); + if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) { + if (dataScratch.readInt() != HEADER_ID) { + throw new IOException("Input not RawCC"); + } + version = dataScratch.readUnsignedByte(); + // no versions use the flag fields yet + return true; + } else { + return false; } - version = dataScratch.readUnsignedByte(); - // no versions use the flag fields yet } private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException, diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Ac3Extractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Ac3Extractor.java index 5e0be0be0..5bb00984d 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Ac3Extractor.java @@ -26,7 +26,6 @@ import org.telegram.messenger.exoplayer2.extractor.SeekMap; import org.telegram.messenger.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; import org.telegram.messenger.exoplayer2.util.Util; - import java.io.IOException; /** @@ -125,7 +124,7 @@ public final class Ac3Extractor implements Extractor { } @Override - public void seek(long position) { + public void seek(long position, long timeUs) { startedPacket = false; reader.seek(); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Ac3Reader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Ac3Reader.java index 22a5b0b2f..d722aaf99 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Ac3Reader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Ac3Reader.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.extractor.ts; +import android.support.annotation.IntDef; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.audio.Ac3Util; @@ -23,12 +24,17 @@ import org.telegram.messenger.exoplayer2.extractor.TrackOutput; import org.telegram.messenger.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import org.telegram.messenger.exoplayer2.util.ParsableBitArray; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Parses a continuous (E-)AC-3 byte stream and extracts individual samples. */ -/* package */ final class Ac3Reader implements ElementaryStreamReader { +public final class Ac3Reader implements ElementaryStreamReader { + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE}) + private @interface State {} private static final int STATE_FINDING_SYNC = 0; private static final int STATE_READING_HEADER = 1; private static final int STATE_READING_SAMPLE = 2; @@ -39,9 +45,10 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private String trackFormatId; private TrackOutput output; - private int state; + @State private int state; private int bytesRead; // Used to find the header. @@ -51,7 +58,6 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; private long sampleDurationUs; private Format format; private int sampleSize; - private boolean isEac3; // Used when reading the samples. private long timeUs; @@ -84,7 +90,9 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { - output = extractorOutput.track(generator.getNextId()); + generator.generateNewId(); + trackFormatId = generator.getFormatId(); + output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override @@ -122,6 +130,8 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; state = STATE_FINDING_SYNC; } break; + default: + break; } } } @@ -174,25 +184,22 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; /** * Parses the sample header. */ + @SuppressWarnings("ReferenceEquality") private void parseHeader() { - if (format == null) { - // We read ahead to distinguish between AC-3 and E-AC-3. - headerScratchBits.skipBits(40); - isEac3 = headerScratchBits.readBits(5) == 16; - headerScratchBits.setPosition(headerScratchBits.getPosition() - 45); - format = isEac3 ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, null, language , null) - : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, null, language, null); + headerScratchBits.setPosition(0); + Ac3Util.Ac3SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); + if (format == null || frameInfo.channelCount != format.channelCount + || frameInfo.sampleRate != format.sampleRate + || frameInfo.mimeType != format.sampleMimeType) { + format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null, + Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null, + null, 0, language); output.format(format); } - sampleSize = isEac3 ? Ac3Util.parseEAc3SyncframeSize(headerScratchBits.data) - : Ac3Util.parseAc3SyncframeSize(headerScratchBits.data); - int audioSamplesPerSyncframe = isEac3 - ? Ac3Util.parseEAc3SyncframeAudioSampleCount(headerScratchBits.data) - : Ac3Util.getAc3SyncframeAudioSampleCount(); + sampleSize = frameInfo.frameSize; // In this class a sample is an access unit (syncframe in AC-3), but the MediaFormat sample rate // specifies the number of PCM audio samples per second. - sampleDurationUs = - (int) (C.MICROS_PER_SECOND * audioSamplesPerSyncframe / format.sampleRate); + sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/AdtsExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/AdtsExtractor.java index 295fd3c8e..d74248b8d 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/AdtsExtractor.java @@ -134,7 +134,7 @@ public final class AdtsExtractor implements Extractor { } @Override - public void seek(long position) { + public void seek(long position, long timeUs) { startedPacket = false; reader.seek(); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/AdtsReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/AdtsReader.java index 0911f92b9..eb3896f2b 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/AdtsReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/AdtsReader.java @@ -33,7 +33,7 @@ import java.util.Collections; /** * Parses a continuous ADTS byte stream and extracts individual frames. */ -/* package */ final class AdtsReader implements ElementaryStreamReader { +public final class AdtsReader implements ElementaryStreamReader { private static final String TAG = "AdtsReader"; @@ -61,6 +61,7 @@ import java.util.Collections; private final ParsableByteArray id3HeaderBuffer; private final String language; + private String formatId; private TrackOutput output; private TrackOutput id3Output; @@ -108,11 +109,14 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); if (exposeId3) { - id3Output = extractorOutput.track(idGenerator.getNextId()); - id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, - Format.NO_VALUE, null)); + idGenerator.generateNewId(); + id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); } else { id3Output = new DummyTrackOutput(); } @@ -300,7 +304,7 @@ import java.util.Collections; Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( audioSpecificConfig); - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, + Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, Collections.singletonList(audioSpecificConfig), null, 0, language); // In this class a sample is an access unit, but the MediaFormat sample rate specifies the diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 8280204e6..00a30609c 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -17,9 +17,15 @@ package org.telegram.messenger.exoplayer2.extractor.ts; import android.support.annotation.IntDef; import android.util.SparseArray; +import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; +import org.telegram.messenger.exoplayer2.util.MimeTypes; +import org.telegram.messenger.exoplayer2.util.ParsableByteArray; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Default implementation for {@link TsPayloadReader.Factory}. @@ -27,27 +33,54 @@ import java.lang.annotation.RetentionPolicy; public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory { /** - * Flags controlling elementary stream readers behaviour. + * Flags controlling elementary stream readers' behavior. */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM, - FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS}) - public @interface Flags { - } + FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS, FLAG_IGNORE_SPLICE_INFO_STREAM, + FLAG_OVERRIDE_CAPTION_DESCRIPTORS}) + public @interface Flags {} public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1; - public static final int FLAG_IGNORE_AAC_STREAM = 2; - public static final int FLAG_IGNORE_H264_STREAM = 4; - public static final int FLAG_DETECT_ACCESS_UNITS = 8; + public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1; + public static final int FLAG_IGNORE_H264_STREAM = 1 << 2; + public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3; + public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4; + public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5; - @Flags - private final int flags; + private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86; + + @Flags private final int flags; + private final List closedCaptionFormats; public DefaultTsPayloadReaderFactory() { this(0); } + /** + * @param flags A combination of {@code FLAG_*} values that control the behavior of the created + * readers. + */ public DefaultTsPayloadReaderFactory(@Flags int flags) { + this(flags, Collections.emptyList()); + } + + /** + * @param flags A combination of {@code FLAG_*} values that control the behavior of the created + * readers. + * @param closedCaptionFormats {@link Format}s to be exposed by payload readers for streams with + * embedded closed captions when no caption service descriptors are provided. If + * {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, {@code closedCaptionFormats} overrides + * any descriptor information. If not set, and {@code closedCaptionFormats} is empty, a + * closed caption track with {@link Format#accessibilityChannel} {@link Format#NO_VALUE} will + * be exposed. + */ + public DefaultTsPayloadReaderFactory(@Flags int flags, List closedCaptionFormats) { this.flags = flags; + if (!isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS) && closedCaptionFormats.isEmpty()) { + closedCaptionFormats = Collections.singletonList(Format.createTextSampleFormat(null, + MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); + } + this.closedCaptionFormats = closedCaptionFormats; } @Override @@ -62,8 +95,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_MPA_LSF: return new PesReader(new MpegAudioReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_AAC: - return (flags & FLAG_IGNORE_AAC_STREAM) != 0 ? null - : new PesReader(new AdtsReader(false, esInfo.language)); + return isSet(FLAG_IGNORE_AAC_STREAM) + ? null : new PesReader(new AdtsReader(false, esInfo.language)); case TsExtractor.TS_STREAM_TYPE_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3: return new PesReader(new Ac3Reader(esInfo.language)); @@ -73,18 +106,75 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_H262: return new PesReader(new H262Reader()); case TsExtractor.TS_STREAM_TYPE_H264: - return (flags & FLAG_IGNORE_H264_STREAM) != 0 ? null - : new PesReader(new H264Reader((flags & FLAG_ALLOW_NON_IDR_KEYFRAMES) != 0, - (flags & FLAG_DETECT_ACCESS_UNITS) != 0)); + return isSet(FLAG_IGNORE_H264_STREAM) ? null + : new PesReader(new H264Reader(buildSeiReader(esInfo), + isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS))); case TsExtractor.TS_STREAM_TYPE_H265: - return new PesReader(new H265Reader()); + return new PesReader(new H265Reader(buildSeiReader(esInfo))); case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO: - return new SectionReader(new SpliceInfoSectionReader()); + return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM) + ? null : new SectionReader(new SpliceInfoSectionReader()); case TsExtractor.TS_STREAM_TYPE_ID3: return new PesReader(new Id3Reader()); + case TsExtractor.TS_STREAM_TYPE_DVBSUBS: + return new PesReader( + new DvbSubtitleReader(esInfo.dvbSubtitleInfos)); default: return null; } } + /** + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for + * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a + * {@link SeiReader} for the declared formats, or {@link #closedCaptionFormats} if the descriptor + * is not present. + * + * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}. + * @return A {@link SeiReader} for closed caption tracks. + */ + private SeiReader buildSeiReader(EsInfo esInfo) { + if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) { + return new SeiReader(closedCaptionFormats); + } + ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes); + List closedCaptionFormats = this.closedCaptionFormats; + while (scratchDescriptorData.bytesLeft() > 0) { + int descriptorTag = scratchDescriptorData.readUnsignedByte(); + int descriptorLength = scratchDescriptorData.readUnsignedByte(); + int nextDescriptorPosition = scratchDescriptorData.getPosition() + descriptorLength; + if (descriptorTag == DESCRIPTOR_TAG_CAPTION_SERVICE) { + // Note: see ATSC A/65 for detailed information about the caption service descriptor. + closedCaptionFormats = new ArrayList<>(); + int numberOfServices = scratchDescriptorData.readUnsignedByte() & 0x1F; + for (int i = 0; i < numberOfServices; i++) { + String language = scratchDescriptorData.readString(3); + int captionTypeByte = scratchDescriptorData.readUnsignedByte(); + boolean isDigital = (captionTypeByte & 0x80) != 0; + String mimeType; + int accessibilityChannel; + if (isDigital) { + mimeType = MimeTypes.APPLICATION_CEA708; + accessibilityChannel = captionTypeByte & 0x3F; + } else { + mimeType = MimeTypes.APPLICATION_CEA608; + accessibilityChannel = 1; + } + closedCaptionFormats.add(Format.createTextSampleFormat(null, mimeType, null, + Format.NO_VALUE, 0, language, accessibilityChannel, null)); + // Skip easy_reader(1), wide_aspect_ratio(1), reserved(14). + scratchDescriptorData.skipBytes(2); + } + } else { + // Unknown descriptor. Ignore. + } + scratchDescriptorData.setPosition(nextDescriptorPosition); + } + return new SeiReader(closedCaptionFormats); + } + + private boolean isSet(@Flags int flag) { + return (flags & flag) != 0; + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/DtsReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/DtsReader.java index d146e434d..1671170a1 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/DtsReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/DtsReader.java @@ -26,7 +26,7 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; /** * Parses a continuous DTS byte stream and extracts individual samples. */ -/* package */ final class DtsReader implements ElementaryStreamReader { +public final class DtsReader implements ElementaryStreamReader { private static final int STATE_FINDING_SYNC = 0; private static final int STATE_READING_HEADER = 1; @@ -39,6 +39,7 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private String formatId; private TrackOutput output; private int state; @@ -79,7 +80,9 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override @@ -165,7 +168,7 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; private void parseHeader() { byte[] frameData = headerScratchBytes.data; if (format == null) { - format = DtsUtil.parseDtsFormat(frameData, null, language, null); + format = DtsUtil.parseDtsFormat(frameData, formatId, language, null); output.format(format); } sampleSize = DtsUtil.getDtsFrameSize(frameData); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/DvbSubtitleReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/DvbSubtitleReader.java new file mode 100755 index 000000000..384fa3fd1 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/DvbSubtitleReader.java @@ -0,0 +1,121 @@ +/* + * 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 org.telegram.messenger.exoplayer2.extractor.ts; + +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.Format; +import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer2.extractor.TrackOutput; +import org.telegram.messenger.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo; +import org.telegram.messenger.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.telegram.messenger.exoplayer2.util.MimeTypes; +import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import java.util.Collections; +import java.util.List; + +/** + * Parses DVB subtitle data and extracts individual frames. + */ +public final class DvbSubtitleReader implements ElementaryStreamReader { + + private final List subtitleInfos; + private final TrackOutput[] outputs; + + private boolean writingSample; + private int bytesToCheck; + private int sampleBytesWritten; + private long sampleTimeUs; + + /** + * @param subtitleInfos Information about the DVB subtitles associated to the stream. + */ + public DvbSubtitleReader(List subtitleInfos) { + this.subtitleInfos = subtitleInfos; + outputs = new TrackOutput[subtitleInfos.size()]; + } + + @Override + public void seek() { + writingSample = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + for (int i = 0; i < outputs.length; i++) { + DvbSubtitleInfo subtitleInfo = subtitleInfos.get(i); + idGenerator.generateNewId(); + TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + output.format(Format.createImageSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_DVBSUBS, null, Format.NO_VALUE, + Collections.singletonList(subtitleInfo.initializationData), subtitleInfo.language, null)); + outputs[i] = output; + } + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + if (!dataAlignmentIndicator) { + return; + } + writingSample = true; + sampleTimeUs = pesTimeUs; + sampleBytesWritten = 0; + bytesToCheck = 2; + } + + @Override + public void packetFinished() { + if (writingSample) { + for (TrackOutput output : outputs) { + output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null); + } + writingSample = false; + } + } + + @Override + public void consume(ParsableByteArray data) { + if (writingSample) { + if (bytesToCheck == 2 && !checkNextByte(data, 0x20)) { + // Failed to check data_identifier + return; + } + if (bytesToCheck == 1 && !checkNextByte(data, 0x00)) { + // Check and discard the subtitle_stream_id + return; + } + int dataPosition = data.getPosition(); + int bytesAvailable = data.bytesLeft(); + for (TrackOutput output : outputs) { + data.setPosition(dataPosition); + output.sampleData(data, bytesAvailable); + } + sampleBytesWritten += bytesAvailable; + } + } + + private boolean checkNextByte(ParsableByteArray data, int expectedValue) { + if (data.bytesLeft() == 0) { + return false; + } + if (data.readUnsignedByte() != expectedValue) { + writingSample = false; + } + bytesToCheck--; + return writingSample; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H262Reader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H262Reader.java index a6e3303aa..749783f88 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H262Reader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H262Reader.java @@ -30,13 +30,14 @@ import java.util.Collections; /** * Parses a continuous H262 byte stream and extracts individual frames. */ -/* package */ final class H262Reader implements ElementaryStreamReader { +public final class H262Reader implements ElementaryStreamReader { private static final int START_PICTURE = 0x00; private static final int START_SEQUENCE_HEADER = 0xB3; private static final int START_EXTENSION = 0xB5; private static final int START_GROUP = 0xB8; + private String formatId; private TrackOutput output; // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. @@ -78,7 +79,9 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); } @Override @@ -126,7 +129,7 @@ import java.util.Collections; int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { // The csd data is complete, so we can decode and output the media format. - Pair result = parseCsdBuffer(csdBuffer); + Pair result = parseCsdBuffer(csdBuffer, formatId); output.format(result.first); frameDurationUs = result.second; hasOutputFormat = true; @@ -166,10 +169,11 @@ import java.util.Collections; * Parses the {@link Format} and frame duration from a csd buffer. * * @param csdBuffer The csd buffer. + * @param formatId The id for the generated format. May be null. * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or * 0 if the duration could not be determined. */ - private static Pair parseCsdBuffer(CsdBuffer csdBuffer) { + private static Pair parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); int firstByte = csdData[4] & 0xFF; @@ -195,7 +199,7 @@ import java.util.Collections; break; } - Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_MPEG2, null, + Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null, Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H264Reader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H264Reader.java index f796632dd..6797446fc 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H264Reader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H264Reader.java @@ -33,12 +33,13 @@ import java.util.List; /** * Parses a continuous H264 byte stream and extracts individual frames. */ -/* package */ final class H264Reader implements ElementaryStreamReader { +public final class H264Reader implements ElementaryStreamReader { private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set + private final SeiReader seiReader; private final boolean allowNonIdrKeyframes; private final boolean detectAccessUnits; private final NalUnitTargetBuffer sps; @@ -47,8 +48,8 @@ import java.util.List; private long totalBytesWritten; private final boolean[] prefixFlags; + private String formatId; private TrackOutput output; - private SeiReader seiReader; private SampleReader sampleReader; // State that should not be reset on seek. @@ -61,15 +62,17 @@ import java.util.List; private final ParsableByteArray seiWrapper; /** + * @param seiReader An SEI reader for consuming closed caption channels. * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as * synchronization samples (key-frames). * @param detectAccessUnits Whether to split the input stream into access units (samples) based on * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs). */ - public H264Reader(boolean allowNonIdrKeyframes, boolean detectAccessUnits) { - prefixFlags = new boolean[3]; + public H264Reader(SeiReader seiReader, boolean allowNonIdrKeyframes, boolean detectAccessUnits) { + this.seiReader = seiReader; this.allowNonIdrKeyframes = allowNonIdrKeyframes; this.detectAccessUnits = detectAccessUnits; + prefixFlags = new boolean[3]; sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); @@ -88,9 +91,11 @@ import java.util.List; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + seiReader.createTracks(extractorOutput, idGenerator); } @Override @@ -175,7 +180,7 @@ import java.util.List; initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength)); NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength); NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength); - output.format(Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, + output.format(Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE, initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio, null)); hasOutputFormat = true; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H265Reader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H265Reader.java index 054eff560..d921fd39d 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H265Reader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/H265Reader.java @@ -30,7 +30,7 @@ import java.util.Collections; /** * Parses a continuous H.265 byte stream and extracts individual frames. */ -/* package */ final class H265Reader implements ElementaryStreamReader { +public final class H265Reader implements ElementaryStreamReader { private static final String TAG = "H265Reader"; @@ -44,9 +44,11 @@ import java.util.Collections; private static final int PREFIX_SEI_NUT = 39; private static final int SUFFIX_SEI_NUT = 40; + private final SeiReader seiReader; + + private String formatId; private TrackOutput output; private SampleReader sampleReader; - private SeiReader seiReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -66,7 +68,11 @@ import java.util.Collections; // Scratch variables to avoid allocations. private final ParsableByteArray seiWrapper; - public H265Reader() { + /** + * @param seiReader An SEI reader for consuming closed caption channels. + */ + public H265Reader(SeiReader seiReader) { + this.seiReader = seiReader; prefixFlags = new boolean[3]; vps = new NalUnitTargetBuffer(VPS_NUT, 128); sps = new NalUnitTargetBuffer(SPS_NUT, 128); @@ -90,9 +96,11 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); sampleReader = new SampleReader(output); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + seiReader.createTracks(extractorOutput, idGenerator); } @Override @@ -183,7 +191,7 @@ import java.util.Collections; sps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding); if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) { - output.format(parseMediaFormat(vps, sps, pps)); + output.format(parseMediaFormat(formatId, vps, sps, pps)); hasOutputFormat = true; } } @@ -205,8 +213,8 @@ import java.util.Collections; } } - private static Format parseMediaFormat(NalUnitTargetBuffer vps, NalUnitTargetBuffer sps, - NalUnitTargetBuffer pps) { + private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps, + NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { // Build codec-specific data. byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); @@ -311,7 +319,7 @@ import java.util.Collections; } } - return Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, + return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE, Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Id3Reader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Id3Reader.java index f6a93d860..32c05ab09 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Id3Reader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/Id3Reader.java @@ -27,7 +27,7 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; /** * Parses ID3 data and extracts individual text information frames. */ -/* package */ final class Id3Reader implements ElementaryStreamReader { +public final class Id3Reader implements ElementaryStreamReader { private static final String TAG = "Id3Reader"; @@ -56,9 +56,10 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, - null)); + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, + null, Format.NO_VALUE, null)); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/MpegAudioReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/MpegAudioReader.java index 56cda3b04..8b1a8a884 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/MpegAudioReader.java @@ -26,7 +26,7 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; /** * Parses a continuous MPEG Audio byte stream and extracts individual frames. */ -/* package */ final class MpegAudioReader implements ElementaryStreamReader { +public final class MpegAudioReader implements ElementaryStreamReader { private static final int STATE_FINDING_HEADER = 0; private static final int STATE_READING_HEADER = 1; @@ -38,6 +38,7 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; private final MpegAudioHeader header; private final String language; + private String formatId; private TrackOutput output; private int state; @@ -76,7 +77,9 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override @@ -176,9 +179,9 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; frameSize = header.frameSize; if (!hasOutputFormat) { frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate; - Format format = Format.createAudioSampleFormat(null, header.mimeType, null, Format.NO_VALUE, - MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, null, null, 0, - language); + Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null, + Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, + null, null, 0, language); output.format(format); hasOutputFormat = true; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/PesReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/PesReader.java index f2d92aa89..2f1b5ee09 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/PesReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/PesReader.java @@ -16,12 +16,11 @@ package org.telegram.messenger.exoplayer2.extractor.ts; import android.util.Log; - import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; import org.telegram.messenger.exoplayer2.util.ParsableBitArray; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; /** * Parses PES packet data and extracts samples. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/PsExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/PsExtractor.java index 3fa1e5eda..97df7d012 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/PsExtractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/PsExtractor.java @@ -23,10 +23,10 @@ import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; import org.telegram.messenger.exoplayer2.extractor.ExtractorsFactory; import org.telegram.messenger.exoplayer2.extractor.PositionHolder; import org.telegram.messenger.exoplayer2.extractor.SeekMap; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; import org.telegram.messenger.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import org.telegram.messenger.exoplayer2.util.ParsableBitArray; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; import java.io.IOException; /** @@ -127,7 +127,7 @@ public final class PsExtractor implements Extractor { } @Override - public void seek(long position) { + public void seek(long position, long timeUs) { timestampAdjuster.reset(); for (int i = 0; i < psPayloadReaders.size(); i++) { psPayloadReaders.valueAt(i).seek(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SectionPayloadReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SectionPayloadReader.java index c93e169bd..377e0d0fa 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SectionPayloadReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SectionPayloadReader.java @@ -16,10 +16,10 @@ package org.telegram.messenger.exoplayer2.extractor.ts; import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; import org.telegram.messenger.exoplayer2.extractor.TrackOutput; import org.telegram.messenger.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; /** * Reads section data. @@ -40,8 +40,9 @@ public interface SectionPayloadReader { /** * Called by a {@link SectionReader} when a full section is received. * - * @param sectionData The data belonging to a section, including the section header but excluding - * the CRC_32 field. + * @param sectionData The data belonging to a section starting from the table_id. If + * section_syntax_indicator is set to '1', {@code sectionData} excludes the CRC_32 field. + * Otherwise, all bytes belonging to the table section are included. */ void consume(ParsableByteArray sectionData); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SectionReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SectionReader.java index 4f356d9a3..4cdfb1774 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SectionReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SectionReader.java @@ -15,74 +15,119 @@ */ package org.telegram.messenger.exoplayer2.extractor.ts; +import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; -import org.telegram.messenger.exoplayer2.util.ParsableBitArray; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; import org.telegram.messenger.exoplayer2.util.Util; /** * Reads section data packets and feeds the whole sections to a given {@link SectionPayloadReader}. + * Useful information on PSI sections can be found in ISO/IEC 13818-1, section 2.4.4. */ public final class SectionReader implements TsPayloadReader { private static final int SECTION_HEADER_LENGTH = 3; + private static final int DEFAULT_SECTION_BUFFER_LENGTH = 32; + private static final int MAX_SECTION_LENGTH = 4098; - private final ParsableByteArray sectionData; - private final ParsableBitArray headerScratch; private final SectionPayloadReader reader; - private int sectionLength; - private int sectionBytesRead; + private final ParsableByteArray sectionData; + + private int totalSectionLength; + private int bytesRead; + private boolean sectionSyntaxIndicator; + private boolean waitingForPayloadStart; public SectionReader(SectionPayloadReader reader) { this.reader = reader; - sectionData = new ParsableByteArray(); - headerScratch = new ParsableBitArray(new byte[SECTION_HEADER_LENGTH]); + sectionData = new ParsableByteArray(DEFAULT_SECTION_BUFFER_LENGTH); } @Override public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { reader.init(timestampAdjuster, extractorOutput, idGenerator); + waitingForPayloadStart = true; } @Override public void seek() { - // Do nothing. + waitingForPayloadStart = true; } @Override public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { - // Skip pointer. + int payloadStartPosition = C.POSITION_UNSET; if (payloadUnitStartIndicator) { - int pointerField = data.readUnsignedByte(); - data.skipBytes(pointerField); - - // Note: see ISO/IEC 13818-1, section 2.4.4.3 for detailed information on the format of - // the header. - data.readBytes(headerScratch, SECTION_HEADER_LENGTH); - data.setPosition(data.getPosition() - SECTION_HEADER_LENGTH); - headerScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), 0 (1), reserved (2) - sectionLength = headerScratch.readBits(12) + SECTION_HEADER_LENGTH; - sectionBytesRead = 0; - - sectionData.reset(sectionLength); + int payloadStartOffset = data.readUnsignedByte(); + payloadStartPosition = data.getPosition() + payloadStartOffset; } - int bytesToRead = Math.min(data.bytesLeft(), sectionLength - sectionBytesRead); - data.readBytes(sectionData.data, sectionBytesRead, bytesToRead); - sectionBytesRead += bytesToRead; - if (sectionBytesRead < sectionLength) { - // Not yet fully read. - return; + if (waitingForPayloadStart) { + if (!payloadUnitStartIndicator) { + return; + } + waitingForPayloadStart = false; + data.setPosition(payloadStartPosition); + bytesRead = 0; } - if (Util.crc(sectionData.data, 0, sectionLength, 0xFFFFFFFF) != 0) { - // CRC Invalid. The section gets discarded. - return; + while (data.bytesLeft() > 0) { + if (bytesRead < SECTION_HEADER_LENGTH) { + // Note: see ISO/IEC 13818-1, section 2.4.4.3 for detailed information on the format of + // the header. + if (bytesRead == 0) { + int tableId = data.readUnsignedByte(); + data.setPosition(data.getPosition() - 1); + if (tableId == 0xFF /* forbidden value */) { + // No more sections in this ts packet. + waitingForPayloadStart = true; + return; + } + } + int headerBytesToRead = Math.min(data.bytesLeft(), SECTION_HEADER_LENGTH - bytesRead); + data.readBytes(sectionData.data, bytesRead, headerBytesToRead); + bytesRead += headerBytesToRead; + if (bytesRead == SECTION_HEADER_LENGTH) { + sectionData.reset(SECTION_HEADER_LENGTH); + sectionData.skipBytes(1); // Skip table id (8). + int secondHeaderByte = sectionData.readUnsignedByte(); + int thirdHeaderByte = sectionData.readUnsignedByte(); + sectionSyntaxIndicator = (secondHeaderByte & 0x80) != 0; + totalSectionLength = + (((secondHeaderByte & 0x0F) << 8) | thirdHeaderByte) + SECTION_HEADER_LENGTH; + if (sectionData.capacity() < totalSectionLength) { + // Ensure there is enough space to keep the whole section. + byte[] bytes = sectionData.data; + sectionData.reset( + Math.min(MAX_SECTION_LENGTH, Math.max(totalSectionLength, bytes.length * 2))); + System.arraycopy(bytes, 0, sectionData.data, 0, SECTION_HEADER_LENGTH); + } + } + } else { + // Reading the body. + int bodyBytesToRead = Math.min(data.bytesLeft(), totalSectionLength - bytesRead); + data.readBytes(sectionData.data, bytesRead, bodyBytesToRead); + bytesRead += bodyBytesToRead; + 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) { + // The CRC is invalid so discard the section. + waitingForPayloadStart = true; + return; + } + sectionData.reset(totalSectionLength - 4); // Exclude the CRC_32 field. + } else { + // This is a private section with private defined syntax. + sectionData.reset(totalSectionLength); + } + reader.consume(sectionData); + bytesRead = 0; + } + } } - sectionData.setLimit(sectionData.limit() - 4); // Exclude the CRC_32 field. - reader.consume(sectionData); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SeiReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SeiReader.java index 08e35ceae..d8fe37255 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SeiReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SeiReader.java @@ -17,65 +17,49 @@ package org.telegram.messenger.exoplayer2.extractor.ts; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; +import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; import org.telegram.messenger.exoplayer2.extractor.TrackOutput; -import org.telegram.messenger.exoplayer2.text.cea.Cea608Decoder; +import org.telegram.messenger.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.telegram.messenger.exoplayer2.text.cea.CeaUtil; +import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.MimeTypes; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import java.util.List; /** * Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */ /* package */ final class SeiReader { - private final TrackOutput output; + private final List closedCaptionFormats; + private final TrackOutput[] outputs; - public SeiReader(TrackOutput output) { - this.output = output; - output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null, - Format.NO_VALUE, 0, null, null)); + /** + * @param closedCaptionFormats A list of formats for the closed caption channels to expose. + */ + public SeiReader(List closedCaptionFormats) { + this.closedCaptionFormats = closedCaptionFormats; + outputs = new TrackOutput[closedCaptionFormats.size()]; } - public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { - int b; - while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { - // Parse payload type. - int payloadType = 0; - do { - b = seiBuffer.readUnsignedByte(); - payloadType += b; - } while (b == 0xFF); - // Parse payload size. - int payloadSize = 0; - do { - b = seiBuffer.readUnsignedByte(); - payloadSize += b; - } while (b == 0xFF); - // Process the payload. - if (Cea608Decoder.isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { - // Ignore country_code (1) + provider_code (2) + user_identifier (4) - // + user_data_type_code (1). - seiBuffer.skipBytes(8); - // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1). - int ccCount = seiBuffer.readUnsignedByte() & 0x1F; - seiBuffer.skipBytes(1); - int sampleBytes = 0; - for (int i = 0; i < ccCount; i++) { - int ccValidityAndType = seiBuffer.peekUnsignedByte() & 0x07; - // Check that validity == 1 and type == 0 (i.e. NTSC_CC_FIELD_1). - if (ccValidityAndType != 0x04) { - seiBuffer.skipBytes(3); - } else { - sampleBytes += 3; - output.sampleData(seiBuffer, 3); - } - } - output.sampleMetadata(pesTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytes, 0, null); - // Ignore trailing information in SEI, if any. - seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3)); - } else { - seiBuffer.skipBytes(payloadSize); - } + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + for (int i = 0; i < outputs.length; i++) { + idGenerator.generateNewId(); + TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + Format channelFormat = closedCaptionFormats.get(i); + String channelMimeType = channelFormat.sampleMimeType; + Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) + || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), + "Invalid closed caption mime type provided: " + channelMimeType); + output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), channelMimeType, null, + Format.NO_VALUE, channelFormat.selectionFlags, channelFormat.language, + channelFormat.accessibilityChannel, null)); + outputs[i] = output; } } + public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { + CeaUtil.consume(pesTimeUs, seiBuffer, outputs); + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SpliceInfoSectionReader.java index 1e3ca1f3e..7148662ff 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SpliceInfoSectionReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/SpliceInfoSectionReader.java @@ -18,31 +18,45 @@ package org.telegram.messenger.exoplayer2.extractor.ts; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; import org.telegram.messenger.exoplayer2.extractor.TrackOutput; import org.telegram.messenger.exoplayer2.util.MimeTypes; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; /** * Parses splice info sections as defined by SCTE35. */ public final class SpliceInfoSectionReader implements SectionPayloadReader { + private TimestampAdjuster timestampAdjuster; private TrackOutput output; + private boolean formatDeclared; @Override public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null, - Format.NO_VALUE, null)); + this.timestampAdjuster = timestampAdjuster; + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35, + null, Format.NO_VALUE, null)); } @Override public void consume(ParsableByteArray sectionData) { + if (!formatDeclared) { + if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) { + // There is not enough information to initialize the timestamp adjuster. + return; + } + output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, + timestampAdjuster.getTimestampOffsetUs())); + formatDeclared = true; + } int sampleSize = sectionData.bytesLeft(); output.sampleData(sectionData, sampleSize); - output.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME, + sampleSize, 0, null); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/TsExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/TsExtractor.java index 58e6729eb..211036275 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/TsExtractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/TsExtractor.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.extractor.ts; +import android.support.annotation.IntDef; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; @@ -25,16 +26,23 @@ import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; import org.telegram.messenger.exoplayer2.extractor.ExtractorsFactory; import org.telegram.messenger.exoplayer2.extractor.PositionHolder; import org.telegram.messenger.exoplayer2.extractor.SeekMap; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; import org.telegram.messenger.exoplayer2.extractor.TrackOutput; +import org.telegram.messenger.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory.Flags; +import org.telegram.messenger.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo; import org.telegram.messenger.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import org.telegram.messenger.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.ParsableBitArray; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * Facilitates the extraction of data from the MPEG-2 TS container format. @@ -53,6 +61,27 @@ public final class TsExtractor implements Extractor { }; + /** + * Modes for the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({MODE_NORMAL, MODE_SINGLE_PMT, MODE_HLS}) + public @interface Mode {} + + /** + * Behave as defined in ISO/IEC 13818-1. + */ + public static final int MODE_NORMAL = 0; + /** + * Assume only one PMT will be contained in the stream, even if more are declared by the PAT. + */ + public static final int MODE_SINGLE_PMT = 1; + /** + * Enable single PMT mode, map {@link TrackOutput}s by their type (instead of PID) and ignore + * continuity counters. + */ + public static final int MODE_HLS = 2; + public static final int TS_STREAM_TYPE_MPA = 0x03; public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; public static final int TS_STREAM_TYPE_AAC = 0x0F; @@ -65,6 +94,7 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_H265 = 0x24; public static final int TS_STREAM_TYPE_ID3 = 0x15; public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86; + public static final int TS_STREAM_TYPE_DVBSUBS = 0x59; private static final int TS_PACKET_SIZE = 188; private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. @@ -78,8 +108,8 @@ public final class TsExtractor implements Extractor { private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2 private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT; - private final boolean mapByType; - private final TimestampAdjuster timestampAdjuster; + @Mode private final int mode; + private final List timestampAdjusters; private final ParsableByteArray tsPacketBuffer; private final ParsableBitArray tsScratch; private final SparseIntArray continuityCounters; @@ -89,31 +119,39 @@ public final class TsExtractor implements Extractor { // Accessed only by the loading thread. private ExtractorOutput output; + private int remainingPmts; private boolean tracksEnded; private TsPayloadReader id3Reader; public TsExtractor() { - this(new TimestampAdjuster(0)); + this(0); } /** - * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. + * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory} + * {@code FLAG_*} values that control the behavior of the payload readers. */ - public TsExtractor(TimestampAdjuster timestampAdjuster) { - this(timestampAdjuster, new DefaultTsPayloadReaderFactory(), false); + public TsExtractor(@Flags int defaultTsPayloadReaderFlags) { + this(MODE_NORMAL, new TimestampAdjuster(0), + new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags)); } /** + * @param mode Mode for the extractor. One of {@link #MODE_NORMAL}, {@link #MODE_SINGLE_PMT} + * and {@link #MODE_HLS}. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param payloadReaderFactory Factory for injecting a custom set of payload readers. - * @param mapByType True if {@link TrackOutput}s should be mapped by their type, false to map them - * by their PID. */ - public TsExtractor(TimestampAdjuster timestampAdjuster, - TsPayloadReader.Factory payloadReaderFactory, boolean mapByType) { - this.timestampAdjuster = timestampAdjuster; + public TsExtractor(@Mode int mode, TimestampAdjuster timestampAdjuster, + TsPayloadReader.Factory payloadReaderFactory) { this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); - this.mapByType = mapByType; + this.mode = mode; + if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) { + timestampAdjusters = Collections.singletonList(timestampAdjuster); + } else { + timestampAdjusters = new ArrayList<>(); + timestampAdjusters.add(timestampAdjuster); + } tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); tsScratch = new ParsableBitArray(new byte[3]); trackIds = new SparseBooleanArray(); @@ -149,8 +187,11 @@ public final class TsExtractor implements Extractor { } @Override - public void seek(long position) { - timestampAdjuster.reset(); + public void seek(long position, long timeUs) { + int timestampAdjustersCount = timestampAdjusters.size(); + for (int i = 0; i < timestampAdjustersCount; i++) { + timestampAdjusters.get(i).reset(); + } tsPacketBuffer.reset(); continuityCounters.clear(); // Elementary stream readers' state should be cleared to get consistent behaviours when seeking. @@ -211,18 +252,22 @@ public final class TsExtractor implements Extractor { tsScratch.skipBits(2); // transport_scrambling_control boolean adaptationFieldExists = tsScratch.readBit(); boolean payloadExists = tsScratch.readBit(); + + // Discontinuity check. boolean discontinuityFound = false; int continuityCounter = tsScratch.readBits(4); - int previousCounter = continuityCounters.get(pid, continuityCounter - 1); - continuityCounters.put(pid, continuityCounter); - if (previousCounter == continuityCounter) { - if (payloadExists) { - // Duplicate packet found. - tsPacketBuffer.setPosition(endOfPacket); - return RESULT_CONTINUE; + if (mode != MODE_HLS) { + int previousCounter = continuityCounters.get(pid, continuityCounter - 1); + continuityCounters.put(pid, continuityCounter); + if (previousCounter == continuityCounter) { + if (payloadExists) { + // Duplicate packet found. + tsPacketBuffer.setPosition(endOfPacket); + return RESULT_CONTINUE; + } + } else if (continuityCounter != (previousCounter + 1) % 16) { + discontinuityFound = true; } - } else if (continuityCounter != (previousCounter + 1) % 16) { - discontinuityFound = true; } // Skip the adaptation field. @@ -283,10 +328,15 @@ public final class TsExtractor implements Extractor { @Override public void consume(ParsableByteArray sectionData) { - // table_id(8), section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), + int tableId = sectionData.readUnsignedByte(); + if (tableId != 0x00 /* program_association_section */) { + // 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(8); + sectionData.skipBytes(7); int programCount = sectionData.bytesLeft() / 4; for (int i = 0; i < programCount; i++) { @@ -298,8 +348,12 @@ public final class TsExtractor implements Extractor { } else { int pid = patScratch.readBits(13); tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid))); + remainingPmts++; } } + if (mode != MODE_HLS) { + tsPayloadReaders.remove(TS_PAT_PID); + } } } @@ -314,6 +368,7 @@ public final class TsExtractor implements Extractor { private static final int TS_PMT_DESC_AC3 = 0x6A; private static final int TS_PMT_DESC_EAC3 = 0x7A; private static final int TS_PMT_DESC_DTS = 0x7B; + private static final int TS_PMT_DESC_DVBSUBS = 0x59; private final ParsableBitArray pmtScratch; private final int pid; @@ -331,11 +386,27 @@ public final class TsExtractor implements Extractor { @Override public void consume(ParsableByteArray sectionData) { - // table_id(8), section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), - // program_number (16), reserved (2), version_number (5), current_next_indicator (1), - // section_number (8), last_section_number (8), reserved (3), PCR_PID (13) - // Skip the rest of the PMT header. - sectionData.skipBytes(10); + int tableId = sectionData.readUnsignedByte(); + if (tableId != 0x02 /* TS_program_map_section */) { + // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment. + return; + } + // TimestampAdjuster assignment. + TimestampAdjuster timestampAdjuster; + if (mode == MODE_SINGLE_PMT || mode == MODE_HLS || remainingPmts == 1) { + timestampAdjuster = timestampAdjusters.get(0); + } else { + timestampAdjuster = new TimestampAdjuster( + timestampAdjusters.get(0).getFirstSampleTimestampUs()); + timestampAdjusters.add(timestampAdjuster); + } + + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12) + sectionData.skipBytes(2); + int programNumber = sectionData.readUnsignedShort(); + // reserved (2), version_number (5), current_next_indicator (1), section_number (8), + // last_section_number (8), reserved (3), PCR_PID (13) + sectionData.skipBytes(5); // Read program_info_length. sectionData.readBytes(pmtScratch, 2); @@ -345,13 +416,13 @@ public final class TsExtractor implements Extractor { // Skip the descriptors. sectionData.skipBytes(programInfoLength); - if (mapByType && id3Reader == null) { + if (mode == MODE_HLS && id3Reader == null) { // Setup an ID3 track regardless of whether there's a corresponding entry, in case one // appears intermittently during playback. See [Internal: b/20261500]. - EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]); + EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, null, new byte[0]); id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo); id3Reader.init(timestampAdjuster, output, - new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); + new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); } int remainingEntriesLength = sectionData.bytesLeft(); @@ -368,19 +439,20 @@ public final class TsExtractor implements Extractor { } remainingEntriesLength -= esInfoLength + 5; - int trackId = mapByType ? streamType : elementaryPid; + int trackId = mode == MODE_HLS ? streamType : elementaryPid; if (trackIds.get(trackId)) { continue; } trackIds.put(trackId, true); TsPayloadReader reader; - if (mapByType && streamType == TS_STREAM_TYPE_ID3) { + if (mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3) { reader = id3Reader; } else { reader = payloadReaderFactory.createPayloadReader(streamType, esInfo); if (reader != null) { - reader.init(timestampAdjuster, output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE)); + reader.init(timestampAdjuster, output, + new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE)); } } @@ -388,16 +460,20 @@ public final class TsExtractor implements Extractor { tsPayloadReaders.put(elementaryPid, reader); } } - if (mapByType) { + if (mode == MODE_HLS) { if (!tracksEnded) { output.endTracks(); + remainingPmts = 0; + tracksEnded = true; } } else { - tsPayloadReaders.remove(TS_PAT_PID); tsPayloadReaders.remove(pid); - output.endTracks(); + remainingPmts = mode == MODE_SINGLE_PMT ? 0 : remainingPmts - 1; + if (remainingPmts == 0) { + output.endTracks(); + tracksEnded = true; + } } - tracksEnded = true; } /** @@ -413,6 +489,7 @@ public final class TsExtractor implements Extractor { int descriptorsEndPosition = descriptorsStartPosition + length; int streamType = -1; String language = null; + List dvbSubtitleInfos = null; while (data.getPosition() < descriptorsEndPosition) { int descriptorTag = data.readUnsignedByte(); int descriptorLength = data.readUnsignedByte(); @@ -433,14 +510,25 @@ public final class TsExtractor implements Extractor { } else if (descriptorTag == TS_PMT_DESC_DTS) { // DTS_descriptor streamType = TS_STREAM_TYPE_DTS; } else if (descriptorTag == TS_PMT_DESC_ISO639_LANG) { - language = new String(data.data, data.getPosition(), 3).trim(); + language = data.readString(3).trim(); // Audio type is ignored. + } else if (descriptorTag == TS_PMT_DESC_DVBSUBS) { + streamType = TS_STREAM_TYPE_DVBSUBS; + dvbSubtitleInfos = new ArrayList<>(); + while (data.getPosition() < positionOfNextDescriptor) { + String dvbLanguage = data.readString(3).trim(); + int dvbSubtitlingType = data.readUnsignedByte(); + byte[] initializationData = new byte[4]; + data.readBytes(initializationData, 0, 4); + dvbSubtitleInfos.add(new DvbSubtitleInfo(dvbLanguage, dvbSubtitlingType, + initializationData)); + } } // Skip unused bytes of current descriptor. data.skipBytes(positionOfNextDescriptor - data.getPosition()); } data.setPosition(descriptorsEndPosition); - return new EsInfo(streamType, language, + return new EsInfo(streamType, language, dvbSubtitleInfos, Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition)); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/TsPayloadReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/TsPayloadReader.java index fccb15a3c..aa89ddb4d 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/ts/TsPayloadReader.java @@ -17,9 +17,11 @@ package org.telegram.messenger.exoplayer2.extractor.ts; import android.util.SparseArray; import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; import org.telegram.messenger.exoplayer2.extractor.TrackOutput; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; +import java.util.Collections; +import java.util.List; /** * Parses TS packet payload data. @@ -60,38 +62,111 @@ public interface TsPayloadReader { public final int streamType; public final String language; + public final List dvbSubtitleInfos; public final byte[] descriptorBytes; /** * @param streamType The type of the stream as defined by the * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}. * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. + * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream. * @param descriptorBytes The descriptor bytes associated to the stream. */ - public EsInfo(int streamType, String language, byte[] descriptorBytes) { + public EsInfo(int streamType, String language, List dvbSubtitleInfos, + byte[] descriptorBytes) { this.streamType = streamType; this.language = language; + this.dvbSubtitleInfos = dvbSubtitleInfos == null ? Collections.emptyList() + : Collections.unmodifiableList(dvbSubtitleInfos); this.descriptorBytes = descriptorBytes; } } + /** + * Holds information about a DVB subtitle, as defined in ETSI EN 300 468 V1.11.1 section 6.2.41. + */ + final class DvbSubtitleInfo { + + public final String language; + public final int type; + public final byte[] initializationData; + + /** + * @param language The ISO 639-2 three character language. + * @param type The subtitling type. + * @param initializationData The composition and ancillary page ids. + */ + public DvbSubtitleInfo(String language, int type, byte[] initializationData) { + this.language = language; + this.type = type; + this.initializationData = initializationData; + } + + } + /** * Generates track ids for initializing {@link TsPayloadReader}s' {@link TrackOutput}s. */ final class TrackIdGenerator { - private final int firstId; - private final int idIncrement; - private int generatedIdCount; + private static final int ID_UNSET = Integer.MIN_VALUE; - public TrackIdGenerator(int firstId, int idIncrement) { - this.firstId = firstId; - this.idIncrement = idIncrement; + private final String formatIdPrefix; + private final int firstTrackId; + private final int trackIdIncrement; + private int trackId; + private String formatId; + + public TrackIdGenerator(int firstTrackId, int trackIdIncrement) { + this(ID_UNSET, firstTrackId, trackIdIncrement); } - public int getNextId() { - return firstId + idIncrement * generatedIdCount++; + public TrackIdGenerator(int programNumber, int firstTrackId, int trackIdIncrement) { + this.formatIdPrefix = programNumber != ID_UNSET ? programNumber + "/" : ""; + this.firstTrackId = firstTrackId; + this.trackIdIncrement = trackIdIncrement; + trackId = ID_UNSET; + } + + /** + * Generates a new set of track and track format ids. Must be called before {@code get*} + * methods. + */ + public void generateNewId() { + trackId = trackId == ID_UNSET ? firstTrackId : trackId + trackIdIncrement; + formatId = formatIdPrefix + trackId; + } + + /** + * Returns the last generated track id. Must be called after the first {@link #generateNewId()} + * call. + * + * @return The last generated track id. + */ + public int getTrackId() { + maybeThrowUninitializedError(); + return trackId; + } + + /** + * Returns the last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as format id. Must be + * called after the first {@link #generateNewId()} call. + * + * @return The last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as + * format id. + */ + public String getFormatId() { + maybeThrowUninitializedError(); + return formatId; + } + + private void maybeThrowUninitializedError() { + if (trackId == ID_UNSET) { + throw new IllegalStateException("generateNewId() must be called before retrieving ids."); + } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/wav/WavExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/wav/WavExtractor.java index aa302ed14..244bcdfd2 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/wav/WavExtractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/wav/WavExtractor.java @@ -60,13 +60,13 @@ public final class WavExtractor implements Extractor, SeekMap { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = output.track(0); + trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); wavHeader = null; output.endTracks(); } @Override - public void seek(long position) { + public void seek(long position, long timeUs) { pendingBytes = 0; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecInfo.java index 9eb8a2fe0..95aff2f01 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecInfo.java @@ -16,6 +16,7 @@ package org.telegram.messenger.exoplayer2.mediacodec; import android.annotation.TargetApi; +import android.graphics.Point; import android.media.MediaCodec; import android.media.MediaCodecInfo.AudioCapabilities; import android.media.MediaCodecInfo.CodecCapabilities; @@ -23,6 +24,7 @@ import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; import android.util.Log; import android.util.Pair; +import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.MimeTypes; import org.telegram.messenger.exoplayer2.util.Util; @@ -51,6 +53,14 @@ public final class MediaCodecInfo { */ public final boolean adaptive; + /** + * Whether the decoder supports tunneling. + * + * @see CodecCapabilities#isFeatureSupported(String) + * @see CodecCapabilities#FEATURE_TunneledPlayback + */ + public final boolean tunneling; + private final String mimeType; private final CodecCapabilities capabilities; @@ -86,6 +96,7 @@ public final class MediaCodecInfo { this.mimeType = mimeType; this.capabilities = capabilities; adaptive = capabilities != null && isAdaptive(capabilities); + tunneling = capabilities != null && isTunneling(capabilities); } /** @@ -132,39 +143,6 @@ public final class MediaCodecInfo { return false; } - /** - * Whether the decoder supports video with a specified width and height. - *

    - * Must not be called if the device SDK version is less than 21. - * - * @param width Width in pixels. - * @param height Height in pixels. - * @return Whether the decoder supports video with the given width and height. - */ - @TargetApi(21) - public boolean isVideoSizeSupportedV21(int width, int height) { - if (capabilities == null) { - logNoSupport("size.caps"); - return false; - } - VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); - if (videoCapabilities == null) { - logNoSupport("size.vCaps"); - return false; - } - if (!videoCapabilities.isSizeSupported(width, height)) { - // 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 || !videoCapabilities.isSizeSupported(height, width)) { - logNoSupport("size.support, " + width + "x" + height); - return false; - } - logAssumedSupport("size.rotated, " + width + "x" + height); - } - return true; - } - /** * Whether the decoder supports video with a given width, height and frame rate. *

    @@ -172,7 +150,8 @@ public final class MediaCodecInfo { * * @param width Width in pixels. * @param height Height in pixels. - * @param frameRate Frame rate in frames per second. + * @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) @@ -186,11 +165,12 @@ public final class MediaCodecInfo { logNoSupport("sizeAndRate.vCaps"); return false; } - if (!videoCapabilities.areSizeAndRateSupported(width, height, frameRate)) { + if (!areSizeAndRateSupported(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 || !videoCapabilities.areSizeAndRateSupported(height, width, frameRate)) { + if (width >= height + || !areSizeAndRateSupported(videoCapabilities, height, width, frameRate)) { logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate); return false; } @@ -199,6 +179,35 @@ public final class MediaCodecInfo { return true; } + /** + * Returns the smallest video size greater than or equal to a specified size that also satisfies + * the {@link MediaCodec}'s width and height alignment requirements. + *

    + * Must not be called if the device SDK version is less than 21. + * + * @param width Width in pixels. + * @param height Height in pixels. + * @return The smallest video size greater than or equal to the specified size that also satisfies + * the {@link MediaCodec}'s width and height alignment requirements, or null if not a video + * codec. + */ + @TargetApi(21) + public Point alignVideoSizeV21(int width, int height) { + if (capabilities == null) { + logNoSupport("align.caps"); + return null; + } + VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); + if (videoCapabilities == null) { + logNoSupport("align.vCaps"); + return null; + } + int widthAlignment = videoCapabilities.getWidthAlignment(); + int heightAlignment = videoCapabilities.getHeightAlignment(); + return new Point(Util.ceilDivide(width, widthAlignment) * widthAlignment, + Util.ceilDivide(height, heightAlignment) * heightAlignment); + } + /** * Whether the decoder supports audio with a given sample rate. *

    @@ -270,4 +279,21 @@ public final class MediaCodecInfo { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); } + @TargetApi(21) + private static boolean areSizeAndRateSupported(VideoCapabilities capabilities, int width, + int height, double frameRate) { + return frameRate == Format.NO_VALUE || frameRate <= 0 + ? capabilities.isSizeSupported(width, height) + : capabilities.areSizeAndRateSupported(width, height, frameRate); + } + + private static boolean isTunneling(CodecCapabilities capabilities) { + return Util.SDK_INT >= 21 && isTunnelingV21(capabilities); + } + + @TargetApi(21) + private static boolean isTunnelingV21(CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback); + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecRenderer.java index dd77059db..2fa2bdac2 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -169,6 +169,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; private final DecoderInputBuffer buffer; + private final DecoderInputBuffer flagsOnlyBuffer; private final FormatHolder formatHolder; private final List decodeOnlyPresentationTimestamps; private final MediaCodec.BufferInfo outputBufferInfo; @@ -183,6 +184,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean codecNeedsAdaptationWorkaround; private boolean codecNeedsEosPropagationWorkaround; private boolean codecNeedsEosFlushWorkaround; + private boolean codecNeedsEosOutputExceptionWorkaround; private boolean codecNeedsMonoChannelCountWorkaround; private boolean codecNeedsAdaptationWorkaroundBuffer; private boolean shouldSkipAdaptationWorkaroundOutputBuffer; @@ -201,6 +203,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; + private boolean waitingForFirstSyncFrame; protected DecoderCounters decoderCounters; @@ -225,6 +228,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); formatHolder = new FormatHolder(); decodeOnlyPresentationTimestamps = new ArrayList<>(); outputBufferInfo = new MediaCodec.BufferInfo(); @@ -276,11 +280,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Configures a newly created {@link MediaCodec}. * + * @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 crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. + * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - protected abstract void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto); + protected abstract void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, + MediaCrypto crypto) throws DecoderQueryException; @SuppressWarnings("deprecation") protected final void maybeInitCodec() throws ExoPlaybackException { @@ -332,12 +339,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } String codecName = decoderInfo.name; - codecIsAdaptive = decoderInfo.adaptive; + codecIsAdaptive = decoderInfo.adaptive && !codecNeedsDisableAdaptationWorkaround(codecName); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); + codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); try { long codecInitializingTimestamp = SystemClock.elapsedRealtime(); @@ -345,7 +353,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codec = MediaCodec.createByCodecName(codecName); TraceUtil.endSection(); TraceUtil.beginSection("configureCodec"); - configureCodec(codec, format, mediaCrypto); + configureCodec(decoderInfo, codec, format, mediaCrypto); TraceUtil.endSection(); TraceUtil.beginSection("startCodec"); codec.start(); @@ -363,6 +371,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) : C.TIME_UNSET; inputIndex = C.INDEX_UNSET; outputIndex = C.INDEX_UNSET; + waitingForFirstSyncFrame = true; decoderCounters.decoderInitCount++; } @@ -441,6 +450,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReinitializationState = REINITIALIZATION_STATE_NONE; decoderCounters.decoderReleaseCount++; + buffer.data = null; try { codec.stop(); } finally { @@ -472,39 +482,64 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (format == null) { - readFormat(); + if (outputStreamEnded) { + renderToEndOfStream(); + return; } + if (format == null) { + // We don't have a format yet, so try and read one. + buffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, true); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + // End of stream read having not read a format. + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + processEndOfStream(); + return; + } else { + // We still don't have a format and can't make progress without one. + return; + } + } + // We have a format. maybeInitCodec(); if (codec != null) { TraceUtil.beginSection("drainAndFeed"); while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } else if (format != null) { - skipToKeyframeBefore(positionUs); + } else { + 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. + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, false); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + processEndOfStream(); + } } decoderCounters.ensureUpdated(); } - private void readFormat() throws ExoPlaybackException { - int result = readSource(formatHolder, null); - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - } - } - protected void flushCodec() throws ExoPlaybackException { codecHotswapDeadlineMs = C.TIME_UNSET; inputIndex = C.INDEX_UNSET; outputIndex = C.INDEX_UNSET; + waitingForFirstSyncFrame = true; waitingForKeys = false; shouldSkipOutputBuffer = false; decodeOnlyPresentationTimestamps.clear(); codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; if (codecNeedsFlushWorkaround || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { - // Workaround framework bugs. See [Internal: b/8347958, b/8578467, b/8543366, b/23361053]. releaseCodec(); maybeInitCodec(); } else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) { @@ -529,10 +564,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @throws ExoPlaybackException If an error occurs feeding the input buffer. */ private boolean feedInputBuffer() throws ExoPlaybackException { - if (inputStreamEnded - || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { - // The input stream has ended, or we need to re-initialize the codec but are still waiting - // for the existing codec to output any final output buffers. + if (codec == null || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM + || inputStreamEnded) { + // We need to reinitialize the codec or the input stream has ended. return false; } @@ -584,7 +618,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING; } adaptiveReconfigurationBytes = buffer.data.position(); - result = readSource(formatHolder, buffer); + result = readSource(formatHolder, buffer, false); } if (result == C.RESULT_NOTHING_READ) { @@ -628,6 +662,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } return false; } + if (waitingForFirstSyncFrame && !buffer.isKeyFrame()) { + buffer.clear(); + if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { + // The buffer we just cleared contained reconfiguration data. We need to re-write this + // data into a subsequent buffer (if there is one). + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + } + return true; + } + waitingForFirstSyncFrame = false; boolean bufferEncrypted = buffer.isEncrypted(); waitingForKeys = shouldWaitForKeys(bufferEncrypted); if (waitingForKeys) { @@ -761,18 +805,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param codec The {@link MediaCodec} instance. * @param outputFormat The new output format. + * @throws ExoPlaybackException Thrown if an error occurs handling the new output format. */ - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { - // Do nothing. - } - - /** - * Called when the output stream ends, meaning that the last output buffer has been processed and - * the {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag has been propagated through the decoder. - *

    - * The default implementation is a no-op. - */ - protected void onOutputStreamEnded() { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { // Do nothing. } @@ -846,12 +882,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @SuppressWarnings("deprecation") private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (outputStreamEnded) { - return false; - } - if (outputIndex < 0) { - outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, + getDequeueOutputBufferTimeoutUs()); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, + getDequeueOutputBufferTimeoutUs()); + } if (outputIndex >= 0) { // We've dequeued a buffer. if (shouldSkipAdaptationWorkaroundOutputBuffer) { @@ -864,7 +911,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // The dequeued buffer indicates the end of the stream. Process it immediately. processEndOfStream(); outputIndex = C.INDEX_UNSET; - return true; + return false; } else { // The dequeued buffer is a media buffer. Do some initial setup. The buffer will be // processed by calling processOutputBuffer (possibly multiple times) below. @@ -885,15 +932,32 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (codecNeedsEosPropagationWorkaround && (inputStreamEnded || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM)) { processEndOfStream(); - return true; } return false; } } - if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex], - outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer)) { + boolean processedOutputBuffer; + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, + outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, + outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + } + + if (processedOutputBuffer) { onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs); outputIndex = C.INDEX_UNSET; return true; @@ -905,7 +969,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Processes a new output format. */ - private void processOutputFormat() { + private void processOutputFormat() throws ExoPlaybackException { MediaFormat format = codec.getOutputFormat(); if (codecNeedsAdaptationWorkaround && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT @@ -961,6 +1025,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, boolean shouldSkip) throws ExoPlaybackException; + /** + * Incrementally renders any remaining output. + *

    + * The default implementation is a no-op. + * + * @throws ExoPlaybackException Thrown if an error occurs rendering remaining output. + */ + protected void renderToEndOfStream() throws ExoPlaybackException { + // Do nothing. + } + /** * Processes an end of stream signal. * @@ -973,7 +1048,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { maybeInitCodec(); } else { outputStreamEnded = true; - onOutputStreamEnded(); + renderToEndOfStream(); } } @@ -995,6 +1070,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

    * If true is returned, the renderer will work around the issue by releasing the decoder and * instantiating a new one rather than flushing the current instance. + *

    + * See [Internal: b/8347958, b/8543366]. * * @param name The name of the decoder. * @return True if the decoder is known to fail when flushed. @@ -1023,7 +1100,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return Util.SDK_INT < 24 && ("OMX.Nvidia.h264.decode".equals(name) || "OMX.Nvidia.h264.decode.secure".equals(name)) && ("flounder".equals(Util.DEVICE) || "flounder_lte".equals(Util.DEVICE) - || "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE)); + || "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE)); } /** @@ -1064,13 +1141,33 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

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

    + * See [Internal: b/8578467, b/23361053]. * * @param name The name of the decoder. * @return True if the decoder is known to behave incorrectly if flushed after receiving an input * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. False otherwise. */ private static boolean codecNeedsEosFlushWorkaround(String name) { - return Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name); + return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name)) + || (Util.SDK_INT <= 19 && "hb2000".equals(Util.DEVICE) + && ("OMX.amlogic.avc.decoder.awesome".equals(name) + || "OMX.amlogic.avc.decoder.awesome.secure".equals(name))); + } + + /** + * Returns whether the decoder may throw an {@link IllegalStateException} from + * {@link MediaCodec#dequeueOutputBuffer(MediaCodec.BufferInfo, long)} or + * {@link MediaCodec#releaseOutputBuffer(int, boolean)} after receiving an input + * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. + *

    + * See [Internal: b/17933838]. + * + * @param name The name of the decoder. + * @return True if the decoder may throw an exception after receiving an end-of-stream buffer. + */ + private static boolean codecNeedsEosOutputExceptionWorkaround(String name) { + return Util.SDK_INT == 21 && "OMX.google.aac.decoder".equals(name); } /** @@ -1082,7 +1179,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param name The decoder name. * @param format The input format. - * @return True if the device is known to set the number of audio channels in the output format + * @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. */ @@ -1091,4 +1188,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && "OMX.MTK.AUDIO.DECODER.MP3".equals(name); } + /** + * Returns whether the decoder is known to fail when adapting, despite advertising itself as an + * adaptive decoder. + *

    + * If true is returned then we explicitly disable adaptation for the decoder. + * + * @param name The decoder name. + * @return True if the decoder is known to fail when adapting. + */ + private static boolean codecNeedsDisableAdaptationWorkaround(String name) { + return Util.SDK_INT <= 19 && Util.MODEL.equals("ODROID-XU3") + && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecSelector.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecSelector.java index 8670d38a5..3d495b9d4 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecSelector.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecSelector.java @@ -46,8 +46,7 @@ public interface MediaCodecSelector { * * @param mimeType The mime type for which a decoder is required. * @param requiresSecureDecoder Whether a secure decoder is required. - * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder - * exists. + * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. * @throws DecoderQueryException Thrown if there was an error querying decoders. */ MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecUtil.java index bf5cd001f..a1e8c71d9 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/mediacodec/MediaCodecUtil.java @@ -81,7 +81,8 @@ public final class MediaCodecUtil { /** * Optional call to warm the codec cache for a given mime type. *

    - * Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)}. + * Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)} + * and {@link #getDecoderInfos(String, boolean)}. * * @param mimeType The mime type. * @param secure Whether the decoder is required to support secure decryption. Always pass false @@ -178,11 +179,10 @@ public final class MediaCodecUtil { boolean secure = mediaCodecList.isSecurePlaybackSupported(mimeType, capabilities); if ((secureDecodersExplicit && key.secure == secure) || (!secureDecodersExplicit && !key.secure)) { - decoderInfos.add( - MediaCodecInfo.newInstance(codecName, mimeType, capabilities)); + decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities)); } else if (!secureDecodersExplicit && secure) { - decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", - mimeType, capabilities)); + decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", mimeType, + capabilities)); // It only makes sense to have one synthesized secure decoder, return immediately. return decoderInfos; } @@ -228,10 +228,12 @@ public final class MediaCodecUtil { || "MP3Decoder".equals(name))) { return false; } + // Work around https://github.com/google/ExoPlayer/issues/398 if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) { return false; } + // Work around https://github.com/google/ExoPlayer/issues/1528 if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) && "a70".equals(Util.DEVICE)) { @@ -268,13 +270,15 @@ public final class MediaCodecUtil { } // Work around https://github.com/google/ExoPlayer/issues/548 - // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3 does not render video. + // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3/Note 2 does not render video. if (Util.SDK_INT <= 19 + && "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")) - && "samsung".equals(Util.MANUFACTURER) && "OMX.SEC.vp8.dec".equals(name)) { + || Util.DEVICE.startsWith("jflte") || Util.DEVICE.startsWith("santos") + || Util.DEVICE.startsWith("t0"))) { return false; } + // VP8 decoder on Samsung Galaxy S4 cannot be queried. if (Util.SDK_INT <= 19 && Util.DEVICE.startsWith("jflte") && "OMX.qcom.video.decoder.vp8".equals(name)) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataDecoder.java index a18afb4ee..78b76f0ef 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataDecoder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataDecoder.java @@ -21,21 +21,12 @@ package org.telegram.messenger.exoplayer2.metadata; public interface MetadataDecoder { /** - * Checks whether the decoder supports a given mime type. + * Decodes a {@link Metadata} element from the provided input buffer. * - * @param mimeType A metadata mime type. - * @return Whether the mime type is supported. - */ - boolean canDecode(String mimeType); - - /** - * Decodes a metadata object from the provided binary data. - * - * @param data The raw binary data from which to decode the metadata. - * @param size The size of the input data. + * @param inputBuffer The input buffer to decode. * @return The decoded metadata object. * @throws MetadataDecoderException If a problem occurred decoding the data. */ - Metadata decode(byte[] data, int size) throws MetadataDecoderException; + Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataDecoderFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataDecoderFactory.java new file mode 100755 index 000000000..1156778ea --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataDecoderFactory.java @@ -0,0 +1,83 @@ +/* + * 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 org.telegram.messenger.exoplayer2.metadata; + +import org.telegram.messenger.exoplayer2.Format; +import org.telegram.messenger.exoplayer2.metadata.emsg.EventMessageDecoder; +import org.telegram.messenger.exoplayer2.metadata.id3.Id3Decoder; +import org.telegram.messenger.exoplayer2.metadata.scte35.SpliceInfoDecoder; +import org.telegram.messenger.exoplayer2.util.MimeTypes; + +/** + * A factory for {@link MetadataDecoder} instances. + */ +public interface MetadataDecoderFactory { + + /** + * Returns whether the factory is able to instantiate a {@link MetadataDecoder} for the given + * {@link Format}. + * + * @param format The {@link Format}. + * @return Whether the factory can instantiate a suitable {@link MetadataDecoder}. + */ + boolean supportsFormat(Format format); + + /** + * Creates a {@link MetadataDecoder} for the given {@link Format}. + * + * @param format The {@link Format}. + * @return A new {@link MetadataDecoder}. + * @throws IllegalArgumentException If the {@link Format} is not supported. + */ + MetadataDecoder createDecoder(Format format); + + /** + * Default {@link MetadataDecoder} implementation. + *

    + * The formats supported by this factory are: + *

      + *
    • ID3 ({@link Id3Decoder})
    • + *
    • EMSG ({@link EventMessageDecoder})
    • + *
    • SCTE-35 ({@link SpliceInfoDecoder})
    • + *
    + */ + MetadataDecoderFactory DEFAULT = new MetadataDecoderFactory() { + + @Override + public boolean supportsFormat(Format format) { + String mimeType = format.sampleMimeType; + return MimeTypes.APPLICATION_ID3.equals(mimeType) + || MimeTypes.APPLICATION_EMSG.equals(mimeType) + || MimeTypes.APPLICATION_SCTE35.equals(mimeType); + } + + @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(); + default: + throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); + } + } + + }; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/PriorityHandlerThread.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataInputBuffer.java similarity index 51% rename from TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/PriorityHandlerThread.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataInputBuffer.java index 6a1c4c72b..d9a8f1b2e 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/PriorityHandlerThread.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataInputBuffer.java @@ -13,31 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.telegram.messenger.exoplayer2.util; +package org.telegram.messenger.exoplayer2.metadata; -import android.os.HandlerThread; -import android.os.Process; +import org.telegram.messenger.exoplayer2.Format; +import org.telegram.messenger.exoplayer2.decoder.DecoderInputBuffer; /** - * A {@link HandlerThread} with a specified process priority. + * A {@link DecoderInputBuffer} for a {@link MetadataDecoder}. */ -public final class PriorityHandlerThread extends HandlerThread { - - private final int priority; +public final class MetadataInputBuffer extends DecoderInputBuffer { /** - * @param name The name of the thread. - * @param priority The priority level. See {@link Process#setThreadPriority(int)} for details. + * An offset that must be added to the metadata's timestamps after it's been decoded, or + * {@link Format#OFFSET_SAMPLE_RELATIVE} if {@link #timeUs} should be added. */ - public PriorityHandlerThread(String name, int priority) { - super(name); - this.priority = priority; - } + public long subsampleOffsetUs; - @Override - public void run() { - Process.setThreadPriority(priority); - super.run(); + public MetadataInputBuffer() { + super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataRenderer.java index 93683cf88..e2530d397 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataRenderer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/MetadataRenderer.java @@ -24,9 +24,8 @@ import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.ExoPlaybackException; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.FormatHolder; -import org.telegram.messenger.exoplayer2.decoder.DecoderInputBuffer; import org.telegram.messenger.exoplayer2.util.Assertions; -import java.nio.ByteBuffer; +import java.util.Arrays; /** * A renderer for metadata. @@ -48,16 +47,23 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } 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 addressed. + private static final int MAX_PENDING_METADATA_COUNT = 5; - private final MetadataDecoder metadataDecoder; + private final MetadataDecoderFactory decoderFactory; private final Output output; private final Handler outputHandler; private final FormatHolder formatHolder; - private final DecoderInputBuffer buffer; + private final MetadataInputBuffer buffer; + private final Metadata[] pendingMetadata; + private final long[] pendingMetadataTimestamps; + private int pendingMetadataIndex; + private int pendingMetadataCount; + private MetadataDecoder decoder; private boolean inputStreamEnded; - private long pendingMetadataTimestamp; - private Metadata pendingMetadata; /** * @param output The output. @@ -66,43 +72,68 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { * looper associated with the application's main thread, which can be obtained using * {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be * called directly on the player's internal rendering thread. - * @param metadataDecoder A decoder for the metadata. */ - public MetadataRenderer(Output output, Looper outputLooper, MetadataDecoder metadataDecoder) { + public MetadataRenderer(Output output, Looper outputLooper) { + this(output, outputLooper, MetadataDecoderFactory.DEFAULT); + } + + /** + * @param output The output. + * @param outputLooper The looper associated with the thread on which the output should be called. + * If the output makes use of standard Android UI components, then this should normally be the + * looper associated with the application's main thread, which can be obtained using + * {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be + * called directly on the player's internal rendering thread. + * @param decoderFactory A factory from which to obtain {@link MetadataDecoder} instances. + */ + public MetadataRenderer(Output output, Looper outputLooper, + MetadataDecoderFactory decoderFactory) { super(C.TRACK_TYPE_METADATA); this.output = Assertions.checkNotNull(output); this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); - this.metadataDecoder = Assertions.checkNotNull(metadataDecoder); + this.decoderFactory = Assertions.checkNotNull(decoderFactory); formatHolder = new FormatHolder(); - buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + buffer = new MetadataInputBuffer(); + pendingMetadata = new Metadata[MAX_PENDING_METADATA_COUNT]; + pendingMetadataTimestamps = new long[MAX_PENDING_METADATA_COUNT]; } @Override public int supportsFormat(Format format) { - return metadataDecoder.canDecode(format.sampleMimeType) ? FORMAT_HANDLED - : FORMAT_UNSUPPORTED_TYPE; + return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + } + + @Override + protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + decoder = decoderFactory.createDecoder(formats[0]); } @Override protected void onPositionReset(long positionUs, boolean joining) { - pendingMetadata = null; + flushPendingMetadata(); inputStreamEnded = false; } @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (!inputStreamEnded && pendingMetadata == null) { + if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) { buffer.clear(); - int result = readSource(formatHolder, buffer); + int result = readSource(formatHolder, buffer, false); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { inputStreamEnded = true; + } else if (buffer.isDecodeOnly()) { + // Do nothing. Note this assumes that all metadata buffers can be decoded independently. + // If we ever need to support a metadata format where this is not the case, we'll need to + // pass the buffer to the decoder and discard the output. } else { - pendingMetadataTimestamp = buffer.timeUs; + buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; + buffer.flip(); try { - buffer.flip(); - ByteBuffer bufferData = buffer.data; - pendingMetadata = metadataDecoder.decode(bufferData.array(), bufferData.limit()); + int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; + pendingMetadata[index] = decoder.decode(buffer); + pendingMetadataTimestamps[index] = buffer.timeUs; + pendingMetadataCount++; } catch (MetadataDecoderException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } @@ -110,15 +141,18 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } - if (pendingMetadata != null && pendingMetadataTimestamp <= positionUs) { - invokeRenderer(pendingMetadata); - pendingMetadata = null; + if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) { + invokeRenderer(pendingMetadata[pendingMetadataIndex]); + pendingMetadata[pendingMetadataIndex] = null; + pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT; + pendingMetadataCount--; } } @Override protected void onDisabled() { - pendingMetadata = null; + flushPendingMetadata(); + decoder = null; super.onDisabled(); } @@ -140,6 +174,12 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } + private void flushPendingMetadata() { + Arrays.fill(pendingMetadata, null); + pendingMetadataIndex = 0; + pendingMetadataCount = 0; + } + @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { @@ -147,8 +187,10 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { case MSG_INVOKE_RENDERER: invokeRendererInternal((Metadata) msg.obj); return true; + default: + // Should never happen. + throw new IllegalStateException(); } - return false; } private void invokeRendererInternal(Metadata metadata) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/emsg/EventMessage.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/emsg/EventMessage.java new file mode 100755 index 000000000..e8b237d7c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/emsg/EventMessage.java @@ -0,0 +1,141 @@ +/* + * 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 org.telegram.messenger.exoplayer2.metadata.emsg; + +import android.os.Parcel; +import android.os.Parcelable; +import org.telegram.messenger.exoplayer2.metadata.Metadata; +import org.telegram.messenger.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * An Event Message (emsg) as defined in ISO 23009-1. + */ +public final class EventMessage implements Metadata.Entry { + + /** + * The message scheme. + */ + public final String schemeIdUri; + + /** + * The value for the event. + */ + public final String value; + + /** + * The duration of the event in milliseconds. + */ + public final long durationMs; + + /** + * The instance identifier. + */ + public final long id; + + /** + * The body of the message. + */ + public final byte[] messageData; + + // Lazily initialized hashcode. + private int hashCode; + + /** + * + * @param schemeIdUri The message scheme. + * @param value The value for the event. + * @param durationMs The duration of the event in milliseconds. + * @param id The instance identifier. + * @param messageData The body of the message. + */ + public EventMessage(String schemeIdUri, String value, long durationMs, long id, + byte[] messageData) { + this.schemeIdUri = schemeIdUri; + this.value = value; + this.durationMs = durationMs; + this.id = id; + this.messageData = messageData; + } + + /* package */ EventMessage(Parcel in) { + schemeIdUri = in.readString(); + value = in.readString(); + durationMs = in.readLong(); + id = in.readLong(); + messageData = in.createByteArray(); + } + + @Override + public int hashCode() { + if (hashCode == 0) { + int result = 17; + result = 31 * result + (schemeIdUri != null ? schemeIdUri.hashCode() : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (int) (durationMs ^ (durationMs >>> 32)); + result = 31 * result + (int) (id ^ (id >>> 32)); + result = 31 * result + Arrays.hashCode(messageData); + hashCode = result; + } + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + EventMessage other = (EventMessage) obj; + return durationMs == other.durationMs && id == other.id + && Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value) + && Arrays.equals(messageData, other.messageData); + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(schemeIdUri); + dest.writeString(value); + dest.writeLong(durationMs); + dest.writeLong(id); + dest.writeByteArray(messageData); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public EventMessage createFromParcel(Parcel in) { + return new EventMessage(in); + } + + @Override + public EventMessage[] newArray(int size) { + return new EventMessage[size]; + } + + }; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/emsg/EventMessageDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/emsg/EventMessageDecoder.java new file mode 100755 index 000000000..026bb3f71 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -0,0 +1,49 @@ +/* + * 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 org.telegram.messenger.exoplayer2.metadata.emsg; + +import org.telegram.messenger.exoplayer2.metadata.Metadata; +import org.telegram.messenger.exoplayer2.metadata.MetadataDecoder; +import org.telegram.messenger.exoplayer2.metadata.MetadataInputBuffer; +import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Decodes Event Message (emsg) atoms, as defined in ISO 23009-1. + *

    + * Atom data should be provided to the decoder without the full atom header (i.e. starting from the + * first byte of the scheme_id_uri field). + */ +public final class EventMessageDecoder implements MetadataDecoder { + + @Override + public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = inputBuffer.data; + byte[] data = buffer.array(); + int size = buffer.limit(); + ParsableByteArray emsgData = new ParsableByteArray(data, size); + String schemeIdUri = emsgData.readNullTerminatedString(); + String value = emsgData.readNullTerminatedString(); + long timescale = emsgData.readUnsignedInt(); + emsgData.skipBytes(4); // presentation_time_delta + long durationMs = (emsgData.readUnsignedInt() * 1000) / timescale; + long id = emsgData.readUnsignedInt(); + byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size); + return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData)); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/ChapterFrame.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/ChapterFrame.java new file mode 100755 index 000000000..11bc08822 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/ChapterFrame.java @@ -0,0 +1,142 @@ +/* + * 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 org.telegram.messenger.exoplayer2.metadata.id3; + +import android.os.Parcel; +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * Chapter information ID3 frame. + */ +public final class ChapterFrame extends Id3Frame { + + public static final String ID = "CHAP"; + + public final String chapterId; + public final int startTimeMs; + public final int endTimeMs; + /** + * The byte offset of the start of the chapter, or {@link C#POSITION_UNSET} if not set. + */ + public final long startOffset; + /** + * The byte offset of the end of the chapter, or {@link C#POSITION_UNSET} if not set. + */ + public final long endOffset; + private final Id3Frame[] subFrames; + + public ChapterFrame(String chapterId, int startTimeMs, int endTimeMs, long startOffset, + long endOffset, Id3Frame[] subFrames) { + super(ID); + this.chapterId = chapterId; + this.startTimeMs = startTimeMs; + this.endTimeMs = endTimeMs; + this.startOffset = startOffset; + this.endOffset = endOffset; + this.subFrames = subFrames; + } + + /* package */ ChapterFrame(Parcel in) { + super(ID); + this.chapterId = in.readString(); + this.startTimeMs = in.readInt(); + this.endTimeMs = in.readInt(); + this.startOffset = in.readLong(); + this.endOffset = in.readLong(); + int subFrameCount = in.readInt(); + subFrames = new Id3Frame[subFrameCount]; + for (int i = 0; i < subFrameCount; i++) { + subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader()); + } + } + + /** + * Returns the number of sub-frames. + */ + public int getSubFrameCount() { + return subFrames.length; + } + + /** + * Returns the sub-frame at {@code index}. + */ + public Id3Frame getSubFrame(int index) { + return subFrames[index]; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ChapterFrame other = (ChapterFrame) obj; + return startTimeMs == other.startTimeMs + && endTimeMs == other.endTimeMs + && startOffset == other.startOffset + && endOffset == other.endOffset + && Util.areEqual(chapterId, other.chapterId) + && Arrays.equals(subFrames, other.subFrames); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + startTimeMs; + result = 31 * result + endTimeMs; + result = 31 * result + (int) startOffset; + result = 31 * result + (int) endOffset; + result = 31 * result + (chapterId != null ? chapterId.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(chapterId); + dest.writeInt(startTimeMs); + dest.writeInt(endTimeMs); + dest.writeLong(startOffset); + dest.writeLong(endOffset); + dest.writeInt(subFrames.length); + for (Id3Frame subFrame : subFrames) { + dest.writeParcelable(subFrame, 0); + } + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + + @Override + public ChapterFrame createFromParcel(Parcel in) { + return new ChapterFrame(in); + } + + @Override + public ChapterFrame[] newArray(int size) { + return new ChapterFrame[size]; + } + + }; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/ChapterTocFrame.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/ChapterTocFrame.java new file mode 100755 index 000000000..3cf33ef4c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/ChapterTocFrame.java @@ -0,0 +1,123 @@ +/* + * 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 org.telegram.messenger.exoplayer2.metadata.id3; + +import android.os.Parcel; +import org.telegram.messenger.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * Chapter table of contents ID3 frame. + */ +public final class ChapterTocFrame extends Id3Frame { + + public static final String ID = "CTOC"; + + public final String elementId; + public final boolean isRoot; + public final boolean isOrdered; + public final String[] children; + private final Id3Frame[] subFrames; + + public ChapterTocFrame(String elementId, boolean isRoot, boolean isOrdered, String[] children, + Id3Frame[] subFrames) { + super(ID); + this.elementId = elementId; + this.isRoot = isRoot; + this.isOrdered = isOrdered; + this.children = children; + this.subFrames = subFrames; + } + + /* package */ ChapterTocFrame(Parcel in) { + super(ID); + this.elementId = in.readString(); + this.isRoot = in.readByte() != 0; + this.isOrdered = in.readByte() != 0; + this.children = in.createStringArray(); + int subFrameCount = in.readInt(); + subFrames = new Id3Frame[subFrameCount]; + for (int i = 0; i < subFrameCount; i++) { + subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader()); + } + } + + /** + * Returns the number of sub-frames. + */ + public int getSubFrameCount() { + return subFrames.length; + } + + /** + * Returns the sub-frame at {@code index}. + */ + public Id3Frame getSubFrame(int index) { + return subFrames[index]; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ChapterTocFrame other = (ChapterTocFrame) obj; + return isRoot == other.isRoot + && isOrdered == other.isOrdered + && Util.areEqual(elementId, other.elementId) + && Arrays.equals(children, other.children) + && Arrays.equals(subFrames, other.subFrames); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (isRoot ? 1 : 0); + result = 31 * result + (isOrdered ? 1 : 0); + result = 31 * result + (elementId != null ? elementId.hashCode() : 0); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(elementId); + dest.writeByte((byte) (isRoot ? 1 : 0)); + dest.writeByte((byte) (isOrdered ? 1 : 0)); + dest.writeStringArray(children); + dest.writeInt(subFrames.length); + for (int i = 0; i < subFrames.length; i++) { + dest.writeParcelable(subFrames[i], 0); + } + } + + public static final Creator CREATOR = new Creator() { + + @Override + public ChapterTocFrame createFromParcel(Parcel in) { + return new ChapterTocFrame(in); + } + + @Override + public ChapterTocFrame[] newArray(int size) { + return new ChapterTocFrame[size]; + } + + }; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/Id3Decoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/Id3Decoder.java index 80b9d2bbe..b3bb3e276 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/Id3Decoder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/Id3Decoder.java @@ -16,12 +16,14 @@ package org.telegram.messenger.exoplayer2.metadata.id3; import android.util.Log; +import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.metadata.Metadata; import org.telegram.messenger.exoplayer2.metadata.MetadataDecoder; -import org.telegram.messenger.exoplayer2.util.MimeTypes; +import org.telegram.messenger.exoplayer2.metadata.MetadataInputBuffer; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; import org.telegram.messenger.exoplayer2.util.Util; import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -32,6 +34,25 @@ import java.util.Locale; */ public final class Id3Decoder implements MetadataDecoder { + /** + * A predicate for determining whether individual frames should be decoded. + */ + public interface FramePredicate { + + /** + * Returns whether a frame with the specified parameters should be decoded. + * + * @param majorVersion The major version of the ID3 tag. + * @param id0 The first byte of the frame ID. + * @param id1 The second byte of the frame ID. + * @param id2 The third byte of the frame ID. + * @param id3 The fourth byte of the frame ID. + * @return Whether the frame should be decoded. + */ + boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3); + + } + private static final String TAG = "Id3Decoder"; /** @@ -43,17 +64,46 @@ public final class Id3Decoder implements MetadataDecoder { */ public static final int ID3_HEADER_LENGTH = 10; + private static final int FRAME_FLAG_V3_IS_COMPRESSED = 0x0080; + private static final int FRAME_FLAG_V3_IS_ENCRYPTED = 0x0040; + private static final int FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER = 0x0020; + private static final int FRAME_FLAG_V4_IS_COMPRESSED = 0x0008; + private static final int FRAME_FLAG_V4_IS_ENCRYPTED = 0x0004; + private static final int FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER = 0x0040; + private static final int FRAME_FLAG_V4_IS_UNSYNCHRONIZED = 0x0002; + private static final int FRAME_FLAG_V4_HAS_DATA_LENGTH = 0x0001; + private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; private static final int ID3_TEXT_ENCODING_UTF_16 = 1; private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; - @Override - public boolean canDecode(String mimeType) { - return mimeType.equals(MimeTypes.APPLICATION_ID3); + private final FramePredicate framePredicate; + + public Id3Decoder() { + this(null); + } + + /** + * @param framePredicate Determines which frames are decoded. May be null to decode all frames. + */ + public Id3Decoder(FramePredicate framePredicate) { + this.framePredicate = framePredicate; } @Override + public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = inputBuffer.data; + return decode(buffer.array(), buffer.limit()); + } + + /** + * Decodes ID3 tags. + * + * @param data The bytes to decode ID3 tags from. + * @param size Amount of bytes in {@code data} to read. + * @return A {@link Metadata} object containing the decoded ID3 tags. + */ public Metadata decode(byte[] data, int size) { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); @@ -64,6 +114,7 @@ public final class Id3Decoder implements MetadataDecoder { } int startPosition = id3Data.getPosition(); + int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; int framesSize = id3Header.framesSize; if (id3Header.isUnsynchronized) { framesSize = removeUnsynchronization(id3Data, id3Header.framesSize); @@ -71,20 +122,18 @@ public final class Id3Decoder implements MetadataDecoder { id3Data.setLimit(startPosition + framesSize); boolean unsignedIntFrameSizeHack = false; - if (id3Header.majorVersion == 4) { - if (!validateV4Frames(id3Data, false)) { - if (validateV4Frames(id3Data, true)) { - unsignedIntFrameSizeHack = true; - } else { - Log.w(TAG, "Failed to validate V4 ID3 tag"); - return null; - } + if (!validateFrames(id3Data, id3Header.majorVersion, frameHeaderSize, false)) { + if (id3Header.majorVersion == 4 && validateFrames(id3Data, 4, frameHeaderSize, true)) { + unsignedIntFrameSizeHack = true; + } else { + Log.w(TAG, "Failed to validate ID3 tag with majorVersion=" + id3Header.majorVersion); + return null; } } - int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; while (id3Data.bytesLeft() >= frameHeaderSize) { - Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack); + Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack, + frameHeaderSize, framePredicate); if (frame != null) { id3Frames.add(frame); } @@ -148,18 +197,30 @@ public final class Id3Decoder implements MetadataDecoder { return new Id3Header(majorVersion, isUnsynchronized, framesSize); } - private static boolean validateV4Frames(ParsableByteArray id3Data, - boolean unsignedIntFrameSizeHack) { + private static boolean validateFrames(ParsableByteArray id3Data, int majorVersion, + int frameHeaderSize, boolean unsignedIntFrameSizeHack) { int startPosition = id3Data.getPosition(); try { - while (id3Data.bytesLeft() >= 10) { - int id = id3Data.readInt(); - int frameSize = id3Data.readUnsignedIntToInt(); - int flags = id3Data.readUnsignedShort(); + while (id3Data.bytesLeft() >= frameHeaderSize) { + // Read the next frame header. + int id; + long frameSize; + int flags; + if (majorVersion >= 3) { + id = id3Data.readInt(); + frameSize = id3Data.readUnsignedInt(); + flags = id3Data.readUnsignedShort(); + } else { + id = id3Data.readUnsignedInt24(); + frameSize = id3Data.readUnsignedInt24(); + flags = 0; + } + // Validate the frame header and skip to the next one. if (id == 0 && frameSize == 0 && flags == 0) { + // We've reached zero padding after the end of the final frame. return true; } else { - if (!unsignedIntFrameSizeHack) { + if (majorVersion == 4 && !unsignedIntFrameSizeHack) { // Parse the data size as a synchsafe integer, as per the spec. if ((frameSize & 0x808080L) != 0) { return false; @@ -167,11 +228,21 @@ public final class Id3Decoder implements MetadataDecoder { frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7) | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21); } + boolean hasGroupIdentifier = false; + boolean hasDataLength = false; + if (majorVersion == 4) { + hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0; + hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0; + } else if (majorVersion == 3) { + hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0; + // A V3 frame has data length if and only if it's compressed. + hasDataLength = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0; + } int minimumFrameSize = 0; - if ((flags & 0x0040) != 0 /* hasGroupIdentifier */) { + if (hasGroupIdentifier) { minimumFrameSize++; } - if ((flags & 0x0001) != 0 /* hasDataLength */) { + if (hasDataLength) { minimumFrameSize += 4; } if (frameSize < minimumFrameSize) { @@ -180,7 +251,7 @@ public final class Id3Decoder implements MetadataDecoder { if (id3Data.bytesLeft() < frameSize) { return false; } - id3Data.skipBytes(frameSize); // flags + id3Data.skipBytes((int) frameSize); // flags } } return true; @@ -190,7 +261,7 @@ public final class Id3Decoder implements MetadataDecoder { } private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data, - boolean unsignedIntFrameSizeHack) { + boolean unsignedIntFrameSizeHack, int frameHeaderSize, FramePredicate framePredicate) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); @@ -224,6 +295,13 @@ public final class Id3Decoder implements MetadataDecoder { return null; } + if (framePredicate != null + && !framePredicate.evaluate(majorVersion, frameId0, frameId1, frameId2, frameId3)) { + // Filtered by the predicate. + id3Data.setPosition(nextFramePosition); + return null; + } + // Frame flags. boolean isCompressed = false; boolean isEncrypted = false; @@ -231,16 +309,17 @@ public final class Id3Decoder implements MetadataDecoder { boolean hasDataLength = false; boolean hasGroupIdentifier = false; if (majorVersion == 3) { - isCompressed = (flags & 0x0080) != 0; - isEncrypted = (flags & 0x0040) != 0; - hasGroupIdentifier = (flags & 0x0020) != 0; + isCompressed = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0; + isEncrypted = (flags & FRAME_FLAG_V3_IS_ENCRYPTED) != 0; + hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0; + // A V3 frame has data length if and only if it's compressed. hasDataLength = isCompressed; } else if (majorVersion == 4) { - hasGroupIdentifier = (flags & 0x0040) != 0; - isCompressed = (flags & 0x0008) != 0; - isEncrypted = (flags & 0x0004) != 0; - isUnsynchronized = (flags & 0x0002) != 0; - hasDataLength = (flags & 0x0001) != 0; + hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0; + isCompressed = (flags & FRAME_FLAG_V4_IS_COMPRESSED) != 0; + isEncrypted = (flags & FRAME_FLAG_V4_IS_ENCRYPTED) != 0; + isUnsynchronized = (flags & FRAME_FLAG_V4_IS_UNSYNCHRONIZED) != 0; + hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0; } if (isCompressed || isEncrypted) { @@ -266,6 +345,15 @@ public final class Id3Decoder implements MetadataDecoder { if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && (majorVersion == 2 || frameId3 == 'X')) { frame = decodeTxxxFrame(id3Data, frameSize); + } else if (frameId0 == 'T') { + String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3); + frame = decodeTextInformationFrame(id3Data, frameSize, id); + } else if (frameId0 == 'W' && frameId1 == 'X' && frameId2 == 'X' + && (majorVersion == 2 || frameId3 == 'X')) { + frame = decodeWxxxFrame(id3Data, frameSize); + } else if (frameId0 == 'W') { + String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3); + frame = decodeUrlLinkFrame(id3Data, frameSize, id); } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { frame = decodePrivFrame(id3Data, frameSize); } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' @@ -274,20 +362,24 @@ public final class Id3Decoder implements MetadataDecoder { } else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C') : (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) { frame = decodeApicFrame(id3Data, frameSize, majorVersion); - } else if (frameId0 == 'T') { - String id = majorVersion == 2 - ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) - : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); - frame = decodeTextInformationFrame(id3Data, frameSize, id); } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' && (frameId3 == 'M' || majorVersion == 2)) { frame = decodeCommentFrame(id3Data, frameSize); + } else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') { + frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, + frameHeaderSize, framePredicate); + } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') { + frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, + frameHeaderSize, framePredicate); } else { - String id = majorVersion == 2 - ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) - : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); + String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3); frame = decodeBinaryFrame(id3Data, frameSize, id); } + if (frame == null) { + Log.w(TAG, "Failed to decode frame: id=" + + getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3) + ", frameSize=" + + frameSize); + } return frame; } catch (UnsupportedEncodingException e) { Log.w(TAG, "Unsupported character encoding"); @@ -297,8 +389,13 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) + private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { + if (frameSize < 1) { + // Frame is malformed. + return null; + } + int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); @@ -308,11 +405,74 @@ public final class Id3Decoder implements MetadataDecoder { int descriptionEndIndex = indexOfEos(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); + String value; int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); - int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); - String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); + if (valueStartIndex < data.length) { + int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); + value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); + } else { + value = ""; + } - return new TxxxFrame(description, value); + return new TextInformationFrame("TXXX", description, value); + } + + private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, + int frameSize, String id) throws UnsupportedEncodingException { + if (frameSize < 1) { + // Frame is malformed. + return null; + } + + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int valueEndIndex = indexOfEos(data, 0, encoding); + String value = new String(data, 0, valueEndIndex, charset); + + return new TextInformationFrame(id, null, value); + } + + private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { + if (frameSize < 1) { + // Frame is malformed. + return null; + } + + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int descriptionEndIndex = indexOfEos(data, 0, encoding); + String description = new String(data, 0, descriptionEndIndex, charset); + + String url; + int urlStartIndex = descriptionEndIndex + delimiterLength(encoding); + if (urlStartIndex < data.length) { + int urlEndIndex = indexOfZeroByte(data, urlStartIndex); + url = new String(data, urlStartIndex, urlEndIndex - urlStartIndex, "ISO-8859-1"); + } else { + url = ""; + } + + return new UrlLinkFrame("WXXX", description, url); + } + + private static UrlLinkFrame decodeUrlLinkFrame(ParsableByteArray id3Data, int frameSize, + String id) throws UnsupportedEncodingException { + byte[] data = new byte[frameSize]; + id3Data.readBytes(data, 0, frameSize); + + int urlEndIndex = indexOfZeroByte(data, 0); + String url = new String(data, 0, urlEndIndex, "ISO-8859-1"); + + return new UrlLinkFrame(id, null, url); } private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize) @@ -323,8 +483,13 @@ public final class Id3Decoder implements MetadataDecoder { int ownerEndIndex = indexOfZeroByte(data, 0); String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1"); + byte[] privateData; int privateDataStartIndex = ownerEndIndex + 1; - byte[] privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length); + if (privateDataStartIndex < data.length) { + privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length); + } else { + privateData = new byte[0]; + } return new PrivFrame(owner, privateData); } @@ -368,13 +533,13 @@ public final class Id3Decoder implements MetadataDecoder { int mimeTypeEndIndex; if (majorVersion == 2) { mimeTypeEndIndex = 2; - mimeType = "image/" + new String(data, 0, 3, "ISO-8859-1").toLowerCase(); + mimeType = "image/" + Util.toLowerInvariant(new String(data, 0, 3, "ISO-8859-1")); if (mimeType.equals("image/jpg")) { mimeType = "image/jpeg"; } } else { mimeTypeEndIndex = indexOfZeroByte(data, 0); - mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1").toLowerCase(); + mimeType = Util.toLowerInvariant(new String(data, 0, mimeTypeEndIndex, "ISO-8859-1")); if (mimeType.indexOf('/') == -1) { mimeType = "image/" + mimeType; } @@ -395,6 +560,11 @@ public final class Id3Decoder implements MetadataDecoder { private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { + if (frameSize < 4) { + // Frame is malformed. + return null; + } + int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); @@ -408,25 +578,88 @@ public final class Id3Decoder implements MetadataDecoder { int descriptionEndIndex = indexOfEos(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); + String text; int textStartIndex = descriptionEndIndex + delimiterLength(encoding); - int textEndIndex = indexOfEos(data, textStartIndex, encoding); - String text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset); + if (textStartIndex < data.length) { + int textEndIndex = indexOfEos(data, textStartIndex, encoding); + text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset); + } else { + text = ""; + } return new CommentFrame(language, description, text); } - private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, - int frameSize, String id) throws UnsupportedEncodingException { - int encoding = id3Data.readUnsignedByte(); - String charset = getCharsetName(encoding); + private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize, + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, + FramePredicate framePredicate) throws UnsupportedEncodingException { + int framePosition = id3Data.getPosition(); + int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); + String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition, + "ISO-8859-1"); + id3Data.setPosition(chapterIdEndIndex + 1); - byte[] data = new byte[frameSize - 1]; - id3Data.readBytes(data, 0, frameSize - 1); + int startTime = id3Data.readInt(); + int endTime = id3Data.readInt(); + long startOffset = id3Data.readUnsignedInt(); + if (startOffset == 0xFFFFFFFFL) { + startOffset = C.POSITION_UNSET; + } + long endOffset = id3Data.readUnsignedInt(); + if (endOffset == 0xFFFFFFFFL) { + endOffset = C.POSITION_UNSET; + } - int descriptionEndIndex = indexOfEos(data, 0, encoding); - String description = new String(data, 0, descriptionEndIndex, charset); + ArrayList subFrames = new ArrayList<>(); + int limit = framePosition + frameSize; + while (id3Data.getPosition() < limit) { + Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, + frameHeaderSize, framePredicate); + if (frame != null) { + subFrames.add(frame); + } + } - return new TextInformationFrame(id, description); + Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()]; + subFrames.toArray(subFrameArray); + return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray); + } + + private static ChapterTocFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize, + int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, + FramePredicate framePredicate) throws UnsupportedEncodingException { + int framePosition = id3Data.getPosition(); + int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); + String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition, + "ISO-8859-1"); + id3Data.setPosition(elementIdEndIndex + 1); + + int ctocFlags = id3Data.readUnsignedByte(); + boolean isRoot = (ctocFlags & 0x0002) != 0; + boolean isOrdered = (ctocFlags & 0x0001) != 0; + + int childCount = id3Data.readUnsignedByte(); + String[] children = new String[childCount]; + for (int i = 0; i < childCount; i++) { + int startIndex = id3Data.getPosition(); + int endIndex = indexOfZeroByte(id3Data.data, startIndex); + children[i] = new String(id3Data.data, startIndex, endIndex - startIndex, "ISO-8859-1"); + id3Data.setPosition(endIndex + 1); + } + + ArrayList subFrames = new ArrayList<>(); + int limit = framePosition + frameSize; + while (id3Data.getPosition() < limit) { + Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, + frameHeaderSize, framePredicate); + if (frame != null) { + subFrames.add(frame); + } + } + + Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()]; + subFrames.toArray(subFrameArray); + return new ChapterTocFrame(elementId, isRoot, isOrdered, children, subFrameArray); } private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, @@ -458,6 +691,7 @@ public final class Id3Decoder implements MetadataDecoder { /** * Maps encoding byte from ID3v2 frame to a Charset. + * * @param encodingByte The value of encoding byte from ID3v2 frame. * @return Charset name. */ @@ -476,6 +710,12 @@ public final class Id3Decoder implements MetadataDecoder { } } + private static String getFrameId(int majorVersion, int frameId0, int frameId1, int frameId2, + int frameId3) { + return majorVersion == 2 ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) + : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); + } + private static int indexOfEos(byte[] data, int fromIndex, int encoding) { int terminationPos = indexOfZeroByte(data, fromIndex); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/TextInformationFrame.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/TextInformationFrame.java index cf421ae3c..0b06d80f2 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/TextInformationFrame.java @@ -20,20 +20,23 @@ import android.os.Parcelable; import org.telegram.messenger.exoplayer2.util.Util; /** - * Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame. + * Text information ID3 frame. */ public final class TextInformationFrame extends Id3Frame { public final String description; + public final String value; - public TextInformationFrame(String id, String description) { + public TextInformationFrame(String id, String description, String value) { super(id); this.description = description; + this.value = value; } /* package */ TextInformationFrame(Parcel in) { super(in.readString()); description = in.readString(); + value = in.readString(); } @Override @@ -45,7 +48,8 @@ public final class TextInformationFrame extends Id3Frame { return false; } TextInformationFrame other = (TextInformationFrame) obj; - return id.equals(other.id) && Util.areEqual(description, other.description); + return id.equals(other.id) && Util.areEqual(description, other.description) + && Util.areEqual(value, other.value); } @Override @@ -53,6 +57,7 @@ public final class TextInformationFrame extends Id3Frame { int result = 17; result = 31 * result + id.hashCode(); result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); return result; } @@ -60,6 +65,7 @@ public final class TextInformationFrame extends Id3Frame { public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); dest.writeString(description); + dest.writeString(value); } public static final Parcelable.Creator CREATOR = diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/TxxxFrame.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/UrlLinkFrame.java similarity index 55% rename from TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/TxxxFrame.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/UrlLinkFrame.java index ce4be0de0..4e324edd6 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/TxxxFrame.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -20,25 +20,23 @@ import android.os.Parcelable; import org.telegram.messenger.exoplayer2.util.Util; /** - * TXXX (User defined text information) ID3 frame. + * Url link ID3 frame. */ -public final class TxxxFrame extends Id3Frame { - - public static final String ID = "TXXX"; +public final class UrlLinkFrame extends Id3Frame { public final String description; - public final String value; + public final String url; - public TxxxFrame(String description, String value) { - super(ID); + public UrlLinkFrame(String id, String description, String url) { + super(id); this.description = description; - this.value = value; + this.url = url; } - /* package */ TxxxFrame(Parcel in) { - super(ID); + /* package */ UrlLinkFrame(Parcel in) { + super(in.readString()); description = in.readString(); - value = in.readString(); + url = in.readString(); } @Override @@ -49,36 +47,40 @@ public final class TxxxFrame extends Id3Frame { if (obj == null || getClass() != obj.getClass()) { return false; } - TxxxFrame other = (TxxxFrame) obj; - return Util.areEqual(description, other.description) && Util.areEqual(value, other.value); + UrlLinkFrame other = (UrlLinkFrame) obj; + return id.equals(other.id) && Util.areEqual(description, other.description) + && Util.areEqual(url, other.url); } @Override public int hashCode() { int result = 17; + result = 31 * result + id.hashCode(); result = 31 * result + (description != null ? description.hashCode() : 0); - result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (url != null ? url.hashCode() : 0); return result; } @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); dest.writeString(description); - dest.writeString(value); + dest.writeString(url); } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { - @Override - public TxxxFrame createFromParcel(Parcel in) { - return new TxxxFrame(in); - } + @Override + public UrlLinkFrame createFromParcel(Parcel in) { + return new UrlLinkFrame(in); + } - @Override - public TxxxFrame[] newArray(int size) { - return new TxxxFrame[size]; - } + @Override + public UrlLinkFrame[] newArray(int size) { + return new UrlLinkFrame[size]; + } - }; + }; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/PrivateCommand.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/PrivateCommand.java index eb820f1b0..6eca86828 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/PrivateCommand.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/PrivateCommand.java @@ -26,7 +26,6 @@ public final class PrivateCommand extends SpliceCommand { public final long ptsAdjustment; public final long identifier; - public final byte[] commandBytes; private PrivateCommand(long identifier, byte[] commandBytes, long ptsAdjustment) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index 208abb928..ca4d34d3d 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -15,13 +15,14 @@ */ package org.telegram.messenger.exoplayer2.metadata.scte35; -import android.text.TextUtils; import org.telegram.messenger.exoplayer2.metadata.Metadata; import org.telegram.messenger.exoplayer2.metadata.MetadataDecoder; import org.telegram.messenger.exoplayer2.metadata.MetadataDecoderException; -import org.telegram.messenger.exoplayer2.util.MimeTypes; +import org.telegram.messenger.exoplayer2.metadata.MetadataInputBuffer; import org.telegram.messenger.exoplayer2.util.ParsableBitArray; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; +import java.nio.ByteBuffer; /** * Decodes splice info sections and produces splice commands. @@ -37,18 +38,25 @@ public final class SpliceInfoDecoder implements MetadataDecoder { private final ParsableByteArray sectionData; private final ParsableBitArray sectionHeader; + private TimestampAdjuster timestampAdjuster; + public SpliceInfoDecoder() { sectionData = new ParsableByteArray(); sectionHeader = new ParsableBitArray(); } @Override - public boolean canDecode(String mimeType) { - return TextUtils.equals(mimeType, MimeTypes.APPLICATION_SCTE35); - } + public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException { + // Internal timestamps adjustment. + if (timestampAdjuster == null + || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { + timestampAdjuster = new TimestampAdjuster(inputBuffer.timeUs); + timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs); + } - @Override - public Metadata decode(byte[] data, int size) throws MetadataDecoderException { + ByteBuffer buffer = inputBuffer.data; + byte[] data = buffer.array(); + int size = buffer.limit(); sectionData.reset(data, size); sectionHeader.reset(data, size); // table_id(8), section_syntax_indicator(1), private_indicator(1), reserved(2), @@ -71,14 +79,18 @@ public final class SpliceInfoDecoder implements MetadataDecoder { command = SpliceScheduleCommand.parseFromSection(sectionData); break; case TYPE_SPLICE_INSERT: - command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment); + command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment, + timestampAdjuster); break; case TYPE_TIME_SIGNAL: - command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment); + command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment, timestampAdjuster); break; case TYPE_PRIVATE_COMMAND: command = PrivateCommand.parseFromSection(sectionData, spliceCommandLength, ptsAdjustment); break; + default: + // Do nothing. + break; } return command == null ? new Metadata() : new Metadata(command); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/SpliceInsertCommand.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/SpliceInsertCommand.java index d67c564d4..c4fd2ac53 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/SpliceInsertCommand.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/SpliceInsertCommand.java @@ -19,6 +19,7 @@ import android.os.Parcel; import android.os.Parcelable; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -34,6 +35,7 @@ public final class SpliceInsertCommand extends SpliceCommand { public final boolean programSpliceFlag; public final boolean spliceImmediateFlag; public final long programSplicePts; + public final long programSplicePlaybackPositionUs; public final List componentSpliceList; public final boolean autoReturn; public final long breakDuration; @@ -43,14 +45,16 @@ public final class SpliceInsertCommand extends SpliceCommand { private SpliceInsertCommand(long spliceEventId, boolean spliceEventCancelIndicator, boolean outOfNetworkIndicator, boolean programSpliceFlag, boolean spliceImmediateFlag, - long programSplicePts, List componentSpliceList, boolean autoReturn, - long breakDuration, int uniqueProgramId, int availNum, int availsExpected) { + long programSplicePts, long programSplicePlaybackPositionUs, + List componentSpliceList, boolean autoReturn, long breakDuration, + int uniqueProgramId, int availNum, int availsExpected) { this.spliceEventId = spliceEventId; this.spliceEventCancelIndicator = spliceEventCancelIndicator; this.outOfNetworkIndicator = outOfNetworkIndicator; this.programSpliceFlag = programSpliceFlag; this.spliceImmediateFlag = spliceImmediateFlag; this.programSplicePts = programSplicePts; + this.programSplicePlaybackPositionUs = programSplicePlaybackPositionUs; this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); this.autoReturn = autoReturn; this.breakDuration = breakDuration; @@ -66,6 +70,7 @@ public final class SpliceInsertCommand extends SpliceCommand { programSpliceFlag = in.readByte() == 1; spliceImmediateFlag = in.readByte() == 1; programSplicePts = in.readLong(); + programSplicePlaybackPositionUs = in.readLong(); int componentSpliceListSize = in.readInt(); List componentSpliceList = new ArrayList<>(componentSpliceListSize); for (int i = 0; i < componentSpliceListSize; i++) { @@ -80,7 +85,7 @@ public final class SpliceInsertCommand extends SpliceCommand { } /* package */ static SpliceInsertCommand parseFromSection(ParsableByteArray sectionData, - long ptsAdjustment) { + long ptsAdjustment, TimestampAdjuster timestampAdjuster) { long spliceEventId = sectionData.readUnsignedInt(); // splice_event_cancel_indicator(1), reserved(7). boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0; @@ -88,7 +93,7 @@ public final class SpliceInsertCommand extends SpliceCommand { boolean programSpliceFlag = false; boolean spliceImmediateFlag = false; long programSplicePts = C.TIME_UNSET; - ArrayList componentSplices = new ArrayList<>(); + List componentSplices = Collections.emptyList(); int uniqueProgramId = 0; int availNum = 0; int availsExpected = 0; @@ -112,7 +117,8 @@ public final class SpliceInsertCommand extends SpliceCommand { if (!spliceImmediateFlag) { componentSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment); } - componentSplices.add(new ComponentSplice(componentTag, componentSplicePts)); + componentSplices.add(new ComponentSplice(componentTag, componentSplicePts, + timestampAdjuster.adjustTsTimestamp(componentSplicePts))); } } if (durationFlag) { @@ -125,7 +131,8 @@ public final class SpliceInsertCommand extends SpliceCommand { availsExpected = sectionData.readUnsignedByte(); } return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator, - programSpliceFlag, spliceImmediateFlag, programSplicePts, componentSplices, autoReturn, + programSpliceFlag, spliceImmediateFlag, programSplicePts, + timestampAdjuster.adjustTsTimestamp(programSplicePts), componentSplices, autoReturn, duration, uniqueProgramId, availNum, availsExpected); } @@ -136,19 +143,23 @@ public final class SpliceInsertCommand extends SpliceCommand { public final int componentTag; public final long componentSplicePts; + public final long componentSplicePlaybackPositionUs; - private ComponentSplice(int componentTag, long componentSplicePts) { + private ComponentSplice(int componentTag, long componentSplicePts, + long componentSplicePlaybackPositionUs) { this.componentTag = componentTag; this.componentSplicePts = componentSplicePts; + this.componentSplicePlaybackPositionUs = componentSplicePlaybackPositionUs; } public void writeToParcel(Parcel dest) { dest.writeInt(componentTag); dest.writeLong(componentSplicePts); + dest.writeLong(componentSplicePlaybackPositionUs); } public static ComponentSplice createFromParcel(Parcel in) { - return new ComponentSplice(in.readInt(), in.readLong()); + return new ComponentSplice(in.readInt(), in.readLong(), in.readLong()); } } @@ -163,6 +174,7 @@ public final class SpliceInsertCommand extends SpliceCommand { dest.writeByte((byte) (programSpliceFlag ? 1 : 0)); dest.writeByte((byte) (spliceImmediateFlag ? 1 : 0)); dest.writeLong(programSplicePts); + dest.writeLong(programSplicePlaybackPositionUs); int componentSpliceListSize = componentSpliceList.size(); dest.writeInt(componentSpliceListSize); for (int i = 0; i < componentSpliceListSize; i++) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/TimeSignalCommand.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/TimeSignalCommand.java index 349266581..2065f592f 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/TimeSignalCommand.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/metadata/scte35/TimeSignalCommand.java @@ -18,6 +18,7 @@ package org.telegram.messenger.exoplayer2.metadata.scte35; import android.os.Parcel; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; /** * Represents a time signal command as defined in SCTE35, Section 9.3.4. @@ -25,14 +26,18 @@ import org.telegram.messenger.exoplayer2.util.ParsableByteArray; public final class TimeSignalCommand extends SpliceCommand { public final long ptsTime; + public final long playbackPositionUs; - private TimeSignalCommand(long ptsTime) { + private TimeSignalCommand(long ptsTime, long playbackPositionUs) { this.ptsTime = ptsTime; + this.playbackPositionUs = playbackPositionUs; } /* package */ static TimeSignalCommand parseFromSection(ParsableByteArray sectionData, - long ptsAdjustment) { - return new TimeSignalCommand(parseSpliceTime(sectionData, ptsAdjustment)); + long ptsAdjustment, TimestampAdjuster timestampAdjuster) { + long ptsTime = parseSpliceTime(sectionData, ptsAdjustment); + long playbackPositionUs = timestampAdjuster.adjustTsTimestamp(ptsTime); + return new TimeSignalCommand(ptsTime, playbackPositionUs); } /** @@ -61,6 +66,7 @@ public final class TimeSignalCommand extends SpliceCommand { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(ptsTime); + dest.writeLong(playbackPositionUs); } public static final Creator CREATOR = @@ -68,7 +74,7 @@ public final class TimeSignalCommand extends SpliceCommand { @Override public TimeSignalCommand createFromParcel(Parcel in) { - return new TimeSignalCommand(in.readLong()); + return new TimeSignalCommand(in.readLong(), in.readLong()); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ClippingMediaPeriod.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ClippingMediaPeriod.java new file mode 100755 index 000000000..a15c40c21 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ClippingMediaPeriod.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2.source; + +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.FormatHolder; +import org.telegram.messenger.exoplayer2.decoder.DecoderInputBuffer; +import org.telegram.messenger.exoplayer2.trackselection.TrackSelection; +import org.telegram.messenger.exoplayer2.util.Assertions; +import java.io.IOException; + +/** + * Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their + * samples. + */ +public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + /** + * The {@link MediaPeriod} wrapped by this clipping media period. + */ + public final MediaPeriod mediaPeriod; + + private MediaPeriod.Callback callback; + private long startUs; + private long endUs; + private ClippingSampleStream[] sampleStreams; + private boolean pendingInitialDiscontinuity; + + /** + * Creates a new clipping media period that provides a clipped view of the specified + * {@link MediaPeriod}'s sample streams. + *

    + * The clipping start/end positions must be specified by calling {@link #setClipping(long, long)} + * on the playback thread before preparation completes. + * + * @param mediaPeriod The media period to clip. + */ + public ClippingMediaPeriod(MediaPeriod mediaPeriod) { + this.mediaPeriod = mediaPeriod; + startUs = C.TIME_UNSET; + endUs = C.TIME_UNSET; + sampleStreams = new ClippingSampleStream[0]; + } + + /** + * Sets the clipping start/end times for this period, in microseconds. + * + * @param startUs The clipping start time, in microseconds. + * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to + * indicate the end of the period. + */ + public void setClipping(long startUs, long endUs) { + this.startUs = startUs; + this.endUs = endUs; + } + + @Override + public void prepare(MediaPeriod.Callback callback) { + this.callback = callback; + mediaPeriod.prepare(this); + } + + @Override + public void maybeThrowPrepareError() throws IOException { + mediaPeriod.maybeThrowPrepareError(); + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + sampleStreams = new ClippingSampleStream[streams.length]; + SampleStream[] internalStreams = new SampleStream[streams.length]; + for (int i = 0; i < streams.length; i++) { + sampleStreams[i] = (ClippingSampleStream) streams[i]; + internalStreams[i] = sampleStreams[i] != null ? sampleStreams[i].stream : null; + } + long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags, + internalStreams, streamResetFlags, positionUs + startUs); + Assertions.checkState(enablePositionUs == positionUs + startUs + || (enablePositionUs >= startUs + && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs))); + for (int i = 0; i < streams.length; i++) { + if (internalStreams[i] == null) { + sampleStreams[i] = null; + } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) { + sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs, + pendingInitialDiscontinuity); + } + streams[i] = sampleStreams[i]; + } + return enablePositionUs - startUs; + } + + @Override + public void discardBuffer(long positionUs) { + mediaPeriod.discardBuffer(positionUs + startUs); + } + + @Override + public long readDiscontinuity() { + if (pendingInitialDiscontinuity) { + for (ClippingSampleStream sampleStream : sampleStreams) { + if (sampleStream != null) { + sampleStream.clearPendingDiscontinuity(); + } + } + pendingInitialDiscontinuity = false; + // Always read an initial discontinuity, using mediaPeriod's discontinuity if set. + long discontinuityUs = readDiscontinuity(); + return discontinuityUs != C.TIME_UNSET ? discontinuityUs : 0; + } + long discontinuityUs = mediaPeriod.readDiscontinuity(); + if (discontinuityUs == C.TIME_UNSET) { + return C.TIME_UNSET; + } + Assertions.checkState(discontinuityUs >= startUs); + Assertions.checkState(endUs == C.TIME_END_OF_SOURCE || discontinuityUs <= endUs); + return discontinuityUs - startUs; + } + + @Override + public long getBufferedPositionUs() { + long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); + if (bufferedPositionUs == C.TIME_END_OF_SOURCE + || (endUs != C.TIME_END_OF_SOURCE && bufferedPositionUs >= endUs)) { + return C.TIME_END_OF_SOURCE; + } + return Math.max(0, bufferedPositionUs - startUs); + } + + @Override + public long seekToUs(long positionUs) { + for (ClippingSampleStream sampleStream : sampleStreams) { + if (sampleStream != null) { + sampleStream.clearSentEos(); + } + } + long seekUs = mediaPeriod.seekToUs(positionUs + startUs); + Assertions.checkState(seekUs == positionUs + startUs + || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs))); + return seekUs - startUs; + } + + @Override + public long getNextLoadPositionUs() { + long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); + if (nextLoadPositionUs == C.TIME_END_OF_SOURCE + || (endUs != C.TIME_END_OF_SOURCE && nextLoadPositionUs >= endUs)) { + return C.TIME_END_OF_SOURCE; + } + return nextLoadPositionUs - startUs; + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod.continueLoading(positionUs + startUs); + } + + // MediaPeriod.Callback implementation. + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET); + // 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, + // which may be before the clipping start point. When the renderer reads a buffer with a + // negative timestamp, its offset timestamp can jump backwards compared to the last timestamp + // read in the previous period. Renderer implementations may not allow this, so we signal a + // discontinuity which resets the renderers before they read the clipping sample stream. + pendingInitialDiscontinuity = startUs != 0; + callback.onPrepared(this); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + callback.onContinueLoadingRequested(this); + } + + /** + * Wraps a {@link SampleStream} and clips its samples. + */ + private static final class ClippingSampleStream implements SampleStream { + + private final MediaPeriod mediaPeriod; + private final SampleStream stream; + private final long startUs; + private final long endUs; + + private boolean pendingDiscontinuity; + private boolean sentEos; + + public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs, + long endUs, boolean pendingDiscontinuity) { + this.mediaPeriod = mediaPeriod; + this.stream = stream; + this.startUs = startUs; + this.endUs = endUs; + this.pendingDiscontinuity = pendingDiscontinuity; + } + + public void clearPendingDiscontinuity() { + pendingDiscontinuity = false; + } + + public void clearSentEos() { + sentEos = false; + } + + @Override + public boolean isReady() { + return stream.isReady(); + } + + @Override + public void maybeThrowError() throws IOException { + stream.maybeThrowError(); + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean requireFormat) { + if (pendingDiscontinuity) { + return C.RESULT_NOTHING_READ; + } + if (sentEos) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } + int result = stream.readData(formatHolder, buffer, requireFormat); + // TODO: Clear gapless playback metadata if a format was read (if applicable). + if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ + && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ + && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { + buffer.clear(); + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + sentEos = true; + return C.RESULT_BUFFER_READ; + } + if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) { + buffer.timeUs -= startUs; + } + return result; + } + + @Override + public void skipData(long positionUs) { + stream.skipData(startUs + positionUs); + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ClippingMediaSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ClippingMediaSource.java new file mode 100755 index 000000000..108754f1c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ClippingMediaSource.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2.source; + +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.ExoPlayer; +import org.telegram.messenger.exoplayer2.Timeline; +import org.telegram.messenger.exoplayer2.upstream.Allocator; +import org.telegram.messenger.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; + +/** + * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end + * positions. The wrapped source may only have a single period/window and it must not be dynamic + * (live). + */ +public final class ClippingMediaSource implements MediaSource, MediaSource.Listener { + + private final MediaSource mediaSource; + private final long startUs; + private final long endUs; + private final ArrayList mediaPeriods; + + private MediaSource.Listener sourceListener; + private ClippingTimeline clippingTimeline; + + /** + * Creates a new clipping source that wraps the specified source. + * + * @param mediaSource The single-period, non-dynamic source to wrap. + * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to + * start providing samples, in microseconds. + * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop + * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples + * from the specified start point up to the end of the source. + */ + public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { + Assertions.checkArgument(startPositionUs >= 0); + this.mediaSource = Assertions.checkNotNull(mediaSource); + startUs = startPositionUs; + endUs = endPositionUs; + mediaPeriods = new ArrayList<>(); + } + + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + this.sourceListener = listener; + mediaSource.prepareSource(player, false, this); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + + @Override + public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( + mediaSource.createPeriod(index, allocator, startUs + positionUs)); + mediaPeriods.add(mediaPeriod); + mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs); + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + Assertions.checkState(mediaPeriods.remove(mediaPeriod)); + mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + } + + @Override + public void releaseSource() { + mediaSource.releaseSource(); + } + + // MediaSource.Listener implementation. + + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); + sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest); + long startUs = clippingTimeline.startUs; + long endUs = clippingTimeline.endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE + : clippingTimeline.endUs; + int count = mediaPeriods.size(); + for (int i = 0; i < count; i++) { + mediaPeriods.get(i).setClipping(startUs, endUs); + } + } + + /** + * Provides a clipped view of a specified timeline. + */ + private static final class ClippingTimeline extends Timeline { + + private final Timeline timeline; + private final long startUs; + private final long endUs; + + /** + * Creates a new clipping timeline that wraps the specified timeline. + * + * @param timeline The timeline to clip. + * @param startUs The number of microseconds to clip from the start of {@code timeline}. + * @param endUs The end position in microseconds for the clipped timeline relative to the start + * of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end. + */ + public ClippingTimeline(Timeline timeline, long startUs, long endUs) { + Assertions.checkArgument(timeline.getWindowCount() == 1); + Assertions.checkArgument(timeline.getPeriodCount() == 1); + Window window = timeline.getWindow(0, new Window(), false); + Assertions.checkArgument(!window.isDynamic); + long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; + if (window.durationUs != C.TIME_UNSET) { + Assertions.checkArgument(startUs == 0 || window.isSeekable); + Assertions.checkArgument(resolvedEndUs <= window.durationUs); + Assertions.checkArgument(startUs <= resolvedEndUs); + } + Period period = timeline.getPeriod(0, new Period()); + Assertions.checkArgument(period.getPositionInWindowUs() == 0); + this.timeline = timeline; + this.startUs = startUs; + this.endUs = resolvedEndUs; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + window = timeline.getWindow(0, window, setIds, defaultPositionProjectionUs); + window.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; + if (window.defaultPositionUs != C.TIME_UNSET) { + window.defaultPositionUs = Math.max(window.defaultPositionUs, startUs); + window.defaultPositionUs = endUs == C.TIME_UNSET ? window.defaultPositionUs + : Math.min(window.defaultPositionUs, endUs); + window.defaultPositionUs -= startUs; + } + long startMs = C.usToMs(startUs); + if (window.presentationStartTimeMs != C.TIME_UNSET) { + window.presentationStartTimeMs += startMs; + } + if (window.windowStartTimeMs != C.TIME_UNSET) { + window.windowStartTimeMs += startMs; + } + return window; + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + period = timeline.getPeriod(0, period, setIds); + period.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline.getIndexOfPeriod(uid); + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ConcatenatingMediaSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ConcatenatingMediaSource.java index b4891d1bf..90b2aadc9 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ConcatenatingMediaSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ConcatenatingMediaSource.java @@ -17,8 +17,10 @@ package org.telegram.messenger.exoplayer2.source; import android.util.Pair; import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.ExoPlayer; import org.telegram.messenger.exoplayer2.Timeline; import org.telegram.messenger.exoplayer2.upstream.Allocator; +import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; import java.util.HashMap; @@ -53,12 +55,12 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - public void prepareSource(Listener listener) { + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { this.listener = listener; for (int i = 0; i < mediaSources.length; i++) { if (!duplicateFlags[i]) { final int index = i; - mediaSources[i].prepareSource(new Listener() { + mediaSources[i].prepareSource(player, false, new Listener() { @Override public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { handleSourceInfoRefreshed(index, timeline, manifest); @@ -151,12 +153,14 @@ public final class ConcatenatingMediaSource implements MediaSource { public ConcatenatedTimeline(Timeline[] timelines) { int[] sourcePeriodOffsets = new int[timelines.length]; int[] sourceWindowOffsets = new int[timelines.length]; - int periodCount = 0; + long periodCount = 0; int windowCount = 0; for (int i = 0; i < timelines.length; i++) { Timeline timeline = timelines[i]; periodCount += timeline.getPeriodCount(); - sourcePeriodOffsets[i] = periodCount; + Assertions.checkState(periodCount <= Integer.MAX_VALUE, + "ConcatenatingMediaSource children contain too many periods"); + sourcePeriodOffsets[i] = (int) periodCount; windowCount += timeline.getWindowCount(); sourceWindowOffsets[i] = windowCount; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/EmptySampleStream.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/EmptySampleStream.java new file mode 100755 index 000000000..a08bde62a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/EmptySampleStream.java @@ -0,0 +1,50 @@ +/* + * 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 org.telegram.messenger.exoplayer2.source; + +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.FormatHolder; +import org.telegram.messenger.exoplayer2.decoder.DecoderInputBuffer; +import java.io.IOException; + +/** + * An empty {@link SampleStream}. + */ +public final class EmptySampleStream implements SampleStream { + + @Override + public boolean isReady() { + return true; + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } + + @Override + public void skipData(long positionUs) { + // Do nothing. + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ExtractorMediaPeriod.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ExtractorMediaPeriod.java index 8f283c330..f08d05606 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ExtractorMediaPeriod.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ExtractorMediaPeriod.java @@ -40,6 +40,7 @@ import org.telegram.messenger.exoplayer2.upstream.Loader.Loadable; import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.ConditionVariable; import org.telegram.messenger.exoplayer2.util.MimeTypes; +import org.telegram.messenger.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; @@ -62,6 +63,7 @@ import java.io.IOException; private final ExtractorMediaSource.EventListener eventListener; private final MediaSource.Listener sourceListener; private final Allocator allocator; + private final String customCacheKey; private final Loader loader; private final ExtractorHolder extractorHolder; private final ConditionVariable loadCondition; @@ -101,11 +103,13 @@ import java.io.IOException; * @param eventListener A listener of events. May be null if delivery of events is not required. * @param sourceListener A listener to notify when the timeline has been loaded. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache + * indexing. May be null. */ public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors, int minLoadableRetryCount, Handler eventHandler, ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener, - Allocator allocator) { + Allocator allocator, String customCacheKey) { this.uri = uri; this.dataSource = dataSource; this.minLoadableRetryCount = minLoadableRetryCount; @@ -113,6 +117,7 @@ import java.io.IOException; this.eventListener = eventListener; this.sourceListener = sourceListener; this.allocator = allocator; + this.customCacheKey = customCacheKey; loader = new Loader("Loader:ExtractorMediaPeriod"); extractorHolder = new ExtractorHolder(extractors, this); loadCondition = new ConditionVariable(); @@ -229,6 +234,11 @@ import java.io.IOException; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long playbackPositionUs) { if (loadingFinished || (prepared && enabledTrackCount == 0)) { @@ -244,7 +254,7 @@ import java.io.IOException; @Override public long getNextLoadPositionUs() { - return getBufferedPositionUs(); + return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs(); } @Override @@ -291,7 +301,7 @@ import java.io.IOException; boolean seekInsideBuffer = !isPendingReset(); for (int i = 0; seekInsideBuffer && i < trackCount; i++) { if (trackEnabledStates[i]) { - seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs); + seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs, false); } } // If we failed to seek within the sample queues, we need to restart. @@ -320,13 +330,23 @@ import java.io.IOException; loader.maybeThrowError(); } - /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer) { + /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { if (notifyReset || isPendingReset()) { return C.RESULT_NOTHING_READ; } - return sampleQueues.valueAt(track).readData(formatHolder, buffer, loadingFinished, - lastSeekPositionUs); + return sampleQueues.valueAt(track).readData(formatHolder, buffer, formatRequired, + loadingFinished, lastSeekPositionUs); + } + + /* package */ void skipData(int track, long positionUs) { + DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track); + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + sampleQueue.skipAll(); + } else { + sampleQueue.skipToKeyframeBefore(positionUs, true); + } } // Loader.Callback implementation. @@ -343,6 +363,7 @@ import java.io.IOException; sourceListener.onSourceInfoRefreshed( new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null); } + callback.onContinueLoadingRequested(this); } @Override @@ -376,7 +397,7 @@ import java.io.IOException; // ExtractorOutput implementation. Called by the loading thread. @Override - public TrackOutput track(int id) { + public TrackOutput track(int id, int type) { DefaultTrackOutput trackOutput = sampleQueues.get(id); if (trackOutput == null) { trackOutput = new DefaultTrackOutput(allocator); @@ -453,7 +474,7 @@ import java.io.IOException; pendingResetPositionUs = C.TIME_UNSET; return; } - loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs)); + loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs), pendingResetPositionUs); pendingResetPositionUs = C.TIME_UNSET; } extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); @@ -486,7 +507,7 @@ import java.io.IOException; for (int i = 0; i < trackCount; i++) { sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]); } - loadable.setLoadPosition(0); + loadable.setLoadPosition(0, 0); } } @@ -514,7 +535,7 @@ import java.io.IOException; } private boolean isLoadableExceptionFatal(IOException e) { - return e instanceof ExtractorMediaSource.UnrecognizedInputFormatException; + return e instanceof UnrecognizedInputFormatException; } private void notifyLoadError(final IOException error) { @@ -547,13 +568,14 @@ import java.io.IOException; } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer); + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired); } @Override - public void skipToKeyframeBefore(long timeUs) { - sampleQueues.valueAt(track).skipToKeyframeBefore(timeUs); + public void skipData(long positionUs) { + ExtractorMediaPeriod.this.skipData(track, positionUs); } } @@ -578,6 +600,7 @@ import java.io.IOException; private volatile boolean loadCanceled; private boolean pendingExtractorSeek; + private long seekTimeUs; private long length; public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, @@ -591,8 +614,9 @@ import java.io.IOException; this.length = C.LENGTH_UNSET; } - public void setLoadPosition(long position) { + public void setLoadPosition(long position, long timeUs) { positionHolder.position = position; + seekTimeUs = timeUs; pendingExtractorSeek = true; } @@ -613,14 +637,14 @@ import java.io.IOException; ExtractorInput input = null; try { long position = positionHolder.position; - length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, null)); + length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey)); if (length != C.LENGTH_UNSET) { length += position; } input = new DefaultExtractorInput(dataSource, position, length); - Extractor extractor = extractorHolder.selectExtractor(input); + Extractor extractor = extractorHolder.selectExtractor(input, dataSource.getUri()); if (pendingExtractorSeek) { - extractor.seek(position); + extractor.seek(position, seekTimeUs); pendingExtractorSeek = false; } while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { @@ -638,7 +662,7 @@ import java.io.IOException; } else if (input != null) { positionHolder.position = input.getPosition(); } - dataSource.close(); + Util.closeQuietly(dataSource); } } } @@ -670,13 +694,13 @@ import java.io.IOException; * later calls. * * @param input The {@link ExtractorInput} from which data should be read. + * @param uri The {@link Uri} of the data. * @return An initialized extractor for reading {@code input}. - * @throws ExtractorMediaSource.UnrecognizedInputFormatException Thrown if the input format - * could not be detected. + * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected. * @throws IOException Thrown if the input could not be read. * @throws InterruptedException Thrown if the thread was interrupted. */ - public Extractor selectExtractor(ExtractorInput input) + public Extractor selectExtractor(ExtractorInput input, Uri uri) throws IOException, InterruptedException { if (extractor != null) { return extractor; @@ -694,7 +718,8 @@ import java.io.IOException; } } if (extractor == null) { - throw new ExtractorMediaSource.UnrecognizedInputFormatException(extractors); + throw new UnrecognizedInputFormatException("None of the available extractors (" + + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.", uri); } extractor.init(extractorOutput); return extractor; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ExtractorMediaSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ExtractorMediaSource.java index 7be255d8e..51c5a4d83 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ExtractorMediaSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/ExtractorMediaSource.java @@ -18,7 +18,7 @@ package org.telegram.messenger.exoplayer2.source; import android.net.Uri; import android.os.Handler; import org.telegram.messenger.exoplayer2.C; -import org.telegram.messenger.exoplayer2.ParserException; +import org.telegram.messenger.exoplayer2.ExoPlayer; import org.telegram.messenger.exoplayer2.Timeline; import org.telegram.messenger.exoplayer2.extractor.DefaultExtractorsFactory; import org.telegram.messenger.exoplayer2.extractor.Extractor; @@ -26,7 +26,6 @@ import org.telegram.messenger.exoplayer2.extractor.ExtractorsFactory; import org.telegram.messenger.exoplayer2.upstream.Allocator; import org.telegram.messenger.exoplayer2.upstream.DataSource; import org.telegram.messenger.exoplayer2.util.Assertions; -import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; /** @@ -56,18 +55,6 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List } - /** - * Thrown if the input format could not recognized. - */ - public static final class UnrecognizedInputFormatException extends ParserException { - - public UnrecognizedInputFormatException(Extractor[] extractors) { - super("None of the available extractors (" - + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream."); - } - - } - /** * The default minimum number of times to retry loading prior to failing for on-demand streams. */ @@ -92,6 +79,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List private final Handler eventHandler; private final EventListener eventListener; private final Timeline.Period period; + private final String customCacheKey; private MediaSource.Listener sourceListener; private Timeline timeline; @@ -109,7 +97,25 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, - eventListener); + eventListener, null); + } + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the + * possible formats are known, pass a factory that instantiates extractors for those formats. + * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. + * @param eventHandler A handler for events. 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 customCacheKey A custom key that uniquely identifies the original stream. Used for cache + * indexing. May be null. + */ + public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, + String customCacheKey) { + this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, + eventListener, customCacheKey); } /** @@ -121,21 +127,24 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. 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 customCacheKey A custom key that uniquely identifies the original stream. Used for cache + * indexing. May be null. */ public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, - EventListener eventListener) { + EventListener eventListener, String customCacheKey) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.eventHandler = eventHandler; this.eventListener = eventListener; + this.customCacheKey = customCacheKey; period = new Timeline.Period(); } @Override - public void prepareSource(MediaSource.Listener listener) { + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { sourceListener = listener; timeline = new SinglePeriodTimeline(C.TIME_UNSET, false); listener.onSourceInfoRefreshed(timeline, null); @@ -151,7 +160,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List Assertions.checkArgument(index == 0); return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, - this, allocator); + this, allocator, customCacheKey); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/LoopingMediaSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/LoopingMediaSource.java index 25d7881cc..9ceefe7c3 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/LoopingMediaSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/LoopingMediaSource.java @@ -18,6 +18,7 @@ package org.telegram.messenger.exoplayer2.source; import android.util.Log; import android.util.Pair; import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.ExoPlayer; import org.telegram.messenger.exoplayer2.Timeline; import org.telegram.messenger.exoplayer2.upstream.Allocator; import org.telegram.messenger.exoplayer2.util.Assertions; @@ -28,6 +29,13 @@ import java.io.IOException; */ public final class LoopingMediaSource implements MediaSource { + /** + * The maximum number of periods that can be exposed by the source. The value of this constant is + * large enough to cause indefinite looping in practice (the total duration of the looping source + * will be approximately five years if the duration of each period is one second). + */ + public static final int MAX_EXPOSED_PERIODS = 157680000; + private static final String TAG = "LoopingMediaSource"; private final MediaSource childSource; @@ -49,8 +57,8 @@ public final class LoopingMediaSource implements MediaSource { * * @param childSource The {@link MediaSource} to loop. * @param loopCount The desired number of loops. Must be strictly positive. The actual number of - * loops will be capped at the maximum value that can achieved without causing the number of - * periods exposed by the source to exceed {@link Integer#MAX_VALUE}. + * loops will be capped at the maximum that can achieved without causing the number of + * periods exposed by the source to exceed {@link #MAX_EXPOSED_PERIODS}. */ public LoopingMediaSource(MediaSource childSource, int loopCount) { Assertions.checkArgument(loopCount > 0); @@ -59,8 +67,8 @@ public final class LoopingMediaSource implements MediaSource { } @Override - public void prepareSource(final Listener listener) { - childSource.prepareSource(new Listener() { + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, final Listener listener) { + childSource.prepareSource(player, false, new Listener() { @Override public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { childPeriodCount = timeline.getPeriodCount(); @@ -100,8 +108,9 @@ public final class LoopingMediaSource implements MediaSource { this.childTimeline = childTimeline; childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); - // This is the maximum number of loops that can be performed without overflow. - int maxLoopCount = Integer.MAX_VALUE / childPeriodCount; + // This is the maximum number of loops that can be performed without exceeding + // MAX_EXPOSED_PERIODS periods. + int maxLoopCount = MAX_EXPOSED_PERIODS / childPeriodCount; if (loopCount > maxLoopCount) { if (loopCount != Integer.MAX_VALUE) { Log.w(TAG, "Capped loops to avoid overflow: " + loopCount + " -> " + maxLoopCount); @@ -157,6 +166,7 @@ public final class LoopingMediaSource implements MediaSource { int periodIndexOffset = loopCount * childPeriodCount; return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset; } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MediaPeriod.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MediaPeriod.java index b5e1454df..89558cc85 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MediaPeriod.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MediaPeriod.java @@ -16,6 +16,7 @@ package org.telegram.messenger.exoplayer2.source; import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.Timeline; import org.telegram.messenger.exoplayer2.trackselection.TrackSelection; import java.io.IOException; @@ -47,6 +48,10 @@ public interface MediaPeriod extends SequenceableLoader { *

    * {@code callback.onPrepared} is called when preparation completes. If preparation fails, * {@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.Listener#onSourceInfoRefreshed(Timeline, Object)} will be + * called before {@code callback.onPrepared}. * * @param callback Callback to receive updates from this period, including being notified when * preparation completes. @@ -99,6 +104,13 @@ public interface MediaPeriod extends SequenceableLoader { long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs); + /** + * Discards buffered media up to the specified position. + * + * @param positionUs The position in microseconds. + */ + void discardBuffer(long positionUs); + /** * Attempts to read a discontinuity. *

    @@ -133,4 +145,32 @@ public interface MediaPeriod extends SequenceableLoader { */ long seekToUs(long positionUs); + // SequenceableLoader interface. Overridden to provide more specific documentation. + + /** + * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished. + *

    + * This method should only be called after the period has been prepared. It may be called when no + * tracks are selected. + */ + @Override + long getNextLoadPositionUs(); + + /** + * Attempts to continue loading. + *

    + * This method may be called both during and after the period has been prepared. + *

    + * A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the + * {@link Callback} passed to {@link #prepare(Callback)} to request that this method be called + * when the period is permitted to continue loading data. A period may do this both during and + * after preparation. + * + * @param positionUs The current playback position. + * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return + * a different value than prior to the call. False otherwise. + */ + @Override + boolean continueLoading(long positionUs); + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MediaSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MediaSource.java index 3776db620..fa7d7b9e9 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MediaSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MediaSource.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.source; +import org.telegram.messenger.exoplayer2.ExoPlayer; import org.telegram.messenger.exoplayer2.Timeline; import org.telegram.messenger.exoplayer2.upstream.Allocator; import java.io.IOException; @@ -42,9 +43,14 @@ public interface MediaSource { /** * Starts preparation of the source. * + * @param player The player for which this source is being prepared. + * @param isTopLevelSource Whether this source has been passed directly to + * {@link ExoPlayer#prepare(MediaSource)} or + * {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. If {@code false}, this source is + * being prepared by another source (e.g. {@link ConcatenatingMediaSource}) for composition. * @param listener The listener for source events. */ - void prepareSource(Listener listener); + void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener); /** * Throws any pending error encountered while loading or refreshing source information. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MergingMediaPeriod.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MergingMediaPeriod.java index b96017675..524b0336f 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MergingMediaPeriod.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MergingMediaPeriod.java @@ -128,6 +128,13 @@ import java.util.IdentityHashMap; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + for (MediaPeriod period : enabledPeriods) { + period.discardBuffer(positionUs); + } + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MergingMediaSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MergingMediaSource.java index ee8c22b06..00e9dd2a0 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MergingMediaSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/MergingMediaSource.java @@ -16,6 +16,7 @@ package org.telegram.messenger.exoplayer2.source; import android.support.annotation.IntDef; +import org.telegram.messenger.exoplayer2.ExoPlayer; import org.telegram.messenger.exoplayer2.Timeline; import org.telegram.messenger.exoplayer2.upstream.Allocator; import java.io.IOException; @@ -56,8 +57,7 @@ public final class MergingMediaSource implements MediaSource { * The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and * {@link #REASON_PERIOD_COUNT_MISMATCH}. */ - @Reason - public final int reason; + @Reason public final int reason; /** * @param reason The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and @@ -92,11 +92,11 @@ public final class MergingMediaSource implements MediaSource { } @Override - public void prepareSource(final Listener listener) { + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { this.listener = listener; for (int i = 0; i < mediaSources.length; i++) { final int sourceIndex = i; - mediaSources[sourceIndex].prepareSource(new Listener() { + mediaSources[sourceIndex].prepareSource(player, false, new Listener() { @Override public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { handleSourceInfoRefreshed(sourceIndex, timeline, manifest); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SampleStream.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SampleStream.java index b9db4364d..032f27210 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SampleStream.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SampleStream.java @@ -29,7 +29,8 @@ public interface SampleStream { * Returns whether data is available to be read. *

    * Note: If the stream has ended then a buffer with the end of stream flag can always be read from - * {@link #readData(FormatHolder, DecoderInputBuffer)}. Hence an ended stream is always ready. + * {@link #readData(FormatHolder, DecoderInputBuffer, boolean)}. Hence an ended stream is always + * ready. * * @return Whether data is available to be read. */ @@ -44,21 +45,32 @@ public interface SampleStream { /** * Attempts to read from the stream. + *

    + * If the stream has ended then {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set on {@code buffer} + * and {@link C#RESULT_BUFFER_READ} is returned. Else if no data is available then + * {@link C#RESULT_NOTHING_READ} is returned. Else if the format of the media is changing or if + * {@code formatRequired} is set then {@code formatHolder} is populated and + * {@link C#RESULT_FORMAT_READ} is returned. Else {@code buffer} is populated and + * {@link C#RESULT_BUFFER_READ} is returned. * * @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. + * @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}. */ - int readData(FormatHolder formatHolder, DecoderInputBuffer buffer); + int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired); /** - * Attempts to skip to the keyframe before the specified time. + * Attempts to skip to the keyframe before the specified position, or to the end of the stream if + * {@code positionUs} is beyond it. * - * @param timeUs The specified time. + * @param positionUs The specified time. */ - void skipToKeyframeBefore(long timeUs); + void skipData(long positionUs); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SequenceableLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SequenceableLoader.java index ed14e6373..47267b1ed 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SequenceableLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SequenceableLoader.java @@ -17,6 +17,7 @@ package org.telegram.messenger.exoplayer2.source; import org.telegram.messenger.exoplayer2.C; +// TODO: Clarify the requirements for implementing this interface [Internal ref: b/36250203]. /** * A loader that can proceed in approximate synchronization with other loaders. */ diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SinglePeriodTimeline.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SinglePeriodTimeline.java index d19f96ca2..e1f5479f3 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SinglePeriodTimeline.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SinglePeriodTimeline.java @@ -99,7 +99,7 @@ public final class SinglePeriodTimeline extends Timeline { public Period getPeriod(int periodIndex, Period period, boolean setIds) { Assertions.checkIndex(periodIndex, 0, 1); Object id = setIds ? ID : null; - return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs); + return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SingleSampleMediaPeriod.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SingleSampleMediaPeriod.java index 045f8d49d..d641b013a 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SingleSampleMediaPeriod.java @@ -28,6 +28,7 @@ import org.telegram.messenger.exoplayer2.upstream.DataSpec; import org.telegram.messenger.exoplayer2.upstream.Loader; import org.telegram.messenger.exoplayer2.upstream.Loader.Loadable; import org.telegram.messenger.exoplayer2.util.Assertions; +import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -110,6 +111,11 @@ import java.util.Arrays; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { if (loadingFinished || loader.isLoading()) { @@ -204,11 +210,12 @@ import java.util.Arrays; } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean requireFormat) { if (streamState == STREAM_STATE_END_OF_STREAM) { buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; - } else if (streamState == STREAM_STATE_SEND_FORMAT) { + } else if (requireFormat || streamState == STREAM_STATE_SEND_FORMAT) { formatHolder.format = format; streamState = STREAM_STATE_SEND_SAMPLE; return C.RESULT_FORMAT_READ; @@ -228,8 +235,10 @@ import java.util.Arrays; } @Override - public void skipToKeyframeBefore(long timeUs) { - // Do nothing. + public void skipData(long positionUs) { + if (positionUs > 0) { + streamState = STREAM_STATE_END_OF_STREAM; + } } } @@ -276,7 +285,7 @@ import java.util.Arrays; result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize); } } finally { - dataSource.close(); + Util.closeQuietly(dataSource); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SingleSampleMediaSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SingleSampleMediaSource.java index 6a07052f9..27859df7d 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SingleSampleMediaSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/SingleSampleMediaSource.java @@ -17,6 +17,7 @@ package org.telegram.messenger.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import org.telegram.messenger.exoplayer2.ExoPlayer; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.Timeline; import org.telegram.messenger.exoplayer2.upstream.Allocator; @@ -84,7 +85,7 @@ public final class SingleSampleMediaSource implements MediaSource { // MediaSource implementation. @Override - public void prepareSource(MediaSource.Listener listener) { + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { listener.onSourceInfoRefreshed(timeline, null); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/UnrecognizedInputFormatException.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/UnrecognizedInputFormatException.java new file mode 100755 index 000000000..86cd4349a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/UnrecognizedInputFormatException.java @@ -0,0 +1,40 @@ +/* + * 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 org.telegram.messenger.exoplayer2.source; + +import android.net.Uri; +import org.telegram.messenger.exoplayer2.ParserException; + +/** + * Thrown if the input format was not recognized. + */ +public class UnrecognizedInputFormatException extends ParserException { + + /** + * The {@link Uri} from which the unrecognized data was read. + */ + public final Uri uri; + + /** + * @param message The detail message for the exception. + * @param uri The {@link Uri} from which the unrecognized data was read. + */ + public UnrecognizedInputFormatException(String message, Uri uri) { + super(message); + this.uri = uri; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/BaseMediaChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/BaseMediaChunk.java index 1f7ff8670..491ec586f 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/BaseMediaChunk.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/BaseMediaChunk.java @@ -16,19 +16,16 @@ package org.telegram.messenger.exoplayer2.source.chunk; import org.telegram.messenger.exoplayer2.Format; -import org.telegram.messenger.exoplayer2.extractor.DefaultTrackOutput; import org.telegram.messenger.exoplayer2.upstream.DataSource; import org.telegram.messenger.exoplayer2.upstream.DataSpec; /** - * A base implementation of {@link MediaChunk}, for chunks that contain a single track. - *

    - * Loaded samples are output to a {@link DefaultTrackOutput}. + * A base implementation of {@link MediaChunk} that outputs to a {@link BaseMediaChunkOutput}. */ public abstract class BaseMediaChunk extends MediaChunk { - private DefaultTrackOutput trackOutput; - private int firstSampleIndex; + private BaseMediaChunkOutput output; + private int[] firstSampleIndices; /** * @param dataSource The source from which the data should be loaded. @@ -48,29 +45,29 @@ public abstract class BaseMediaChunk extends MediaChunk { } /** - * Initializes the chunk for loading, setting the {@link DefaultTrackOutput} that will receive + * Initializes the chunk for loading, setting the {@link BaseMediaChunkOutput} that will receive * samples as they are loaded. * - * @param trackOutput The output that will receive the loaded samples. + * @param output The output that will receive the loaded media samples. */ - public void init(DefaultTrackOutput trackOutput) { - this.trackOutput = trackOutput; - this.firstSampleIndex = trackOutput.getWriteIndex(); + public void init(BaseMediaChunkOutput output) { + this.output = output; + firstSampleIndices = output.getWriteIndices(); } /** - * Returns the index of the first sample in the output that was passed to - * {@link #init(DefaultTrackOutput)} that will originate from this chunk. + * Returns the index of the first sample in the specified track of the output that will originate + * from this chunk. */ - public final int getFirstSampleIndex() { - return firstSampleIndex; + public final int getFirstSampleIndex(int trackIndex) { + return firstSampleIndices[trackIndex]; } /** - * Returns the track output most recently passed to {@link #init(DefaultTrackOutput)}. + * Returns the output most recently passed to {@link #init(BaseMediaChunkOutput)}. */ - protected final DefaultTrackOutput getTrackOutput() { - return trackOutput; + protected final BaseMediaChunkOutput getOutput() { + return output; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/BaseMediaChunkOutput.java new file mode 100755 index 000000000..ea6cae6b8 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -0,0 +1,79 @@ +/* + * 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 org.telegram.messenger.exoplayer2.source.chunk; + +import android.util.Log; +import org.telegram.messenger.exoplayer2.extractor.DefaultTrackOutput; +import org.telegram.messenger.exoplayer2.extractor.DummyTrackOutput; +import org.telegram.messenger.exoplayer2.extractor.TrackOutput; +import org.telegram.messenger.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; + +/** + * An output for {@link BaseMediaChunk}s. + */ +/* package */ final class BaseMediaChunkOutput implements TrackOutputProvider { + + private static final String TAG = "BaseMediaChunkOutput"; + + private final int[] trackTypes; + private final DefaultTrackOutput[] trackOutputs; + + /** + * @param trackTypes The track types of the individual track outputs. + * @param trackOutputs The individual track outputs. + */ + public BaseMediaChunkOutput(int[] trackTypes, DefaultTrackOutput[] trackOutputs) { + this.trackTypes = trackTypes; + this.trackOutputs = trackOutputs; + } + + @Override + public TrackOutput track(int id, int type) { + for (int i = 0; i < trackTypes.length; i++) { + if (type == trackTypes[i]) { + return trackOutputs[i]; + } + } + Log.e(TAG, "Unmatched track of type: " + type); + return new DummyTrackOutput(); + } + + /** + * Returns the current absolute write indices of the individual track outputs. + */ + public int[] getWriteIndices() { + int[] writeIndices = new int[trackOutputs.length]; + for (int i = 0; i < trackOutputs.length; i++) { + if (trackOutputs[i] != null) { + writeIndices[i] = trackOutputs[i].getWriteIndex(); + } + } + return writeIndices; + } + + /** + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples + * subsequently written to the track outputs. + */ + public void setSampleOffsetUs(long sampleOffsetUs) { + for (DefaultTrackOutput trackOutput : trackOutputs) { + if (trackOutput != null) { + trackOutput.setSampleOffsetUs(sampleOffsetUs); + } + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 17394db63..e6d041b61 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -15,9 +15,10 @@ */ package org.telegram.messenger.exoplayer2.source.chunk; +import android.util.SparseArray; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; -import org.telegram.messenger.exoplayer2.drm.DrmInitData; +import org.telegram.messenger.exoplayer2.extractor.DummyTrackOutput; import org.telegram.messenger.exoplayer2.extractor.Extractor; import org.telegram.messenger.exoplayer2.extractor.ExtractorInput; import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; @@ -30,132 +31,165 @@ import java.io.IOException; /** * An {@link Extractor} wrapper for loading chunks containing a single track. *

    - * The wrapper allows switching of the {@link SingleTrackMetadataOutput} and {@link TrackOutput} - * which receive parsed data. + * The wrapper allows switching of the {@link TrackOutput} that receives parsed data. */ -public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput { +public final class ChunkExtractorWrapper implements ExtractorOutput { /** - * Receives metadata associated with the track as extracted by the wrapped {@link Extractor}. + * Provides {@link TrackOutput} instances to be written to by the wrapper. */ - public interface SingleTrackMetadataOutput { + public interface TrackOutputProvider { /** - * @see ExtractorOutput#seekMap(SeekMap) + * Called to get the {@link TrackOutput} for a specific track. + *

    + * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. + * + * @param id A track identifier. + * @param type The type of the track. Typically one of the + * {@link org.telegram.messenger.exoplayer2.C} {@code TRACK_TYPE_*} constants. + * @return The {@link TrackOutput} for the given track identifier. */ - void seekMap(SeekMap seekMap); + TrackOutput track(int id, int type); } - private final Extractor extractor; + public final Extractor extractor; + private final Format manifestFormat; - private final boolean preferManifestDrmInitData; - private final boolean resendFormatOnInit; + private final SparseArray bindingTrackOutputs; private boolean extractorInitialized; - private SingleTrackMetadataOutput metadataOutput; - private TrackOutput trackOutput; - private Format sentFormat; - - // Accessed only on the loader thread. - private boolean seenTrack; - private int seenTrackId; + private TrackOutputProvider trackOutputProvider; + private SeekMap seekMap; + private Format[] sampleFormats; /** * @param extractor The extractor to wrap. * @param manifestFormat A manifest defined {@link Format} whose data should be merged into any * sample {@link Format} output from the {@link Extractor}. - * @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat} - * should be preferred when the sample and manifest {@link Format}s are merged. - * @param resendFormatOnInit Whether the extractor should resend the previous {@link Format} when - * it is initialized via {@link #init(SingleTrackMetadataOutput, TrackOutput)}. */ - public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, - boolean preferManifestDrmInitData, boolean resendFormatOnInit) { + public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat) { this.extractor = extractor; this.manifestFormat = manifestFormat; - this.preferManifestDrmInitData = preferManifestDrmInitData; - this.resendFormatOnInit = resendFormatOnInit; + bindingTrackOutputs = new SparseArray<>(); } /** - * Initializes the extractor to output to the provided {@link SingleTrackMetadataOutput} and - * {@link TrackOutput} instances, and configures it to receive data from a new chunk. - * - * @param metadataOutput The {@link SingleTrackMetadataOutput} that will receive metadata. - * @param trackOutput The {@link TrackOutput} that will receive sample data. + * Returns the {@link SeekMap} most recently output by the extractor, or null. */ - public void init(SingleTrackMetadataOutput metadataOutput, TrackOutput trackOutput) { - this.metadataOutput = metadataOutput; - this.trackOutput = trackOutput; + public SeekMap getSeekMap() { + return seekMap; + } + + /** + * Returns the sample {@link Format}s most recently output by the extractor, or null. + */ + public Format[] getSampleFormats() { + return sampleFormats; + } + + /** + * Initializes the extractor to output to the provided {@link TrackOutput}, and configures it to + * receive data from a new chunk. + * + * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data. + */ + public void init(TrackOutputProvider trackOutputProvider) { + this.trackOutputProvider = trackOutputProvider; if (!extractorInitialized) { extractor.init(this); extractorInitialized = true; } else { - extractor.seek(0); - if (resendFormatOnInit && sentFormat != null) { - trackOutput.format(sentFormat); + extractor.seek(0, 0); + for (int i = 0; i < bindingTrackOutputs.size(); i++) { + bindingTrackOutputs.valueAt(i).bind(trackOutputProvider); } } } - /** - * Reads from the provided {@link ExtractorInput}. - * - * @param input The {@link ExtractorInput} from which to read. - * @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}. - * @throws IOException If an error occurred reading from the source. - * @throws InterruptedException If the thread was interrupted. - */ - public int read(ExtractorInput input) throws IOException, InterruptedException { - int result = extractor.read(input, null); - Assertions.checkState(result != Extractor.RESULT_SEEK); - return result; - } - // ExtractorOutput implementation. @Override - public TrackOutput track(int id) { - Assertions.checkState(!seenTrack || seenTrackId == id); - seenTrack = true; - seenTrackId = id; - return this; + public TrackOutput track(int id, int type) { + BindingTrackOutput bindingTrackOutput = bindingTrackOutputs.get(id); + if (bindingTrackOutput == null) { + // Assert that if we're seeing a new track we have not seen endTracks. + Assertions.checkState(sampleFormats == null); + bindingTrackOutput = new BindingTrackOutput(id, type, manifestFormat); + bindingTrackOutput.bind(trackOutputProvider); + bindingTrackOutputs.put(id, bindingTrackOutput); + } + return bindingTrackOutput; } @Override public void endTracks() { - Assertions.checkState(seenTrack); + Format[] sampleFormats = new Format[bindingTrackOutputs.size()]; + for (int i = 0; i < bindingTrackOutputs.size(); i++) { + sampleFormats[i] = bindingTrackOutputs.valueAt(i).sampleFormat; + } + this.sampleFormats = sampleFormats; } @Override public void seekMap(SeekMap seekMap) { - metadataOutput.seekMap(seekMap); + this.seekMap = seekMap; } - // TrackOutput implementation. + // Internal logic. - @Override - public void format(Format format) { - sentFormat = format.copyWithManifestFormatInfo(manifestFormat, preferManifestDrmInitData); - trackOutput.format(sentFormat); - } + private static final class BindingTrackOutput implements TrackOutput { - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - return trackOutput.sampleData(input, length, allowEndOfInput); - } + private final int id; + private final int type; + private final Format manifestFormat; - @Override - public void sampleData(ParsableByteArray data, int length) { - trackOutput.sampleData(data, length); - } + public Format sampleFormat; + private TrackOutput trackOutput; + + public BindingTrackOutput(int id, int type, Format manifestFormat) { + this.id = id; + this.type = type; + this.manifestFormat = manifestFormat; + } + + public void bind(TrackOutputProvider trackOutputProvider) { + if (trackOutputProvider == null) { + trackOutput = new DummyTrackOutput(); + return; + } + trackOutput = trackOutputProvider.track(id, type); + if (trackOutput != null) { + trackOutput.format(sampleFormat); + } + } + + @Override + public void format(Format format) { + // TODO: This should only happen for the primary track. Additional metadata/text tracks need + // to be copied with different manifest derived formats. + sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); + trackOutput.format(sampleFormat); + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return trackOutput.sampleData(input, length, allowEndOfInput); + } + + @Override + public void sampleData(ParsableByteArray data, int length) { + trackOutput.sampleData(data, length); + } + + @Override + public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, + byte[] encryptionKey) { + trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey); + } - @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkSampleStream.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkSampleStream.java index 754b42ee0..6cb657b35 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkSampleStream.java @@ -33,30 +33,35 @@ import java.util.List; /** * A {@link SampleStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}. + * May also be configured to expose additional embedded {@link SampleStream}s. */ public class ChunkSampleStream implements SampleStream, SequenceableLoader, Loader.Callback { - private final int trackType; + private final int primaryTrackType; + private final int[] embeddedTrackTypes; + private final boolean[] embeddedTracksSelected; private final T chunkSource; private final SequenceableLoader.Callback> callback; private final EventDispatcher eventDispatcher; private final int minLoadableRetryCount; + private final Loader loader; + private final ChunkHolder nextChunkHolder; private final LinkedList mediaChunks; private final List readOnlyMediaChunks; - private final DefaultTrackOutput sampleQueue; - private final ChunkHolder nextChunkHolder; - private final Loader loader; + private final DefaultTrackOutput primarySampleQueue; + private final DefaultTrackOutput[] embeddedSampleQueues; + private final BaseMediaChunkOutput mediaChunkOutput; - private Format downstreamTrackFormat; - - private long lastSeekPositionUs; + private Format primaryDownstreamTrackFormat; private long pendingResetPositionUs; - - private boolean loadingFinished; + /* package */ long lastSeekPositionUs; + /* package */ boolean loadingFinished; /** - * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @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 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. @@ -65,10 +70,11 @@ public class ChunkSampleStream implements SampleStream, S * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. */ - public ChunkSampleStream(int trackType, T chunkSource, - SequenceableLoader.Callback> callback, Allocator allocator, - long positionUs, int minLoadableRetryCount, EventDispatcher eventDispatcher) { - this.trackType = trackType; + public ChunkSampleStream(int primaryTrackType, int[] embeddedTrackTypes, T chunkSource, + Callback> callback, Allocator allocator, long positionUs, + int minLoadableRetryCount, EventDispatcher eventDispatcher) { + this.primaryTrackType = primaryTrackType; + this.embeddedTrackTypes = embeddedTrackTypes; this.chunkSource = chunkSource; this.callback = callback; this.eventDispatcher = eventDispatcher; @@ -77,15 +83,68 @@ public class ChunkSampleStream implements SampleStream, S nextChunkHolder = new ChunkHolder(); mediaChunks = new LinkedList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); - sampleQueue = new DefaultTrackOutput(allocator); - lastSeekPositionUs = positionUs; + + int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; + embeddedSampleQueues = new DefaultTrackOutput[embeddedTrackCount]; + embeddedTracksSelected = new boolean[embeddedTrackCount]; + int[] trackTypes = new int[1 + embeddedTrackCount]; + DefaultTrackOutput[] sampleQueues = new DefaultTrackOutput[1 + embeddedTrackCount]; + + primarySampleQueue = new DefaultTrackOutput(allocator); + trackTypes[0] = primaryTrackType; + sampleQueues[0] = primarySampleQueue; + + for (int i = 0; i < embeddedTrackCount; i++) { + DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator); + embeddedSampleQueues[i] = trackOutput; + sampleQueues[i + 1] = trackOutput; + trackTypes[i + 1] = embeddedTrackTypes[i]; + } + + mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); pendingResetPositionUs = positionUs; + lastSeekPositionUs = positionUs; + } + + /** + * Discards buffered media for embedded tracks that are not currently selected, up to the + * specified position. + * + * @param positionUs The position to discard up to, in microseconds. + */ + public void discardUnselectedEmbeddedTracksTo(long positionUs) { + for (int i = 0; i < embeddedSampleQueues.length; i++) { + if (!embeddedTracksSelected[i]) { + embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + } + } + } + + /** + * Selects the embedded track, returning a new {@link EmbeddedSampleStream} from which the track's + * samples can be consumed. {@link EmbeddedSampleStream#release()} must be called on the returned + * stream when the track is no longer required, and before calling this method again to obtain + * another stream for the same track. + * + * @param positionUs The current playback position in microseconds. + * @param trackType The type of the embedded track to enable. + * @return The {@link EmbeddedSampleStream} for the embedded track. + */ + public EmbeddedSampleStream selectEmbeddedTrack(long positionUs, int trackType) { + for (int i = 0; i < embeddedSampleQueues.length; i++) { + if (embeddedTrackTypes[i] == trackType) { + Assertions.checkState(!embeddedTracksSelected[i]); + embeddedTracksSelected[i] = true; + embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i); + } + } + // Should never happen. + throw new IllegalStateException(); } /** * Returns the {@link ChunkSource} used by this stream. - * - * @return The {@link ChunkSource}. */ public T getChunkSource() { return chunkSource; @@ -110,7 +169,7 @@ public class ChunkSampleStream implements SampleStream, S if (lastCompletedMediaChunk != null) { bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); } - return Math.max(bufferedPositionUs, sampleQueue.getLargestQueuedTimestampUs()); + return Math.max(bufferedPositionUs, primarySampleQueue.getLargestQueuedTimestampUs()); } } @@ -121,14 +180,21 @@ public class ChunkSampleStream implements SampleStream, S */ public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; - // If we're not pending a reset, see if we can seek within the sample queue. - boolean seekInsideBuffer = !isPendingReset() && sampleQueue.skipToKeyframeBefore(positionUs); + // If we're not pending a reset, see if we can seek within the primary sample queue. + boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.skipToKeyframeBefore( + positionUs, positionUs < getNextLoadPositionUs()); if (seekInsideBuffer) { - // We succeeded. All we need to do is discard any chunks that we've moved past. + // We succeeded. We need to discard any chunks that we've moved past and perform the seek for + // any embedded streams as well. while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) { + && mediaChunks.get(1).getFirstSampleIndex(0) <= primarySampleQueue.getReadIndex()) { mediaChunks.removeFirst(); } + // TODO: For this to work correctly, the embedded streams must not discard anything from their + // sample queues beyond the current read position of the primary stream. + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.skipToKeyframeBefore(positionUs, true); + } } else { // We failed, and need to restart. pendingResetPositionUs = positionUs; @@ -137,7 +203,10 @@ public class ChunkSampleStream implements SampleStream, S if (loader.isLoading()) { loader.cancelLoading(); } else { - sampleQueue.reset(true); + primarySampleQueue.reset(true); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(true); + } } } } @@ -148,7 +217,10 @@ public class ChunkSampleStream implements SampleStream, S * This method should be called when the stream is no longer required. */ public void release() { - sampleQueue.disable(); + primarySampleQueue.disable(); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.disable(); + } loader.release(); } @@ -156,7 +228,7 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty()); + return loadingFinished || (!isPendingReset() && !primarySampleQueue.isEmpty()); } @Override @@ -168,30 +240,23 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - - while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) { - mediaChunks.removeFirst(); - } - BaseMediaChunk currentChunk = mediaChunks.getFirst(); - - Format trackFormat = currentChunk.trackFormat; - if (!trackFormat.equals(downstreamTrackFormat)) { - eventDispatcher.downstreamFormatChanged(trackType, trackFormat, - currentChunk.trackSelectionReason, currentChunk.trackSelectionData, - currentChunk.startTimeUs); - } - downstreamTrackFormat = trackFormat; - return sampleQueue.readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs); + discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); + return primarySampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + lastSeekPositionUs); } @Override - public void skipToKeyframeBefore(long timeUs) { - sampleQueue.skipToKeyframeBefore(timeUs); + public void skipData(long positionUs) { + if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { + primarySampleQueue.skipAll(); + } else { + primarySampleQueue.skipToKeyframeBefore(positionUs, true); + } } // Loader.Callback implementation. @@ -199,20 +264,25 @@ public class ChunkSampleStream implements SampleStream, S @Override public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { chunkSource.onChunkLoadCompleted(loadable); - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); + eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, + loadable.bytesLoaded()); callback.onContinueLoadingRequested(this); } @Override public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); + eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, + loadable.bytesLoaded()); if (!released) { - sampleQueue.reset(true); + primarySampleQueue.reset(true); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(true); + } callback.onContinueLoadingRequested(this); } } @@ -229,16 +299,19 @@ public class ChunkSampleStream implements SampleStream, S if (isMediaChunk) { BaseMediaChunk removed = mediaChunks.removeLast(); Assertions.checkState(removed == loadable); - sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex()); + primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + } if (mediaChunks.isEmpty()) { pendingResetPositionUs = lastSeekPositionUs; } } } - eventDispatcher.loadError(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, bytesLoaded, error, - canceled); + eventDispatcher.loadError(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, bytesLoaded, + error, canceled); if (canceled) { callback.onContinueLoadingRequested(this); return Loader.DONT_RETRY; @@ -274,13 +347,13 @@ public class ChunkSampleStream implements SampleStream, S if (isMediaChunk(loadable)) { pendingResetPositionUs = C.TIME_UNSET; BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable; - mediaChunk.init(sampleQueue); + mediaChunk.init(mediaChunkOutput); mediaChunks.add(mediaChunk); } long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs); + eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, primaryTrackType, + loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, + loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs); return true; } @@ -311,10 +384,25 @@ public class ChunkSampleStream implements SampleStream, S return chunk instanceof BaseMediaChunk; } - private boolean isPendingReset() { + /* package */ boolean isPendingReset() { return pendingResetPositionUs != C.TIME_UNSET; } + private void discardDownstreamMediaChunks(int primaryStreamReadIndex) { + while (mediaChunks.size() > 1 + && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) { + mediaChunks.removeFirst(); + } + BaseMediaChunk currentChunk = mediaChunks.getFirst(); + Format trackFormat = currentChunk.trackFormat; + if (!trackFormat.equals(primaryDownstreamTrackFormat)) { + eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, + currentChunk.trackSelectionReason, currentChunk.trackSelectionData, + currentChunk.startTimeUs); + } + primaryDownstreamTrackFormat = trackFormat; + } + /** * Discard upstream media chunks until the queue length is equal to the length specified. * @@ -327,16 +415,71 @@ public class ChunkSampleStream implements SampleStream, S } long startTimeUs = 0; long endTimeUs = mediaChunks.getLast().endTimeUs; - BaseMediaChunk removed = null; while (mediaChunks.size() > queueLength) { removed = mediaChunks.removeLast(); startTimeUs = removed.startTimeUs; loadingFinished = false; } - sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex()); - eventDispatcher.upstreamDiscarded(trackType, startTimeUs, endTimeUs); + primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + } + eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); return true; } + /** + * A {@link SampleStream} embedded in a {@link ChunkSampleStream}. + */ + public final class EmbeddedSampleStream implements SampleStream { + + public final ChunkSampleStream parent; + + private final DefaultTrackOutput sampleQueue; + private final int index; + + public EmbeddedSampleStream(ChunkSampleStream parent, DefaultTrackOutput sampleQueue, + int index) { + this.parent = parent; + this.sampleQueue = sampleQueue; + this.index = index; + } + + @Override + public boolean isReady() { + return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty()); + } + + @Override + public void skipData(long positionUs) { + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + sampleQueue.skipAll(); + } else { + sampleQueue.skipToKeyframeBefore(positionUs, true); + } + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. Errors will be thrown from the primary stream. + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + if (isPendingReset()) { + return C.RESULT_NOTHING_READ; + } + return sampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + lastSeekPositionUs); + } + + public void release() { + Assertions.checkState(embeddedTracksSelected[index]); + embeddedTracksSelected[index] = false; + } + + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java index bf79be62f..cefd2eb86 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java @@ -51,9 +51,9 @@ public final class ChunkedTrackBlacklistUtil { /** * Blacklists {@code trackSelectionIndex} in {@code trackSelection} for - * {@code blacklistDurationMs} if {@code e} is an {@link InvalidResponseCodeException} with - * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. Else does nothing. Note - * that blacklisting will fail if the track is the only non-blacklisted track in the selection. + * {@code blacklistDurationMs} if calling {@link #shouldBlacklist(Exception)} for {@code e} + * returns true. Else does nothing. Note that blacklisting will fail if the track is the only + * non-blacklisted track in the selection. * * @param trackSelection The track selection. * @param trackSelectionIndex The index in the selection to consider blacklisting. @@ -63,24 +63,33 @@ public final class ChunkedTrackBlacklistUtil { */ public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex, Exception e, long blacklistDurationMs) { - if (trackSelection.length() == 1) { - // Blacklisting won't ever work if there's only one track in the selection. - return false; - } - if (e instanceof InvalidResponseCodeException) { - InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e; - int responseCode = responseCodeException.responseCode; - if (responseCode == 404 || responseCode == 410) { - boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs); - if (blacklisted) { - Log.w(TAG, "Blacklisted: duration=" + blacklistDurationMs + ", responseCode=" - + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); - } else { - Log.w(TAG, "Blacklisting failed (cannot blacklist last enabled track): responseCode=" - + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); - } - return blacklisted; + if (shouldBlacklist(e)) { + boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs); + int responseCode = ((InvalidResponseCodeException) e).responseCode; + if (blacklisted) { + Log.w(TAG, "Blacklisted: duration=" + blacklistDurationMs + ", responseCode=" + + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); + } else { + Log.w(TAG, "Blacklisting failed (cannot blacklist last enabled track): responseCode=" + + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); } + return blacklisted; + } + return false; + } + + /** + * Returns whether a loading error is an {@link InvalidResponseCodeException} with + * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. + * + * @param e The loading error. + * @return Wheter the loading error is an {@link InvalidResponseCodeException} with + * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. + */ + public static boolean shouldBlacklist(Exception e) { + if (e instanceof InvalidResponseCodeException) { + int responseCode = ((InvalidResponseCodeException) e).responseCode; + return responseCode == 404 || responseCode == 410; } return false; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ContainerMediaChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ContainerMediaChunk.java index 3d116e53b..d55a34abf 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -17,25 +17,22 @@ package org.telegram.messenger.exoplayer2.source.chunk; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.extractor.DefaultExtractorInput; -import org.telegram.messenger.exoplayer2.extractor.DefaultTrackOutput; import org.telegram.messenger.exoplayer2.extractor.Extractor; import org.telegram.messenger.exoplayer2.extractor.ExtractorInput; -import org.telegram.messenger.exoplayer2.extractor.SeekMap; -import org.telegram.messenger.exoplayer2.source.chunk.ChunkExtractorWrapper.SingleTrackMetadataOutput; import org.telegram.messenger.exoplayer2.upstream.DataSource; import org.telegram.messenger.exoplayer2.upstream.DataSpec; +import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; /** * A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data. */ -public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMetadataOutput { +public class ContainerMediaChunk extends BaseMediaChunk { private final int chunkCount; private final long sampleOffsetUs; private final ChunkExtractorWrapper extractorWrapper; - private final Format sampleFormat; private volatile int bytesLoaded; private volatile boolean loadCanceled; @@ -55,19 +52,15 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe * underlying media are being merged into a single load. * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor. * @param extractorWrapper A wrapped extractor to use for parsing the data. - * @param sampleFormat The {@link Format} of the samples in the chunk, if known. May be null if - * the data is known to define its own sample format. */ public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper, - Format sampleFormat) { + int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); this.chunkCount = chunkCount; this.sampleOffsetUs = sampleOffsetUs; this.extractorWrapper = extractorWrapper; - this.sampleFormat = sampleFormat; } @Override @@ -85,13 +78,6 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe return bytesLoaded; } - // SingleTrackMetadataOutput implementation. - - @Override - public final void seekMap(SeekMap seekMap) { - // Do nothing. - } - // Loadable implementation. @Override @@ -113,22 +99,24 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); if (bytesLoaded == 0) { - // Set the target to ourselves. - DefaultTrackOutput trackOutput = getTrackOutput(); - trackOutput.formatWithOffset(sampleFormat, sampleOffsetUs); - extractorWrapper.init(this, trackOutput); + // Configure the output and set it as the target for the extractor wrapper. + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(sampleOffsetUs); + extractorWrapper.init(output); } // Load and decode the sample data. try { + Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractorWrapper.read(input); + result = extractor.read(input, null); } + Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } } finally { - dataSource.close(); + Util.closeQuietly(dataSource); } loadCompleted = true; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/DataChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/DataChunk.java index c244c5cd6..99e95a709 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/DataChunk.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/DataChunk.java @@ -19,6 +19,7 @@ import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.upstream.DataSource; import org.telegram.messenger.exoplayer2.upstream.DataSpec; +import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; import java.util.Arrays; @@ -96,7 +97,7 @@ public abstract class DataChunk extends Chunk { consume(data, limit); } } finally { - dataSource.close(); + Util.closeQuietly(dataSource); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/InitializationChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/InitializationChunk.java index 36b50e7dd..750b1f284 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/InitializationChunk.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/InitializationChunk.java @@ -20,29 +20,19 @@ import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.extractor.DefaultExtractorInput; import org.telegram.messenger.exoplayer2.extractor.Extractor; import org.telegram.messenger.exoplayer2.extractor.ExtractorInput; -import org.telegram.messenger.exoplayer2.extractor.SeekMap; -import org.telegram.messenger.exoplayer2.extractor.TrackOutput; -import org.telegram.messenger.exoplayer2.source.chunk.ChunkExtractorWrapper.SingleTrackMetadataOutput; import org.telegram.messenger.exoplayer2.upstream.DataSource; import org.telegram.messenger.exoplayer2.upstream.DataSpec; -import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; /** * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track. */ -public final class InitializationChunk extends Chunk implements SingleTrackMetadataOutput, - TrackOutput { +public final class InitializationChunk extends Chunk { private final ChunkExtractorWrapper extractorWrapper; - // Initialization results. Set by the loader thread and read by any thread that knows loading - // has completed. These variables do not need to be volatile, since a memory barrier must occur - // for the reading thread to know that loading has completed. - private Format sampleFormat; - private SeekMap seekMap; - private volatile int bytesLoaded; private volatile boolean loadCanceled; @@ -67,55 +57,6 @@ public final class InitializationChunk extends Chunk implements SingleTrackMetad return bytesLoaded; } - /** - * Returns a {@link Format} parsed from the chunk, or null. - *

    - * Should be called after loading has completed. - */ - public Format getSampleFormat() { - return sampleFormat; - } - - /** - * Returns a {@link SeekMap} parsed from the chunk, or null. - *

    - * Should be called after loading has completed. - */ - public SeekMap getSeekMap() { - return seekMap; - } - - // SingleTrackMetadataOutput implementation. - - @Override - public void seekMap(SeekMap seekMap) { - this.seekMap = seekMap; - } - - // TrackOutput implementation. - - @Override - public void format(Format format) { - this.sampleFormat = format; - } - - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - - @Override - public void sampleData(ParsableByteArray data, int length) { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - - @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - // Loadable implementation. @Override @@ -137,20 +78,21 @@ public final class InitializationChunk extends Chunk implements SingleTrackMetad ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); if (bytesLoaded == 0) { - // Set the target to ourselves. - extractorWrapper.init(this, this); + extractorWrapper.init(null); } // Load and decode the initialization data. try { + Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractorWrapper.read(input); + result = extractor.read(input, null); } + Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } } finally { - dataSource.close(); + Util.closeQuietly(dataSource); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/SingleSampleMediaChunk.java index e184385af..f51d5c16d 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -18,8 +18,8 @@ package org.telegram.messenger.exoplayer2.source.chunk; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.extractor.DefaultExtractorInput; -import org.telegram.messenger.exoplayer2.extractor.DefaultTrackOutput; import org.telegram.messenger.exoplayer2.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer2.extractor.TrackOutput; import org.telegram.messenger.exoplayer2.upstream.DataSource; import org.telegram.messenger.exoplayer2.upstream.DataSpec; import org.telegram.messenger.exoplayer2.util.Util; @@ -30,6 +30,7 @@ import java.io.IOException; */ public final class SingleSampleMediaChunk extends BaseMediaChunk { + private final int trackType; private final Format sampleFormat; private volatile int bytesLoaded; @@ -45,15 +46,20 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param chunkIndex The index of the chunk. + * @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*} + * constants. + * @param sampleFormat The {@link Format} of the sample in the chunk. */ public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, Format sampleFormat) { + int chunkIndex, int trackType, Format sampleFormat) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + this.trackType = trackType; this.sampleFormat = sampleFormat; } + @Override public boolean isLoadCompleted() { return loadCompleted; @@ -87,8 +93,10 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { length += bytesLoaded; } ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, bytesLoaded, length); - DefaultTrackOutput trackOutput = getTrackOutput(); - trackOutput.formatWithOffset(sampleFormat, 0); + 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) { @@ -98,7 +106,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { int sampleSize = bytesLoaded; trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); } finally { - dataSource.close(); + Util.closeQuietly(dataSource); } loadCompleted = true; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashChunkSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashChunkSource.java index 9824e49ec..6e63d66d6 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashChunkSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashChunkSource.java @@ -29,7 +29,8 @@ public interface DashChunkSource extends ChunkSource { DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, - TrackSelection trackSelection, long elapsedRealtimeOffsetMs); + TrackSelection trackSelection, long elapsedRealtimeOffsetMs, + boolean enableEventMessageTrack, boolean enableCea608Track); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashMediaPeriod.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashMediaPeriod.java index db64cb321..134a6c2b4 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashMediaPeriod.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashMediaPeriod.java @@ -15,25 +15,30 @@ */ package org.telegram.messenger.exoplayer2.source.dash; +import android.util.Pair; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import org.telegram.messenger.exoplayer2.source.CompositeSequenceableLoader; +import org.telegram.messenger.exoplayer2.source.EmptySampleStream; import org.telegram.messenger.exoplayer2.source.MediaPeriod; import org.telegram.messenger.exoplayer2.source.SampleStream; import org.telegram.messenger.exoplayer2.source.SequenceableLoader; import org.telegram.messenger.exoplayer2.source.TrackGroup; import org.telegram.messenger.exoplayer2.source.TrackGroupArray; import org.telegram.messenger.exoplayer2.source.chunk.ChunkSampleStream; +import org.telegram.messenger.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream; import org.telegram.messenger.exoplayer2.source.dash.manifest.AdaptationSet; import org.telegram.messenger.exoplayer2.source.dash.manifest.DashManifest; -import org.telegram.messenger.exoplayer2.source.dash.manifest.Period; import org.telegram.messenger.exoplayer2.source.dash.manifest.Representation; +import org.telegram.messenger.exoplayer2.source.dash.manifest.SchemeValuePair; import org.telegram.messenger.exoplayer2.trackselection.TrackSelection; import org.telegram.messenger.exoplayer2.upstream.Allocator; import org.telegram.messenger.exoplayer2.upstream.LoaderErrorThrower; +import org.telegram.messenger.exoplayer2.util.MimeTypes; import java.io.IOException; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; /** @@ -50,21 +55,22 @@ import java.util.List; private final LoaderErrorThrower manifestLoaderErrorThrower; private final Allocator allocator; private final TrackGroupArray trackGroups; + private final EmbeddedTrackInfo[] embeddedTrackInfos; private Callback callback; private ChunkSampleStream[] sampleStreams; private CompositeSequenceableLoader sequenceableLoader; private DashManifest manifest; - private int index; - private Period period; + private int periodIndex; + private List adaptationSets; - public DashMediaPeriod(int id, DashManifest manifest, int index, + public DashMediaPeriod(int id, DashManifest manifest, int periodIndex, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, long elapsedRealtimeOffset, LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) { this.id = id; this.manifest = manifest; - this.index = index; + this.periodIndex = periodIndex; this.chunkSourceFactory = chunkSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; @@ -73,17 +79,19 @@ import java.util.List; this.allocator = allocator; sampleStreams = newSampleStreamArray(0); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); - period = manifest.getPeriod(index); - trackGroups = buildTrackGroups(period); + adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; + Pair result = buildTrackGroups(adaptationSets); + trackGroups = result.first; + embeddedTrackInfos = result.second; } - public void updateManifest(DashManifest manifest, int index) { + public void updateManifest(DashManifest manifest, int periodIndex) { this.manifest = manifest; - this.index = index; - period = manifest.getPeriod(index); + this.periodIndex = periodIndex; + adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; if (sampleStreams != null) { for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.getChunkSource().updateManifest(manifest, index); + sampleStream.getChunkSource().updateManifest(manifest, periodIndex); } callback.onContinueLoadingRequested(this); } @@ -114,31 +122,75 @@ import java.util.List; @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - ArrayList> sampleStreamsList = new ArrayList<>(); + int adaptationSetCount = adaptationSets.size(); + HashMap> primarySampleStreams = new HashMap<>(); + // First pass for primary tracks. for (int i = 0; i < selections.length; i++) { - if (streams[i] != null) { + if (streams[i] instanceof ChunkSampleStream) { @SuppressWarnings("unchecked") ChunkSampleStream stream = (ChunkSampleStream) streams[i]; if (selections[i] == null || !mayRetainStreamFlags[i]) { stream.release(); streams[i] = null; } else { - sampleStreamsList.add(stream); + int adaptationSetIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + primarySampleStreams.put(adaptationSetIndex, stream); } } if (streams[i] == null && selections[i] != null) { - ChunkSampleStream stream = buildSampleStream(selections[i], positionUs); - sampleStreamsList.add(stream); - streams[i] = stream; - streamResetFlags[i] = true; + int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + if (trackGroupIndex < adaptationSetCount) { + ChunkSampleStream stream = buildSampleStream(trackGroupIndex, + selections[i], positionUs); + primarySampleStreams.put(trackGroupIndex, stream); + streams[i] = stream; + streamResetFlags[i] = true; + } } } - sampleStreams = newSampleStreamArray(sampleStreamsList.size()); - sampleStreamsList.toArray(sampleStreams); + // Second pass for embedded tracks. + for (int i = 0; i < selections.length; i++) { + if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream) + && (selections[i] == null || !mayRetainStreamFlags[i])) { + // The stream is for an embedded track and is either no longer selected or needs replacing. + releaseIfEmbeddedSampleStream(streams[i]); + streams[i] = null; + } + // We need to consider replacing the stream even if it's non-null because the primary stream + // may have been replaced, selected or deselected. + if (selections[i] != null) { + int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + if (trackGroupIndex >= adaptationSetCount) { + int embeddedTrackIndex = trackGroupIndex - adaptationSetCount; + EmbeddedTrackInfo embeddedTrackInfo = embeddedTrackInfos[embeddedTrackIndex]; + int adaptationSetIndex = embeddedTrackInfo.adaptationSetIndex; + ChunkSampleStream primaryStream = primarySampleStreams.get(adaptationSetIndex); + SampleStream stream = streams[i]; + boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream + : (stream instanceof EmbeddedSampleStream + && ((EmbeddedSampleStream) stream).parent == primaryStream); + if (!mayRetainStream) { + releaseIfEmbeddedSampleStream(stream); + streams[i] = primaryStream == null ? new EmptySampleStream() + : primaryStream.selectEmbeddedTrack(positionUs, embeddedTrackInfo.trackType); + streamResetFlags[i] = true; + } + } + } + } + sampleStreams = newSampleStreamArray(primarySampleStreams.size()); + primarySampleStreams.values().toArray(sampleStreams); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); return positionUs; } + @Override + public void discardBuffer(long positionUs) { + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.discardUnselectedEmbeddedTracksTo(positionUs); + } + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); @@ -183,29 +235,98 @@ import java.util.List; // Internal methods. - private static TrackGroupArray buildTrackGroups(Period period) { - TrackGroup[] trackGroupArray = new TrackGroup[period.adaptationSets.size()]; - for (int i = 0; i < period.adaptationSets.size(); i++) { - AdaptationSet adaptationSet = period.adaptationSets.get(i); + private static Pair buildTrackGroups( + List adaptationSets) { + int adaptationSetCount = adaptationSets.size(); + int embeddedTrackCount = getEmbeddedTrackCount(adaptationSets); + TrackGroup[] trackGroupArray = new TrackGroup[adaptationSetCount + embeddedTrackCount]; + EmbeddedTrackInfo[] embeddedTrackInfos = new EmbeddedTrackInfo[embeddedTrackCount]; + + int embeddedTrackIndex = 0; + for (int i = 0; i < adaptationSetCount; i++) { + AdaptationSet adaptationSet = adaptationSets.get(i); List representations = adaptationSet.representations; Format[] formats = new Format[representations.size()]; for (int j = 0; j < formats.length; j++) { formats[j] = representations.get(j).format; } trackGroupArray[i] = new TrackGroup(formats); + if (hasEventMessageTrack(adaptationSet)) { + Format format = Format.createSampleFormat(adaptationSet.id + ":emsg", + MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); + trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format); + embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_METADATA); + } + if (hasCea608Track(adaptationSet)) { + Format format = Format.createTextSampleFormat(adaptationSet.id + ":cea608", + MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null); + trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format); + embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_TEXT); + } } - return new TrackGroupArray(trackGroupArray); + + return Pair.create(new TrackGroupArray(trackGroupArray), embeddedTrackInfos); } - private ChunkSampleStream buildSampleStream(TrackSelection selection, - long positionUs) { - int adaptationSetIndex = trackGroups.indexOf(selection.getTrackGroup()); - AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); + private ChunkSampleStream buildSampleStream(int adaptationSetIndex, + TrackSelection selection, long positionUs) { + AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); + int embeddedTrackCount = 0; + int[] embeddedTrackTypes = new int[2]; + boolean enableEventMessageTrack = hasEventMessageTrack(adaptationSet); + if (enableEventMessageTrack) { + embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_METADATA; + } + boolean enableCea608Track = hasCea608Track(adaptationSet); + if (enableCea608Track) { + embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_TEXT; + } + if (embeddedTrackCount < embeddedTrackTypes.length) { + embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount); + } DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( - manifestLoaderErrorThrower, manifest, index, adaptationSetIndex, selection, - elapsedRealtimeOffset); - return new ChunkSampleStream<>(adaptationSet.type, chunkSource, this, allocator, positionUs, - minLoadableRetryCount, eventDispatcher); + manifestLoaderErrorThrower, manifest, periodIndex, adaptationSetIndex, selection, + elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track); + ChunkSampleStream stream = new ChunkSampleStream<>(adaptationSet.type, + embeddedTrackTypes, chunkSource, this, allocator, positionUs, minLoadableRetryCount, + eventDispatcher); + return stream; + } + + private static int getEmbeddedTrackCount(List adaptationSets) { + int embeddedTrackCount = 0; + for (int i = 0; i < adaptationSets.size(); i++) { + AdaptationSet adaptationSet = adaptationSets.get(i); + if (hasEventMessageTrack(adaptationSet)) { + embeddedTrackCount++; + } + if (hasCea608Track(adaptationSet)) { + embeddedTrackCount++; + } + } + return embeddedTrackCount; + } + + private static boolean hasEventMessageTrack(AdaptationSet adaptationSet) { + List representations = adaptationSet.representations; + for (int i = 0; i < representations.size(); i++) { + Representation representation = representations.get(i); + if (!representation.inbandEventStreams.isEmpty()) { + return true; + } + } + return false; + } + + private static boolean hasCea608Track(AdaptationSet adaptationSet) { + List descriptors = adaptationSet.accessibilityDescriptors; + for (int i = 0; i < descriptors.size(); i++) { + SchemeValuePair descriptor = descriptors.get(i); + if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) { + return true; + } + } + return false; } @SuppressWarnings("unchecked") @@ -213,4 +334,22 @@ import java.util.List; return new ChunkSampleStream[length]; } + private static void releaseIfEmbeddedSampleStream(SampleStream sampleStream) { + if (sampleStream instanceof EmbeddedSampleStream) { + ((EmbeddedSampleStream) sampleStream).release(); + } + } + + private static final class EmbeddedTrackInfo { + + public final int adaptationSetIndex; + public final int trackType; + + public EmbeddedTrackInfo(int adaptationSetIndex, int trackType) { + this.adaptationSetIndex = adaptationSetIndex; + this.trackType = trackType; + } + + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashMediaSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashMediaSource.java index 4db9ef729..334d1ce01 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashMediaSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashMediaSource.java @@ -21,6 +21,7 @@ import android.os.SystemClock; import android.util.Log; import android.util.SparseArray; import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.ExoPlayer; import org.telegram.messenger.exoplayer2.ParserException; import org.telegram.messenger.exoplayer2.Timeline; import org.telegram.messenger.exoplayer2.source.AdaptiveMediaSourceEventListener; @@ -87,14 +88,14 @@ public final class DashMediaSource implements MediaSource { private final int minLoadableRetryCount; private final long livePresentationDelayMs; private final EventDispatcher eventDispatcher; - private final DashManifestParser manifestParser; + private final ParsingLoadable.Parser manifestParser; private final ManifestCallback manifestCallback; private final Object manifestUriLock; private final SparseArray periodsById; private final Runnable refreshManifestRunnable; private final Runnable simulateManifestRefreshRunnable; - private MediaSource.Listener sourceListener; + private Listener sourceListener; private DataSource dataSource; private Loader loader; private LoaderErrorThrower loaderErrorThrower; @@ -199,15 +200,17 @@ public final class DashMediaSource implements MediaSource { * @param eventListener A listener of events. May be null if delivery of events is not required. */ public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - DashManifestParser manifestParser, DashChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, + ParsingLoadable.Parser manifestParser, + DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, + long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } private DashMediaSource(DashManifest manifest, Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, DashManifestParser manifestParser, + DataSource.Factory manifestDataSourceFactory, + ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -258,7 +261,7 @@ public final class DashMediaSource implements MediaSource { // MediaSource implementation. @Override - public void prepareSource(MediaSource.Listener listener) { + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { sourceListener = listener; if (sideloadedManifest) { loaderErrorThrower = new LoaderErrorThrower.Dummy(); @@ -571,22 +574,28 @@ public final class DashMediaSource implements MediaSource { long availableStartTimeUs = 0; long availableEndTimeUs = Long.MAX_VALUE; boolean isIndexExplicit = false; + boolean seenEmptyIndex = false; for (int i = 0; i < adaptationSetCount; i++) { DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex(); if (index == null) { return new PeriodSeekInfo(true, 0, durationUs); } - int firstSegmentNum = index.getFirstSegmentNum(); - int lastSegmentNum = index.getLastSegmentNum(durationUs); isIndexExplicit |= index.isExplicit(); - long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); - availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); - if (lastSegmentNum != DashSegmentIndex.INDEX_UNBOUNDED) { - long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) - + index.getDurationUs(lastSegmentNum, durationUs); - availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); - } else { - // The available end time is unmodified, because this index is unbounded. + int segmentCount = index.getSegmentCount(durationUs); + if (segmentCount == 0) { + seenEmptyIndex = true; + availableStartTimeUs = 0; + availableEndTimeUs = 0; + } else if (!seenEmptyIndex) { + int firstSegmentNum = index.getFirstSegmentNum(); + long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); + availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); + if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED) { + int lastSegmentNum = firstSegmentNum + segmentCount - 1; + long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) + + index.getDurationUs(lastSegmentNum, durationUs); + availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); + } } } return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs); @@ -641,7 +650,7 @@ public final class DashMediaSource implements MediaSource { + Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null; return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex), C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs) - - offsetInFirstPeriodUs); + - offsetInFirstPeriodUs, false); } @Override @@ -703,8 +712,8 @@ public final class DashMediaSource implements MediaSource { // not correspond to the start of a segment in both, but this is an edge case. DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex) .representations.get(0).getIndex(); - if (snapIndex == null) { - // Video adaptation set does not include an index for snapping. + if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) { + // Video adaptation set does not include a non-empty index for snapping. return windowDefaultStartPositionUs; } int segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashSegmentIndex.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashSegmentIndex.java index 43e8ff78b..9f792f45b 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashSegmentIndex.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashSegmentIndex.java @@ -26,12 +26,10 @@ public interface DashSegmentIndex { int INDEX_UNBOUNDED = -1; /** - * Returns the segment number of the segment containing a given media time. - *

    - * If the given media time is outside the range of the index, then the returned segment number is - * clamped to {@link #getFirstSegmentNum()} (if the given media time is earlier the start of the - * first segment) or {@link #getLastSegmentNum(long)} (if the given media time is later then the - * end of the last segment). + * Returns {@code getFirstSegmentNum()} if the index has no segments or if the given media time is + * earlier than the start of the first segment. Returns {@code getFirstSegmentNum() + + * getSegmentCount() - 1} if the given media time is later than the end of the last segment. + * Otherwise, returns the segment number of the segment containing the given media time. * * @param timeUs The time in microseconds. * @param periodDurationUs The duration of the enclosing period in microseconds, or @@ -74,7 +72,7 @@ public interface DashSegmentIndex { int getFirstSegmentNum(); /** - * Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}. + * Returns the number of segments in the index, or {@link #INDEX_UNBOUNDED}. *

    * An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a * SegmentTimeline element, and if the period duration is not yet known. In this case the caller @@ -82,9 +80,9 @@ public interface DashSegmentIndex { * * @param periodDurationUs The duration of the enclosing period in microseconds, or * {@link C#TIME_UNSET} if the period's duration is not yet known. - * @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}. + * @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}. */ - int getLastSegmentNum(long periodDurationUs); + int getSegmentCount(long periodDurationUs); /** * Returns true if segments are defined explicitly by the index. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashUtil.java new file mode 100755 index 000000000..8463ea445 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashUtil.java @@ -0,0 +1,241 @@ +/* + * 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 org.telegram.messenger.exoplayer2.source.dash; + +import android.net.Uri; +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.Format; +import org.telegram.messenger.exoplayer2.drm.DrmInitData; +import org.telegram.messenger.exoplayer2.extractor.ChunkIndex; +import org.telegram.messenger.exoplayer2.extractor.Extractor; +import org.telegram.messenger.exoplayer2.extractor.mkv.MatroskaExtractor; +import org.telegram.messenger.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import org.telegram.messenger.exoplayer2.source.chunk.ChunkExtractorWrapper; +import org.telegram.messenger.exoplayer2.source.chunk.InitializationChunk; +import org.telegram.messenger.exoplayer2.source.dash.manifest.AdaptationSet; +import org.telegram.messenger.exoplayer2.source.dash.manifest.DashManifest; +import org.telegram.messenger.exoplayer2.source.dash.manifest.DashManifestParser; +import org.telegram.messenger.exoplayer2.source.dash.manifest.Period; +import org.telegram.messenger.exoplayer2.source.dash.manifest.RangedUri; +import org.telegram.messenger.exoplayer2.source.dash.manifest.Representation; +import org.telegram.messenger.exoplayer2.upstream.DataSource; +import org.telegram.messenger.exoplayer2.upstream.DataSourceInputStream; +import org.telegram.messenger.exoplayer2.upstream.DataSpec; +import org.telegram.messenger.exoplayer2.upstream.HttpDataSource; +import org.telegram.messenger.exoplayer2.util.MimeTypes; +import java.io.IOException; +import java.util.List; + +/** + * Utility methods for DASH streams. + */ +public final class DashUtil { + + /** + * Loads a DASH manifest. + * + * @param dataSource The {@link HttpDataSource} from which the manifest should be read. + * @param manifestUri The URI of the manifest to be read. + * @return An instance of {@link DashManifest}. + * @throws IOException Thrown when there is an error while loading. + */ + public static DashManifest loadManifest(DataSource dataSource, String manifestUri) + throws IOException { + DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, + new DataSpec(Uri.parse(manifestUri), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); + try { + inputStream.open(); + DashManifestParser parser = new DashManifestParser(); + return parser.parse(dataSource.getUri(), inputStream); + } finally { + inputStream.close(); + } + } + + /** + * Loads {@link DrmInitData} for a given manifest. + * + * @param dataSource The {@link HttpDataSource} from which data should be loaded. + * @param dashManifest The {@link DashManifest} of the DASH content. + * @return The loaded {@link DrmInitData}. + */ + public static DrmInitData loadDrmInitData(DataSource dataSource, DashManifest dashManifest) + throws IOException, InterruptedException { + // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, + // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. + if (dashManifest.getPeriodCount() < 1) { + return null; + } + Period period = dashManifest.getPeriod(0); + int adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO); + if (adaptationSetIndex == C.INDEX_UNSET) { + adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_AUDIO); + if (adaptationSetIndex == C.INDEX_UNSET) { + return null; + } + } + AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); + if (adaptationSet.representations.isEmpty()) { + return null; + } + Representation representation = adaptationSet.representations.get(0); + DrmInitData drmInitData = representation.format.drmInitData; + if (drmInitData == null) { + Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation); + if (sampleFormat != null) { + drmInitData = sampleFormat.drmInitData; + } + if (drmInitData == null) { + return null; + } + } + return drmInitData; + } + + /** + * Loads initialization data for the {@code representation} and returns the sample {@link + * Format}. + * Loads {@link DrmInitData} for a given period in a DASH manifest. + * + * @param dataSource The {@link HttpDataSource} from which data should be loaded. + * @param period The {@link Period}. + * @return The loaded {@link DrmInitData}, or null if none is defined. + * @throws IOException Thrown when there is an error while loading. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public static DrmInitData loadDrmInitData(DataSource dataSource, Period period) + throws IOException, InterruptedException { + Representation representation = getFirstRepresentation(period, C.TRACK_TYPE_VIDEO); + if (representation == null) { + representation = getFirstRepresentation(period, C.TRACK_TYPE_AUDIO); + if (representation == null) { + return null; + } + } + DrmInitData drmInitData = representation.format.drmInitData; + if (drmInitData != null) { + // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, + // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. + return drmInitData; + } + Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation); + return sampleFormat == null ? null : sampleFormat.drmInitData; + } + + /** + * Loads initialization data for the {@code representation} and returns the sample {@link Format}. + * + * @param dataSource The source from which the data should be loaded. + * @param representation The representation which initialization chunk belongs to. + * @return the sample {@link Format} of the given representation. + * @throws IOException Thrown when there is an error while loading. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public static Format loadSampleFormat(DataSource dataSource, Representation representation) + throws IOException, InterruptedException { + ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, + false); + return extractorWrapper == null ? null : extractorWrapper.getSampleFormats()[0]; + } + + /** + * Loads initialization and index data for the {@code representation} and returns the {@link + * ChunkIndex}. + * + * @param dataSource The source from which the data should be loaded. + * @param representation The representation which initialization chunk belongs to. + * @return {@link ChunkIndex} of the given representation. + * @throws IOException Thrown when there is an error while loading. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public static ChunkIndex loadChunkIndex(DataSource dataSource, Representation representation) + throws IOException, InterruptedException { + ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, + true); + return extractorWrapper == null ? null : (ChunkIndex) extractorWrapper.getSeekMap(); + } + + /** + * Loads initialization data for the {@code representation} and optionally index data then + * returns a {@link ChunkExtractorWrapper} which contains the output. + * + * @param dataSource The source from which the data should be loaded. + * @param representation The representation which initialization chunk belongs to. + * @param loadIndex Whether to load index data too. + * @return A {@link ChunkExtractorWrapper} for the {@code representation}, or null if no + * initialization or (if requested) index data exists. + * @throws IOException Thrown when there is an error while loading. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + private static ChunkExtractorWrapper loadInitializationData(DataSource dataSource, + Representation representation, boolean loadIndex) + throws IOException, InterruptedException { + RangedUri initializationUri = representation.getInitializationUri(); + if (initializationUri == null) { + return null; + } + ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format); + RangedUri requestUri; + if (loadIndex) { + RangedUri indexUri = representation.getIndexUri(); + if (indexUri == null) { + return null; + } + // It's common for initialization and index data to be stored adjacently. Attempt to merge + // the two requests together to request both at once. + requestUri = initializationUri.attemptMerge(indexUri, representation.baseUrl); + if (requestUri == null) { + loadInitializationData(dataSource, representation, extractorWrapper, initializationUri); + requestUri = indexUri; + } + } else { + requestUri = initializationUri; + } + loadInitializationData(dataSource, representation, extractorWrapper, requestUri); + return extractorWrapper; + } + + private static void loadInitializationData(DataSource dataSource, + Representation representation, ChunkExtractorWrapper extractorWrapper, RangedUri requestUri) + throws IOException, InterruptedException { + DataSpec dataSpec = new DataSpec(requestUri.resolveUri(representation.baseUrl), + requestUri.start, requestUri.length, representation.getCacheKey()); + InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec, + representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */, + extractorWrapper); + initializationChunk.load(); + } + + private static ChunkExtractorWrapper newWrappedExtractor(Format format) { + String mimeType = format.containerMimeType; + boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) + || mimeType.startsWith(MimeTypes.AUDIO_WEBM); + Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); + return new ChunkExtractorWrapper(extractor, format); + } + + private static Representation getFirstRepresentation(Period period, int type) { + int index = period.getAdaptationSetIndex(type); + if (index == C.INDEX_UNSET) { + return null; + } + List representations = period.adaptationSets.get(index).representations; + return representations.isEmpty() ? null : representations.get(0); + } + + private DashUtil() {} + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashWrappingSegmentIndex.java index a04adfc86..fccbd7ca5 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashWrappingSegmentIndex.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DashWrappingSegmentIndex.java @@ -22,15 +22,14 @@ import org.telegram.messenger.exoplayer2.source.dash.manifest.RangedUri; * An implementation of {@link DashSegmentIndex} that wraps a {@link ChunkIndex} parsed from a * media stream. */ -/* package */ final class DashWrappingSegmentIndex implements DashSegmentIndex { +public final class DashWrappingSegmentIndex implements DashSegmentIndex { private final ChunkIndex chunkIndex; /** * @param chunkIndex The {@link ChunkIndex} to wrap. - * @param uri The URI where the data is located. */ - public DashWrappingSegmentIndex(ChunkIndex chunkIndex, String uri) { + public DashWrappingSegmentIndex(ChunkIndex chunkIndex) { this.chunkIndex = chunkIndex; } @@ -40,8 +39,8 @@ import org.telegram.messenger.exoplayer2.source.dash.manifest.RangedUri; } @Override - public int getLastSegmentNum(long periodDurationUs) { - return chunkIndex.length - 1; + public int getSegmentCount(long periodDurationUs) { + return chunkIndex.length; } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DefaultDashChunkSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DefaultDashChunkSource.java index 1a7480482..460c23af4 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -34,6 +34,7 @@ import org.telegram.messenger.exoplayer2.source.chunk.ContainerMediaChunk; import org.telegram.messenger.exoplayer2.source.chunk.InitializationChunk; import org.telegram.messenger.exoplayer2.source.chunk.MediaChunk; import org.telegram.messenger.exoplayer2.source.chunk.SingleSampleMediaChunk; +import org.telegram.messenger.exoplayer2.source.dash.manifest.AdaptationSet; import org.telegram.messenger.exoplayer2.source.dash.manifest.DashManifest; import org.telegram.messenger.exoplayer2.source.dash.manifest.RangedUri; import org.telegram.messenger.exoplayer2.source.dash.manifest.Representation; @@ -69,11 +70,12 @@ public class DefaultDashChunkSource implements DashChunkSource { @Override public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, - TrackSelection trackSelection, long elapsedRealtimeOffsetMs) { + TrackSelection trackSelection, long elapsedRealtimeOffsetMs, + boolean enableEventMessageTrack, boolean enableCea608Track) { DataSource dataSource = dataSourceFactory.createDataSource(); return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs, - maxSegmentsPerLoad); + maxSegmentsPerLoad, enableEventMessageTrack, enableCea608Track); } } @@ -105,10 +107,14 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. * Note that segments will only be combined if their {@link Uri}s are the same and if their * data ranges are adjacent. + * @param enableEventMessageTrack Whether the chunks generated by the source may output an event + * message track. + * @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 track. */ public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection, - DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad) { + DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, + boolean enableEventMessageTrack, boolean enableCea608Track) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.adaptationSetIndex = adaptationSetIndex; @@ -119,11 +125,13 @@ public class DefaultDashChunkSource implements DashChunkSource { this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getRepresentations(); + AdaptationSet adaptationSet = getAdaptationSet(); + List representations = adaptationSet.representations; representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); - representationHolders[i] = new RepresentationHolder(periodDurationUs, representation); + representationHolders[i] = new RepresentationHolder(periodDurationUs, representation, + enableEventMessageTrack, enableCea608Track, adaptationSet.type); } } @@ -133,7 +141,7 @@ public class DefaultDashChunkSource implements DashChunkSource { manifest = newManifest; periodIndex = newPeriodIndex; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getRepresentations(); + List representations = getAdaptationSet().representations; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i].updateRepresentation(periodDurationUs, representation); @@ -171,32 +179,37 @@ public class DefaultDashChunkSource implements DashChunkSource { RepresentationHolder representationHolder = representationHolders[trackSelection.getSelectedIndex()]; - Representation selectedRepresentation = representationHolder.representation; - DashSegmentIndex segmentIndex = representationHolder.segmentIndex; - RangedUri pendingInitializationUri = null; - RangedUri pendingIndexUri = null; - Format sampleFormat = representationHolder.sampleFormat; - if (sampleFormat == null) { - pendingInitializationUri = selectedRepresentation.getInitializationUri(); - } - if (segmentIndex == null) { - pendingIndexUri = selectedRepresentation.getIndexUri(); - } - if (pendingInitializationUri != null || pendingIndexUri != null) { - // We have initialization and/or index requests to make. - Chunk initializationChunk = newInitializationChunk(representationHolder, dataSource, - trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), - trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri); - out.chunk = initializationChunk; - return; + if (representationHolder.extractorWrapper != null) { + Representation selectedRepresentation = representationHolder.representation; + RangedUri pendingInitializationUri = null; + RangedUri pendingIndexUri = null; + if (representationHolder.extractorWrapper.getSampleFormats() == null) { + pendingInitializationUri = selectedRepresentation.getInitializationUri(); + } + if (representationHolder.segmentIndex == null) { + pendingIndexUri = selectedRepresentation.getIndexUri(); + } + if (pendingInitializationUri != null || pendingIndexUri != null) { + // We have initialization and/or index requests to make. + out.chunk = newInitializationChunk(representationHolder, dataSource, + trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri); + return; + } } long nowUs = getNowUnixTimeUs(); + int availableSegmentCount = representationHolder.getSegmentCount(); + if (availableSegmentCount == 0) { + // The index doesn't define any segments. + out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); + return; + } + int firstAvailableSegmentNum = representationHolder.getFirstSegmentNum(); - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; - if (indexUnbounded) { + int lastAvailableSegmentNum; + if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000; @@ -210,6 +223,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the // index of the last completed segment. lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1; + } else { + lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1; } int segmentNum; @@ -233,10 +248,9 @@ public class DefaultDashChunkSource implements DashChunkSource { } int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); - Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, - trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), - trackSelection.getSelectionData(), sampleFormat, segmentNum, maxSegmentCount); - out.chunk = nextMediaChunk; + out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(), + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segmentNum, + maxSegmentCount); } @Override @@ -245,18 +259,13 @@ public class DefaultDashChunkSource implements DashChunkSource { InitializationChunk initializationChunk = (InitializationChunk) chunk; RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(initializationChunk.trackFormat)]; - Format sampleFormat = initializationChunk.getSampleFormat(); - if (sampleFormat != null) { - representationHolder.setSampleFormat(sampleFormat); - } // The null check avoids overwriting an index obtained from the manifest with one obtained // from the stream. If the manifest defines an index then the stream shouldn't, but in cases // where it does we should ignore it. if (representationHolder.segmentIndex == null) { - SeekMap seekMap = initializationChunk.getSeekMap(); + SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap(); if (seekMap != null) { - representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap, - initializationChunk.dataSpec.uri.toString()); + representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap); } } } @@ -273,10 +282,13 @@ public class DefaultDashChunkSource implements DashChunkSource { && ((InvalidResponseCodeException) e).responseCode == 404) { RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(chunk.trackFormat)]; - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { - missingLastSegment = true; - return true; + int segmentCount = representationHolder.getSegmentCount(); + if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) { + int lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1; + if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { + missingLastSegment = true; + return true; + } } } // Blacklist if appropriate. @@ -286,8 +298,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // Private methods. - private List getRepresentations() { - return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex).representations; + private AdaptationSet getAdaptationSet() { + return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex); } private long getNowUnixTimeUs() { @@ -321,7 +333,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private static Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, Format sampleFormat, int firstSegmentNum, int maxSegmentCount) { + Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) { Representation representation = representationHolder.representation; long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); @@ -331,7 +343,8 @@ public class DefaultDashChunkSource implements DashChunkSource { DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), segmentUri.start, segmentUri.length, representation.getCacheKey()); return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, - trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackFormat); + trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, + representationHolder.trackType, trackFormat); } else { int segmentCount = 1; for (int i = 1; i < maxSegmentCount; i++) { @@ -350,7 +363,7 @@ public class DefaultDashChunkSource implements DashChunkSource { long sampleOffsetUs = -representation.presentationTimeOffsetUs; return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, segmentCount, - sampleOffsetUs, representationHolder.extractorWrapper, sampleFormat); + sampleOffsetUs, representationHolder.extractorWrapper); } } @@ -358,45 +371,46 @@ public class DefaultDashChunkSource implements DashChunkSource { protected static final class RepresentationHolder { + public final int trackType; public final ChunkExtractorWrapper extractorWrapper; public Representation representation; public DashSegmentIndex segmentIndex; - public Format sampleFormat; private long periodDurationUs; private int segmentNumShift; - public RepresentationHolder(long periodDurationUs, Representation representation) { + public RepresentationHolder(long periodDurationUs, Representation representation, + boolean enableEventMessageTrack, boolean enableCea608Track, int trackType) { this.periodDurationUs = periodDurationUs; this.representation = representation; + this.trackType = trackType; String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { extractorWrapper = null; } else { - boolean resendFormatOnInit = false; Extractor extractor; if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { extractor = new RawCcExtractor(representation.format); - resendFormatOnInit = true; } else if (mimeTypeIsWebm(containerMimeType)) { - extractor = new MatroskaExtractor(); + extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES); } else { - extractor = new FragmentedMp4Extractor(); + int flags = 0; + if (enableEventMessageTrack) { + flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK; + } + if (enableCea608Track) { + flags |= FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK; + } + extractor = new FragmentedMp4Extractor(flags); } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = new ChunkExtractorWrapper(extractor, - representation.format, true /* preferManifestDrmInitData */, - resendFormatOnInit); + extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format); } segmentIndex = representation.getIndex(); } - public void setSampleFormat(Format sampleFormat) { - this.sampleFormat = sampleFormat; - } - public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) throws BehindLiveWindowException{ DashSegmentIndex oldIndex = representation.getIndex(); @@ -415,15 +429,20 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(periodDurationUs); + int oldIndexSegmentCount = oldIndex.getSegmentCount(periodDurationUs); + if (oldIndexSegmentCount == 0) { + // Segment numbers cannot shift if the old index was empty. + return; + } + + int oldIndexLastSegmentNum = oldIndex.getFirstSegmentNum() + oldIndexSegmentCount - 1; long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs); int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); if (oldIndexEndTimeUs == newIndexStartTimeUs) { // The new index continues where the old one ended, with no overlap. - segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 1 - - newIndexFirstSegmentNum; + segmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum; } else if (oldIndexEndTimeUs < newIndexStartTimeUs) { // There's a gap between the old index and the new one which means we've slipped behind the // live window and can't proceed. @@ -439,12 +458,8 @@ public class DefaultDashChunkSource implements DashChunkSource { return segmentIndex.getFirstSegmentNum() + segmentNumShift; } - public int getLastSegmentNum() { - int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs); - if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) { - return DashSegmentIndex.INDEX_UNBOUNDED; - } - return lastSegmentNum + segmentNumShift; + public int getSegmentCount() { + return segmentIndex.getSegmentCount(periodDurationUs); } public long getSegmentStartTimeUs(int segmentNum) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/AdaptationSet.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/AdaptationSet.java index cc7cedcb8..0b23205b2 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/AdaptationSet.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/AdaptationSet.java @@ -23,18 +23,49 @@ import java.util.List; */ public class AdaptationSet { - public static final int UNSET_ID = -1; + /** + * Value of {@link #id} indicating no value is set.= + */ + public static final int ID_UNSET = -1; + /** + * A non-negative identifier for the adaptation set that's unique in the scope of its containing + * period, or {@link #ID_UNSET} if not specified. + */ public final int id; + /** + * The type of the adaptation set. One of the {@link org.telegram.messenger.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. + */ public final int type; + /** + * The {@link Representation}s in the adaptation set. + */ public final List representations; - public AdaptationSet(int id, int type, List representations) { + /** + * The accessibility descriptors in the adaptation set. + */ + public final List accessibilityDescriptors; + + /** + * @param id A non-negative identifier for the adaptation set that's unique in the scope of its + * containing period, or {@link #ID_UNSET} if not specified. + * @param type The type of the adaptation set. One of the {@link org.telegram.messenger.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. + * @param representations The {@link Representation}s in the adaptation set. + * @param accessibilityDescriptors The accessibility descriptors in the adaptation set. + */ + public AdaptationSet(int id, int type, List representations, + List accessibilityDescriptors) { this.id = id; this.type = type; this.representations = Collections.unmodifiableList(representations); + this.accessibilityDescriptors = accessibilityDescriptors == null + ? Collections.emptyList() + : Collections.unmodifiableList(accessibilityDescriptors); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/DashManifest.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/DashManifest.java index 4b6a0afe7..e95881a1f 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/DashManifest.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/DashManifest.java @@ -17,7 +17,9 @@ package org.telegram.messenger.exoplayer2.source.dash.manifest; import android.net.Uri; import org.telegram.messenger.exoplayer2.C; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedList; import java.util.List; /** @@ -79,4 +81,64 @@ public class DashManifest { return C.msToUs(getPeriodDurationMs(index)); } + /** + * Creates a copy of this manifest which includes only the representations identified by the given + * keys. + * + * @param representationKeys List of keys for the representations to be included in the copy. + * @return A copy of this manifest with the selected representations. + * @throws IndexOutOfBoundsException If a key has an invalid index. + */ + public final DashManifest copy(List representationKeys) { + LinkedList keys = new LinkedList<>(representationKeys); + Collections.sort(keys); + keys.add(new RepresentationKey(-1, -1, -1)); // Add a stopper key to the end + + ArrayList copyPeriods = new ArrayList<>(); + long shiftMs = 0; + for (int periodIndex = 0; periodIndex < getPeriodCount(); periodIndex++) { + if (keys.peek().periodIndex != periodIndex) { + // No representations selected in this period. + long periodDurationMs = getPeriodDurationMs(periodIndex); + if (periodDurationMs != C.TIME_UNSET) { + shiftMs += periodDurationMs; + } + } else { + Period period = getPeriod(periodIndex); + ArrayList copyAdaptationSets = + copyAdaptationSets(period.adaptationSets, keys); + copyPeriods.add(new Period(period.id, period.startMs - shiftMs, copyAdaptationSets)); + } + } + long newDuration = duration != C.TIME_UNSET ? duration - shiftMs : C.TIME_UNSET; + return new DashManifest(availabilityStartTime, newDuration, minBufferTime, dynamic, + minUpdatePeriod, timeShiftBufferDepth, suggestedPresentationDelay, utcTiming, location, + copyPeriods); + } + + private static ArrayList copyAdaptationSets( + List adaptationSets, LinkedList keys) { + RepresentationKey key = keys.poll(); + int periodIndex = key.periodIndex; + ArrayList copyAdaptationSets = new ArrayList<>(); + do { + int adaptationSetIndex = key.adaptationSetIndex; + AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); + + List representations = adaptationSet.representations; + ArrayList copyRepresentations = new ArrayList<>(); + do { + Representation representation = representations.get(key.representationIndex); + copyRepresentations.add(representation); + key = keys.poll(); + } while(key.periodIndex == periodIndex && key.adaptationSetIndex == adaptationSetIndex); + + copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type, + copyRepresentations, adaptationSet.accessibilityDescriptors)); + } while(key.periodIndex == periodIndex); + // Add back the last key which doesn't belong to the period being processed + keys.addFirst(key); + return copyAdaptationSets; + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/DashManifestParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/DashManifestParser.java index 2aa6068ac..9ea7e1c21 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -227,7 +227,7 @@ public class DashManifestParser extends DefaultHandler protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, SegmentBase segmentBase) throws XmlPullParserException, IOException { - int id = parseInt(xpp, "id", AdaptationSet.UNSET_ID); + int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET); int contentType = parseContentType(xpp); String mimeType = xpp.getAttributeValue(null, "mimeType"); @@ -238,9 +238,11 @@ public class DashManifestParser extends DefaultHandler int audioChannels = Format.NO_VALUE; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); String language = xpp.getAttributeValue(null, "lang"); - int accessibilityChannel = Format.NO_VALUE; ArrayList drmSchemeDatas = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList accessibilityDescriptors = new ArrayList<>(); List representationInfos = new ArrayList<>(); + @C.SelectionFlags int selectionFlags = 0; boolean seenFirstBaseUrl = false; do { @@ -258,40 +260,45 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "ContentComponent")) { language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang")); contentType = checkContentTypeConsistency(contentType, parseContentType(xpp)); - } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { - RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs, - width, height, frameRate, audioChannels, audioSamplingRate, language, - accessibilityChannel, segmentBase); - contentType = checkContentTypeConsistency(contentType, - getContentType(representationInfo.format)); - representationInfos.add(representationInfo); + } else if (XmlPullParserUtil.isStartTag(xpp, "Role")) { + selectionFlags |= parseRole(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { audioChannels = parseAudioChannelConfiguration(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) { - accessibilityChannel = parseAccessibilityValue(xpp); + accessibilityDescriptors.add(parseAccessibility(xpp)); + } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { + RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs, + width, height, frameRate, audioChannels, audioSamplingRate, language, + selectionFlags, accessibilityDescriptors, segmentBase); + 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); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); + } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { + inbandEventStreams.add(parseInbandEventStream(xpp)); } else if (XmlPullParserUtil.isStartTag(xpp)) { parseAdaptationSetChild(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "AdaptationSet")); + // Build the representations. List representations = new ArrayList<>(representationInfos.size()); for (int i = 0; i < representationInfos.size(); i++) { representations.add(buildRepresentation(representationInfos.get(i), contentId, - drmSchemeDatas)); + drmSchemeDatas, inbandEventStreams)); } - return buildAdaptationSet(id, contentType, representations); + return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors); } protected AdaptationSet buildAdaptationSet(int id, int contentType, - List representations) { - return new AdaptationSet(id, contentType, representations); + List representations, List accessibilityDescriptors) { + return new AdaptationSet(id, contentType, representations, accessibilityDescriptors); } protected int parseContentType(XmlPullParser xpp) { @@ -311,8 +318,7 @@ public class DashManifestParser extends DefaultHandler return C.TRACK_TYPE_VIDEO; } else if (MimeTypes.isAudio(sampleMimeType)) { return C.TRACK_TYPE_AUDIO; - } else if (mimeTypeIsRawText(sampleMimeType) - || MimeTypes.APPLICATION_RAWCC.equals(format.containerMimeType)) { + } else if (mimeTypeIsRawText(sampleMimeType)) { return C.TRACK_TYPE_TEXT; } return C.TRACK_TYPE_UNKNOWN; @@ -329,30 +335,79 @@ public class DashManifestParser extends DefaultHandler */ protected SchemeData parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri); byte[] data = null; UUID uuid = null; - boolean seenPsshElement = false; boolean requiresSecureDecoder = false; do { xpp.next(); - // The cenc:pssh element is defined in 23001-7:2015. - if (XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { - seenPsshElement = true; + if (data == null && XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") + && xpp.next() == XmlPullParser.TEXT) { + // The cenc:pssh element is defined in 23001-7:2015. data = Base64.decode(xpp.getText(), Base64.DEFAULT); uuid = PsshAtomUtil.parseUuid(data); + if (uuid == null) { + Log.w(TAG, "Skipping malformed cenc:pssh data"); + data = null; + } + } else if (data == null && isPlayReady && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") + && xpp.next() == XmlPullParser.TEXT) { + // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. + data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, + Base64.decode(xpp.getText(), Base64.DEFAULT)); + uuid = C.PLAYREADY_UUID; } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - if (!seenPsshElement) { - return null; - } else if (uuid != null) { - return new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder); - } else { - Log.w(TAG, "Skipped unsupported ContentProtection element"); - return null; - } + return data != null ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) + : null; + } + + /** + * Parses an InbandEventStream element. + * + * @param xpp The parser from which to read. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return A {@link SchemeValuePair} parsed from the element. + */ + protected SchemeValuePair parseInbandEventStream(XmlPullParser xpp) + throws XmlPullParserException, IOException { + return parseSchemeValuePair(xpp, "InbandEventStream"); + } + + /** + * Parses an Accessibility element. + * + * @param xpp The parser from which to read. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return A {@link SchemeValuePair} parsed from the element. + */ + protected SchemeValuePair parseAccessibility(XmlPullParser xpp) + throws XmlPullParserException, IOException { + return parseSchemeValuePair(xpp, "Accessibility"); + } + + /** + * Parses a Role element. + * + * @param xpp The parser from which to read. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return {@link C.SelectionFlags} parsed from the element. + */ + protected int parseRole(XmlPullParser xpp) throws XmlPullParserException, IOException { + String schemeIdUri = parseString(xpp, "schemeIdUri", null); + String value = parseString(xpp, "value", null); + do { + xpp.next(); + } while (!XmlPullParserUtil.isEndTag(xpp, "Role")); + return "urn:mpeg:dash:role:2011".equals(schemeIdUri) && "main".equals(value) + ? C.SELECTION_FLAG_DEFAULT : 0; } /** @@ -373,7 +428,8 @@ public class DashManifestParser extends DefaultHandler String adaptationSetMimeType, String adaptationSetCodecs, int adaptationSetWidth, int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels, int adaptationSetAudioSamplingRate, String adaptationSetLanguage, - int adaptationSetAccessibilityChannel, SegmentBase segmentBase) + @C.SelectionFlags int adaptationSetSelectionFlags, + List adaptationSetAccessibilityDescriptors, SegmentBase segmentBase) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -386,6 +442,7 @@ public class DashManifestParser extends DefaultHandler int audioChannels = adaptationSetAudioChannels; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate); ArrayList drmSchemeDatas = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { @@ -408,52 +465,61 @@ public class DashManifestParser extends DefaultHandler if (contentProtection != null) { drmSchemeDatas.add(contentProtection); } + } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { + inbandEventStreams.add(parseInbandEventStream(xpp)); } } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels, - audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetAccessibilityChannel, - codecs); - segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(baseUrl); + audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetSelectionFlags, + adaptationSetAccessibilityDescriptors, codecs); + segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); - return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeDatas); + return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeDatas, inbandEventStreams); } protected Format buildFormat(String id, String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language, - int accessiblityChannel, String codecs) { + @C.SelectionFlags int selectionFlags, List accessibilityDescriptors, + String codecs) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); if (sampleMimeType != null) { if (MimeTypes.isVideo(sampleMimeType)) { return Format.createVideoContainerFormat(id, containerMimeType, sampleMimeType, codecs, - bitrate, width, height, frameRate, null); + bitrate, width, height, frameRate, null, selectionFlags); } else if (MimeTypes.isAudio(sampleMimeType)) { return Format.createAudioContainerFormat(id, containerMimeType, sampleMimeType, codecs, - bitrate, audioChannels, audioSamplingRate, null, 0, language); + bitrate, audioChannels, audioSamplingRate, null, selectionFlags, language); } else if (mimeTypeIsRawText(sampleMimeType)) { + int accessibilityChannel; + if (MimeTypes.APPLICATION_CEA608.equals(sampleMimeType)) { + accessibilityChannel = parseCea608AccessibilityChannel(accessibilityDescriptors); + } else if (MimeTypes.APPLICATION_CEA708.equals(sampleMimeType)) { + accessibilityChannel = parseCea708AccessibilityChannel(accessibilityDescriptors); + } else { + accessibilityChannel = Format.NO_VALUE; + } return Format.createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, - bitrate, 0, language, accessiblityChannel); - } else if (containerMimeType.equals(MimeTypes.APPLICATION_RAWCC)) { - return Format.createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, - bitrate, 0, language, accessiblityChannel); - } else { - return Format.createContainerFormat(id, containerMimeType, codecs, sampleMimeType, bitrate); + bitrate, selectionFlags, language, accessibilityChannel); } - } else { - return Format.createContainerFormat(id, containerMimeType, codecs, sampleMimeType, bitrate); } + return Format.createContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, + selectionFlags, language); } protected Representation buildRepresentation(RepresentationInfo representationInfo, - String contentId, ArrayList extraDrmSchemeDatas) { + String contentId, ArrayList extraDrmSchemeDatas, + ArrayList extraInbandEventStreams) { Format format = representationInfo.format; ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; drmSchemeDatas.addAll(extraDrmSchemeDatas); if (!drmSchemeDatas.isEmpty()) { format = format.copyWithDrmInitData(new DrmInitData(drmSchemeDatas)); } + ArrayList inbandEventStremas = representationInfo.inbandEventStreams; + inbandEventStremas.addAll(extraInbandEventStreams); return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format, - representationInfo.baseUrl, representationInfo.segmentBase); + representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStremas); } // SegmentBase, SegmentList and SegmentTemplate parsing. @@ -664,6 +730,14 @@ public class DashManifestParser extends DefaultHandler return MimeTypes.getAudioMediaMimeType(codecs); } else if (MimeTypes.isVideo(containerMimeType)) { return MimeTypes.getVideoMediaMimeType(codecs); + } else if (mimeTypeIsRawText(containerMimeType)) { + return containerMimeType; + } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) { + if ("stpp".equals(codecs)) { + return MimeTypes.APPLICATION_TTML; + } else if ("wvtt".equals(codecs)) { + return MimeTypes.APPLICATION_MP4VTT; + } } else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { if (codecs != null) { if (codecs.contains("cea708")) { @@ -673,14 +747,6 @@ public class DashManifestParser extends DefaultHandler } } return null; - } else if (mimeTypeIsRawText(containerMimeType)) { - return containerMimeType; - } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) { - if ("stpp".equals(codecs)) { - return MimeTypes.APPLICATION_TTML; - } else if ("wvtt".equals(codecs)) { - return MimeTypes.APPLICATION_MP4VTT; - } } return null; } @@ -692,7 +758,11 @@ public class DashManifestParser extends DefaultHandler * @return Whether the mimeType is a text sample mimeType. */ private static boolean mimeTypeIsRawText(String mimeType) { - return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType); + return MimeTypes.isText(mimeType) + || MimeTypes.APPLICATION_TTML.equals(mimeType) + || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) + || MimeTypes.APPLICATION_CEA708.equals(mimeType) + || MimeTypes.APPLICATION_CEA608.equals(mimeType); } /** @@ -738,52 +808,57 @@ public class DashManifestParser extends DefaultHandler } } - private static int parseAccessibilityValue(XmlPullParser xpp) - throws IOException, XmlPullParserException { + /** + * Parses a {@link SchemeValuePair} from an element. + * + * @param xpp The parser from which to read. + * @param tag The tag of the element being parsed. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The parsed {@link SchemeValuePair}. + */ + protected static SchemeValuePair parseSchemeValuePair(XmlPullParser xpp, String tag) + throws XmlPullParserException, IOException { String schemeIdUri = parseString(xpp, "schemeIdUri", null); - String valueString = parseString(xpp, "value", null); - int accessibilityValue; - if (schemeIdUri == null || valueString == null) { - accessibilityValue = Format.NO_VALUE; - } else if ("urn:scte:dash:cc:cea-608:2015".equals(schemeIdUri)) { - accessibilityValue = parseCea608AccessibilityChannel(valueString); - } else if ("urn:scte:dash:cc:cea-708:2015".equals(schemeIdUri)) { - accessibilityValue = parseCea708AccessibilityChannel(valueString); - } else { - accessibilityValue = Format.NO_VALUE; - } + String value = parseString(xpp, "value", null); do { xpp.next(); - } while (!XmlPullParserUtil.isEndTag(xpp, "Accessibility")); - return accessibilityValue; + } while (!XmlPullParserUtil.isEndTag(xpp, tag)); + return new SchemeValuePair(schemeIdUri, value); } - static int parseCea608AccessibilityChannel(String accessibilityValueString) { - if (accessibilityValueString == null) { - return Format.NO_VALUE; - } - Matcher accessibilityValueMatcher = - CEA_608_ACCESSIBILITY_PATTERN.matcher(accessibilityValueString); - if (accessibilityValueMatcher.matches()) { - return Integer.parseInt(accessibilityValueMatcher.group(1)); - } else { - Log.w(TAG, "Unable to parse channel number from " + accessibilityValueString); - return Format.NO_VALUE; + protected static int parseCea608AccessibilityChannel( + List accessibilityDescriptors) { + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + SchemeValuePair descriptor = accessibilityDescriptors.get(i); + if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri) + && descriptor.value != null) { + Matcher accessibilityValueMatcher = CEA_608_ACCESSIBILITY_PATTERN.matcher(descriptor.value); + if (accessibilityValueMatcher.matches()) { + return Integer.parseInt(accessibilityValueMatcher.group(1)); + } else { + Log.w(TAG, "Unable to parse CEA-608 channel number from: " + descriptor.value); + } + } } + return Format.NO_VALUE; } - static int parseCea708AccessibilityChannel(String accessibilityValueString) { - if (accessibilityValueString == null) { - return Format.NO_VALUE; - } - Matcher accessibilityValueMatcher = - CEA_708_ACCESSIBILITY_PATTERN.matcher(accessibilityValueString); - if (accessibilityValueMatcher.matches()) { - return Integer.parseInt(accessibilityValueMatcher.group(1)); - } else { - Log.w(TAG, "Unable to parse service block number from " + accessibilityValueString); - return Format.NO_VALUE; + protected static int parseCea708AccessibilityChannel( + List accessibilityDescriptors) { + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + SchemeValuePair descriptor = accessibilityDescriptors.get(i); + if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri) + && descriptor.value != null) { + Matcher accessibilityValueMatcher = CEA_708_ACCESSIBILITY_PATTERN.matcher(descriptor.value); + if (accessibilityValueMatcher.matches()) { + return Integer.parseInt(accessibilityValueMatcher.group(1)); + } else { + Log.w(TAG, "Unable to parse CEA-708 service block number from: " + descriptor.value); + } + } } + return Format.NO_VALUE; } protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) { @@ -850,13 +925,15 @@ public class DashManifestParser extends DefaultHandler public final String baseUrl; public final SegmentBase segmentBase; public final ArrayList drmSchemeDatas; + public final ArrayList inbandEventStreams; public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, - ArrayList drmSchemeDatas) { + ArrayList drmSchemeDatas, ArrayList inbandEventStreams) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; this.drmSchemeDatas = drmSchemeDatas; + this.inbandEventStreams = inbandEventStreams; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/Representation.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/Representation.java index b36a691b0..c47ccf2d5 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/Representation.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/Representation.java @@ -21,6 +21,8 @@ import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.source.dash.DashSegmentIndex; import org.telegram.messenger.exoplayer2.source.dash.manifest.SegmentBase.MultiSegmentBase; import org.telegram.messenger.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; +import java.util.Collections; +import java.util.List; /** * A DASH representation. @@ -60,6 +62,10 @@ 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. + */ + public final List inbandEventStreams; private final RangedUri initializationUri; @@ -78,6 +84,23 @@ public abstract class Representation { return newInstance(contentId, revisionId, format, baseUrl, segmentBase, null); } + /** + * Constructs a new instance. + * + * @param contentId Identifies the piece of content to which this representation belongs. + * @param revisionId Identifies the revision of the content. + * @param format The format of the representation. + * @param baseUrl The base URL. + * @param segmentBase A segment base element for the representation. + * @param inbandEventStreams The in-band event streams in the representation. May be null. + * @return The constructed instance. + */ + public static Representation newInstance(String contentId, long revisionId, Format format, + String baseUrl, SegmentBase segmentBase, List inbandEventStreams) { + return newInstance(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams, + null); + } + /** * Constructs a new instance. * @@ -86,18 +109,20 @@ public abstract class Representation { * @param format The format of the representation. * @param baseUrl The base URL of the representation. * @param segmentBase A segment base element for the representation. + * @param inbandEventStreams The in-band event streams in the representation. May be null. * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. This * parameter is ignored if {@code segmentBase} consists of multiple segments. * @return The constructed instance. */ public static Representation newInstance(String contentId, long revisionId, Format format, - String baseUrl, SegmentBase segmentBase, String customCacheKey) { + String baseUrl, SegmentBase segmentBase, List inbandEventStreams, + String customCacheKey) { if (segmentBase instanceof SingleSegmentBase) { return new SingleSegmentRepresentation(contentId, revisionId, format, baseUrl, - (SingleSegmentBase) segmentBase, customCacheKey, C.LENGTH_UNSET); + (SingleSegmentBase) segmentBase, inbandEventStreams, customCacheKey, C.LENGTH_UNSET); } else if (segmentBase instanceof MultiSegmentBase) { return new MultiSegmentRepresentation(contentId, revisionId, format, baseUrl, - (MultiSegmentBase) segmentBase); + (MultiSegmentBase) segmentBase, inbandEventStreams); } else { throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or " + "MultiSegmentBase"); @@ -105,11 +130,14 @@ public abstract class Representation { } private Representation(String contentId, long revisionId, Format format, String baseUrl, - SegmentBase segmentBase) { + SegmentBase segmentBase, List inbandEventStreams) { this.contentId = contentId; this.revisionId = revisionId; this.format = format; this.baseUrl = baseUrl; + this.inbandEventStreams = inbandEventStreams == null + ? Collections.emptyList() + : Collections.unmodifiableList(inbandEventStreams); initializationUri = segmentBase.getInitialization(this); presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs(); } @@ -167,18 +195,20 @@ public abstract class Representation { * @param initializationEnd The offset of the last byte of initialization data. * @param indexStart The offset of the first byte of index data. * @param indexEnd The offset of the last byte of index data. + * @param inbandEventStreams The in-band event streams in the representation. May be null. * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown. */ public static SingleSegmentRepresentation newInstance(String contentId, long revisionId, Format format, String uri, long initializationStart, long initializationEnd, - long indexStart, long indexEnd, String customCacheKey, long contentLength) { + long indexStart, long indexEnd, List inbandEventStreams, + String customCacheKey, long contentLength) { RangedUri rangedUri = new RangedUri(null, initializationStart, initializationEnd - initializationStart + 1); SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, indexStart, indexEnd - indexStart + 1); return new SingleSegmentRepresentation(contentId, revisionId, - format, uri, segmentBase, customCacheKey, contentLength); + format, uri, segmentBase, inbandEventStreams, customCacheKey, contentLength); } /** @@ -187,12 +217,14 @@ public abstract class Representation { * @param format The format of the representation. * @param baseUrl The base URL of the representation. * @param segmentBase The segment base underlying the representation. + * @param inbandEventStreams The in-band event streams in the representation. May be null. * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown. */ public SingleSegmentRepresentation(String contentId, long revisionId, Format format, - String baseUrl, SingleSegmentBase segmentBase, String customCacheKey, long contentLength) { - super(contentId, revisionId, format, baseUrl, segmentBase); + String baseUrl, SingleSegmentBase segmentBase, List inbandEventStreams, + String customCacheKey, long contentLength) { + super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.uri = Uri.parse(baseUrl); this.indexUri = segmentBase.getIndex(); this.cacheKey = customCacheKey != null ? customCacheKey @@ -235,10 +267,11 @@ public abstract class Representation { * @param format The format of the representation. * @param baseUrl The base URL of the representation. * @param segmentBase The segment base underlying the representation. + * @param inbandEventStreams The in-band event streams in the representation. May be null. */ public MultiSegmentRepresentation(String contentId, long revisionId, Format format, - String baseUrl, MultiSegmentBase segmentBase) { - super(contentId, revisionId, format, baseUrl, segmentBase); + String baseUrl, MultiSegmentBase segmentBase, List inbandEventStreams) { + super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.segmentBase = segmentBase; } @@ -285,8 +318,8 @@ public abstract class Representation { } @Override - public int getLastSegmentNum(long periodDurationUs) { - return segmentBase.getLastSegmentNum(periodDurationUs); + public int getSegmentCount(long periodDurationUs) { + return segmentBase.getSegmentCount(periodDurationUs); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/RepresentationKey.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/RepresentationKey.java new file mode 100755 index 000000000..5ca750885 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/RepresentationKey.java @@ -0,0 +1,83 @@ +/* + * 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 org.telegram.messenger.exoplayer2.source.dash.manifest; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; + +/** + * Uniquely identifies a {@link Representation} in a {@link DashManifest}. + */ +public final class RepresentationKey implements Parcelable, Comparable { + + public final int periodIndex; + public final int adaptationSetIndex; + public final int representationIndex; + + public RepresentationKey(int periodIndex, int adaptationSetIndex, int representationIndex) { + this.periodIndex = periodIndex; + this.adaptationSetIndex = adaptationSetIndex; + this.representationIndex = representationIndex; + } + + @Override + public String toString() { + return periodIndex + "." + adaptationSetIndex + "." + representationIndex; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(periodIndex); + dest.writeInt(adaptationSetIndex); + dest.writeInt(representationIndex); + } + + public static final Creator CREATOR = + new Creator() { + @Override + public RepresentationKey createFromParcel(Parcel in) { + return new RepresentationKey(in.readInt(), in.readInt(), in.readInt()); + } + + @Override + public RepresentationKey[] newArray(int size) { + return new RepresentationKey[size]; + } + }; + + // Comparable implementation. + + @Override + public int compareTo(@NonNull RepresentationKey o) { + int result = periodIndex - o.periodIndex; + if (result == 0) { + result = adaptationSetIndex - o.adaptationSetIndex; + if (result == 0) { + result = representationIndex - o.representationIndex; + } + } + return result; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/SchemeValuePair.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/SchemeValuePair.java new file mode 100755 index 000000000..b63bced78 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/SchemeValuePair.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2.source.dash.manifest; + +import org.telegram.messenger.exoplayer2.util.Util; + +/** + * A pair consisting of a scheme ID and value. + */ +public class SchemeValuePair { + + public final String schemeIdUri; + public final String value; + + public SchemeValuePair(String schemeIdUri, String value) { + this.schemeIdUri = schemeIdUri; + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SchemeValuePair other = (SchemeValuePair) obj; + return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value); + } + + @Override + public int hashCode() { + return 31 * (schemeIdUri != null ? schemeIdUri.hashCode() : 0) + + (value != null ? value.hashCode() : 0); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/SegmentBase.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/SegmentBase.java index cf6337e8c..8607333d9 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/SegmentBase.java @@ -84,7 +84,7 @@ public abstract class SegmentBase { this.indexLength = indexLength; } - public SingleSegmentBase(String uri) { + public SingleSegmentBase() { this(null, 1, 0, 0, 0); } @@ -130,20 +130,24 @@ public abstract class SegmentBase { */ public int getSegmentNum(long timeUs, long periodDurationUs) { final int firstSegmentNum = getFirstSegmentNum(); - int lowIndex = firstSegmentNum; - int highIndex = getLastSegmentNum(periodDurationUs); + final int segmentCount = getSegmentCount(periodDurationUs); + if (segmentCount == 0) { + return firstSegmentNum; + } if (segmentTimeline == null) { // All segments are of equal duration (with the possible exception of the last one). long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; int segmentNum = startNumber + (int) (timeUs / durationUs); // Ensure we stay within bounds. - return segmentNum < lowIndex ? lowIndex - : highIndex != DashSegmentIndex.INDEX_UNBOUNDED && segmentNum > highIndex ? highIndex - : segmentNum; + return segmentNum < firstSegmentNum ? firstSegmentNum + : segmentCount == DashSegmentIndex.INDEX_UNBOUNDED ? segmentNum + : Math.min(segmentNum, firstSegmentNum + segmentCount - 1); } else { - // The high index cannot be unbounded. Identify the segment using binary search. + // The index cannot be unbounded. Identify the segment using binary search. + int lowIndex = firstSegmentNum; + int highIndex = firstSegmentNum + segmentCount - 1; while (lowIndex <= highIndex) { - int midIndex = (lowIndex + highIndex) / 2; + int midIndex = lowIndex + (highIndex - lowIndex) / 2; long midTimeUs = getSegmentTimeUs(midIndex); if (midTimeUs < timeUs) { lowIndex = midIndex + 1; @@ -165,7 +169,9 @@ public abstract class SegmentBase { long duration = segmentTimeline.get(sequenceNumber - startNumber).duration; return (duration * C.MICROS_PER_SECOND) / timescale; } else { - return sequenceNumber == getLastSegmentNum(periodDurationUs) + int segmentCount = getSegmentCount(periodDurationUs); + return segmentCount != DashSegmentIndex.INDEX_UNBOUNDED + && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1) ? (periodDurationUs - getSegmentTimeUs(sequenceNumber)) : ((duration * C.MICROS_PER_SECOND) / timescale); } @@ -201,9 +207,9 @@ public abstract class SegmentBase { } /** - * @see DashSegmentIndex#getLastSegmentNum(long) + * @see DashSegmentIndex#getSegmentCount(long) */ - public abstract int getLastSegmentNum(long periodDurationUs); + public abstract int getSegmentCount(long periodDurationUs); /** * @see DashSegmentIndex#isExplicit() @@ -250,8 +256,8 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum(long periodDurationUs) { - return startNumber + mediaSegments.size() - 1; + public int getSegmentCount(long periodDurationUs) { + return mediaSegments.size(); } @Override @@ -322,14 +328,14 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum(long periodDurationUs) { + public int getSegmentCount(long periodDurationUs) { if (segmentTimeline != null) { - return segmentTimeline.size() + startNumber - 1; - } else if (periodDurationUs == C.TIME_UNSET) { - return DashSegmentIndex.INDEX_UNBOUNDED; - } else { + return segmentTimeline.size(); + } else if (periodDurationUs != C.TIME_UNSET) { long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; - return startNumber + (int) Util.ceilDivide(periodDurationUs, durationUs) - 1; + return (int) Util.ceilDivide(periodDurationUs, durationUs); + } else { + return DashSegmentIndex.INDEX_UNBOUNDED; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/SingleSegmentIndex.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/SingleSegmentIndex.java index bccc4538c..eb1d0e97f 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/SingleSegmentIndex.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/dash/manifest/SingleSegmentIndex.java @@ -57,8 +57,8 @@ import org.telegram.messenger.exoplayer2.source.dash.DashSegmentIndex; } @Override - public int getLastSegmentNum(long periodDurationUs) { - return 0; + public int getSegmentCount(long periodDurationUs) { + return 1; } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java new file mode 100755 index 000000000..5441e97ce --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java @@ -0,0 +1,39 @@ +/* + * 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 org.telegram.messenger.exoplayer2.source.hls; + +import org.telegram.messenger.exoplayer2.upstream.DataSource; + +/** + * Default implementation of {@link HlsDataSourceFactory}. + */ +public final class DefaultHlsDataSourceFactory implements HlsDataSourceFactory { + + private final DataSource.Factory dataSourceFactory; + + /** + * @param dataSourceFactory The {@link DataSource.Factory} to use for all data types. + */ + public DefaultHlsDataSourceFactory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + } + + @Override + public DataSource createDataSource(int dataType) { + return dataSourceFactory.createDataSource(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsChunkSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsChunkSource.java index 27cd3ba63..2202d022a 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsChunkSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsChunkSource.java @@ -19,7 +19,6 @@ import android.net.Uri; import android.os.SystemClock; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; import org.telegram.messenger.exoplayer2.source.BehindLiveWindowException; import org.telegram.messenger.exoplayer2.source.TrackGroup; import org.telegram.messenger.exoplayer2.source.chunk.Chunk; @@ -33,11 +32,13 @@ import org.telegram.messenger.exoplayer2.trackselection.BaseTrackSelection; import org.telegram.messenger.exoplayer2.trackselection.TrackSelection; import org.telegram.messenger.exoplayer2.upstream.DataSource; import org.telegram.messenger.exoplayer2.upstream.DataSpec; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; import org.telegram.messenger.exoplayer2.util.UriUtil; import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; +import java.util.List; import java.util.Locale; /** @@ -80,11 +81,13 @@ import java.util.Locale; } - private final DataSource dataSource; + private final DataSource mediaDataSource; + private final DataSource encryptionDataSource; private final TimestampAdjusterProvider timestampAdjusterProvider; private final HlsUrl[] variants; private final HlsPlaylistTracker playlistTracker; private final TrackGroup trackGroup; + private final List muxedCaptionFormats; private boolean isTimestampMaster; private byte[] scratchSpace; @@ -103,24 +106,28 @@ import java.util.Locale; /** * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. * @param variants The available variants. - * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the + * chunks. * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. */ public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, - DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider) { + HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider, + List muxedCaptionFormats) { this.playlistTracker = playlistTracker; this.variants = variants; - this.dataSource = dataSource; this.timestampAdjusterProvider = timestampAdjusterProvider; - + this.muxedCaptionFormats = muxedCaptionFormats; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { variantFormats[i] = variants[i].format; initialTrackSelection[i] = i; } + mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA); + encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM); trackGroup = new TrackGroup(variantFormats); trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection); } @@ -190,19 +197,20 @@ import java.util.Locale; // Use start time of the previous chunk rather than its end time because switching format will // require downloading overlapping segments. long bufferedDurationUs = previous == null ? 0 - : Math.max(0, previous.getAdjustedStartTimeUs() - playbackPositionUs); + : Math.max(0, previous.startTimeUs - playbackPositionUs); // Select the variant. trackSelection.updateSelectedTrack(bufferedDurationUs); - int newVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); + int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); - boolean switchingVariant = oldVariantIndex != newVariantIndex; - HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(variants[newVariantIndex]); - if (mediaPlaylist == null) { - out.playlist = variants[newVariantIndex]; + boolean switchingVariant = oldVariantIndex != selectedVariantIndex; + HlsUrl selectedUrl = variants[selectedVariantIndex]; + if (!playlistTracker.isSnapshotValid(selectedUrl)) { + out.playlist = selectedUrl; // Retry when playlist is refreshed. return; } + HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); // Select the chunk. int chunkMediaSequence; @@ -212,13 +220,15 @@ import java.util.Locale; // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { - chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, targetPositionUs, true, + chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, + targetPositionUs - mediaPlaylist.startTimeUs, true, !playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence; if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) { // We try getting the next chunk without adapting in case that's the reason for falling // behind the live window. - newVariantIndex = oldVariantIndex; - mediaPlaylist = playlistTracker.getPlaylistSnapshot(variants[newVariantIndex]); + selectedVariantIndex = oldVariantIndex; + selectedUrl = variants[selectedVariantIndex]; + mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); chunkMediaSequence = previous.getNextChunkIndex(); } } @@ -235,7 +245,7 @@ import java.util.Locale; if (mediaPlaylist.hasEndTag) { out.endOfStream = true; } else /* Live */ { - out.playlist = variants[newVariantIndex]; + out.playlist = selectedUrl; } return; } @@ -248,7 +258,7 @@ import java.util.Locale; Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); if (!keyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. - out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, newVariantIndex, + out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex, trackSelection.getSelectionReason(), trackSelection.getSelectionData()); return; } @@ -259,16 +269,6 @@ import java.util.Locale; clearEncryptionData(); } - // Compute start time and sequence number of the next chunk. - long startTimeUs = segment.startTimeUs; - if (previous != null && !switchingVariant) { - startTimeUs = previous.getAdjustedEndTimeUs(); - } - Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); - - TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( - segment.discontinuitySequenceNumber, startTimeUs); - DataSpec initDataSpec = null; Segment initSegment = mediaPlaylist.initializationSegment; if (initSegment != null) { @@ -277,13 +277,21 @@ import java.util.Locale; initSegment.byterangeLength, null); } + // Compute start time of the next chunk. + long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs; + int discontinuitySequence = mediaPlaylist.discontinuitySequence + + segment.relativeDiscontinuitySequence; + TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( + discontinuitySequence); + // Configure the data source and spec for the chunk. + Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); - out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex], - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segment, - chunkMediaSequence, isTimestampMaster, timestampAdjuster, previous, encryptionKey, - encryptionIv); + out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl, + muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), + startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, + isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv); } /** @@ -293,11 +301,7 @@ import java.util.Locale; * @param chunk The chunk whose load has been completed. */ public void onChunkLoadCompleted(Chunk chunk) { - if (chunk instanceof HlsMediaChunk) { - HlsMediaChunk mediaChunk = (HlsMediaChunk) chunk; - playlistTracker.onChunkLoaded(mediaChunk.hlsUrl, mediaChunk.chunkIndex, - mediaChunk.getAdjustedStartTimeUs()); - } else if (chunk instanceof EncryptionKeyChunk) { + if (chunk instanceof EncryptionKeyChunk) { EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk; scratchSpace = encryptionKeyChunk.getDataHolder(); setEncryptionData(encryptionKeyChunk.dataSpec.uri, encryptionKeyChunk.iv, @@ -320,19 +324,19 @@ import java.util.Locale; } /** - * Called when an error is encountered while loading a playlist. + * Called when a playlist is blacklisted. * - * @param url The url that references the playlist whose load encountered the error. - * @param error The error. + * @param url The url that references the blacklisted playlist. + * @param blacklistMs The amount of milliseconds for which the playlist was blacklisted. */ - public void onPlaylistLoadError(HlsUrl url, IOException error) { + public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { int trackGroupIndex = trackGroup.indexOf(url.format); - if (trackGroupIndex == C.INDEX_UNSET) { - // The url is not handled by this chunk source. - return; + if (trackGroupIndex != C.INDEX_UNSET) { + int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex); + if (trackSelectionIndex != C.INDEX_UNSET) { + trackSelection.blacklist(trackSelectionIndex, blacklistMs); + } } - ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection, - trackSelection.indexOf(trackGroupIndex), error); } // Private methods. @@ -340,7 +344,7 @@ import java.util.Locale; private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, int trackSelectionReason, Object trackSelectionData) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); - return new EncryptionKeyChunk(dataSource, dataSpec, variants[variantIndex].format, + return new EncryptionKeyChunk(encryptionDataSource, dataSpec, variants[variantIndex].format, trackSelectionReason, trackSelectionData, scratchSpace, iv); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsDataSourceFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsDataSourceFactory.java new file mode 100755 index 000000000..580003455 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsDataSourceFactory.java @@ -0,0 +1,35 @@ +/* + * 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 org.telegram.messenger.exoplayer2.source.hls; + +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.upstream.DataSource; + +/** + * Creates {@link DataSource}s for HLS playlists, encryption and media chunks. + */ +public interface HlsDataSourceFactory { + + /** + * Creates a {@link DataSource} for the given data type. + * + * @param dataType The data type for which the {@link DataSource} will be used. One of {@link C} + * {@code .DATA_TYPE_*} constants. + * @return A {@link DataSource} for the given data type. + */ + DataSource createDataSource(int dataType); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsManifest.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsManifest.java new file mode 100755 index 000000000..e19f3405f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsManifest.java @@ -0,0 +1,44 @@ +/* + * 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 org.telegram.messenger.exoplayer2.source.hls; + +import org.telegram.messenger.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import org.telegram.messenger.exoplayer2.source.hls.playlist.HlsMediaPlaylist; + +/** + * Holds a master playlist along with a snapshot of one of its media playlists. + */ +public final class HlsManifest { + + /** + * The master playlist of an HLS stream. + */ + public final HlsMasterPlaylist masterPlaylist; + /** + * A snapshot of a media playlist referred to by {@link #masterPlaylist}. + */ + public final HlsMediaPlaylist mediaPlaylist; + + /** + * @param masterPlaylist The master playlist. + * @param mediaPlaylist The media playlist. + */ + HlsManifest(HlsMasterPlaylist masterPlaylist, HlsMediaPlaylist mediaPlaylist) { + this.masterPlaylist = masterPlaylist; + this.mediaPlaylist = mediaPlaylist; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaChunk.java index e3239448d..416b675ae 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaChunk.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaChunk.java @@ -16,24 +16,30 @@ package org.telegram.messenger.exoplayer2.source.hls; import android.text.TextUtils; +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.extractor.DefaultExtractorInput; import org.telegram.messenger.exoplayer2.extractor.Extractor; import org.telegram.messenger.exoplayer2.extractor.ExtractorInput; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; import org.telegram.messenger.exoplayer2.extractor.mp3.Mp3Extractor; import org.telegram.messenger.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import org.telegram.messenger.exoplayer2.extractor.ts.Ac3Extractor; import org.telegram.messenger.exoplayer2.extractor.ts.AdtsExtractor; import org.telegram.messenger.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import org.telegram.messenger.exoplayer2.extractor.ts.TsExtractor; +import org.telegram.messenger.exoplayer2.metadata.Metadata; +import org.telegram.messenger.exoplayer2.metadata.id3.Id3Decoder; +import org.telegram.messenger.exoplayer2.metadata.id3.PrivFrame; import org.telegram.messenger.exoplayer2.source.chunk.MediaChunk; import org.telegram.messenger.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; -import org.telegram.messenger.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import org.telegram.messenger.exoplayer2.upstream.DataSource; import org.telegram.messenger.exoplayer2.upstream.DataSpec; import org.telegram.messenger.exoplayer2.util.MimeTypes; +import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** @@ -43,11 +49,15 @@ import java.util.concurrent.atomic.AtomicInteger; private static final AtomicInteger UID_SOURCE = new AtomicInteger(); + private static final String PRIV_TIMESTAMP_FRAME_OWNER = + "com.apple.streaming.transportStreamTimestamp"; + private static final String AAC_FILE_EXTENSION = ".aac"; private static final String AC3_FILE_EXTENSION = ".ac3"; private static final String EC3_FILE_EXTENSION = ".ec3"; private static final String MP3_FILE_EXTENSION = ".mp3"; private static final String MP4_FILE_EXTENSION = ".mp4"; + private static final String M4_FILE_EXTENSION_PREFIX = ".m4"; private static final String VTT_FILE_EXTENSION = ".vtt"; private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; @@ -71,14 +81,21 @@ import java.util.concurrent.atomic.AtomicInteger; private final boolean isEncrypted; private final boolean isMasterTimestampSource; private final TimestampAdjuster timestampAdjuster; - private final HlsMediaChunk previousChunk; + private final String lastPathSegment; + private final Extractor previousExtractor; + private final boolean shouldSpliceIn; + private final boolean needNewExtractor; + private final List muxedCaptionFormats; + + private final boolean isPackedAudio; + private final Id3Decoder id3Decoder; + private final ParsableByteArray id3Data; private Extractor extractor; private int initSegmentBytesLoaded; private int bytesLoaded; private boolean initLoadCompleted; private HlsSampleStreamWrapper extractorOutput; - private long adjustedEndTimeUs; private volatile boolean loadCanceled; private volatile boolean loadCompleted; @@ -87,10 +104,13 @@ import java.util.concurrent.atomic.AtomicInteger; * @param dataSpec Defines the data to be loaded. * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. * @param hlsUrl The url of the playlist from which this chunk was obtained. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. - * @param segment The {@link Segment} for which this media chunk is created. + * @param startTimeUs The start time of the chunk in microseconds. + * @param endTimeUs The end time of the chunk in microseconds. * @param chunkIndex The media sequence number of the chunk. + * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. @@ -98,22 +118,41 @@ import java.util.concurrent.atomic.AtomicInteger; * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, - HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, Segment segment, - int chunkIndex, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, - HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) { + HlsUrl hlsUrl, List muxedCaptionFormats, int trackSelectionReason, + Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, + int discontinuitySequenceNumber, boolean isMasterTimestampSource, + TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey, + byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, - trackSelectionReason, trackSelectionData, segment.startTimeUs, - segment.startTimeUs + segment.durationUs, chunkIndex); + trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; + this.muxedCaptionFormats = muxedCaptionFormats; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; - this.previousChunk = previousChunk; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; + lastPathSegment = dataSpec.uri.getLastPathSegment(); + isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION) + || lastPathSegment.endsWith(AC3_FILE_EXTENSION) + || lastPathSegment.endsWith(EC3_FILE_EXTENSION) + || lastPathSegment.endsWith(MP3_FILE_EXTENSION); + if (previousChunk != null) { + id3Decoder = previousChunk.id3Decoder; + id3Data = previousChunk.id3Data; + previousExtractor = previousChunk.extractor; + shouldSpliceIn = previousChunk.hlsUrl != hlsUrl; + needNewExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber + || shouldSpliceIn; + } else { + id3Decoder = isPackedAudio ? new Id3Decoder() : null; + id3Data = isPackedAudio ? new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH) : null; + previousExtractor = null; + shouldSpliceIn = false; + needNewExtractor = true; + } initDataSource = dataSource; - discontinuitySequenceNumber = segment.discontinuitySequenceNumber; - adjustedEndTimeUs = endTimeUs; uid = UID_SOURCE.getAndIncrement(); } @@ -125,21 +164,7 @@ import java.util.concurrent.atomic.AtomicInteger; */ public void init(HlsSampleStreamWrapper output) { extractorOutput = output; - output.init(uid, previousChunk != null && previousChunk.hlsUrl != hlsUrl); - } - - /** - * Returns the presentation time in microseconds of the first sample in the chunk. - */ - public long getAdjustedStartTimeUs() { - return adjustedEndTimeUs - getDurationUs(); - } - - /** - * Returns the presentation time in microseconds of the last sample in the chunk - */ - public long getAdjustedEndTimeUs() { - return adjustedEndTimeUs; + output.init(uid, shouldSpliceIn); } @Override @@ -166,8 +191,9 @@ import java.util.concurrent.atomic.AtomicInteger; @Override public void load() throws IOException, InterruptedException { - if (extractor == null) { - extractor = buildExtractor(); + if (extractor == null && !isPackedAudio) { + // See HLS spec, version 20, Section 3.4 for more information on packed audio extraction. + extractor = createExtractor(); } maybeLoadInitData(); if (!loadCanceled) { @@ -175,64 +201,11 @@ import java.util.concurrent.atomic.AtomicInteger; } } - // Private methods. - - private Extractor buildExtractor() { - // Set the extractor that will read the chunk. - Extractor extractor; - boolean needNewExtractor = previousChunk == null - || previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber - || trackFormat != previousChunk.trackFormat; - boolean usingNewExtractor = true; - String lastPathSegment = dataSpec.uri.getLastPathSegment(); - if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { - // TODO: Inject a timestamp adjuster and use it along with ID3 PRIV tag values with owner - // identifier com.apple.streaming.transportStreamTimestamp. This may also apply to the MP3 - // case below. - extractor = new AdtsExtractor(startTimeUs); - } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION) - || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { - extractor = new Ac3Extractor(startTimeUs); - } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { - extractor = new Mp3Extractor(startTimeUs); - } else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) - || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { - extractor = new WebvttExtractor(trackFormat.language, timestampAdjuster); - } else if (!needNewExtractor) { - // Only reuse TS and fMP4 extractors. - usingNewExtractor = false; - extractor = previousChunk.extractor; - } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) { - extractor = new FragmentedMp4Extractor(0, timestampAdjuster); - } else { - // MPEG-2 TS segments, but we need a new extractor. - // This flag ensures the change of pid between streams does not affect the sample queues. - @DefaultTsPayloadReaderFactory.Flags - int esReaderFactoryFlags = 0; - String codecs = trackFormat.codecs; - if (!TextUtils.isEmpty(codecs)) { - // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really - // exist. If we know from the codec attribute that they don't exist, then we can - // explicitly ignore them even if they're declared. - if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; - } - if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; - } - } - extractor = new TsExtractor(timestampAdjuster, - new DefaultTsPayloadReaderFactory(esReaderFactoryFlags), true); - } - if (usingNewExtractor) { - extractor.init(extractorOutput); - } - return extractor; - } + // Internal loading methods. private void maybeLoadInitData() throws IOException, InterruptedException { - if (previousChunk == null || previousChunk.extractor != extractor || initLoadCompleted - || initDataSpec == null) { + if (previousExtractor == extractor || initLoadCompleted || initDataSpec == null) { + // According to spec, for packed audio, initDataSpec is expected to be null. return; } DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded); @@ -245,7 +218,7 @@ import java.util.concurrent.atomic.AtomicInteger; result = extractor.read(input, null); } } finally { - initSegmentBytesLoaded += (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + initSegmentBytesLoaded = (int) (input.getPosition() - initDataSpec.absoluteStreamPosition); } } finally { Util.closeQuietly(dataSource); @@ -267,24 +240,29 @@ import java.util.concurrent.atomic.AtomicInteger; loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); skipLoadedBytes = false; } + if (!isMasterTimestampSource) { + timestampAdjuster.waitUntilInitialized(); + } else if (timestampAdjuster.getFirstSampleTimestampUs() == TimestampAdjuster.DO_NOT_OFFSET) { + // We're the master and we haven't set the desired first sample timestamp yet. + timestampAdjuster.setFirstSampleTimestampUs(startTimeUs); + } try { ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); + if (extractor == null) { + // Media segment format is packed audio. + long id3Timestamp = peekId3PrivTimestamp(input); + extractor = buildPackedAudioExtractor(id3Timestamp != C.TIME_UNSET + ? timestampAdjuster.adjustTsTimestamp(id3Timestamp) : startTimeUs); + } if (skipLoadedBytes) { input.skipFully(bytesLoaded); } try { int result = Extractor.RESULT_CONTINUE; - if (!isMasterTimestampSource && timestampAdjuster != null) { - timestampAdjuster.waitUntilInitialized(); - } while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { result = extractor.read(input, null); } - long adjustedEndTimeUs = extractorOutput.getLargestQueuedTimestampUs(); - if (adjustedEndTimeUs != Long.MIN_VALUE) { - this.adjustedEndTimeUs = adjustedEndTimeUs; - } } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } @@ -294,6 +272,58 @@ import java.util.concurrent.atomic.AtomicInteger; loadCompleted = true; } + /** + * Peek the presentation timestamp of the first sample in the chunk from an ID3 PRIV as defined + * in the HLS spec, version 20, Section 3.4. Returns {@link C#TIME_UNSET} if the frame is not + * found. This method only modifies the peek position. + * + * @param input The {@link ExtractorInput} to obtain the PRIV frame from. + * @return The parsed, adjusted timestamp in microseconds + * @throws IOException If an error occurred peeking from the input. + * @throws InterruptedException If the thread was interrupted. + */ + private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException { + input.resetPeekPosition(); + if (!input.peekFully(id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH, true)) { + return C.TIME_UNSET; + } + id3Data.reset(Id3Decoder.ID3_HEADER_LENGTH); + int id = id3Data.readUnsignedInt24(); + if (id != Id3Decoder.ID3_TAG) { + return C.TIME_UNSET; + } + id3Data.skipBytes(3); // version(2), flags(1). + int id3Size = id3Data.readSynchSafeInt(); + int requiredCapacity = id3Size + Id3Decoder.ID3_HEADER_LENGTH; + if (requiredCapacity > id3Data.capacity()) { + byte[] data = id3Data.data; + id3Data.reset(requiredCapacity); + System.arraycopy(data, 0, id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH); + } + if (!input.peekFully(id3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size, true)) { + return C.TIME_UNSET; + } + Metadata metadata = id3Decoder.decode(id3Data.data, id3Size); + if (metadata == null) { + return C.TIME_UNSET; + } + int metadataLength = metadata.length(); + for (int i = 0; i < metadataLength; i++) { + Metadata.Entry frame = metadata.get(i); + if (frame instanceof PrivFrame) { + PrivFrame privFrame = (PrivFrame) frame; + if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { + System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); + id3Data.reset(8); + return id3Data.readLong(); + } + } + } + return C.TIME_UNSET; + } + + // Internal factory methods. + /** * If the content is encrypted, returns an {@link Aes128DataSource} that wraps the original in * order to decrypt the loaded data. Else returns the original. @@ -306,4 +336,65 @@ import java.util.concurrent.atomic.AtomicInteger; return new Aes128DataSource(dataSource, encryptionKey, encryptionIv); } + private Extractor createExtractor() { + // Select the extractor that will read the chunk. + Extractor extractor; + boolean usingNewExtractor = true; + if (MimeTypes.TEXT_VTT.equals(hlsUrl.format.sampleMimeType) + || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) + || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { + extractor = new WebvttExtractor(trackFormat.language, timestampAdjuster); + } else if (!needNewExtractor) { + // Only reuse TS and fMP4 extractors. + usingNewExtractor = false; + extractor = previousExtractor; + } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) + || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { + extractor = new FragmentedMp4Extractor(0, timestampAdjuster); + } else { + // MPEG-2 TS segments, but we need a new extractor. + // This flag ensures the change of pid between streams does not affect the sample queues. + @DefaultTsPayloadReaderFactory.Flags + int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM; + if (!muxedCaptionFormats.isEmpty()) { + // The playlist declares closed caption renditions, we should ignore descriptors. + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; + } + String codecs = trackFormat.codecs; + if (!TextUtils.isEmpty(codecs)) { + // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really + // exist. If we know from the codec attribute that they don't exist, then we can + // explicitly ignore them even if they're declared. + if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; + } + if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; + } + } + extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster, + new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats)); + } + if (usingNewExtractor) { + extractor.init(extractorOutput); + } + return extractor; + } + + private Extractor buildPackedAudioExtractor(long startTimeUs) { + Extractor extractor; + if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { + extractor = new AdtsExtractor(startTimeUs); + } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION) + || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { + extractor = new Ac3Extractor(startTimeUs); + } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { + extractor = new Mp3Extractor(0, startTimeUs); + } else { + throw new IllegalArgumentException("Unkown extension for audio file: " + lastPathSegment); + } + extractor.init(extractorOutput); + return extractor; + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaPeriod.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaPeriod.java index 7c39fe75a..5b502dde2 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaPeriod.java @@ -30,11 +30,10 @@ import org.telegram.messenger.exoplayer2.source.hls.playlist.HlsMasterPlaylist.H import org.telegram.messenger.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import org.telegram.messenger.exoplayer2.trackselection.TrackSelection; import org.telegram.messenger.exoplayer2.upstream.Allocator; -import org.telegram.messenger.exoplayer2.upstream.DataSource; -import org.telegram.messenger.exoplayer2.upstream.Loader; import org.telegram.messenger.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; @@ -42,17 +41,16 @@ import java.util.List; * A {@link MediaPeriod} that loads an HLS stream. */ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback, - HlsPlaylistTracker.PlaylistRefreshCallback { + HlsPlaylistTracker.PlaylistEventListener { private final HlsPlaylistTracker playlistTracker; - private final DataSource.Factory dataSourceFactory; + private final HlsDataSourceFactory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final Allocator allocator; private final IdentityHashMap streamWrapperIndices; private final TimestampAdjusterProvider timestampAdjusterProvider; private final Handler continueLoadingHandler; - private final Loader manifestFetcher; private final long preparePositionUs; private Callback callback; @@ -63,7 +61,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private CompositeSequenceableLoader sequenceableLoader; - public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, DataSource.Factory dataSourceFactory, + public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator, long positionUs) { this.playlistTracker = playlistTracker; @@ -74,13 +72,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); continueLoadingHandler = new Handler(); - manifestFetcher = new Loader("Loader:ManifestFetcher"); preparePositionUs = positionUs; } public void release() { + playlistTracker.removeListener(this); continueLoadingHandler.removeCallbacksAndMessages(null); - manifestFetcher.release(); if (sampleStreamWrappers != null) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { sampleStreamWrapper.release(); @@ -90,15 +87,14 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public void prepare(Callback callback) { + playlistTracker.addListener(this); this.callback = callback; buildAndPrepareSampleStreamWrappers(); } @Override public void maybeThrowPrepareError() throws IOException { - if (sampleStreamWrappers == null) { - manifestFetcher.maybeThrowError(); - } else { + if (sampleStreamWrappers != null) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { sampleStreamWrapper.maybeThrowPrepareError(); } @@ -193,6 +189,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); @@ -255,7 +256,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public void onPlaylistRefreshRequired(HlsUrl url) { - playlistTracker.refreshPlaylist(url, this); + playlistTracker.refreshPlaylist(url); } @Override @@ -271,22 +272,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public void onPlaylistChanged() { - if (trackGroups != null) { - callback.onContinueLoadingRequested(this); - } else { - // Some of the wrappers were waiting for their media playlist to prepare. - for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) { - wrapper.continuePreparing(); - } - } + continuePreparingOrLoading(); } @Override - public void onPlaylistLoadError(HlsUrl url, IOException error) { - for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { - sampleStreamWrapper.onPlaylistLoadError(url, error); + public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { + for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) { + streamWrapper.onPlaylistBlacklisted(url, blacklistMs); } - callback.onContinueLoadingRequested(this); + continuePreparingOrLoading(); } // Internal methods. @@ -328,7 +322,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; selectedVariants.toArray(variants); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, - variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); + variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.setIsTimestampMaster(true); sampleStreamWrapper.continuePreparing(); @@ -338,7 +332,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Build audio stream wrappers. for (int i = 0; i < audioRenditions.size(); i++) { sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, - new HlsUrl[] {audioRenditions.get(i)}, null, null); + new HlsUrl[] {audioRenditions.get(i)}, null, Collections.emptyList()); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.continuePreparing(); } @@ -347,20 +341,29 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper for (int i = 0; i < subtitleRenditions.size(); i++) { HlsUrl url = subtitleRenditions.get(i); sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, - null); + Collections.emptyList()); sampleStreamWrapper.prepareSingleTrack(url.format); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; } } private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, - Format muxedAudioFormat, Format muxedCaptionFormat) { - DataSource dataSource = dataSourceFactory.createDataSource(); - HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSource, - timestampAdjusterProvider); + Format muxedAudioFormat, List muxedCaptionFormats) { + HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, + dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats); return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, - preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount, - eventDispatcher); + preparePositionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher); + } + + private void continuePreparingOrLoading() { + if (trackGroups != null) { + callback.onContinueLoadingRequested(this); + } else { + // Some of the wrappers were waiting for their media playlist to prepare. + for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) { + wrapper.continuePreparing(); + } + } } private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaSource.java index 6f64354d9..601d6048e 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsMediaSource.java @@ -18,6 +18,7 @@ package org.telegram.messenger.exoplayer2.source.hls; import android.net.Uri; import android.os.Handler; import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.ExoPlayer; import org.telegram.messenger.exoplayer2.source.AdaptiveMediaSourceEventListener; import org.telegram.messenger.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import org.telegram.messenger.exoplayer2.source.MediaPeriod; @@ -43,12 +44,12 @@ public final class HlsMediaSource implements MediaSource, public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; private final Uri manifestUri; - private final DataSource.Factory dataSourceFactory; + private final HlsDataSourceFactory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private HlsPlaylistTracker playlistTracker; - private MediaSource.Listener sourceListener; + private Listener sourceListener; public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -59,6 +60,13 @@ public final class HlsMediaSource implements MediaSource, public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), minLoadableRetryCount, + eventHandler, eventListener); + } + + public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; @@ -66,7 +74,7 @@ public final class HlsMediaSource implements MediaSource, } @Override - public void prepareSource(MediaSource.Listener listener) { + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assertions.checkState(playlistTracker == null); playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, minLoadableRetryCount, this); @@ -76,7 +84,7 @@ public final class HlsMediaSource implements MediaSource, @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - playlistTracker.maybeThrowPrimaryPlaylistRefreshError(); + playlistTracker.maybeThrowPlaylistRefreshError(); } @Override @@ -93,27 +101,36 @@ public final class HlsMediaSource implements MediaSource, @Override public void releaseSource() { - playlistTracker.release(); - playlistTracker = null; + if (playlistTracker != null) { + playlistTracker.release(); + playlistTracker = null; + } sourceListener = null; } @Override public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { SinglePeriodTimeline timeline; + long windowDefaultStartPositionUs = playlist.startOffsetUs; if (playlistTracker.isLive()) { - // TODO: fix windowPositionInPeriodUs when playlist is empty. - long windowPositionInPeriodUs = playlist.getStartTimeUs(); + long periodDurationUs = playlist.hasEndTag ? (playlist.startTimeUs + playlist.durationUs) + : C.TIME_UNSET; List segments = playlist.segments; - long windowDefaultStartPositionUs = segments.isEmpty() ? 0 - : segments.get(Math.max(0, segments.size() - 3)).startTimeUs - windowPositionInPeriodUs; - timeline = new SinglePeriodTimeline(C.TIME_UNSET, playlist.durationUs, - windowPositionInPeriodUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); + if (windowDefaultStartPositionUs == C.TIME_UNSET) { + windowDefaultStartPositionUs = segments.isEmpty() ? 0 + : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs; + } + timeline = new SinglePeriodTimeline(periodDurationUs, playlist.durationUs, + playlist.startTimeUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); } else /* not live */ { - timeline = new SinglePeriodTimeline(playlist.durationUs, playlist.durationUs, 0, 0, true, - false); + if (windowDefaultStartPositionUs == C.TIME_UNSET) { + windowDefaultStartPositionUs = 0; + } + timeline = new SinglePeriodTimeline(playlist.startTimeUs + playlist.durationUs, + playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, true, false); } - sourceListener.onSourceInfoRefreshed(timeline, playlist); + sourceListener.onSourceInfoRefreshed(timeline, + new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsSampleStream.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsSampleStream.java index 37ef1539a..801f46542 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsSampleStream.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsSampleStream.java @@ -26,7 +26,7 @@ import java.io.IOException; /* package */ final class HlsSampleStream implements SampleStream { public final int group; - + private final HlsSampleStreamWrapper sampleStreamWrapper; public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int group) { @@ -45,13 +45,13 @@ import java.io.IOException; } @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - return sampleStreamWrapper.readData(group, formatHolder, buffer); + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { + return sampleStreamWrapper.readData(group, formatHolder, buffer, requireFormat); } @Override - public void skipToKeyframeBefore(long timeUs) { - sampleStreamWrapper.skipToKeyframeBefore(group, timeUs); + public void skipData(long positionUs) { + sampleStreamWrapper.skipData(group, positionUs); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 8129bce7c..9fe156fd8 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -77,7 +77,6 @@ import java.util.LinkedList; private final HlsChunkSource chunkSource; private final Allocator allocator; private final Format muxedAudioFormat; - private final Format muxedCaptionFormat; private final int minLoadableRetryCount; private final Loader loader; private final EventDispatcher eventDispatcher; @@ -112,23 +111,19 @@ import java.util.LinkedList; * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. - * @param muxedAudioFormat If HLS master playlist indicates that the stream contains muxed audio, - * this is the audio {@link Format} as defined by the playlist. - * @param muxedCaptionFormat If HLS master playlist indicates that the stream contains muxed - * captions, this is the audio {@link Format} as defined by the playlist. + * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. * @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. */ public HlsSampleStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource, - Allocator allocator, long positionUs, Format muxedAudioFormat, Format muxedCaptionFormat, - int minLoadableRetryCount, EventDispatcher eventDispatcher) { + Allocator allocator, long positionUs, Format muxedAudioFormat, int minLoadableRetryCount, + EventDispatcher eventDispatcher) { this.trackType = trackType; this.callback = callback; this.chunkSource = chunkSource; this.allocator = allocator; this.muxedAudioFormat = muxedAudioFormat; - this.muxedCaptionFormat = muxedCaptionFormat; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; loader = new Loader("Loader:HlsSampleStreamWrapper"); @@ -157,7 +152,7 @@ import java.util.LinkedList; * prepare. */ public void prepareSingleTrack(Format format) { - track(0).format(format); + track(0, C.TRACK_TYPE_UNKNOWN).format(format); sampleQueuesBuilt = true; maybeFinishPrepare(); } @@ -183,6 +178,7 @@ import java.util.LinkedList; } } // Enable new tracks. + TrackSelection primaryTrackSelection = null; boolean selectedNewTracks = false; for (int i = 0; i < selections.length; i++) { if (streams[i] == null && selections[i] != null) { @@ -190,6 +186,7 @@ import java.util.LinkedList; int group = trackGroups.indexOf(selection.getTrackGroup()); setTrackGroupEnabledState(group, true); if (group == primaryTrackGroupIndex) { + primaryTrackSelection = selection; chunkSource.selectTracks(selection); } streams[i] = new HlsSampleStream(this, group); @@ -206,6 +203,14 @@ import java.util.LinkedList; sampleQueues.valueAt(i).disable(); } } + if (primaryTrackSelection != null && !mediaChunks.isEmpty()) { + primaryTrackSelection.updateSelectedTrack(0); + int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); + if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { + // The loaded preparation chunk does match the selection. We discard it. + seekTo(lastSeekPositionUs); + } + } } // Cancel requests if necessary. if (enabledTrackCount == 0) { @@ -266,21 +271,12 @@ import java.util.LinkedList; released = true; } - public long getLargestQueuedTimestampUs() { - long largestQueuedTimestampUs = Long.MIN_VALUE; - for (int i = 0; i < sampleQueues.size(); i++) { - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); - } - return largestQueuedTimestampUs; - } - public void setIsTimestampMaster(boolean isTimestampMaster) { chunkSource.setIsTimestampMaster(isTimestampMaster); } - public void onPlaylistLoadError(HlsUrl url, IOException error) { - chunkSource.onPlaylistLoadError(url, error); + public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { + chunkSource.onPlaylistBlacklisted(url, blacklistMs); } // SampleStream implementation. @@ -294,7 +290,8 @@ import java.util.LinkedList; chunkSource.maybeThrowError(); } - /* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer) { + /* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean requireFormat) { if (isPendingReset()) { return C.RESULT_NOTHING_READ; } @@ -311,12 +308,17 @@ import java.util.LinkedList; } downstreamTrackFormat = trackFormat; - return sampleQueues.valueAt(group).readData(formatHolder, buffer, loadingFinished, - lastSeekPositionUs); + return sampleQueues.valueAt(group).readData(formatHolder, buffer, requireFormat, + loadingFinished, lastSeekPositionUs); } - /* package */ void skipToKeyframeBefore(int group, long timeUs) { - sampleQueues.valueAt(group).skipToKeyframeBefore(timeUs); + /* package */ void skipData(int group, long positionUs) { + DefaultTrackOutput sampleQueue = sampleQueues.valueAt(group); + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + sampleQueue.skipAll(); + } else { + sampleQueue.skipToKeyframeBefore(positionUs, true); + } } private boolean finishedReadingChunk(HlsMediaChunk chunk) { @@ -466,7 +468,7 @@ import java.util.LinkedList; // ExtractorOutput implementation. Called by the loading thread. @Override - public DefaultTrackOutput track(int id) { + public DefaultTrackOutput track(int id, int type) { if (sampleQueues.indexOfKey(id) >= 0) { return sampleQueues.get(id); } @@ -589,14 +591,8 @@ import java.util.LinkedList; trackGroups[i] = new TrackGroup(formats); primaryTrackGroupIndex = i; } else { - Format trackFormat = null; - if (primaryExtractorTrackType == PRIMARY_TYPE_VIDEO) { - if (MimeTypes.isAudio(sampleFormat.sampleMimeType)) { - trackFormat = muxedAudioFormat; - } else if (MimeTypes.APPLICATION_CEA608.equals(sampleFormat.sampleMimeType)) { - trackFormat = muxedCaptionFormat; - } - } + Format trackFormat = primaryExtractorTrackType == PRIMARY_TYPE_VIDEO + && MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null; trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat)); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/TimestampAdjusterProvider.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/TimestampAdjusterProvider.java index ce7e5d09b..af3b72d9a 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/TimestampAdjusterProvider.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/TimestampAdjusterProvider.java @@ -16,7 +16,7 @@ package org.telegram.messenger.exoplayer2.source.hls; import android.util.SparseArray; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; /** * Provides {@link TimestampAdjuster} instances for use during HLS playbacks. @@ -36,13 +36,12 @@ public final class TimestampAdjusterProvider { * a chunk with a given discontinuity sequence. * * @param discontinuitySequence The chunk's discontinuity sequence. - * @param startTimeUs The chunk's start time. * @return A {@link TimestampAdjuster}. */ - public TimestampAdjuster getAdjuster(int discontinuitySequence, long startTimeUs) { + public TimestampAdjuster getAdjuster(int discontinuitySequence) { TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence); if (adjuster == null) { - adjuster = new TimestampAdjuster(startTimeUs); + adjuster = new TimestampAdjuster(TimestampAdjuster.DO_NOT_OFFSET); timestampAdjusters.put(discontinuitySequence, adjuster); } return adjuster; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/WebvttExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/WebvttExtractor.java index 212c451e8..d3c2644ca 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/WebvttExtractor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/WebvttExtractor.java @@ -24,12 +24,12 @@ import org.telegram.messenger.exoplayer2.extractor.ExtractorInput; import org.telegram.messenger.exoplayer2.extractor.ExtractorOutput; import org.telegram.messenger.exoplayer2.extractor.PositionHolder; import org.telegram.messenger.exoplayer2.extractor.SeekMap; -import org.telegram.messenger.exoplayer2.extractor.TimestampAdjuster; import org.telegram.messenger.exoplayer2.extractor.TrackOutput; import org.telegram.messenger.exoplayer2.text.SubtitleDecoderException; import org.telegram.messenger.exoplayer2.text.webvtt.WebvttParserUtil; import org.telegram.messenger.exoplayer2.util.MimeTypes; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.TimestampAdjuster; import java.io.IOException; import java.util.Arrays; import java.util.regex.Matcher; @@ -79,7 +79,7 @@ import java.util.regex.Pattern; } @Override - public void seek(long position) { + public void seek(long position, long timeUs) { // This extractor is only used for the HLS use case, which should not call this method. throw new IllegalStateException(); } @@ -167,7 +167,7 @@ import java.util.regex.Pattern; } private TrackOutput buildTrackOutput(long subsampleOffsetUs) { - TrackOutput trackOutput = output.track(0); + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT); trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, language, null, subsampleOffsetUs)); output.endTracks(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index db8b539a2..0a0b314a3 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -17,7 +17,6 @@ package org.telegram.messenger.exoplayer2.source.hls.playlist; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.util.MimeTypes; - import java.util.Collections; import java.util.List; @@ -31,27 +30,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist { */ public static final class HlsUrl { - public final String name; public final String url; public final Format format; - public final Format videoFormat; - public final Format audioFormat; - public final Format[] textFormats; public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) { Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null, - Format.NO_VALUE); - return new HlsUrl(null, baseUri, format, null, null, null); + Format.NO_VALUE, 0, null); + return new HlsUrl(baseUri, format); } - public HlsUrl(String name, String url, Format format, Format videoFormat, Format audioFormat, - Format[] textFormats) { - this.name = name; + public HlsUrl(String url, Format format) { this.url = url; this.format = format; - this.videoFormat = videoFormat; - this.audioFormat = audioFormat; - this.textFormats = textFormats; } } @@ -61,22 +51,23 @@ public final class HlsMasterPlaylist extends HlsPlaylist { public final List subtitles; public final Format muxedAudioFormat; - public final Format muxedCaptionFormat; + public final List muxedCaptionFormats; public HlsMasterPlaylist(String baseUri, List variants, List audios, - List subtitles, Format muxedAudioFormat, Format muxedCaptionFormat) { - super(baseUri, HlsPlaylist.TYPE_MASTER); + List subtitles, Format muxedAudioFormat, List muxedCaptionFormats) { + super(baseUri); this.variants = Collections.unmodifiableList(variants); this.audios = Collections.unmodifiableList(audios); this.subtitles = Collections.unmodifiableList(subtitles); this.muxedAudioFormat = muxedAudioFormat; - this.muxedCaptionFormat = muxedCaptionFormat; + this.muxedCaptionFormats = Collections.unmodifiableList(muxedCaptionFormats); } public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) { List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri)); List emptyList = Collections.emptyList(); - return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null); + return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, + Collections.emptyList()); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index f056ae38d..8bafc464a 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -15,8 +15,11 @@ */ package org.telegram.messenger.exoplayer2.source.hls.playlist; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; import org.telegram.messenger.exoplayer2.C; -import java.util.ArrayList; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; @@ -32,8 +35,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final String url; public final long durationUs; - public final int discontinuitySequenceNumber; - public final long startTimeUs; + public final int relativeDiscontinuitySequence; + public final long relativeStartTimeUs; public final boolean isEncrypted; public final String encryptionKeyUri; public final String encryptionIV; @@ -44,13 +47,13 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength); } - public Segment(String uri, long durationUs, int discontinuitySequenceNumber, - long startTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, + public Segment(String uri, long durationUs, int relativeDiscontinuitySequence, + long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, long byterangeOffset, long byterangeLength) { this.url = uri; this.durationUs = durationUs; - this.discontinuitySequenceNumber = discontinuitySequenceNumber; - this.startTimeUs = startTimeUs; + this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; + this.relativeStartTimeUs = relativeStartTimeUs; this.isEncrypted = isEncrypted; this.encryptionKeyUri = encryptionKeyUri; this.encryptionIV = encryptionIV; @@ -59,64 +62,115 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } @Override - public int compareTo(Long startTimeUs) { - return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0); - } - - public Segment copyWithStartTimeUs(long startTimeUs) { - return new Segment(url, durationUs, discontinuitySequenceNumber, startTimeUs, isEncrypted, - encryptionKeyUri, encryptionIV, byterangeOffset, byterangeLength); + public int compareTo(@NonNull Long relativeStartTimeUs) { + return this.relativeStartTimeUs > relativeStartTimeUs + ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0); } } + /** + * Type of the playlist as specified by #EXT-X-PLAYLIST-TYPE. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({PLAYLIST_TYPE_UNKNOWN, PLAYLIST_TYPE_VOD, PLAYLIST_TYPE_EVENT}) + public @interface PlaylistType {} + public static final int PLAYLIST_TYPE_UNKNOWN = 0; + public static final int PLAYLIST_TYPE_VOD = 1; + public static final int PLAYLIST_TYPE_EVENT = 2; + + @PlaylistType public final int playlistType; + public final long startOffsetUs; + public final long startTimeUs; + public final boolean hasDiscontinuitySequence; + public final int discontinuitySequence; public final int mediaSequence; public final int version; + public final long targetDurationUs; + public final boolean hasEndTag; + public final boolean hasProgramDateTime; public final Segment initializationSegment; public final List segments; - public final boolean hasEndTag; public final long durationUs; - public HlsMediaPlaylist(String baseUri, int mediaSequence, int version, - boolean hasEndTag, Segment initializationSegment, List segments) { - super(baseUri, HlsPlaylist.TYPE_MEDIA); + public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs, + long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, + int mediaSequence, int version, long targetDurationUs, boolean hasEndTag, + boolean hasProgramDateTime, Segment initializationSegment, List segments) { + super(baseUri); + this.playlistType = playlistType; + this.startTimeUs = startTimeUs; + this.hasDiscontinuitySequence = hasDiscontinuitySequence; + this.discontinuitySequence = discontinuitySequence; this.mediaSequence = mediaSequence; this.version = version; + this.targetDurationUs = targetDurationUs; this.hasEndTag = hasEndTag; + this.hasProgramDateTime = hasProgramDateTime; this.initializationSegment = initializationSegment; this.segments = Collections.unmodifiableList(segments); - if (!segments.isEmpty()) { - Segment first = segments.get(0); Segment last = segments.get(segments.size() - 1); - durationUs = last.startTimeUs + last.durationUs - first.startTimeUs; + durationUs = last.relativeStartTimeUs + last.durationUs; } else { durationUs = 0; } + this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET + : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs; } - public long getStartTimeUs() { - return segments.isEmpty() ? 0 : segments.get(0).startTimeUs; + /** + * Returns whether this playlist is newer than {@code other}. + * + * @param other The playlist to compare. + * @return Whether this playlist is newer than {@code other}. + */ + public boolean isNewerThan(HlsMediaPlaylist other) { + if (other == null || mediaSequence > other.mediaSequence) { + return true; + } + if (mediaSequence < other.mediaSequence) { + return false; + } + // The media sequences are equal. + int segmentCount = segments.size(); + int otherSegmentCount = other.segments.size(); + return segmentCount > otherSegmentCount + || (segmentCount == otherSegmentCount && hasEndTag && !other.hasEndTag); } public long getEndTimeUs() { - return getStartTimeUs() + durationUs; + return startTimeUs + durationUs; } - public HlsMediaPlaylist copyWithStartTimeUs(long newStartTimeUs) { - long startTimeOffsetUs = newStartTimeUs - getStartTimeUs(); - int segmentsSize = segments.size(); - List newSegments = new ArrayList<>(segmentsSize); - for (int i = 0; i < segmentsSize; i++) { - Segment segment = segments.get(i); - newSegments.add(segment.copyWithStartTimeUs(segment.startTimeUs + startTimeOffsetUs)); + /** + * Returns a playlist identical to this one except for the start time, the discontinuity sequence + * and {@code hasDiscontinuitySequence} values. The first two are set to the specified values, + * {@code hasDiscontinuitySequence} is set to true. + * + * @param startTimeUs The start time for the returned playlist. + * @param discontinuitySequence The discontinuity sequence for the returned playlist. + * @return The playlist. + */ + public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { + return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true, + discontinuitySequence, mediaSequence, version, targetDurationUs, hasEndTag, + hasProgramDateTime, initializationSegment, segments); + } + + /** + * Returns a playlist identical to this one except that an end tag is added. If an end tag is + * already present then the playlist will return itself. + * + * @return The playlist. + */ + public HlsMediaPlaylist copyWithEndTag() { + if (this.hasEndTag) { + return this; } - return copyWithSegments(newSegments); - } - - public HlsMediaPlaylist copyWithSegments(List segments) { - return new HlsMediaPlaylist(baseUri, mediaSequence, version, hasEndTag, - initializationSegment, segments); + return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, + hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, + true, hasProgramDateTime, initializationSegment, segments); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsPlaylist.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsPlaylist.java index 2fb70bda2..967a42a83 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsPlaylist.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsPlaylist.java @@ -15,31 +15,15 @@ */ package org.telegram.messenger.exoplayer2.source.hls.playlist; -import android.support.annotation.IntDef; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - /** * Represents an HLS playlist. */ public abstract class HlsPlaylist { - /** - * The type of playlist. - */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_MASTER, TYPE_MEDIA}) - public @interface Type {} - public static final int TYPE_MASTER = 0; - public static final int TYPE_MEDIA = 1; - public final String baseUri; - @Type - public final int type; - protected HlsPlaylist(String baseUri, @Type int type) { + protected HlsPlaylist(String baseUri) { this.baseUri = baseUri; - this.type = type; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 51943033d..2325e1eca 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -19,9 +19,11 @@ import android.net.Uri; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.ParserException; +import org.telegram.messenger.exoplayer2.source.UnrecognizedInputFormatException; import org.telegram.messenger.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import org.telegram.messenger.exoplayer2.upstream.ParsingLoadable; import org.telegram.messenger.exoplayer2.util.MimeTypes; +import org.telegram.messenger.exoplayer2.util.Util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -38,15 +40,20 @@ import java.util.regex.Pattern; */ public final class HlsPlaylistParser implements ParsingLoadable.Parser { + private static final String PLAYLIST_HEADER = "#EXTM3U"; + private static final String TAG_VERSION = "#EXT-X-VERSION"; + private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE"; private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF"; private static final String TAG_MEDIA = "#EXT-X-MEDIA"; + private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION"; private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY"; private static final String TAG_DISCONTINUITY_SEQUENCE = "#EXT-X-DISCONTINUITY-SEQUENCE"; + private static final String TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME"; private static final String TAG_INIT_SEGMENT = "#EXT-X-MAP"; private static final String TAG_MEDIA_DURATION = "#EXTINF"; private static final String TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE"; - private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION"; + private static final String TAG_START = "#EXT-X-START"; private static final String TAG_ENDLIST = "#EXT-X-ENDLIST"; private static final String TAG_KEY = "#EXT-X-KEY"; private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE"; @@ -62,21 +69,19 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser extraLines = new LinkedList<>(); String line; try { + if (!checkPlaylistHeader(reader)) { + throw new UnrecognizedInputFormatException("Input does not start with the #EXTM3U header.", + uri); + } while ((line = reader.readLine()) != null) { line = line.trim(); if (line.isEmpty()) { @@ -122,18 +132,47 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants = new ArrayList<>(); ArrayList audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); Format muxedAudioFormat = null; - Format muxedCaptionFormat = null; + ArrayList muxedCaptionFormats = new ArrayList<>(); String line; while (iterator.hasNext()) { @@ -141,31 +180,37 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(); long segmentDurationUs = 0; - int discontinuitySequenceNumber = 0; + boolean hasDiscontinuitySequence = false; + int playlistDiscontinuitySequence = 0; + int relativeDiscontinuitySequence = 0; + long playlistStartTimeUs = 0; long segmentStartTimeUs = 0; long segmentByteRangeOffset = 0; long segmentByteRangeLength = C.LENGTH_UNSET; @@ -231,7 +281,18 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser playlistBundles; private final Handler playlistRefreshHandler; private final PrimaryPlaylistListener primaryPlaylistListener; + private final List listeners; private final Loader initialPlaylistLoader; private final EventDispatcher eventDispatcher; private HlsMasterPlaylist masterPlaylist; private HlsUrl primaryHlsUrl; + private HlsMediaPlaylist primaryUrlSnapshot; private boolean isLive; /** @@ -105,7 +106,7 @@ public final class HlsPlaylistTracker implements Loader.Callback(); initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); playlistParser = new HlsPlaylistParser(); playlistBundles = new IdentityHashMap<>(); playlistRefreshHandler = new Handler(); } + /** + * Registers a listener to receive events from the playlist tracker. + * + * @param listener The listener. + */ + public void addListener(PlaylistEventListener listener) { + listeners.add(listener); + } + + /** + * Unregisters a listener. + * + * @param listener The listener to unregister. + */ + public void removeListener(PlaylistEventListener listener) { + listeners.remove(listener); + } + /** * Starts tracking all the playlists related to the provided Uri. */ public void start() { ParsingLoadable masterPlaylistLoadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(), initialPlaylistUri, C.DATA_TYPE_MANIFEST, - playlistParser); + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), initialPlaylistUri, + C.DATA_TYPE_MANIFEST, playlistParser); initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); } @@ -147,7 +167,24 @@ public final class HlsPlaylistTracker implements Loader.Callback variants = masterPlaylist.variants; + int variantsSize = variants.size(); + long currentTimeMs = SystemClock.elapsedRealtime(); + for (int i = 0; i < variantsSize; i++) { + MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i)); + if (currentTimeMs > bundle.blacklistUntilMs) { + primaryHlsUrl = bundle.playlistUrl; + bundle.loadPlaylist(); + return true; + } + } + return false; + } + + private void maybeSetPrimaryUrl(HlsUrl url) { + if (!masterPlaylist.variants.contains(url) + || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) { + // Only allow variant urls to be chosen as primary. Also prevent changing the primary url if + // the last primary snapshot contains an end tag. + return; + } + MediaPlaylistBundle currentPrimaryBundle = playlistBundles.get(primaryHlsUrl); + long primarySnapshotAccessAgeMs = + currentPrimaryBundle.lastSnapshotAccessTimeMs - SystemClock.elapsedRealtime(); + if (primarySnapshotAccessAgeMs > PRIMARY_URL_KEEPALIVE_MS) { + primaryHlsUrl = url; + playlistBundles.get(primaryHlsUrl).loadPlaylist(); + } + } + private void createBundles(List urls) { int listSize = urls.size(); + long currentTimeMs = SystemClock.elapsedRealtime(); for (int i = 0; i < listSize; i++) { HlsUrl url = urls.get(i); - MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); - playlistBundles.put(urls.get(i), bundle); + MediaPlaylistBundle bundle = new MediaPlaylistBundle(url, currentTimeMs); + playlistBundles.put(url, bundle); } } @@ -271,69 +325,99 @@ public final class HlsPlaylistTracker implements Loader.Callback oldSegments = oldPlaylist.segments; - int oldPlaylistSize = oldSegments.size(); - int newPlaylistSize = newPlaylist.segments.size(); - int mediaSequenceOffset = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; - if (newPlaylistSize == oldPlaylistSize && mediaSequenceOffset == 0 - && oldPlaylist.hasEndTag == newPlaylist.hasEndTag) { - // Playlist has not changed. - return oldPlaylist; + long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist); + int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist); + return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence); + } + + private long getLoadedPlaylistStartTimeUs(HlsMediaPlaylist oldPlaylist, + HlsMediaPlaylist loadedPlaylist) { + if (loadedPlaylist.hasProgramDateTime) { + return loadedPlaylist.startTimeUs; } - if (mediaSequenceOffset < 0) { - // Playlist has changed but media sequence has regressed. - return oldPlaylist; + long primarySnapshotStartTimeUs = primaryUrlSnapshot != null + ? primaryUrlSnapshot.startTimeUs : 0; + if (oldPlaylist == null) { + return primarySnapshotStartTimeUs; } - if (mediaSequenceOffset <= oldPlaylistSize) { - // We can extrapolate the start time of new segments from the segments of the old snapshot. - ArrayList newSegments = new ArrayList<>(newPlaylistSize); - for (int i = mediaSequenceOffset; i < oldPlaylistSize; i++) { - newSegments.add(oldSegments.get(i)); - } - HlsMediaPlaylist.Segment lastSegment = oldSegments.get(oldPlaylistSize - 1); - for (int i = newSegments.size(); i < newPlaylistSize; i++) { - lastSegment = newPlaylist.segments.get(i).copyWithStartTimeUs( - lastSegment.startTimeUs + lastSegment.durationUs); - newSegments.add(lastSegment); - } - return newPlaylist.copyWithSegments(newSegments); + int oldPlaylistSize = oldPlaylist.segments.size(); + Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); + if (firstOldOverlappingSegment != null) { + return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs; + } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) { + return oldPlaylist.getEndTimeUs(); } else { // No segments overlap, we assume the new playlist start coincides with the primary playlist. - return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.getStartTimeUs()); + return primarySnapshotStartTimeUs; } } + private int getLoadedPlaylistDiscontinuitySequence(HlsMediaPlaylist oldPlaylist, + HlsMediaPlaylist loadedPlaylist) { + if (loadedPlaylist.hasDiscontinuitySequence) { + return loadedPlaylist.discontinuitySequence; + } + // TODO: Improve cross-playlist discontinuity adjustment. + int primaryUrlDiscontinuitySequence = primaryUrlSnapshot != null + ? primaryUrlSnapshot.discontinuitySequence : 0; + if (oldPlaylist == null) { + return primaryUrlDiscontinuitySequence; + } + Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); + if (firstOldOverlappingSegment != null) { + return oldPlaylist.discontinuitySequence + + firstOldOverlappingSegment.relativeDiscontinuitySequence + - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence; + } + return primaryUrlDiscontinuitySequence; + } + + private static Segment getFirstOldOverlappingSegment(HlsMediaPlaylist oldPlaylist, + HlsMediaPlaylist loadedPlaylist) { + int mediaSequenceOffset = loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence; + List oldSegments = oldPlaylist.segments; + return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null; + } + /** * Holds all information related to a specific Media Playlist. */ @@ -344,72 +428,64 @@ public final class HlsPlaylistTracker implements Loader.Callback mediaPlaylistLoadable; - private PlaylistRefreshCallback callback; - private HlsMediaPlaylist latestPlaylistSnapshot; + private HlsMediaPlaylist playlistSnapshot; + private long lastSnapshotLoadMs; + private long lastSnapshotAccessTimeMs; + private long blacklistUntilMs; + private boolean pendingRefresh; - public MediaPlaylistBundle(HlsUrl playlistUrl) { - this(playlistUrl, null); - } - - public MediaPlaylistBundle(HlsUrl playlistUrl, HlsMediaPlaylist initialSnapshot) { + public MediaPlaylistBundle(HlsUrl playlistUrl, long initialLastSnapshotAccessTimeMs) { this.playlistUrl = playlistUrl; - latestPlaylistSnapshot = initialSnapshot; + lastSnapshotAccessTimeMs = initialLastSnapshotAccessTimeMs; mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist"); - mediaPlaylistLoadable = new ParsingLoadable<>(dataSourceFactory.createDataSource(), + mediaPlaylistLoadable = new ParsingLoadable<>( + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, playlistParser); } + public HlsMediaPlaylist getPlaylistSnapshot() { + lastSnapshotAccessTimeMs = SystemClock.elapsedRealtime(); + return playlistSnapshot; + } + + public boolean isSnapshotValid() { + if (playlistSnapshot == null) { + return false; + } + long currentTimeMs = SystemClock.elapsedRealtime(); + long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs)); + return playlistSnapshot.hasEndTag + || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT + || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD + || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs; + } + public void release() { mediaPlaylistLoader.release(); } public void loadPlaylist() { - if (!mediaPlaylistLoader.isLoading()) { + blacklistUntilMs = 0; + if (!pendingRefresh && !mediaPlaylistLoader.isLoading()) { mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); } } - public void setCallback(PlaylistRefreshCallback callback) { - this.callback = callback; - } - - public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedStartTimeUs) { - ArrayList segments = new ArrayList<>(latestPlaylistSnapshot.segments); - int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence; - if (indexOfChunk < 0) { - return; - } - Segment actualSegment = segments.get(indexOfChunk); - long timestampDriftUs = Math.abs(actualSegment.startTimeUs - adjustedStartTimeUs); - if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) { - return; - } - segments.set(indexOfChunk, actualSegment.copyWithStartTimeUs(adjustedStartTimeUs)); - // Propagate the adjustment backwards. - for (int i = indexOfChunk - 1; i >= 0; i--) { - Segment segment = segments.get(i); - segments.set(i, - segment.copyWithStartTimeUs(segments.get(i + 1).startTimeUs - segment.durationUs)); - } - // Propagate the adjustment forward. - int segmentsSize = segments.size(); - for (int i = indexOfChunk + 1; i < segmentsSize; i++) { - Segment segment = segments.get(i); - segments.set(i, - segment.copyWithStartTimeUs(segments.get(i - 1).startTimeUs + segment.durationUs)); - } - latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithSegments(segments); - } - // Loader.Callback implementation. @Override public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - processLoadedPlaylist((HlsMediaPlaylist) loadable.getResult()); - eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); + HlsPlaylist result = loadable.getResult(); + if (result instanceof HlsMediaPlaylist) { + processLoadedPlaylist((HlsMediaPlaylist) result); + eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } else { + onLoadError(loadable, elapsedRealtimeMs, loadDurationMs, + new ParserException("Loaded playlist has unexpected type.")); + } } @Override @@ -422,45 +498,48 @@ public final class HlsPlaylistTracker implements Loader.Callback loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - // TODO: Change primary playlist if this is the primary playlist bundle. boolean isFatal = error instanceof ParserException; eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error, isFatal); - if (callback != null) { - callback.onPlaylistLoadError(playlistUrl, error); - } if (isFatal) { return Loader.DONT_RETRY_FATAL; - } else { - return primaryHlsUrl == playlistUrl ? Loader.RETRY : Loader.DONT_RETRY; } + boolean shouldRetry = true; + if (ChunkedTrackBlacklistUtil.shouldBlacklist(error)) { + blacklistUntilMs = + SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS; + notifyPlaylistBlacklisting(playlistUrl, + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS); + shouldRetry = primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); + } + return shouldRetry ? Loader.RETRY : Loader.DONT_RETRY; } // Runnable implementation. @Override public void run() { + pendingRefresh = false; loadPlaylist(); } // Internal methods. - private void processLoadedPlaylist(HlsMediaPlaylist loadedMediaPlaylist) { - HlsMediaPlaylist oldPlaylist = latestPlaylistSnapshot; - latestPlaylistSnapshot = adjustPlaylistTimestamps(oldPlaylist, loadedMediaPlaylist); - boolean shouldScheduleRefresh; - if (oldPlaylist != latestPlaylistSnapshot) { - if (callback != null) { - callback.onPlaylistChanged(); - callback = null; + private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) { + HlsMediaPlaylist oldPlaylist = playlistSnapshot; + lastSnapshotLoadMs = SystemClock.elapsedRealtime(); + playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); + long refreshDelayUs = C.TIME_UNSET; + if (playlistSnapshot != oldPlaylist) { + if (onPlaylistUpdated(playlistUrl, playlistSnapshot)) { + refreshDelayUs = playlistSnapshot.targetDurationUs; } - shouldScheduleRefresh = onPlaylistUpdated(playlistUrl, latestPlaylistSnapshot, - oldPlaylist == null); - } else { - shouldScheduleRefresh = !loadedMediaPlaylist.hasEndTag; + } else if (!playlistSnapshot.hasEndTag) { + refreshDelayUs = playlistSnapshot.targetDurationUs / 2; } - if (shouldScheduleRefresh) { - playlistRefreshHandler.postDelayed(this, PLAYLIST_REFRESH_PERIOD_MS); + if (refreshDelayUs != C.TIME_UNSET) { + // See HLS spec v20, section 6.3.4 for more information on media playlist refreshing. + pendingRefresh = playlistRefreshHandler.postDelayed(this, C.usToMs(refreshDelayUs)); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index 5c20c4663..cd7600529 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -101,8 +101,8 @@ public class DefaultSsChunkSource implements SsChunkSource { trackEncryptionBoxes, nalUnitLengthFieldLength, null, null); FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME - | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track, null); - extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, false, false); + | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track); + extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format); } } @@ -219,7 +219,7 @@ public class DefaultSsChunkSource implements SsChunkSource { long sampleOffsetUs = chunkStartTimeUs; return new ContainerMediaChunk(dataSource, dataSpec, format, trackSelectionReason, trackSelectionData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, 1, sampleOffsetUs, - extractorWrapper, format); + extractorWrapper); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 8ae2da4d4..6cc4549af 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -136,6 +136,11 @@ import java.util.ArrayList; return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); @@ -185,8 +190,8 @@ import java.util.ArrayList; int streamElementIndex = trackGroups.indexOf(selection.getTrackGroup()); SsChunkSource chunkSource = chunkSourceFactory.createChunkSource(manifestLoaderErrorThrower, manifest, streamElementIndex, selection, trackEncryptionBoxes); - return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, chunkSource, - this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); + return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, null, + chunkSource, this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); } private static TrackGroupArray buildTrackGroups(SsManifest manifest) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/SsMediaSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/SsMediaSource.java index 65635aa46..0e3544180 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.os.Handler; import android.os.SystemClock; import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.ExoPlayer; import org.telegram.messenger.exoplayer2.ParserException; import org.telegram.messenger.exoplayer2.Timeline; import org.telegram.messenger.exoplayer2.source.AdaptiveMediaSourceEventListener; @@ -70,10 +71,10 @@ public final class SsMediaSource implements MediaSource, private final int minLoadableRetryCount; private final long livePresentationDelayMs; private final EventDispatcher eventDispatcher; - private final SsManifestParser manifestParser; + private final ParsingLoadable.Parser manifestParser; private final ArrayList mediaPeriods; - private MediaSource.Listener sourceListener; + private Listener sourceListener; private DataSource manifestDataSource; private Loader manifestLoader; private LoaderErrorThrower manifestLoaderErrorThrower; @@ -170,15 +171,17 @@ public final class SsMediaSource implements MediaSource, * @param eventListener A listener of events. May be null if delivery of events is not required. */ public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - SsManifestParser manifestParser, SsChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, + ParsingLoadable.Parser manifestParser, + SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, + long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } private SsMediaSource(SsManifest manifest, Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, SsManifestParser manifestParser, + DataSource.Factory manifestDataSourceFactory, + ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -199,7 +202,7 @@ public final class SsMediaSource implements MediaSource, // MediaSource implementation. @Override - public void prepareSource(MediaSource.Listener listener) { + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { sourceListener = listener; if (manifest != null) { manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index 934e0dd63..4ae567186 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -128,8 +128,10 @@ public class SsManifest { */ public static class StreamElement { - private static final String URL_PLACEHOLDER_START_TIME = "{start time}"; - private static final String URL_PLACEHOLDER_BITRATE = "{bitrate}"; + private static final String URL_PLACEHOLDER_START_TIME_1 = "{start time}"; + private static final String URL_PLACEHOLDER_START_TIME_2 = "{start_time}"; + private static final String URL_PLACEHOLDER_BITRATE_1 = "{bitrate}"; + private static final String URL_PLACEHOLDER_BITRATE_2 = "{Bitrate}"; public final int type; public final String subType; @@ -216,9 +218,13 @@ public class SsManifest { Assertions.checkState(formats != null); Assertions.checkState(chunkStartTimes != null); Assertions.checkState(chunkIndex < chunkStartTimes.size()); + String bitrateString = Integer.toString(formats[track].bitrate); + String startTimeString = chunkStartTimes.get(chunkIndex).toString(); String chunkUrl = chunkTemplate - .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(formats[track].bitrate)) - .replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString()); + .replace(URL_PLACEHOLDER_BITRATE_1, bitrateString) + .replace(URL_PLACEHOLDER_BITRATE_2, bitrateString) + .replace(URL_PLACEHOLDER_START_TIME_1, startTimeString) + .replace(URL_PLACEHOLDER_START_TIME_2, startTimeString); return UriUtil.resolveToUri(baseUri, chunkUrl); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java index 6496befe5..08f693a7e 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java @@ -625,7 +625,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { List codecSpecificData = buildCodecSpecificData( parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA)); format = Format.createVideoContainerFormat(id, MimeTypes.VIDEO_MP4, sampleMimeType, null, - bitrate, width, height, Format.NO_VALUE, codecSpecificData); + bitrate, width, height, Format.NO_VALUE, codecSpecificData, 0); } else if (type == C.TRACK_TYPE_AUDIO) { sampleMimeType = sampleMimeType == null ? MimeTypes.AUDIO_AAC : sampleMimeType; int channels = parseRequiredInt(parser, KEY_CHANNELS); @@ -644,8 +644,8 @@ public class SsManifestParser implements ParsingLoadable.Parser { format = Format.createTextContainerFormat(id, MimeTypes.APPLICATION_MP4, sampleMimeType, null, bitrate, 0, language); } else { - format = Format.createContainerFormat(id, MimeTypes.APPLICATION_MP4, null, sampleMimeType, - bitrate); + format = Format.createContainerFormat(id, MimeTypes.APPLICATION_MP4, sampleMimeType, null, + bitrate, 0, null); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/CaptionStyleCompat.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/CaptionStyleCompat.java index 32c798cfa..dec95262b 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/CaptionStyleCompat.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/CaptionStyleCompat.java @@ -94,8 +94,7 @@ public final class CaptionStyleCompat { *

  • {@link #EDGE_TYPE_DEPRESSED} *
*/ - @EdgeType - public final int edgeType; + @EdgeType public final int edgeType; /** * The preferred edge color, if using an edge type other than {@link #EDGE_TYPE_NONE}. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/Cue.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/Cue.java index bc0a8b55b..931a8d45a 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/Cue.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/Cue.java @@ -15,6 +15,8 @@ */ package org.telegram.messenger.exoplayer2.text; +import android.graphics.Bitmap; +import android.graphics.Color; import android.support.annotation.IntDef; import android.text.Layout.Alignment; import java.lang.annotation.Retention; @@ -36,19 +38,23 @@ public class Cue { @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_UNSET, ANCHOR_TYPE_START, ANCHOR_TYPE_MIDDLE, ANCHOR_TYPE_END}) public @interface AnchorType {} + /** * An unset anchor or line type value. */ public static final int TYPE_UNSET = Integer.MIN_VALUE; + /** * Anchors the left (for horizontal positions) or top (for vertical positions) edge of the cue * box. */ public static final int ANCHOR_TYPE_START = 0; + /** * Anchors the middle of the cue box. */ public static final int ANCHOR_TYPE_MIDDLE = 1; + /** * Anchors the right (for horizontal positions) or bottom (for vertical positions) edge of the cue * box. @@ -61,23 +67,33 @@ public class Cue { @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_UNSET, LINE_TYPE_FRACTION, LINE_TYPE_NUMBER}) public @interface LineType {} + /** * Value for {@link #lineType} when {@link #line} is a fractional position. */ public static final int LINE_TYPE_FRACTION = 0; + /** * Value for {@link #lineType} when {@link #line} is a line number. */ public static final int LINE_TYPE_NUMBER = 1; /** - * The cue text. Note the {@link CharSequence} may be decorated with styling spans. + * 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; + /** * The alignment of the cue text within the cue box, or null if the alignment is undefined. */ public final Alignment textAlignment; + + /** + * The cue image, or null if this is a text cue. + */ + public final Bitmap bitmap; + /** * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction * orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of @@ -87,6 +103,7 @@ public class Cue { * fractional vertical position relative to the top of the viewport. */ public final float line; + /** * The type of the {@link #line} value. *

@@ -95,13 +112,25 @@ public class Cue { *

* {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each * line is taken to be the size of the first line of the cue. When {@link #line} is greater than - * or equal to 0, lines count from the start of the viewport (the first line is numbered 0). When - * {@link #line} is negative, lines count from the end of the viewport (the last line is numbered - * -1). For horizontal text the size of the first line of the cue is its height, and the start - * and end of the viewport are the top and bottom respectively. + * or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset from + * the start edge. When {@link #line} is negative lines count from the end of the viewport, with + * -1 indicating zero offset from the end edge. For horizontal text the line spacing is the height + * of the first line of the cue, and the start and end of the viewport are the top and bottom + * respectively. + *

+ * Note that it's particularly important to consider the effect of {@link #lineAnchor} when using + * {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a + * (potentially multi-line) cue at the very top of the viewport. + * {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue + * at the very bottom of the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} + * and {@code (line == -1 && lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of + * the viewport. {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only + * the last line is visible at the top of the viewport. + * {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first + * line is visible at the bottom of the viewport. */ - @LineType - public final int lineType; + @LineType public final int lineType; + /** * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. @@ -110,8 +139,8 @@ public class Cue { * and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box * respectively. */ - @AnchorType - public final int lineAnchor; + @AnchorType public final int lineAnchor; + /** * The fractional position of the {@link #positionAnchor} of the cue box within the viewport in * the direction orthogonal to {@link #line}, or {@link #DIMEN_UNSET}. @@ -121,6 +150,7 @@ public class Cue { * text. */ public final float position; + /** * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. @@ -129,8 +159,8 @@ public class Cue { * and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box * respectively. */ - @AnchorType - public final int positionAnchor; + @AnchorType public final int positionAnchor; + /** * The size of the cue box in the writing direction specified as a fraction of the viewport size * in that direction, or {@link #DIMEN_UNSET}. @@ -138,7 +168,47 @@ public class Cue { public final float size; /** - * Constructs a cue whose {@link #textAlignment} is null, whose type parameters are set to + * The bitmap height as a fraction of the of the viewport size, or {@link #DIMEN_UNSET} if the + * bitmap should be displayed at its natural height given the bitmap dimensions and the specified + * {@link #size}. + */ + public final float bitmapHeight; + + /** + * Specifies whether or not the {@link #windowColor} property is set. + */ + public final boolean windowColorSet; + + /** + * The fill color of the window. + */ + public final int windowColor; + + /** + * Creates an image cue. + * + * @param bitmap See {@link #bitmap}. + * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed + * as a fraction of the viewport width. + * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START}, + * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a + * fraction of the viewport height. + * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, + * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * @param width The width of the cue as a fraction of the viewport width. + * @param height The height of the cue as a fraction of the viewport height, or + * {@link #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the + * specified {@code width}. + */ + public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, + float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) { + this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, + horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK); + } + + /** + * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. * * @param text See {@link #text}. @@ -148,6 +218,8 @@ public class Cue { } /** + * Creates a text cue. + * * @param text See {@link #text}. * @param textAlignment See {@link #textAlignment}. * @param line See {@link #line}. @@ -159,14 +231,47 @@ public class Cue { */ public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) { + this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false, + Color.BLACK); + } + + /** + * Creates a text cue. + * + * @param text See {@link #text}. + * @param textAlignment See {@link #textAlignment}. + * @param line See {@link #line}. + * @param lineType See {@link #lineType}. + * @param lineAnchor See {@link #lineAnchor}. + * @param position See {@link #position}. + * @param positionAnchor See {@link #positionAnchor}. + * @param size See {@link #size}. + * @param windowColorSet See {@link #windowColorSet}. + * @param windowColor See {@link #windowColor}. + */ + public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, + @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, + boolean windowColorSet, int windowColor) { + this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size, + DIMEN_UNSET, windowColorSet, windowColor); + } + + private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line, + @LineType int lineType, @AnchorType int lineAnchor, float position, + @AnchorType int positionAnchor, float size, float bitmapHeight, boolean windowColorSet, + int windowColor) { this.text = text; this.textAlignment = textAlignment; + this.bitmap = bitmap; this.line = line; this.lineType = lineType; this.lineAnchor = lineAnchor; this.position = position; this.positionAnchor = positionAnchor; this.size = size; + this.bitmapHeight = bitmapHeight; + this.windowColorSet = windowColorSet; + this.windowColor = windowColor; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SimpleSubtitleDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SimpleSubtitleDecoder.java index 69c2a6074..c5c79c67a 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SimpleSubtitleDecoder.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.text; +import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.decoder.SimpleDecoder; import java.nio.ByteBuffer; @@ -66,8 +67,10 @@ public abstract class SimpleSubtitleDecoder extends SubtitleOutputBuffer outputBuffer, boolean reset) { try { ByteBuffer inputData = inputBuffer.data; - Subtitle subtitle = decode(inputData.array(), inputData.limit()); + Subtitle subtitle = decode(inputData.array(), inputData.limit(), reset); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); + // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). + outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); return null; } catch (SubtitleDecoderException e) { return e; @@ -79,9 +82,11 @@ public abstract class SimpleSubtitleDecoder extends * * @param data An array holding the data to be decoded, starting at position 0. * @param size The size of the data to be decoded. + * @param reset Whether the decoder must be reset before decoding. * @return The decoded {@link Subtitle}. * @throws SubtitleDecoderException If a decoding error occurs. */ - protected abstract Subtitle decode(byte[] data, int size) throws SubtitleDecoderException; + protected abstract Subtitle decode(byte[] data, int size, boolean reset) + throws SubtitleDecoderException; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SubtitleDecoderFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SubtitleDecoderFactory.java index f6c7ed5d0..76791bb7f 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SubtitleDecoderFactory.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SubtitleDecoderFactory.java @@ -17,6 +17,8 @@ package org.telegram.messenger.exoplayer2.text; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.text.cea.Cea608Decoder; +import org.telegram.messenger.exoplayer2.text.cea.Cea708Decoder; +import org.telegram.messenger.exoplayer2.text.dvb.DvbDecoder; import org.telegram.messenger.exoplayer2.text.subrip.SubripDecoder; import org.telegram.messenger.exoplayer2.text.ttml.TtmlDecoder; import org.telegram.messenger.exoplayer2.text.tx3g.Tx3gDecoder; @@ -58,56 +60,48 @@ public interface SubtitleDecoderFactory { *

  • SubRip ({@link SubripDecoder})
  • *
  • TX3G ({@link Tx3gDecoder})
  • *
  • Cea608 ({@link Cea608Decoder})
  • + *
  • Cea708 ({@link Cea708Decoder})
  • + *
  • DVB ({@link DvbDecoder})
  • * */ SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() { @Override public boolean supportsFormat(Format format) { - return getDecoderClass(format.sampleMimeType) != null; + String mimeType = format.sampleMimeType; + return MimeTypes.TEXT_VTT.equals(mimeType) + || MimeTypes.APPLICATION_TTML.equals(mimeType) + || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) + || MimeTypes.APPLICATION_SUBRIP.equals(mimeType) + || MimeTypes.APPLICATION_TX3G.equals(mimeType) + || MimeTypes.APPLICATION_CEA608.equals(mimeType) + || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) + || MimeTypes.APPLICATION_CEA708.equals(mimeType) + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType); } @Override public SubtitleDecoder createDecoder(Format format) { - try { - Class clazz = getDecoderClass(format.sampleMimeType); - if (clazz == null) { + switch (format.sampleMimeType) { + case MimeTypes.TEXT_VTT: + return new WebvttDecoder(); + 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); + case MimeTypes.APPLICATION_DVBSUBS: + return new DvbDecoder(format.initializationData); + default: throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); - } - if (clazz == Cea608Decoder.class) { - return clazz.asSubclass(SubtitleDecoder.class) - .getConstructor(Integer.TYPE).newInstance(format.accessibilityChannel); - } else { - return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance(); - } - } catch (Exception e) { - throw new IllegalStateException("Unexpected error instantiating decoder", e); - } - } - - private Class getDecoderClass(String mimeType) { - if (mimeType == null) { - return null; - } - try { - switch (mimeType) { - case MimeTypes.TEXT_VTT: - return Class.forName("org.telegram.messenger.exoplayer2.text.webvtt.WebvttDecoder"); - case MimeTypes.APPLICATION_TTML: - return Class.forName("org.telegram.messenger.exoplayer2.text.ttml.TtmlDecoder"); - case MimeTypes.APPLICATION_MP4VTT: - return Class.forName("org.telegram.messenger.exoplayer2.text.webvtt.Mp4WebvttDecoder"); - case MimeTypes.APPLICATION_SUBRIP: - return Class.forName("org.telegram.messenger.exoplayer2.text.subrip.SubripDecoder"); - case MimeTypes.APPLICATION_TX3G: - return Class.forName("org.telegram.messenger.exoplayer2.text.tx3g.Tx3gDecoder"); - case MimeTypes.APPLICATION_CEA608: - return Class.forName("org.telegram.messenger.exoplayer2.text.cea.Cea608Decoder"); - default: - return null; - } - } catch (ClassNotFoundException e) { - return null; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SubtitleInputBuffer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SubtitleInputBuffer.java index 9c3654788..eeaf5f489 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SubtitleInputBuffer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/SubtitleInputBuffer.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.text; +import android.support.annotation.NonNull; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.decoder.DecoderInputBuffer; @@ -35,7 +36,7 @@ public final class SubtitleInputBuffer extends DecoderInputBuffer } @Override - public int compareTo(SubtitleInputBuffer other) { + public int compareTo(@NonNull SubtitleInputBuffer other) { long delta = timeUs - other.timeUs; if (delta == 0) { return 0; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/TextRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/TextRenderer.java index b4c87f251..028c155c0 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/TextRenderer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/TextRenderer.java @@ -19,6 +19,7 @@ import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; +import android.support.annotation.IntDef; import org.telegram.messenger.exoplayer2.BaseRenderer; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.ExoPlaybackException; @@ -26,6 +27,8 @@ import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.FormatHolder; import org.telegram.messenger.exoplayer2.util.Assertions; import org.telegram.messenger.exoplayer2.util.MimeTypes; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; @@ -52,6 +55,27 @@ public final class TextRenderer extends BaseRenderer implements Callback { } + @Retention(RetentionPolicy.SOURCE) + @IntDef({REPLACEMENT_STATE_NONE, REPLACEMENT_STATE_SIGNAL_END_OF_STREAM, + REPLACEMENT_STATE_WAIT_END_OF_STREAM}) + private @interface ReplacementState {} + /** + * The decoder does not need to be replaced. + */ + private static final int REPLACEMENT_STATE_NONE = 0; + /** + * The decoder needs to be replaced, 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 REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1; + /** + * The decoder needs to be replaced, 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 REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2; + private static final int MSG_UPDATE_OUTPUT = 0; private final Handler outputHandler; @@ -61,6 +85,8 @@ 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; @@ -105,20 +131,25 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + streamFormat = formats[0]; if (decoder != null) { - decoder.release(); - nextInputBuffer = null; + decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM; + } else { + decoder = decoderFactory.createDecoder(streamFormat); } - decoder = decoderFactory.createDecoder(formats[0]); } @Override protected void onPositionReset(long positionUs, boolean joining) { clearOutput(); - resetBuffers(); - decoder.flush(); inputStreamEnded = false; outputStreamEnded = false; + if (decoderReplacementState != REPLACEMENT_STATE_NONE) { + replaceDecoder(); + } else { + releaseBuffers(); + decoder.flush(); + } } @Override @@ -155,13 +186,12 @@ public final class TextRenderer extends BaseRenderer implements Callback { if (nextSubtitle != null) { if (nextSubtitle.isEndOfStream()) { if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) { - if (subtitle != null) { - subtitle.release(); - subtitle = null; + if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) { + replaceDecoder(); + } else { + releaseBuffers(); + outputStreamEnded = true; } - nextSubtitle.release(); - nextSubtitle = null; - outputStreamEnded = true; } } else if (nextSubtitle.timeUs <= positionUs) { // Advance to the next subtitle. Sync the next event index and trigger an update. @@ -180,6 +210,10 @@ public final class TextRenderer extends BaseRenderer implements Callback { updateOutput(subtitle.getCues(positionUs)); } + if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) { + return; + } + try { while (!inputStreamEnded) { if (nextInputBuffer == null) { @@ -188,11 +222,16 @@ public final class TextRenderer extends BaseRenderer implements Callback { return; } } + if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) { + nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + decoder.queueInputBuffer(nextInputBuffer); + nextInputBuffer = null; + decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM; + return; + } // Try and read the next subtitle from the source. - int result = readSource(formatHolder, nextInputBuffer); + int result = readSource(formatHolder, nextInputBuffer, false); if (result == C.RESULT_BUFFER_READ) { - // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]) and queue the buffer. - nextInputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); if (nextInputBuffer.isEndOfStream()) { inputStreamEnded = true; } else { @@ -202,7 +241,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { decoder.queueInputBuffer(nextInputBuffer); nextInputBuffer = null; } else if (result == C.RESULT_NOTHING_READ) { - break; + return; } } } catch (SubtitleDecoderException e) { @@ -212,10 +251,9 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override protected void onDisabled() { + streamFormat = null; clearOutput(); - resetBuffers(); - decoder.release(); - decoder = null; + releaseDecoder(); super.onDisabled(); } @@ -231,7 +269,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { return true; } - private void resetBuffers() { + private void releaseBuffers() { nextInputBuffer = null; nextSubtitleEventIndex = C.INDEX_UNSET; if (subtitle != null) { @@ -244,6 +282,18 @@ public final class TextRenderer extends BaseRenderer implements Callback { } } + private void releaseDecoder() { + releaseBuffers(); + decoder.release(); + decoder = null; + decoderReplacementState = REPLACEMENT_STATE_NONE; + } + + private void replaceDecoder() { + releaseDecoder(); + decoder = decoderFactory.createDecoder(streamFormat); + } + private long getNextEventTime() { return ((nextSubtitleEventIndex == C.INDEX_UNSET) || (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE @@ -269,8 +319,9 @@ public final class TextRenderer extends BaseRenderer implements Callback { case MSG_UPDATE_OUTPUT: invokeUpdateOutputInternal((List) msg.obj); return true; + default: + throw new IllegalStateException(); } - return false; } private void invokeUpdateOutputInternal(List cues) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/Cea608Decoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/Cea608Decoder.java index 100e4e335..0e604c49f 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/Cea608Decoder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/Cea608Decoder.java @@ -15,21 +15,32 @@ */ package org.telegram.messenger.exoplayer2.text.cea; -import android.text.TextUtils; +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.Layout.Alignment; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.CharacterStyle; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.text.Cue; import org.telegram.messenger.exoplayer2.text.Subtitle; import org.telegram.messenger.exoplayer2.text.SubtitleDecoder; import org.telegram.messenger.exoplayer2.text.SubtitleInputBuffer; +import org.telegram.messenger.exoplayer2.util.MimeTypes; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; /** * A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608"). */ public final class Cea608Decoder extends CeaDecoder { - private static final String TAG = "Cea608Decoder"; - private static final int CC_VALID_FLAG = 0x04; private static final int CC_TYPE_FLAG = 0x02; private static final int CC_FIELD_FLAG = 0x01; @@ -38,20 +49,30 @@ public final class Cea608Decoder extends CeaDecoder { private static final int NTSC_CC_FIELD_2 = 0x01; private static final int CC_VALID_608_ID = 0x04; - private static final int PAYLOAD_TYPE_CC = 4; - private static final int COUNTRY_CODE = 0xB5; - private static final int PROVIDER_CODE = 0x31; - private static final int USER_ID = 0x47413934; // "GA94" - private static final int USER_DATA_TYPE_CODE = 0x3; - private static final int CC_MODE_UNKNOWN = 0; private static final int CC_MODE_ROLL_UP = 1; private static final int CC_MODE_POP_ON = 2; private static final int CC_MODE_PAINT_ON = 3; + private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9}; + private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28}; + private static final int[] COLORS = new int[] { + Color.WHITE, + Color.GREEN, + Color.BLUE, + Color.CYAN, + Color.RED, + Color.YELLOW, + Color.MAGENTA, + }; + // The default number of rows to display in roll-up captions mode. private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; + // An implied first byte for packets that are only 2 bytes long, consisting of marker bits + // (0b11111) + valid bit (0b1) + NTSC field 1 type bits (0b00). + private static final byte CC_IMPLICIT_DATA_HEADER = (byte) 0xFC; + /** * Command initiating pop-on style captioning. Subsequent data should be loaded into a * non-displayed memory and held there until the {@link #CTRL_END_OF_CAPTION} command is received, @@ -89,12 +110,10 @@ public final class Cea608Decoder extends CeaDecoder { private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C; private static final byte CTRL_CARRIAGE_RETURN = 0x2D; private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E; + private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24; private static final byte CTRL_BACKSPACE = 0x21; - private static final byte CTRL_MISC_CHAN_1 = 0x14; - private static final byte CTRL_MISC_CHAN_2 = 0x1C; - // Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20). private static final int[] BASIC_CHARACTER_SET = new int[] { 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & ' @@ -164,25 +183,26 @@ public final class Cea608Decoder extends CeaDecoder { }; private final ParsableByteArray ccData; - - private final StringBuilder captionStringBuilder; - + private final int packetLength; private final int selectedField; + private final LinkedList cueBuilders; + + private CueBuilder currentCueBuilder; + private List cues; + private List lastCues; private int captionMode; private int captionRowCount; - private String captionString; - - private String lastCaptionString; private boolean repeatableControlSet; private byte repeatableControlCc1; private byte repeatableControlCc2; - public Cea608Decoder(int accessibilityChannel) { + public Cea608Decoder(String mimeType, int accessibilityChannel) { ccData = new ParsableByteArray(); - - captionStringBuilder = new StringBuilder(); + cueBuilders = new LinkedList<>(); + currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT); + packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; switch (accessibilityChannel) { case 3: case 4: @@ -196,7 +216,7 @@ public final class Cea608Decoder extends CeaDecoder { } setCaptionMode(CC_MODE_UNKNOWN); - captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; + resetCueBuilders(); } @Override @@ -207,11 +227,11 @@ public final class Cea608Decoder extends CeaDecoder { @Override public void flush() { super.flush(); + cues = null; + lastCues = null; setCaptionMode(CC_MODE_UNKNOWN); + resetCueBuilders(); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; - captionStringBuilder.setLength(0); - captionString = null; - lastCaptionString = null; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -224,13 +244,13 @@ public final class Cea608Decoder extends CeaDecoder { @Override protected boolean isNewSubtitleDataAvailable() { - return !TextUtils.equals(captionString, lastCaptionString); + return cues != lastCues; } @Override protected Subtitle createSubtitle() { - lastCaptionString = captionString; - return new CeaSubtitle(new Cue(captionString)); + lastCues = cues; + return new CeaSubtitle(cues); } @Override @@ -238,12 +258,16 @@ public final class Cea608Decoder extends CeaDecoder { ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); boolean captionDataProcessed = false; boolean isRepeatableControl = false; - while (ccData.bytesLeft() > 0) { - byte ccDataHeader = (byte) ccData.readUnsignedByte(); - byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); - byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); + while (ccData.bytesLeft() >= packetLength) { + byte ccDataHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER + : (byte) ccData.readUnsignedByte(); + byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit + byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit // Only examine valid CEA-608 packets + // TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according + // to the CEA-608 specification. We need to determine if the data should be handled + // differently when that is not the case. if ((ccDataHeader & (CC_VALID_FLAG | CC_TYPE_FLAG)) != CC_VALID_608_ID) { continue; } @@ -258,49 +282,47 @@ public final class Cea608Decoder extends CeaDecoder { if (ccData1 == 0 && ccData2 == 0) { continue; } + // If we've reached this point then there is data to process; flag that work has been done. captionDataProcessed = true; // Special North American character set. - // ccData1 - P|0|0|1|C|0|0|1 - // ccData2 - P|0|1|1|X|X|X|X - if ((ccData1 == 0x11 || ccData1 == 0x19) && ((ccData2 & 0x70) == 0x30)) { - // TODO: Make use of the channel bit - captionStringBuilder.append(getSpecialChar(ccData2)); + // ccData1 - 0|0|0|1|C|0|0|1 + // ccData2 - 0|0|1|1|X|X|X|X + if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) { + // TODO: Make use of the channel toggle + currentCueBuilder.append(getSpecialChar(ccData2)); continue; } // Extended Western European character set. - // ccData1 - P|0|0|1|C|0|1|S - // ccData2 - P|0|1|X|X|X|X|X - if ((ccData2 & 0x60) == 0x20) { - // Extended Spanish/Miscellaneous and French character set (S = 0). - if (ccData1 == 0x12 || ccData1 == 0x1A) { - // TODO: Make use of the channel bit - backspace(); // Remove standard equivalent of the special extended char. - captionStringBuilder.append(getExtendedEsFrChar(ccData2)); - continue; - } - - // Extended Portuguese and German/Danish character set (S = 1). - if (ccData1 == 0x13 || ccData1 == 0x1B) { - // TODO: Make use of the channel bit - backspace(); // Remove standard equivalent of the special extended char. - captionStringBuilder.append(getExtendedPtDeChar(ccData2)); - continue; + // ccData1 - 0|0|0|1|C|0|1|S + // ccData2 - 0|0|1|X|X|X|X|X + if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) { + // TODO: Make use of the channel toggle + // Remove standard equivalent of the special extended char before appending new one + currentCueBuilder.backspace(); + if ((ccData1 & 0x01) == 0x00) { + // Extended Spanish/Miscellaneous and French character set (S = 0). + currentCueBuilder.append(getExtendedEsFrChar(ccData2)); + } else { + // Extended Portuguese and German/Danish character set (S = 1). + currentCueBuilder.append(getExtendedPtDeChar(ccData2)); } + continue; } // Control character. - if (ccData1 < 0x20) { + // ccData1 - 0|0|0|X|X|X|X|X + if ((ccData1 & 0xE0) == 0x00) { isRepeatableControl = handleCtrl(ccData1, ccData2); continue; } // Basic North American character set. - captionStringBuilder.append(getChar(ccData1)); - if (ccData2 >= 0x20) { - captionStringBuilder.append(getChar(ccData2)); + currentCueBuilder.append(getChar(ccData1)); + if ((ccData2 & 0xE0) != 0x00) { + currentCueBuilder.append(getChar(ccData2)); } } @@ -309,34 +331,106 @@ public final class Cea608Decoder extends CeaDecoder { repeatableControlSet = false; } if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { - captionString = getDisplayCaption(); + cues = getDisplayCues(); } } } private boolean handleCtrl(byte cc1, byte cc2) { boolean isRepeatableControl = isRepeatable(cc1); + + // Most control commands are sent twice in succession to ensure they are received properly. + // We don't want to process duplicate commands, so if we see the same repeatable command twice + // in a row, ignore the second one. if (isRepeatableControl) { if (repeatableControlSet && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) { + // This is a duplicate. Clear the repeatable control flag and return. repeatableControlSet = false; return true; } else { + // This is a repeatable command, but we haven't see it yet, so set the repeabable control + // flag (to ensure we ignore the next one should it be a duplicate) and continue processing + // the command. repeatableControlSet = true; repeatableControlCc1 = cc1; repeatableControlCc2 = cc2; } } - if (isMiscCode(cc1, cc2)) { - handleMiscCode(cc2); + + if (isMidrowCtrlCode(cc1, cc2)) { + handleMidrowCtrl(cc2); } else if (isPreambleAddressCode(cc1, cc2)) { - // TODO: Add better handling of this with specific positioning. - maybeAppendNewline(); + handlePreambleAddressCode(cc1, cc2); + } else if (isTabCtrlCode(cc1, cc2)) { + currentCueBuilder.setTab(cc2 - 0x20); + } else if (isMiscCode(cc1, cc2)) { + handleMiscCode(cc2); } + return isRepeatableControl; } + private void handleMidrowCtrl(byte cc2) { + // TODO: support the extended styles (i.e. backgrounds and transparencies) + + // cc2 - 0|0|1|0|ATRBT|U + // ATRBT is the 3-byte encoded attribute, and U is the underline toggle + boolean isUnderlined = (cc2 & 0x01) == 0x01; + currentCueBuilder.setUnderline(isUnderlined); + + int attribute = (cc2 >> 1) & 0x0F; + if (attribute == 0x07) { + currentCueBuilder.setMidrowStyle(new StyleSpan(Typeface.ITALIC), 2); + currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(Color.WHITE), 1); + } else { + currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(COLORS[attribute]), 1); + } + } + + private void handlePreambleAddressCode(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|E|ROW + // C is the channel toggle, E is the extended flag, and ROW is the encoded row + int row = ROW_INDICES[cc1 & 0x07]; + // TODO: Make use of the channel toggle + // TODO: support the extended address and style + + // cc2 - 0|1|N|ATTRBTE|U + // N is the next row down toggle, ATTRBTE is the 4-byte encoded attribute, and U is the + // underline toggle. + boolean nextRowDown = (cc2 & 0x20) != 0; + if (nextRowDown) { + row++; + } + + if (row != currentCueBuilder.getRow()) { + if (captionMode != CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) { + currentCueBuilder = new CueBuilder(captionMode, captionRowCount); + cueBuilders.add(currentCueBuilder); + } + currentCueBuilder.setRow(row); + } + + if ((cc2 & 0x01) == 0x01) { + currentCueBuilder.setPreambleStyle(new UnderlineSpan()); + } + + // cc2 - 0|1|N|0|STYLE|U + // cc2 - 0|1|N|1|CURSR|U + int attribute = cc2 >> 1 & 0x0F; + if (attribute <= 0x07) { + if (attribute == 0x07) { + currentCueBuilder.setPreambleStyle(new StyleSpan(Typeface.ITALIC)); + currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(Color.WHITE)); + } else { + currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(COLORS[attribute])); + } + } else { + currentCueBuilder.setIndent(COLUMN_INDICES[attribute & 0x07]); + } + } + private void handleMiscCode(byte cc2) { switch (cc2) { case CTRL_ROLL_UP_CAPTIONS_2_ROWS: @@ -365,68 +459,43 @@ public final class Cea608Decoder extends CeaDecoder { switch (cc2) { case CTRL_ERASE_DISPLAYED_MEMORY: - captionString = null; + cues = null; if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { - captionStringBuilder.setLength(0); + resetCueBuilders(); } break; case CTRL_ERASE_NON_DISPLAYED_MEMORY: - captionStringBuilder.setLength(0); + resetCueBuilders(); break; case CTRL_END_OF_CAPTION: - captionString = getDisplayCaption(); - captionStringBuilder.setLength(0); + cues = getDisplayCues(); + resetCueBuilders(); break; case CTRL_CARRIAGE_RETURN: - maybeAppendNewline(); - break; - case CTRL_BACKSPACE: - if (captionStringBuilder.length() > 0) { - captionStringBuilder.setLength(captionStringBuilder.length() - 1); + // carriage returns only apply to rollup captions; don't bother if we don't have anything + // to add a carriage return to + if (captionMode == CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) { + currentCueBuilder.rollUp(); } break; + case CTRL_BACKSPACE: + currentCueBuilder.backspace(); + break; + case CTRL_DELETE_TO_END_OF_ROW: + // TODO: implement + break; } } - private void backspace() { - if (captionStringBuilder.length() > 0) { - captionStringBuilder.setLength(captionStringBuilder.length() - 1); + private List getDisplayCues() { + List displayCues = new ArrayList<>(); + for (int i = 0; i < cueBuilders.size(); i++) { + Cue cue = cueBuilders.get(i).build(); + if (cue != null) { + displayCues.add(cue); + } } - } - - private void maybeAppendNewline() { - int buildLength = captionStringBuilder.length(); - if (buildLength > 0 && captionStringBuilder.charAt(buildLength - 1) != '\n') { - captionStringBuilder.append('\n'); - } - } - - private String getDisplayCaption() { - int buildLength = captionStringBuilder.length(); - if (buildLength == 0) { - return null; - } - - boolean endsWithNewline = captionStringBuilder.charAt(buildLength - 1) == '\n'; - if (buildLength == 1 && endsWithNewline) { - return null; - } - - int endIndex = endsWithNewline ? buildLength - 1 : buildLength; - if (captionMode != CC_MODE_ROLL_UP) { - return captionStringBuilder.substring(0, endIndex); - } - - int startIndex = 0; - int searchBackwardFromIndex = endIndex; - for (int i = 0; i < captionRowCount && searchBackwardFromIndex != -1; i++) { - searchBackwardFromIndex = captionStringBuilder.lastIndexOf("\n", searchBackwardFromIndex - 1); - } - if (searchBackwardFromIndex != -1) { - startIndex = searchBackwardFromIndex + 1; - } - captionStringBuilder.delete(0, startIndex); - return captionStringBuilder.substring(0, endIndex - startIndex); + return displayCues; } private void setCaptionMode(int captionMode) { @@ -434,22 +503,31 @@ public final class Cea608Decoder extends CeaDecoder { return; } + int oldCaptionMode = this.captionMode; this.captionMode = captionMode; + // Clear the working memory. - captionStringBuilder.setLength(0); - if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) { - // When switching to roll-up or unknown, we also need to clear the caption. - captionString = null; + resetCueBuilders(); + if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP + || captionMode == CC_MODE_UNKNOWN) { + // When switching from paint-on or to roll-up or unknown, we also need to clear the caption. + cues = null; } } + private void resetCueBuilders() { + currentCueBuilder.reset(captionMode, captionRowCount); + cueBuilders.clear(); + cueBuilders.add(currentCueBuilder); + } + private static char getChar(byte ccData) { int index = (ccData & 0x7F) - 0x20; return (char) BASIC_CHARACTER_SET[index]; } private static char getSpecialChar(byte ccData) { - int index = ccData & 0xF; + int index = ccData & 0x0F; return (char) SPECIAL_CHARACTER_SET[index]; } @@ -463,42 +541,248 @@ public final class Cea608Decoder extends CeaDecoder { return (char) SPECIAL_PT_DE_CHARACTER_SET[index]; } - private static boolean isMiscCode(byte cc1, byte cc2) { - return (cc1 == CTRL_MISC_CHAN_1 || cc1 == CTRL_MISC_CHAN_2) - && (cc2 >= 0x20 && cc2 <= 0x2F); + private static boolean isMidrowCtrlCode(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|0|0|1 + // cc2 - 0|0|1|0|X|X|X|X + return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x20); } private static boolean isPreambleAddressCode(byte cc1, byte cc2) { - return (cc1 >= 0x10 && cc1 <= 0x1F) && (cc2 >= 0x40 && cc2 <= 0x7F); + // cc1 - 0|0|0|1|C|X|X|X + // cc2 - 0|1|X|X|X|X|X|X + return ((cc1 & 0xF0) == 0x10) && ((cc2 & 0xC0) == 0x40); + } + + private static boolean isTabCtrlCode(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|1|1|1 + // cc2 - 0|0|1|0|0|0|0|1 to 0|0|1|0|0|0|1|1 + return ((cc1 & 0xF7) == 0x17) && (cc2 >= 0x21 && cc2 <= 0x23); + } + + private static boolean isMiscCode(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|1|0|0 + // cc2 - 0|0|1|0|X|X|X|X + return ((cc1 & 0xF7) == 0x14) && ((cc2 & 0xF0) == 0x20); } private static boolean isRepeatable(byte cc1) { - return cc1 >= 0x10 && cc1 <= 0x1F; + // cc1 - 0|0|0|1|X|X|X|X + return (cc1 & 0xF0) == 0x10; } - /** - * Inspects an sei message to determine whether it contains CEA-608. - *

    - * The position of {@code payload} is left unchanged. - * - * @param payloadType The payload type of the message. - * @param payloadLength The length of the payload. - * @param payload A {@link ParsableByteArray} containing the payload. - * @return Whether the sei message contains CEA-608. - */ - public static boolean isSeiMessageCea608(int payloadType, int payloadLength, - ParsableByteArray payload) { - if (payloadType != PAYLOAD_TYPE_CC || payloadLength < 8) { - return false; + private static class CueBuilder { + + private static final int POSITION_UNSET = -1; + + // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608 + // positions to normalized screen position. + private static final int SCREEN_CHARWIDTH = 32; + private static final int BASE_ROW = 15; + + private final List preambleStyles; + private final List midrowStyles; + private final List rolledUpCaptions; + private final SpannableStringBuilder captionStringBuilder; + + private int row; + private int indent; + private int tabOffset; + private int captionMode; + private int captionRowCount; + private int underlineStartPosition; + + public CueBuilder(int captionMode, int captionRowCount) { + preambleStyles = new ArrayList<>(); + midrowStyles = new ArrayList<>(); + rolledUpCaptions = new LinkedList<>(); + captionStringBuilder = new SpannableStringBuilder(); + reset(captionMode, captionRowCount); } - int startPosition = payload.getPosition(); - int countryCode = payload.readUnsignedByte(); - int providerCode = payload.readUnsignedShort(); - int userIdentifier = payload.readInt(); - int userDataTypeCode = payload.readUnsignedByte(); - payload.setPosition(startPosition); - return countryCode == COUNTRY_CODE && providerCode == PROVIDER_CODE - && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE; + + public void reset(int captionMode, int captionRowCount) { + preambleStyles.clear(); + midrowStyles.clear(); + rolledUpCaptions.clear(); + captionStringBuilder.clear(); + row = BASE_ROW; + indent = 0; + tabOffset = 0; + this.captionMode = captionMode; + this.captionRowCount = captionRowCount; + underlineStartPosition = POSITION_UNSET; + } + + public boolean isEmpty() { + return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty() + && captionStringBuilder.length() == 0; + } + + public void backspace() { + int length = captionStringBuilder.length(); + if (length > 0) { + captionStringBuilder.delete(length - 1, length); + } + } + + public int getRow() { + return row; + } + + public void setRow(int row) { + this.row = row; + } + + public void rollUp() { + rolledUpCaptions.add(buildSpannableString()); + captionStringBuilder.clear(); + preambleStyles.clear(); + midrowStyles.clear(); + underlineStartPosition = POSITION_UNSET; + + int numRows = Math.min(captionRowCount, row); + while (rolledUpCaptions.size() >= numRows) { + rolledUpCaptions.remove(0); + } + } + + public void setIndent(int indent) { + this.indent = indent; + } + + public void setTab(int tabs) { + tabOffset = tabs; + } + + public void setPreambleStyle(CharacterStyle style) { + preambleStyles.add(style); + } + + public void setMidrowStyle(CharacterStyle style, int nextStyleIncrement) { + midrowStyles.add(new CueStyle(style, captionStringBuilder.length(), nextStyleIncrement)); + } + + public void setUnderline(boolean enabled) { + if (enabled) { + underlineStartPosition = captionStringBuilder.length(); + } else if (underlineStartPosition != POSITION_UNSET) { + // underline spans won't overlap, so it's safe to modify the builder directly with them + captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, + captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + underlineStartPosition = POSITION_UNSET; + } + } + + public void append(char text) { + captionStringBuilder.append(text); + } + + public SpannableString buildSpannableString() { + int length = captionStringBuilder.length(); + + // preamble styles apply to the entire cue + for (int i = 0; i < preambleStyles.size(); i++) { + captionStringBuilder.setSpan(preambleStyles.get(i), 0, length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + // midrow styles only apply to part of the cue, and after preamble styles + for (int i = 0; i < midrowStyles.size(); i++) { + CueStyle cueStyle = midrowStyles.get(i); + int end = (i < midrowStyles.size() - cueStyle.nextStyleIncrement) + ? midrowStyles.get(i + cueStyle.nextStyleIncrement).start + : length; + captionStringBuilder.setSpan(cueStyle.style, cueStyle.start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + // special case for midrow underlines that went to the end of the cue + if (underlineStartPosition != POSITION_UNSET) { + captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + return new SpannableString(captionStringBuilder); + } + + public Cue build() { + SpannableStringBuilder cueString = new SpannableStringBuilder(); + // Add any rolled up captions, separated by new lines. + for (int i = 0; i < rolledUpCaptions.size(); i++) { + cueString.append(rolledUpCaptions.get(i)); + cueString.append('\n'); + } + // Add the current line. + cueString.append(buildSpannableString()); + + if (cueString.length() == 0) { + // The cue is empty. + return null; + } + + float position; + int positionAnchor; + // The number of empty columns before the start of the text, in the range [0-31]. + int startPadding = indent + tabOffset; + // The number of empty columns after the end of the text, in the same range. + int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length(); + int startEndPaddingDelta = startPadding - endPadding; + if (captionMode == CC_MODE_POP_ON && Math.abs(startEndPaddingDelta) < 3) { + // Treat approximately centered pop-on captions are middle aligned. + position = 0.5f; + positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; + } else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) { + // Treat pop-on captions with less padding at the end than the start as end aligned. + position = (float) (SCREEN_CHARWIDTH - endPadding) / SCREEN_CHARWIDTH; + // Adjust the position to fit within the safe area. + position = position * 0.8f + 0.1f; + positionAnchor = Cue.ANCHOR_TYPE_END; + } else { + // For all other cases assume start aligned. + position = (float) startPadding / SCREEN_CHARWIDTH; + // Adjust the position to fit within the safe area. + position = position * 0.8f + 0.1f; + positionAnchor = Cue.ANCHOR_TYPE_START; + } + + int lineAnchor; + int line; + // Note: Row indices are in the range [1-15]. + if (captionMode == CC_MODE_ROLL_UP || row > (BASE_ROW / 2)) { + lineAnchor = Cue.ANCHOR_TYPE_END; + line = row - BASE_ROW; + // Two line adjustments. The first is because line indices from the bottom of the window + // start from -1 rather than 0. The second is a blank row to act as the safe area. + line -= 2; + } else { + lineAnchor = Cue.ANCHOR_TYPE_START; + // Line indices from the top of the window start from 0, but we want a blank row to act as + // the safe area. As a result no adjustment is necessary. + line = row; + } + + return new Cue(cueString, Alignment.ALIGN_NORMAL, line, Cue.LINE_TYPE_NUMBER, lineAnchor, + position, positionAnchor, Cue.DIMEN_UNSET); + } + + @Override + public String toString() { + return captionStringBuilder.toString(); + } + + private static class CueStyle { + + public final CharacterStyle style; + public final int start; + public final int nextStyleIncrement; + + public CueStyle(CharacterStyle style, int start, int nextStyleIncrement) { + this.style = style; + this.start = start; + this.nextStyleIncrement = nextStyleIncrement; + } + + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/Cea708Cue.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/Cea708Cue.java new file mode 100755 index 000000000..802bd6ab1 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/Cea708Cue.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2.text.cea; + +import android.support.annotation.NonNull; +import android.text.Layout.Alignment; +import org.telegram.messenger.exoplayer2.text.Cue; + +/** + * A {@link Cue} for CEA-708. + */ +/* package */ final class Cea708Cue extends Cue implements Comparable { + + /** + * An unset priority. + */ + public static final int PRIORITY_UNSET = -1; + + /** + * The priority of the cue box. + */ + public final int priority; + + /** + * @param text See {@link #text}. + * @param textAlignment See {@link #textAlignment}. + * @param line See {@link #line}. + * @param lineType See {@link #lineType}. + * @param lineAnchor See {@link #lineAnchor}. + * @param position See {@link #position}. + * @param positionAnchor See {@link #positionAnchor}. + * @param size See {@link #size}. + * @param windowColorSet See {@link #windowColorSet}. + * @param windowColor See {@link #windowColor}. + * @param priority See (@link #priority}. + */ + public Cea708Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, + @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, + boolean windowColorSet, int windowColor, int priority) { + super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, + windowColorSet, windowColor); + this.priority = priority; + } + + @Override + public int compareTo(@NonNull Cea708Cue other) { + if (other.priority < priority) { + return -1; + } else if (other.priority > priority) { + return 1; + } + return 0; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/Cea708Decoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/Cea708Decoder.java new file mode 100755 index 000000000..0c3b6f072 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/Cea708Decoder.java @@ -0,0 +1,1248 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2.text.cea; + +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.Layout.Alignment; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; +import android.util.Log; +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.Format; +import org.telegram.messenger.exoplayer2.text.Cue; +import org.telegram.messenger.exoplayer2.text.Cue.AnchorType; +import org.telegram.messenger.exoplayer2.text.Subtitle; +import org.telegram.messenger.exoplayer2.text.SubtitleDecoder; +import org.telegram.messenger.exoplayer2.text.SubtitleInputBuffer; +import org.telegram.messenger.exoplayer2.util.Assertions; +import org.telegram.messenger.exoplayer2.util.ParsableBitArray; +import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708"). + */ +public final class Cea708Decoder extends CeaDecoder { + + private static final String TAG = "Cea708Decoder"; + + private static final int NUM_WINDOWS = 8; + + private static final int DTVCC_PACKET_DATA = 0x02; + private static final int DTVCC_PACKET_START = 0x03; + private static final int CC_VALID_FLAG = 0x04; + + // Base Commands + private static final int GROUP_C0_END = 0x1F; // Miscellaneous Control Codes + private static final int GROUP_G0_END = 0x7F; // ASCII Printable Characters + private static final int GROUP_C1_END = 0x9F; // Captioning Command Control Codes + private static final int GROUP_G1_END = 0xFF; // ISO 8859-1 LATIN-1 Character Set + + // Extended Commands + private static final int GROUP_C2_END = 0x1F; // Extended Control Code Set 1 + private static final int GROUP_G2_END = 0x7F; // Extended Miscellaneous Characters + private static final int GROUP_C3_END = 0x9F; // Extended Control Code Set 2 + private static final int GROUP_G3_END = 0xFF; // Future Expansion + + // Group C0 Commands + private static final int COMMAND_NUL = 0x00; // Nul + private static final int COMMAND_ETX = 0x03; // EndOfText + private static final int COMMAND_BS = 0x08; // Backspace + private static final int COMMAND_FF = 0x0C; // FormFeed (Flush) + private static final int COMMAND_CR = 0x0D; // CarriageReturn + private static final int COMMAND_HCR = 0x0E; // ClearLine + private static final int COMMAND_EXT1 = 0x10; // Extended Control Code Flag + private static final int COMMAND_EXT1_START = 0x11; + private static final int COMMAND_EXT1_END = 0x17; + private static final int COMMAND_P16_START = 0x18; + private static final int COMMAND_P16_END = 0x1F; + + // Group C1 Commands + private static final int COMMAND_CW0 = 0x80; // SetCurrentWindow to 0 + private static final int COMMAND_CW1 = 0x81; // SetCurrentWindow to 1 + private static final int COMMAND_CW2 = 0x82; // SetCurrentWindow to 2 + private static final int COMMAND_CW3 = 0x83; // SetCurrentWindow to 3 + private static final int COMMAND_CW4 = 0x84; // SetCurrentWindow to 4 + private static final int COMMAND_CW5 = 0x85; // SetCurrentWindow to 5 + private static final int COMMAND_CW6 = 0x86; // SetCurrentWindow to 6 + private static final int COMMAND_CW7 = 0x87; // SetCurrentWindow to 7 + private static final int COMMAND_CLW = 0x88; // ClearWindows (+1 byte) + private static final int COMMAND_DSW = 0x89; // DisplayWindows (+1 byte) + private static final int COMMAND_HDW = 0x8A; // HideWindows (+1 byte) + private static final int COMMAND_TGW = 0x8B; // ToggleWindows (+1 byte) + private static final int COMMAND_DLW = 0x8C; // DeleteWindows (+1 byte) + private static final int COMMAND_DLY = 0x8D; // Delay (+1 byte) + private static final int COMMAND_DLC = 0x8E; // DelayCancel + private static final int COMMAND_RST = 0x8F; // Reset + private static final int COMMAND_SPA = 0x90; // SetPenAttributes (+2 bytes) + private static final int COMMAND_SPC = 0x91; // SetPenColor (+3 bytes) + private static final int COMMAND_SPL = 0x92; // SetPenLocation (+2 bytes) + private static final int COMMAND_SWA = 0x97; // SetWindowAttributes (+4 bytes) + private static final int COMMAND_DF0 = 0x98; // DefineWindow 0 (+6 bytes) + private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes) + private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes) + private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes) + private static final int COMMAND_DS4 = 0x9C; // DefineWindow 4 (+6 bytes) + private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes) + private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes) + private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes) + + // G0 Table Special Chars + private static final int CHARACTER_MN = 0x7F; // MusicNote + + // G2 Table Special Chars + private static final int CHARACTER_TSP = 0x20; + private static final int CHARACTER_NBTSP = 0x21; + private static final int CHARACTER_ELLIPSIS = 0x25; + private static final int CHARACTER_BIG_CARONS = 0x2A; + private static final int CHARACTER_BIG_OE = 0x2C; + private static final int CHARACTER_SOLID_BLOCK = 0x30; + private static final int CHARACTER_OPEN_SINGLE_QUOTE = 0x31; + private static final int CHARACTER_CLOSE_SINGLE_QUOTE = 0x32; + private static final int CHARACTER_OPEN_DOUBLE_QUOTE = 0x33; + private static final int CHARACTER_CLOSE_DOUBLE_QUOTE = 0x34; + private static final int CHARACTER_BOLD_BULLET = 0x35; + private static final int CHARACTER_TM = 0x39; + private static final int CHARACTER_SMALL_CARONS = 0x3A; + private static final int CHARACTER_SMALL_OE = 0x3C; + private static final int CHARACTER_SM = 0x3D; + private static final int CHARACTER_DIAERESIS_Y = 0x3F; + private static final int CHARACTER_ONE_EIGHTH = 0x76; + private static final int CHARACTER_THREE_EIGHTHS = 0x77; + private static final int CHARACTER_FIVE_EIGHTHS = 0x78; + private static final int CHARACTER_SEVEN_EIGHTHS = 0x79; + private static final int CHARACTER_VERTICAL_BORDER = 0x7A; + private static final int CHARACTER_UPPER_RIGHT_BORDER = 0x7B; + private static final int CHARACTER_LOWER_LEFT_BORDER = 0x7C; + private static final int CHARACTER_HORIZONTAL_BORDER = 0x7D; + private static final int CHARACTER_LOWER_RIGHT_BORDER = 0x7E; + private static final int CHARACTER_UPPER_LEFT_BORDER = 0x7F; + + private final ParsableByteArray ccData; + private final ParsableBitArray serviceBlockPacket; + + private final int selectedServiceNumber; + private final CueBuilder[] cueBuilders; + + private CueBuilder currentCueBuilder; + private List cues; + private List lastCues; + + private DtvCcPacket currentDtvCcPacket; + private int currentWindow; + + public Cea708Decoder(int accessibilityChannel) { + ccData = new ParsableByteArray(); + serviceBlockPacket = new ParsableBitArray(); + selectedServiceNumber = (accessibilityChannel == Format.NO_VALUE) ? 1 : accessibilityChannel; + + cueBuilders = new CueBuilder[NUM_WINDOWS]; + for (int i = 0; i < NUM_WINDOWS; i++) { + cueBuilders[i] = new CueBuilder(); + } + + currentCueBuilder = cueBuilders[0]; + resetCueBuilders(); + } + + @Override + public String getName() { + return "Cea708Decoder"; + } + + @Override + public void flush() { + super.flush(); + cues = null; + lastCues = null; + currentWindow = 0; + currentCueBuilder = cueBuilders[currentWindow]; + resetCueBuilders(); + currentDtvCcPacket = null; + } + + @Override + protected boolean isNewSubtitleDataAvailable() { + return cues != lastCues; + } + + @Override + protected Subtitle createSubtitle() { + lastCues = cues; + return new CeaSubtitle(cues); + } + + @Override + protected void decode(SubtitleInputBuffer inputBuffer) { + ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); + while (ccData.bytesLeft() >= 3) { + int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07); + + int ccType = ccTypeAndValid & (DTVCC_PACKET_DATA | DTVCC_PACKET_START); + boolean ccValid = (ccTypeAndValid & CC_VALID_FLAG) == CC_VALID_FLAG; + byte ccData1 = (byte) ccData.readUnsignedByte(); + byte ccData2 = (byte) ccData.readUnsignedByte(); + + // Ignore any non-CEA-708 data + if (ccType != DTVCC_PACKET_DATA && ccType != DTVCC_PACKET_START) { + continue; + } + + if (!ccValid) { + // This byte-pair isn't valid, ignore it and continue. + continue; + } + + if (ccType == DTVCC_PACKET_START) { + finalizeCurrentPacket(); + + int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits + int packetSize = ccData1 & 0x3F; // last 6 bits + if (packetSize == 0) { + packetSize = 64; + } + + currentDtvCcPacket = new DtvCcPacket(sequenceNumber, packetSize); + currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2; + } else { + // The only remaining valid packet type is DTVCC_PACKET_DATA + Assertions.checkArgument(ccType == DTVCC_PACKET_DATA); + + if (currentDtvCcPacket == null) { + Log.e(TAG, "Encountered DTVCC_PACKET_DATA before DTVCC_PACKET_START"); + continue; + } + + currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData1; + currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2; + } + + if (currentDtvCcPacket.currentIndex == (currentDtvCcPacket.packetSize * 2 - 1)) { + finalizeCurrentPacket(); + } + } + } + + private void finalizeCurrentPacket() { + if (currentDtvCcPacket == null) { + // No packet to finalize; + return; + } + + processCurrentPacket(); + currentDtvCcPacket = null; + } + + private void processCurrentPacket() { + if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) { + Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1) + + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number " + + currentDtvCcPacket.sequenceNumber + "); ignoring packet"); + return; + } + + serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex); + + int serviceNumber = serviceBlockPacket.readBits(3); + int blockSize = serviceBlockPacket.readBits(5); + if (serviceNumber == 7) { + // extended service numbers + serviceBlockPacket.skipBits(2); + serviceNumber += serviceBlockPacket.readBits(6); + } + + // Ignore packets in which blockSize is 0 + if (blockSize == 0) { + if (serviceNumber != 0) { + Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0"); + } + return; + } + + if (serviceNumber != selectedServiceNumber) { + return; + } + + // The cues should be updated if we receive a C0 ETX command, any C1 command, or if after + // processing the service block any text has been added to the buffer. See CEA-708-B Section + // 8.10.4 for more details. + boolean cuesNeedUpdate = false; + + while (serviceBlockPacket.bitsLeft() > 0) { + int command = serviceBlockPacket.readBits(8); + if (command != COMMAND_EXT1) { + if (command <= GROUP_C0_END) { + handleC0Command(command); + // If the C0 command was an ETX command, the cues are updated in handleC0Command. + } else if (command <= GROUP_G0_END) { + handleG0Character(command); + cuesNeedUpdate = true; + } else if (command <= GROUP_C1_END) { + handleC1Command(command); + cuesNeedUpdate = true; + } else if (command <= GROUP_G1_END) { + handleG1Character(command); + cuesNeedUpdate = true; + } else { + Log.w(TAG, "Invalid base command: " + command); + } + } else { + // Read the extended command + command = serviceBlockPacket.readBits(8); + if (command <= GROUP_C2_END) { + handleC2Command(command); + } else if (command <= GROUP_G2_END) { + handleG2Character(command); + cuesNeedUpdate = true; + } else if (command <= GROUP_C3_END) { + handleC3Command(command); + } else if (command <= GROUP_G3_END) { + handleG3Character(command); + cuesNeedUpdate = true; + } else { + Log.w(TAG, "Invalid extended command: " + command); + } + } + } + + if (cuesNeedUpdate) { + cues = getDisplayCues(); + } + } + + private void handleC0Command(int command) { + switch (command) { + case COMMAND_NUL: + // Do nothing. + break; + case COMMAND_ETX: + cues = getDisplayCues(); + break; + case COMMAND_BS: + currentCueBuilder.backspace(); + break; + case COMMAND_FF: + resetCueBuilders(); + break; + case COMMAND_CR: + currentCueBuilder.append('\n'); + break; + case COMMAND_HCR: + // TODO: Add support for this command. + break; + default: + if (command >= COMMAND_EXT1_START && command <= COMMAND_EXT1_END) { + Log.w(TAG, "Currently unsupported COMMAND_EXT1 Command: " + command); + serviceBlockPacket.skipBits(8); + } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) { + Log.w(TAG, "Currently unsupported COMMAND_P16 Command: " + command); + serviceBlockPacket.skipBits(16); + } else { + Log.w(TAG, "Invalid C0 command: " + command); + } + } + } + + private void handleC1Command(int command) { + int window; + switch (command) { + case COMMAND_CW0: + case COMMAND_CW1: + case COMMAND_CW2: + case COMMAND_CW3: + case COMMAND_CW4: + case COMMAND_CW5: + case COMMAND_CW6: + case COMMAND_CW7: + window = (command - COMMAND_CW0); + if (currentWindow != window) { + currentWindow = window; + currentCueBuilder = cueBuilders[window]; + } + break; + case COMMAND_CLW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (serviceBlockPacket.readBit()) { + cueBuilders[NUM_WINDOWS - i].clear(); + } + } + break; + case COMMAND_DSW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (serviceBlockPacket.readBit()) { + cueBuilders[NUM_WINDOWS - i].setVisibility(true); + } + } + break; + case COMMAND_HDW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (serviceBlockPacket.readBit()) { + cueBuilders[NUM_WINDOWS - i].setVisibility(false); + } + } + break; + case COMMAND_TGW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (serviceBlockPacket.readBit()) { + CueBuilder cueBuilder = cueBuilders[NUM_WINDOWS - i]; + cueBuilder.setVisibility(!cueBuilder.isVisible()); + } + } + break; + case COMMAND_DLW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (serviceBlockPacket.readBit()) { + cueBuilders[NUM_WINDOWS - i].reset(); + } + } + break; + case COMMAND_DLY: + // TODO: Add support for delay commands. + serviceBlockPacket.skipBits(8); + break; + case COMMAND_DLC: + // TODO: Add support for delay commands. + break; + case COMMAND_RST: + resetCueBuilders(); + break; + case COMMAND_SPA: + if (!currentCueBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + serviceBlockPacket.skipBits(16); + } else { + handleSetPenAttributes(); + } + break; + case COMMAND_SPC: + if (!currentCueBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + serviceBlockPacket.skipBits(24); + } else { + handleSetPenColor(); + } + break; + case COMMAND_SPL: + if (!currentCueBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + serviceBlockPacket.skipBits(16); + } else { + handleSetPenLocation(); + } + break; + case COMMAND_SWA: + if (!currentCueBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + serviceBlockPacket.skipBits(32); + } else { + handleSetWindowAttributes(); + } + break; + case COMMAND_DF0: + case COMMAND_DF1: + case COMMAND_DF2: + case COMMAND_DF3: + case COMMAND_DS4: + case COMMAND_DF5: + case COMMAND_DF6: + case COMMAND_DF7: + window = (command - COMMAND_DF0); + handleDefineWindow(window); + // We also set the current window to the newly defined window. + if (currentWindow != window) { + currentWindow = window; + currentCueBuilder = cueBuilders[window]; + } + break; + default: + Log.w(TAG, "Invalid C1 command: " + command); + } + } + + private void handleC2Command(int command) { + // C2 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes + if (command <= 0x07) { + // Do nothing. + } else if (command <= 0x0F) { + serviceBlockPacket.skipBits(8); + } else if (command <= 0x17) { + serviceBlockPacket.skipBits(16); + } else if (command <= 0x1F) { + serviceBlockPacket.skipBits(24); + } + } + + private void handleC3Command(int command) { + // C3 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes + if (command <= 0x87) { + serviceBlockPacket.skipBits(32); + } else if (command <= 0x8F) { + serviceBlockPacket.skipBits(40); + } else if (command <= 0x9F) { + // 90-9F are variable length codes; the first byte defines the header with the first + // 2 bits specifying the type and the last 6 bits specifying the remaining length of the + // command in bytes + serviceBlockPacket.skipBits(2); + int length = serviceBlockPacket.readBits(6); + serviceBlockPacket.skipBits(8 * length); + } + } + + private void handleG0Character(int characterCode) { + if (characterCode == CHARACTER_MN) { + currentCueBuilder.append('\u266B'); + } else { + currentCueBuilder.append((char) (characterCode & 0xFF)); + } + } + + private void handleG1Character(int characterCode) { + currentCueBuilder.append((char) (characterCode & 0xFF)); + } + + private void handleG2Character(int characterCode) { + switch (characterCode) { + case CHARACTER_TSP: + currentCueBuilder.append('\u0020'); + break; + case CHARACTER_NBTSP: + currentCueBuilder.append('\u00A0'); + break; + case CHARACTER_ELLIPSIS: + currentCueBuilder.append('\u2026'); + break; + case CHARACTER_BIG_CARONS: + currentCueBuilder.append('\u0160'); + break; + case CHARACTER_BIG_OE: + currentCueBuilder.append('\u0152'); + break; + case CHARACTER_SOLID_BLOCK: + currentCueBuilder.append('\u2588'); + break; + case CHARACTER_OPEN_SINGLE_QUOTE: + currentCueBuilder.append('\u2018'); + break; + case CHARACTER_CLOSE_SINGLE_QUOTE: + currentCueBuilder.append('\u2019'); + break; + case CHARACTER_OPEN_DOUBLE_QUOTE: + currentCueBuilder.append('\u201C'); + break; + case CHARACTER_CLOSE_DOUBLE_QUOTE: + currentCueBuilder.append('\u201D'); + break; + case CHARACTER_BOLD_BULLET: + currentCueBuilder.append('\u2022'); + break; + case CHARACTER_TM: + currentCueBuilder.append('\u2122'); + break; + case CHARACTER_SMALL_CARONS: + currentCueBuilder.append('\u0161'); + break; + case CHARACTER_SMALL_OE: + currentCueBuilder.append('\u0153'); + break; + case CHARACTER_SM: + currentCueBuilder.append('\u2120'); + break; + case CHARACTER_DIAERESIS_Y: + currentCueBuilder.append('\u0178'); + break; + case CHARACTER_ONE_EIGHTH: + currentCueBuilder.append('\u215B'); + break; + case CHARACTER_THREE_EIGHTHS: + currentCueBuilder.append('\u215C'); + break; + case CHARACTER_FIVE_EIGHTHS: + currentCueBuilder.append('\u215D'); + break; + case CHARACTER_SEVEN_EIGHTHS: + currentCueBuilder.append('\u215E'); + break; + case CHARACTER_VERTICAL_BORDER: + currentCueBuilder.append('\u2502'); + break; + case CHARACTER_UPPER_RIGHT_BORDER: + currentCueBuilder.append('\u2510'); + break; + case CHARACTER_LOWER_LEFT_BORDER: + currentCueBuilder.append('\u2514'); + break; + case CHARACTER_HORIZONTAL_BORDER: + currentCueBuilder.append('\u2500'); + break; + case CHARACTER_LOWER_RIGHT_BORDER: + currentCueBuilder.append('\u2518'); + break; + case CHARACTER_UPPER_LEFT_BORDER: + currentCueBuilder.append('\u250C'); + break; + default: + Log.w(TAG, "Invalid G2 character: " + characterCode); + // The CEA-708 specification doesn't specify what to do in the case of an unexpected + // value in the G2 character range, so we ignore it. + } + } + + private void handleG3Character(int characterCode) { + if (characterCode == 0xA0) { + currentCueBuilder.append('\u33C4'); + } else { + Log.w(TAG, "Invalid G3 character: " + characterCode); + // Substitute any unsupported G3 character with an underscore as per CEA-708 specification. + currentCueBuilder.append('_'); + } + } + + private void handleSetPenAttributes() { + // the SetPenAttributes command contains 2 bytes of data + // first byte + int textTag = serviceBlockPacket.readBits(4); + int offset = serviceBlockPacket.readBits(2); + int penSize = serviceBlockPacket.readBits(2); + // second byte + boolean italicsToggle = serviceBlockPacket.readBit(); + boolean underlineToggle = serviceBlockPacket.readBit(); + int edgeType = serviceBlockPacket.readBits(3); + int fontStyle = serviceBlockPacket.readBits(3); + + currentCueBuilder.setPenAttributes(textTag, offset, penSize, italicsToggle, underlineToggle, + edgeType, fontStyle); + } + + private void handleSetPenColor() { + // the SetPenColor command contains 3 bytes of data + // first byte + int foregroundO = serviceBlockPacket.readBits(2); + int foregroundR = serviceBlockPacket.readBits(2); + int foregroundG = serviceBlockPacket.readBits(2); + int foregroundB = serviceBlockPacket.readBits(2); + int foregroundColor = CueBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB, + foregroundO); + // second byte + int backgroundO = serviceBlockPacket.readBits(2); + int backgroundR = serviceBlockPacket.readBits(2); + int backgroundG = serviceBlockPacket.readBits(2); + int backgroundB = serviceBlockPacket.readBits(2); + int backgroundColor = CueBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB, + backgroundO); + // third byte + serviceBlockPacket.skipBits(2); // null padding + int edgeR = serviceBlockPacket.readBits(2); + int edgeG = serviceBlockPacket.readBits(2); + int edgeB = serviceBlockPacket.readBits(2); + int edgeColor = CueBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB); + + currentCueBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor); + } + + private void handleSetPenLocation() { + // the SetPenLocation command contains 2 bytes of data + // first byte + serviceBlockPacket.skipBits(4); + int row = serviceBlockPacket.readBits(4); + // second byte + serviceBlockPacket.skipBits(2); + int column = serviceBlockPacket.readBits(6); + + currentCueBuilder.setPenLocation(row, column); + } + + private void handleSetWindowAttributes() { + // the SetWindowAttributes command contains 4 bytes of data + // first byte + int fillO = serviceBlockPacket.readBits(2); + int fillR = serviceBlockPacket.readBits(2); + int fillG = serviceBlockPacket.readBits(2); + int fillB = serviceBlockPacket.readBits(2); + int fillColor = CueBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO); + // second byte + int borderType = serviceBlockPacket.readBits(2); // only the lower 2 bits of borderType + int borderR = serviceBlockPacket.readBits(2); + int borderG = serviceBlockPacket.readBits(2); + int borderB = serviceBlockPacket.readBits(2); + int borderColor = CueBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB); + // third byte + if (serviceBlockPacket.readBit()) { + borderType |= 0x04; // set the top bit of the 3-bit borderType + } + boolean wordWrapToggle = serviceBlockPacket.readBit(); + int printDirection = serviceBlockPacket.readBits(2); + int scrollDirection = serviceBlockPacket.readBits(2); + int justification = serviceBlockPacket.readBits(2); + // fourth byte + // Note that we don't intend to support display effects + serviceBlockPacket.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2) + + currentCueBuilder.setWindowAttributes(fillColor, borderColor, wordWrapToggle, borderType, + printDirection, scrollDirection, justification); + } + + private void handleDefineWindow(int window) { + CueBuilder cueBuilder = cueBuilders[window]; + + // the DefineWindow command contains 6 bytes of data + // first byte + serviceBlockPacket.skipBits(2); // null padding + boolean visible = serviceBlockPacket.readBit(); + boolean rowLock = serviceBlockPacket.readBit(); + boolean columnLock = serviceBlockPacket.readBit(); + int priority = serviceBlockPacket.readBits(3); + // second byte + boolean relativePositioning = serviceBlockPacket.readBit(); + int verticalAnchor = serviceBlockPacket.readBits(7); + // third byte + int horizontalAnchor = serviceBlockPacket.readBits(8); + // fourth byte + int anchorId = serviceBlockPacket.readBits(4); + int rowCount = serviceBlockPacket.readBits(4); + // fifth byte + serviceBlockPacket.skipBits(2); // null padding + int columnCount = serviceBlockPacket.readBits(6); + // sixth byte + serviceBlockPacket.skipBits(2); // null padding + int windowStyle = serviceBlockPacket.readBits(3); + int penStyle = serviceBlockPacket.readBits(3); + + cueBuilder.defineWindow(visible, rowLock, columnLock, priority, relativePositioning, + verticalAnchor, horizontalAnchor, rowCount, columnCount, anchorId, windowStyle, penStyle); + } + + private List getDisplayCues() { + List displayCues = new ArrayList<>(); + for (int i = 0; i < NUM_WINDOWS; i++) { + if (!cueBuilders[i].isEmpty() && cueBuilders[i].isVisible()) { + displayCues.add(cueBuilders[i].build()); + } + } + Collections.sort(displayCues); + return Collections.unmodifiableList(displayCues); + } + + private void resetCueBuilders() { + for (int i = 0; i < NUM_WINDOWS; i++) { + cueBuilders[i].reset(); + } + } + + private static final class DtvCcPacket { + + public final int sequenceNumber; + public final int packetSize; + public final byte[] packetData; + + int currentIndex; + + public DtvCcPacket(int sequenceNumber, int packetSize) { + this.sequenceNumber = sequenceNumber; + this.packetSize = packetSize; + packetData = new byte[2 * packetSize - 1]; + currentIndex = 0; + } + + } + + // TODO: There is a lot of overlap between Cea708Decoder.CueBuilder and Cea608Decoder.CueBuilder + // which could be refactored into a separate class. + private static final class CueBuilder { + + private static final int RELATIVE_CUE_SIZE = 99; + private static final int VERTICAL_SIZE = 74; + private static final int HORIZONTAL_SIZE = 209; + + private static final int DEFAULT_PRIORITY = 4; + + private static final int MAXIMUM_ROW_COUNT = 15; + + private static final int JUSTIFICATION_LEFT = 0; + private static final int JUSTIFICATION_RIGHT = 1; + private static final int JUSTIFICATION_CENTER = 2; + private static final int JUSTIFICATION_FULL = 3; + + private static final int DIRECTION_LEFT_TO_RIGHT = 0; + private static final int DIRECTION_RIGHT_TO_LEFT = 1; + private static final int DIRECTION_TOP_TO_BOTTOM = 2; + private static final int DIRECTION_BOTTOM_TO_TOP = 3; + + // TODO: Add other border/edge types when utilized. + private static final int BORDER_AND_EDGE_TYPE_NONE = 0; + private static final int BORDER_AND_EDGE_TYPE_UNIFORM = 3; + + public static final int COLOR_SOLID_WHITE = getArgbColorFromCeaColor(2, 2, 2, 0); + public static final int COLOR_SOLID_BLACK = getArgbColorFromCeaColor(0, 0, 0, 0); + public static final int COLOR_TRANSPARENT = getArgbColorFromCeaColor(0, 0, 0, 3); + + // TODO: Add other sizes when utilized. + private static final int PEN_SIZE_STANDARD = 1; + + // TODO: Add other pen font styles when utilized. + private static final int PEN_FONT_STYLE_DEFAULT = 0; + private static final int PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS = 1; + private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS = 2; + private static final int PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS = 3; + private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS = 4; + + // TODO: Add other pen offsets when utilized. + private static final int PEN_OFFSET_NORMAL = 1; + + // The window style properties are specified in the CEA-708 specification. + private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[]{ + JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, + JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER, + JUSTIFICATION_LEFT + }; + private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[]{ + DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, + DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, + DIRECTION_TOP_TO_BOTTOM + }; + private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[]{ + DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, + DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, + DIRECTION_RIGHT_TO_LEFT + }; + private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[]{ + false, false, false, true, true, true, false + }; + private static final int[] WINDOW_STYLE_FILL = new int[]{ + COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, + COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK + }; + + // The pen style properties are specified in the CEA-708 specification. + private static final int[] PEN_STYLE_FONT_STYLE = new int[]{ + PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS, + PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, + PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS, + PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, + PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS + }; + private static final int[] PEN_STYLE_EDGE_TYPE = new int[]{ + BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, + BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM, + BORDER_AND_EDGE_TYPE_UNIFORM + }; + private static final int[] PEN_STYLE_BACKGROUND = new int[]{ + COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, + COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT}; + + private final List rolledUpCaptions; + private final SpannableStringBuilder captionStringBuilder; + + // Window/Cue properties + private boolean defined; + private boolean visible; + private int priority; + private boolean relativePositioning; + private int verticalAnchor; + private int horizontalAnchor; + private int anchorId; + private int rowCount; + private boolean rowLock; + private int justification; + private int windowStyleId; + private int penStyleId; + private int windowFillColor; + + // Pen/Text properties + private int italicsStartPosition; + private int underlineStartPosition; + private int foregroundColorStartPosition; + private int foregroundColor; + private int backgroundColorStartPosition; + private int backgroundColor; + private int row; + + public CueBuilder() { + rolledUpCaptions = new LinkedList<>(); + captionStringBuilder = new SpannableStringBuilder(); + reset(); + } + + public boolean isEmpty() { + return !isDefined() || (rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0); + } + + public void reset() { + clear(); + + defined = false; + visible = false; + priority = DEFAULT_PRIORITY; + relativePositioning = false; + verticalAnchor = 0; + horizontalAnchor = 0; + anchorId = 0; + rowCount = MAXIMUM_ROW_COUNT; + rowLock = true; + justification = JUSTIFICATION_LEFT; + windowStyleId = 0; + penStyleId = 0; + windowFillColor = COLOR_SOLID_BLACK; + + foregroundColor = COLOR_SOLID_WHITE; + backgroundColor = COLOR_SOLID_BLACK; + } + + public void clear() { + rolledUpCaptions.clear(); + captionStringBuilder.clear(); + italicsStartPosition = C.POSITION_UNSET; + underlineStartPosition = C.POSITION_UNSET; + foregroundColorStartPosition = C.POSITION_UNSET; + backgroundColorStartPosition = C.POSITION_UNSET; + row = 0; + } + + public boolean isDefined() { + return defined; + } + + public void setVisibility(boolean visible) { + this.visible = visible; + } + + public boolean isVisible() { + return visible; + } + + public void defineWindow(boolean visible, boolean rowLock, boolean columnLock, int priority, + boolean relativePositioning, int verticalAnchor, int horizontalAnchor, int rowCount, + int columnCount, int anchorId, int windowStyleId, int penStyleId) { + this.defined = true; + this.visible = visible; + this.rowLock = rowLock; + this.priority = priority; + this.relativePositioning = relativePositioning; + this.verticalAnchor = verticalAnchor; + this.horizontalAnchor = horizontalAnchor; + this.anchorId = anchorId; + + // Decoders must add one to rowCount to get the desired number of rows. + if (this.rowCount != rowCount + 1) { + this.rowCount = rowCount + 1; + + // Trim any rolled up captions that are no longer valid, if applicable. + while ((rowLock && (rolledUpCaptions.size() >= this.rowCount)) + || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) { + rolledUpCaptions.remove(0); + } + } + + // TODO: Add support for column lock and count. + + if (windowStyleId != 0 && this.windowStyleId != windowStyleId) { + this.windowStyleId = windowStyleId; + // windowStyleId is 1-based. + int windowStyleIdIndex = windowStyleId - 1; + // Note that Border type and border color are the same for all window styles. + setWindowAttributes(WINDOW_STYLE_FILL[windowStyleIdIndex], COLOR_TRANSPARENT, + WINDOW_STYLE_WORD_WRAP[windowStyleIdIndex], BORDER_AND_EDGE_TYPE_NONE, + WINDOW_STYLE_PRINT_DIRECTION[windowStyleIdIndex], + WINDOW_STYLE_SCROLL_DIRECTION[windowStyleIdIndex], + WINDOW_STYLE_JUSTIFICATION[windowStyleIdIndex]); + } + + if (penStyleId != 0 && this.penStyleId != penStyleId) { + this.penStyleId = penStyleId; + // penStyleId is 1-based. + int penStyleIdIndex = penStyleId - 1; + // Note that pen size, offset, italics, underline, foreground color, and foreground + // opacity are the same for all pen styles. + setPenAttributes(0, PEN_OFFSET_NORMAL, PEN_SIZE_STANDARD, false, false, + PEN_STYLE_EDGE_TYPE[penStyleIdIndex], PEN_STYLE_FONT_STYLE[penStyleIdIndex]); + setPenColor(COLOR_SOLID_WHITE, PEN_STYLE_BACKGROUND[penStyleIdIndex], COLOR_SOLID_BLACK); + } + } + + + public void setWindowAttributes(int fillColor, int borderColor, boolean wordWrapToggle, + int borderType, int printDirection, int scrollDirection, int justification) { + this.windowFillColor = fillColor; + // TODO: Add support for border color and types. + // TODO: Add support for word wrap. + // TODO: Add support for other scroll directions. + // TODO: Add support for other print directions. + this.justification = justification; + + } + + public void setPenAttributes(int textTag, int offset, int penSize, boolean italicsToggle, + boolean underlineToggle, int edgeType, int fontStyle) { + // TODO: Add support for text tags. + // TODO: Add support for other offsets. + // TODO: Add support for other pen sizes. + + if (italicsStartPosition != C.POSITION_UNSET) { + if (!italicsToggle) { + captionStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition, + captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + italicsStartPosition = C.POSITION_UNSET; + } + } else if (italicsToggle) { + italicsStartPosition = captionStringBuilder.length(); + } + + if (underlineStartPosition != C.POSITION_UNSET) { + if (!underlineToggle) { + captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, + captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + underlineStartPosition = C.POSITION_UNSET; + } + } else if (underlineToggle) { + underlineStartPosition = captionStringBuilder.length(); + } + + // TODO: Add support for edge types. + // TODO: Add support for other font styles. + } + + public void setPenColor(int foregroundColor, int backgroundColor, int edgeColor) { + if (foregroundColorStartPosition != C.POSITION_UNSET) { + if (this.foregroundColor != foregroundColor) { + captionStringBuilder.setSpan(new ForegroundColorSpan(this.foregroundColor), + foregroundColorStartPosition, captionStringBuilder.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + if (foregroundColor != COLOR_SOLID_WHITE) { + foregroundColorStartPosition = captionStringBuilder.length(); + this.foregroundColor = foregroundColor; + } + + if (backgroundColorStartPosition != C.POSITION_UNSET) { + if (this.backgroundColor != backgroundColor) { + captionStringBuilder.setSpan(new BackgroundColorSpan(this.backgroundColor), + backgroundColorStartPosition, captionStringBuilder.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + if (backgroundColor != COLOR_SOLID_BLACK) { + backgroundColorStartPosition = captionStringBuilder.length(); + this.backgroundColor = backgroundColor; + } + + // TODO: Add support for edge color. + } + + public void setPenLocation(int row, int column) { + // TODO: Support moving the pen location with a window properly. + + // Until we support proper pen locations, if we encounter a row that's different from the + // previous one, we should append a new line. Otherwise, we'll see strings that should be + // on new lines concatenated with the previous, resulting in 2 words being combined, as + // well as potentially drawing beyond the width of the window/screen. + if (this.row != row) { + append('\n'); + } + this.row = row; + } + + public void backspace() { + int length = captionStringBuilder.length(); + if (length > 0) { + captionStringBuilder.delete(length - 1, length); + } + } + + public void append(char text) { + if (text == '\n') { + rolledUpCaptions.add(buildSpannableString()); + captionStringBuilder.clear(); + + if (italicsStartPosition != C.POSITION_UNSET) { + italicsStartPosition = 0; + } + if (underlineStartPosition != C.POSITION_UNSET) { + underlineStartPosition = 0; + } + if (foregroundColorStartPosition != C.POSITION_UNSET) { + foregroundColorStartPosition = 0; + } + if (backgroundColorStartPosition != C.POSITION_UNSET) { + backgroundColorStartPosition = 0; + } + + while ((rowLock && (rolledUpCaptions.size() >= rowCount)) + || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) { + rolledUpCaptions.remove(0); + } + } else { + captionStringBuilder.append(text); + } + } + + public SpannableString buildSpannableString() { + SpannableStringBuilder spannableStringBuilder = + new SpannableStringBuilder(captionStringBuilder); + int length = spannableStringBuilder.length(); + + if (length > 0) { + if (italicsStartPosition != C.POSITION_UNSET) { + spannableStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition, + length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (underlineStartPosition != C.POSITION_UNSET) { + spannableStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, + length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (foregroundColorStartPosition != C.POSITION_UNSET) { + spannableStringBuilder.setSpan(new ForegroundColorSpan(foregroundColor), + foregroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (backgroundColorStartPosition != C.POSITION_UNSET) { + spannableStringBuilder.setSpan(new BackgroundColorSpan(backgroundColor), + backgroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + return new SpannableString(spannableStringBuilder); + } + + public Cea708Cue build() { + if (isEmpty()) { + // The cue is empty. + return null; + } + + SpannableStringBuilder cueString = new SpannableStringBuilder(); + + // Add any rolled up captions, separated by new lines. + for (int i = 0; i < rolledUpCaptions.size(); i++) { + cueString.append(rolledUpCaptions.get(i)); + cueString.append('\n'); + } + // Add the current line. + cueString.append(buildSpannableString()); + + // TODO: Add support for right-to-left languages (i.e. where right would correspond to normal + // alignment). + Alignment alignment; + switch (justification) { + case JUSTIFICATION_FULL: + // TODO: Add support for full justification. + case JUSTIFICATION_LEFT: + alignment = Alignment.ALIGN_NORMAL; + break; + case JUSTIFICATION_RIGHT: + alignment = Alignment.ALIGN_OPPOSITE; + break; + case JUSTIFICATION_CENTER: + alignment = Alignment.ALIGN_CENTER; + break; + default: + throw new IllegalArgumentException("Unexpected justification value: " + justification); + } + + float position; + float line; + if (relativePositioning) { + position = (float) horizontalAnchor / RELATIVE_CUE_SIZE; + line = (float) verticalAnchor / RELATIVE_CUE_SIZE; + } else { + position = (float) horizontalAnchor / HORIZONTAL_SIZE; + line = (float) verticalAnchor / VERTICAL_SIZE; + } + // Apply screen-edge padding to the line and position. + position = (position * 0.9f) + 0.05f; + line = (line * 0.9f) + 0.05f; + + // anchorId specifies where the anchor should be placed on the caption cue/window. The 9 + // possible configurations are as follows: + // 0-----1-----2 + // | | + // 3 4 5 + // | | + // 6-----7-----8 + @AnchorType int verticalAnchorType; + if (anchorId % 3 == 0) { + verticalAnchorType = Cue.ANCHOR_TYPE_START; + } else if (anchorId % 3 == 1) { + verticalAnchorType = Cue.ANCHOR_TYPE_MIDDLE; + } else { + verticalAnchorType = Cue.ANCHOR_TYPE_END; + } + // TODO: Add support for right-to-left languages (i.e. where start is on the right). + @AnchorType int horizontalAnchorType; + if (anchorId / 3 == 0) { + horizontalAnchorType = Cue.ANCHOR_TYPE_START; + } else if (anchorId / 3 == 1) { + horizontalAnchorType = Cue.ANCHOR_TYPE_MIDDLE; + } else { + horizontalAnchorType = Cue.ANCHOR_TYPE_END; + } + + boolean windowColorSet = (windowFillColor != COLOR_SOLID_BLACK); + + return new Cea708Cue(cueString, alignment, line, Cue.LINE_TYPE_FRACTION, verticalAnchorType, + position, horizontalAnchorType, Cue.DIMEN_UNSET, windowColorSet, windowFillColor, + priority); + } + + public static int getArgbColorFromCeaColor(int red, int green, int blue) { + return getArgbColorFromCeaColor(red, green, blue, 0); + } + + public static int getArgbColorFromCeaColor(int red, int green, int blue, int opacity) { + Assertions.checkIndex(red, 0, 4); + Assertions.checkIndex(green, 0, 4); + Assertions.checkIndex(blue, 0, 4); + Assertions.checkIndex(opacity, 0, 4); + + int alpha; + switch (opacity) { + case 0: + case 1: + // Note the value of '1' is actually FLASH, but we don't support that. + alpha = 255; + break; + case 2: + alpha = 127; + break; + case 3: + alpha = 0; + break; + default: + alpha = 255; + } + + // TODO: Add support for the Alternative Minimum Color List or the full 64 RGB combinations. + + // Return values based on the Minimum Color List + return Color.argb(alpha, + (red > 1 ? 255 : 0), + (green > 1 ? 255 : 0), + (blue > 1 ? 255 : 0)); + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/CeaDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/CeaDecoder.java index 34b4d6a54..2329f7f66 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/CeaDecoder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/CeaDecoder.java @@ -16,6 +16,7 @@ package org.telegram.messenger.exoplayer2.text.cea; import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.Format; import org.telegram.messenger.exoplayer2.text.Subtitle; import org.telegram.messenger.exoplayer2.text.SubtitleDecoder; import org.telegram.messenger.exoplayer2.text.SubtitleDecoderException; @@ -74,7 +75,13 @@ import java.util.TreeSet; public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException { Assertions.checkArgument(inputBuffer != null); Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); - queuedInputBuffers.add(inputBuffer); + if (inputBuffer.isDecodeOnly()) { + // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow + // for decoding to begin mid-stream. + releaseInputBuffer(inputBuffer); + } else { + queuedInputBuffers.add(inputBuffer); + } dequeuedInputBuffer = null; } @@ -109,7 +116,7 @@ import java.util.TreeSet; Subtitle subtitle = createSubtitle(); if (!inputBuffer.isDecodeOnly()) { SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst(); - outputBuffer.setContent(inputBuffer.timeUs, subtitle, 0); + outputBuffer.setContent(inputBuffer.timeUs, subtitle, Format.OFFSET_SAMPLE_RELATIVE); releaseInputBuffer(inputBuffer); return outputBuffer; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/CeaSubtitle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/CeaSubtitle.java index b189c240c..357cc12bb 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/CeaSubtitle.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/CeaSubtitle.java @@ -15,8 +15,10 @@ */ package org.telegram.messenger.exoplayer2.text.cea; +import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.text.Cue; import org.telegram.messenger.exoplayer2.text.Subtitle; +import org.telegram.messenger.exoplayer2.util.Assertions; import java.util.Collections; import java.util.List; @@ -28,19 +30,15 @@ import java.util.List; private final List cues; /** - * @param cue The subtitle cue. + * @param cues The subtitle cues. */ - public CeaSubtitle(Cue cue) { - if (cue == null) { - cues = Collections.emptyList(); - } else { - cues = Collections.singletonList(cue); - } + public CeaSubtitle(List cues) { + this.cues = cues; } @Override public int getNextEventTimeIndex(long timeUs) { - return 0; + return timeUs < 0 ? 0 : C.INDEX_UNSET; } @Override @@ -50,13 +48,13 @@ import java.util.List; @Override public long getEventTime(int index) { + Assertions.checkArgument(index == 0); return 0; } @Override public List getCues(long timeUs) { - return cues; - + return timeUs >= 0 ? cues : Collections.emptyList(); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/CeaUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/CeaUtil.java new file mode 100755 index 000000000..07464e07c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/cea/CeaUtil.java @@ -0,0 +1,127 @@ +/* + * 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 org.telegram.messenger.exoplayer2.text.cea; + +import android.util.Log; +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.extractor.TrackOutput; +import org.telegram.messenger.exoplayer2.util.ParsableByteArray; + +/** + * Utility methods for handling CEA-608/708 messages. + */ +public final class CeaUtil { + + private static final String TAG = "CeaUtil"; + + private static final int PAYLOAD_TYPE_CC = 4; + private static final int COUNTRY_CODE = 0xB5; + private static final int PROVIDER_CODE = 0x31; + private static final int USER_ID = 0x47413934; // "GA94" + private static final int USER_DATA_TYPE_CODE = 0x3; + + /** + * Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages + * as samples to all of the provided outputs. + * + * @param presentationTimeUs The presentation time in microseconds for any samples. + * @param seiBuffer The unescaped SEI NAL unit data, excluding the NAL unit start code and type. + * @param outputs The outputs to which any samples should be written. + */ + public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer, + TrackOutput[] outputs) { + while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { + int payloadType = readNon255TerminatedValue(seiBuffer); + int payloadSize = readNon255TerminatedValue(seiBuffer); + // Process the payload. + if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) { + // This might occur if we're trying to read an encrypted SEI NAL unit. + Log.w(TAG, "Skipping remainder of malformed SEI NAL unit."); + seiBuffer.setPosition(seiBuffer.limit()); + } else if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { + // Ignore country_code (1) + provider_code (2) + user_identifier (4) + // + user_data_type_code (1). + seiBuffer.skipBytes(8); + // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1). + int ccCount = seiBuffer.readUnsignedByte() & 0x1F; + // Ignore em_data (1) + seiBuffer.skipBytes(1); + // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2) + // + cc_data_1 (8) + cc_data_2 (8). + int sampleLength = ccCount * 3; + int sampleStartPosition = seiBuffer.getPosition(); + for (TrackOutput output : outputs) { + seiBuffer.setPosition(sampleStartPosition); + output.sampleData(seiBuffer, sampleLength); + output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); + } + // Ignore trailing information in SEI, if any. + seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3)); + } else { + seiBuffer.skipBytes(payloadSize); + } + } + } + + /** + * Reads a value from the provided buffer consisting of zero or more 0xFF bytes followed by a + * terminating byte not equal to 0xFF. The returned value is ((0xFF * N) + T), where N is the + * number of 0xFF bytes and T is the value of the terminating byte. + * + * @param buffer The buffer from which to read the value. + * @returns The read value, or -1 if the end of the buffer is reached before a value is read. + */ + private static int readNon255TerminatedValue(ParsableByteArray buffer) { + int b; + int value = 0; + do { + if (buffer.bytesLeft() == 0) { + return -1; + } + b = buffer.readUnsignedByte(); + value += b; + } while (b == 0xFF); + return value; + } + + /** + * Inspects an sei message to determine whether it contains CEA-608. + *

    + * The position of {@code payload} is left unchanged. + * + * @param payloadType The payload type of the message. + * @param payloadLength The length of the payload. + * @param payload A {@link ParsableByteArray} containing the payload. + * @return Whether the sei message contains CEA-608. + */ + private static boolean isSeiMessageCea608(int payloadType, int payloadLength, + ParsableByteArray payload) { + if (payloadType != PAYLOAD_TYPE_CC || payloadLength < 8) { + return false; + } + int startPosition = payload.getPosition(); + int countryCode = payload.readUnsignedByte(); + int providerCode = payload.readUnsignedShort(); + int userIdentifier = payload.readInt(); + int userDataTypeCode = payload.readUnsignedByte(); + payload.setPosition(startPosition); + return countryCode == COUNTRY_CODE && providerCode == PROVIDER_CODE + && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE; + } + + private CeaUtil() {} + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/dvb/DvbDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/dvb/DvbDecoder.java new file mode 100755 index 000000000..75743c78b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/dvb/DvbDecoder.java @@ -0,0 +1,50 @@ +/* + * 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 org.telegram.messenger.exoplayer2.text.dvb; + +import org.telegram.messenger.exoplayer2.text.SimpleSubtitleDecoder; +import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import java.util.List; + +/** + * A {@link SimpleSubtitleDecoder} for DVB Subtitles. + */ +public final class DvbDecoder extends SimpleSubtitleDecoder { + + private final DvbParser parser; + + /** + * @param initializationData The initialization data for the decoder. The initialization data + * must consist of a single byte array containing 5 bytes: flag_pes_stripped (1), + * composition_page (2), ancillary_page (2). + */ + public DvbDecoder(List initializationData) { + super("DvbDecoder"); + ParsableByteArray data = new ParsableByteArray(initializationData.get(0)); + int subtitleCompositionPage = data.readUnsignedShort(); + int subtitleAncillaryPage = data.readUnsignedShort(); + parser = new DvbParser(subtitleCompositionPage, subtitleAncillaryPage); + } + + @Override + protected DvbSubtitle decode(byte[] data, int length, boolean reset) { + if (reset) { + parser.reset(); + } + return new DvbSubtitle(parser.decode(data, length)); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/dvb/DvbParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/dvb/DvbParser.java new file mode 100755 index 000000000..5cd0cfeb2 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/dvb/DvbParser.java @@ -0,0 +1,1025 @@ +/* + * 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 org.telegram.messenger.exoplayer2.text.dvb; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Region; +import android.util.Log; +import android.util.SparseArray; +import org.telegram.messenger.exoplayer2.text.Cue; +import org.telegram.messenger.exoplayer2.util.ParsableBitArray; +import org.telegram.messenger.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Parses {@link Cue}s from a DVB subtitle bitstream. + */ +/* package */ final class DvbParser { + + private static final String TAG = "DvbParser"; + + // Segment types, as defined by ETSI EN 300 743 Table 2 + private static final int SEGMENT_TYPE_PAGE_COMPOSITION = 0x10; + private static final int SEGMENT_TYPE_REGION_COMPOSITION = 0x11; + private static final int SEGMENT_TYPE_CLUT_DEFINITION = 0x12; + private static final int SEGMENT_TYPE_OBJECT_DATA = 0x13; + private static final int SEGMENT_TYPE_DISPLAY_DEFINITION = 0x14; + + // Page states, as defined by ETSI EN 300 743 Table 3 + private static final int PAGE_STATE_NORMAL = 0; // Update. Only changed elements. + // private static final int PAGE_STATE_ACQUISITION = 1; // Refresh. All elements. + // private static final int PAGE_STATE_CHANGE = 2; // New. All elements. + + // Region depths, as defined by ETSI EN 300 743 Table 5 + // private static final int REGION_DEPTH_2_BIT = 1; + private static final int REGION_DEPTH_4_BIT = 2; + private static final int REGION_DEPTH_8_BIT = 3; + + // Object codings, as defined by ETSI EN 300 743 Table 8 + private static final int OBJECT_CODING_PIXELS = 0; + private static final int OBJECT_CODING_STRING = 1; + + // Pixel-data types, as defined by ETSI EN 300 743 Table 9 + private static final int DATA_TYPE_2BP_CODE_STRING = 0x10; + private static final int DATA_TYPE_4BP_CODE_STRING = 0x11; + private static final int DATA_TYPE_8BP_CODE_STRING = 0x12; + private static final int DATA_TYPE_24_TABLE_DATA = 0x20; + private static final int DATA_TYPE_28_TABLE_DATA = 0x21; + private static final int DATA_TYPE_48_TABLE_DATA = 0x22; + private static final int DATA_TYPE_END_LINE = 0xF0; + + // Clut mapping tables, as defined by ETSI EN 300 743 10.4, 10.5, 10.6 + private static final byte[] defaultMap2To4 = { + (byte) 0x00, (byte) 0x07, (byte) 0x08, (byte) 0x0F}; + private static final byte[] defaultMap2To8 = { + (byte) 0x00, (byte) 0x77, (byte) 0x88, (byte) 0xFF}; + private static final byte[] defaultMap4To8 = { + (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33, + (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, + (byte) 0x88, (byte) 0x99, (byte) 0xAA, (byte) 0xBB, + (byte) 0xCC, (byte) 0xDD, (byte) 0xEE, (byte) 0xFF}; + + private final Paint defaultPaint; + private final Paint fillRegionPaint; + private final Canvas canvas; + private final DisplayDefinition defaultDisplayDefinition; + private final ClutDefinition defaultClutDefinition; + private final SubtitleService subtitleService; + + private Bitmap bitmap; + + /** + * Construct an instance for the given subtitle and ancillary page ids. + * + * @param subtitlePageId The id of the subtitle page carrying the subtitle to be parsed. + * @param ancillaryPageId The id of the ancillary page containing additional data. + */ + public DvbParser(int subtitlePageId, int ancillaryPageId) { + defaultPaint = new Paint(); + defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE); + defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + defaultPaint.setPathEffect(null); + fillRegionPaint = new Paint(); + fillRegionPaint.setStyle(Paint.Style.FILL); + fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); + fillRegionPaint.setPathEffect(null); + canvas = new Canvas(); + defaultDisplayDefinition = new DisplayDefinition(719, 575, 0, 719, 0, 575); + defaultClutDefinition = new ClutDefinition(0, generateDefault2BitClutEntries(), + generateDefault4BitClutEntries(), generateDefault8BitClutEntries()); + subtitleService = new SubtitleService(subtitlePageId, ancillaryPageId); + } + + /** + * Resets the parser. + */ + public void reset() { + subtitleService.reset(); + } + + /** + * Decodes a subtitling packet, returning a list of parsed {@link Cue}s. + * + * @param data The subtitling packet data to decode. + * @param limit The limit in {@code data} at which to stop decoding. + * @return The parsed {@link Cue}s. + */ + public List decode(byte[] data, int limit) { + // Parse the input data. + ParsableBitArray dataBitArray = new ParsableBitArray(data, limit); + while (dataBitArray.bitsLeft() >= 48 // sync_byte (8) + segment header (40) + && dataBitArray.readBits(8) == 0x0F) { + parseSubtitlingSegment(dataBitArray, subtitleService); + } + + if (subtitleService.pageComposition == null) { + return Collections.emptyList(); + } + + // Update the canvas bitmap if necessary. + DisplayDefinition displayDefinition = subtitleService.displayDefinition != null + ? subtitleService.displayDefinition : defaultDisplayDefinition; + if (bitmap == null || displayDefinition.width + 1 != bitmap.getWidth() + || displayDefinition.height + 1 != bitmap.getHeight()) { + bitmap = Bitmap.createBitmap(displayDefinition.width + 1, displayDefinition.height + 1, + Bitmap.Config.ARGB_8888); + canvas.setBitmap(bitmap); + } + + // Build the cues. + List cues = new ArrayList<>(); + SparseArray pageRegions = subtitleService.pageComposition.regions; + for (int i = 0; i < pageRegions.size(); i++) { + PageRegion pageRegion = pageRegions.valueAt(i); + int regionId = pageRegions.keyAt(i); + RegionComposition regionComposition = subtitleService.regions.get(regionId); + + // Clip drawing to the current region and display definition window. + int baseHorizontalAddress = pageRegion.horizontalAddress + + displayDefinition.horizontalPositionMinimum; + int baseVerticalAddress = pageRegion.verticalAddress + + displayDefinition.verticalPositionMinimum; + int clipRight = Math.min(baseHorizontalAddress + regionComposition.width, + displayDefinition.horizontalPositionMaximum); + int clipBottom = Math.min(baseVerticalAddress + regionComposition.height, + displayDefinition.verticalPositionMaximum); + canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom, + Region.Op.REPLACE); + + ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId); + if (clutDefinition == null) { + clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId); + if (clutDefinition == null) { + clutDefinition = defaultClutDefinition; + } + } + + SparseArray regionObjects = regionComposition.regionObjects; + for (int j = 0; j < regionObjects.size(); j++) { + int objectId = regionObjects.keyAt(j); + RegionObject regionObject = regionObjects.valueAt(j); + ObjectData objectData = subtitleService.objects.get(objectId); + if (objectData == null) { + objectData = subtitleService.ancillaryObjects.get(objectId); + } + if (objectData != null) { + Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; + paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth, + baseHorizontalAddress + regionObject.horizontalPosition, + baseVerticalAddress + regionObject.verticalPosition, paint, canvas); + } + } + + if (regionComposition.fillFlag) { + int color; + if (regionComposition.depth == REGION_DEPTH_8_BIT) { + color = clutDefinition.clutEntries8Bit[regionComposition.pixelCode8Bit]; + } else if (regionComposition.depth == REGION_DEPTH_4_BIT) { + color = clutDefinition.clutEntries4Bit[regionComposition.pixelCode4Bit]; + } else { + color = clutDefinition.clutEntries2Bit[regionComposition.pixelCode2Bit]; + } + fillRegionPaint.setColor(color); + canvas.drawRect(baseHorizontalAddress, baseVerticalAddress, + baseHorizontalAddress + regionComposition.width, + baseVerticalAddress + regionComposition.height, + fillRegionPaint); + } + + Bitmap cueBitmap = Bitmap.createBitmap(bitmap, baseHorizontalAddress, baseVerticalAddress, + regionComposition.width, regionComposition.height); + cues.add(new Cue(cueBitmap, (float) baseHorizontalAddress / displayDefinition.width, + Cue.ANCHOR_TYPE_START, (float) baseVerticalAddress / displayDefinition.height, + Cue.ANCHOR_TYPE_START, (float) regionComposition.width / displayDefinition.width, + (float) regionComposition.height / displayDefinition.height)); + + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + } + + return cues; + } + + // Static parsing. + + /** + * Parses a subtitling segment, as defined by ETSI EN 300 743 7.2 + *

    + * The {@link SubtitleService} is updated with the parsed segment data. + */ + private static void parseSubtitlingSegment(ParsableBitArray data, SubtitleService service) { + int segmentType = data.readBits(8); + int pageId = data.readBits(16); + int dataFieldLength = data.readBits(16); + int dataFieldLimit = data.getBytePosition() + dataFieldLength; + + if ((dataFieldLength * 8) > data.bitsLeft()) { + Log.w(TAG, "Data field length exceeds limit"); + // Skip to the very end. + data.skipBits(data.bitsLeft()); + return; + } + + switch (segmentType) { + case SEGMENT_TYPE_DISPLAY_DEFINITION: + if (pageId == service.subtitlePageId) { + service.displayDefinition = parseDisplayDefinition(data); + } + break; + case SEGMENT_TYPE_PAGE_COMPOSITION: + if (pageId == service.subtitlePageId) { + PageComposition current = service.pageComposition; + PageComposition pageComposition = parsePageComposition(data, dataFieldLength); + if (pageComposition.state != PAGE_STATE_NORMAL) { + service.pageComposition = pageComposition; + service.regions.clear(); + service.cluts.clear(); + service.objects.clear(); + } else if (current != null && current.version != pageComposition.version) { + service.pageComposition = pageComposition; + } + } + break; + case SEGMENT_TYPE_REGION_COMPOSITION: + 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)); + } + service.regions.put(regionComposition.id, regionComposition); + } + break; + case SEGMENT_TYPE_CLUT_DEFINITION: + if (pageId == service.subtitlePageId) { + ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength); + service.cluts.put(clutDefinition.id, clutDefinition); + } else if (pageId == service.ancillaryPageId) { + ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength); + service.ancillaryCluts.put(clutDefinition.id, clutDefinition); + } + break; + case SEGMENT_TYPE_OBJECT_DATA: + if (pageId == service.subtitlePageId) { + ObjectData objectData = parseObjectData(data); + service.objects.put(objectData.id, objectData); + } else if (pageId == service.ancillaryPageId) { + ObjectData objectData = parseObjectData(data); + service.ancillaryObjects.put(objectData.id, objectData); + } + break; + default: + // Do nothing. + break; + } + + // Skip to the next segment. + data.skipBytes(dataFieldLimit - data.getBytePosition()); + } + + /** + * Parses a display definition segment, as defined by ETSI EN 300 743 7.2.1. + */ + private static DisplayDefinition parseDisplayDefinition(ParsableBitArray data) { + data.skipBits(4); // dds_version_number (4). + boolean displayWindowFlag = data.readBit(); + data.skipBits(3); // Skip reserved. + int width = data.readBits(16); + int height = data.readBits(16); + + int horizontalPositionMinimum; + int horizontalPositionMaximum; + int verticalPositionMinimum; + int verticalPositionMaximum; + if (displayWindowFlag) { + horizontalPositionMinimum = data.readBits(16); + horizontalPositionMaximum = data.readBits(16); + verticalPositionMinimum = data.readBits(16); + verticalPositionMaximum = data.readBits(16); + } else { + horizontalPositionMinimum = 0; + horizontalPositionMaximum = width; + verticalPositionMinimum = 0; + verticalPositionMaximum = height; + } + + return new DisplayDefinition(width, height, horizontalPositionMinimum, + horizontalPositionMaximum, verticalPositionMinimum, verticalPositionMaximum); + } + + /** + * Parses a page composition segment, as defined by ETSI EN 300 743 7.2.2. + */ + private static PageComposition parsePageComposition(ParsableBitArray data, int length) { + int timeoutSecs = data.readBits(8); + int version = data.readBits(4); + int state = data.readBits(2); + data.skipBits(2); + int remainingLength = length - 2; + + SparseArray regions = new SparseArray<>(); + while (remainingLength > 0) { + int regionId = data.readBits(8); + data.skipBits(8); // Skip reserved. + int regionHorizontalAddress = data.readBits(16); + int regionVerticalAddress = data.readBits(16); + remainingLength -= 6; + regions.put(regionId, new PageRegion(regionHorizontalAddress, regionVerticalAddress)); + } + + return new PageComposition(timeoutSecs, version, state, regions); + } + + /** + * Parses a region composition segment, as defined by ETSI EN 300 743 7.2.3. + */ + private static RegionComposition parseRegionComposition(ParsableBitArray data, int length) { + int id = data.readBits(8); + data.skipBits(4); // Skip region_version_number + boolean fillFlag = data.readBit(); + data.skipBits(3); // Skip reserved. + int width = data.readBits(16); + int height = data.readBits(16); + int levelOfCompatibility = data.readBits(3); + int depth = data.readBits(3); + data.skipBits(2); // Skip reserved. + int clutId = data.readBits(8); + int pixelCode8Bit = data.readBits(8); + int pixelCode4Bit = data.readBits(4); + int pixelCode2Bit = data.readBits(2); + data.skipBits(2); // Skip reserved + int remainingLength = length - 10; + + SparseArray regionObjects = new SparseArray<>(); + while (remainingLength > 0) { + int objectId = data.readBits(16); + int objectType = data.readBits(2); + int objectProvider = data.readBits(2); + int objectHorizontalPosition = data.readBits(12); + data.skipBits(4); // Skip reserved. + int objectVerticalPosition = data.readBits(12); + remainingLength -= 6; + + int foregroundPixelCode = 0; + int backgroundPixelCode = 0; + if (objectType == 0x01 || objectType == 0x02) { // Only seems to affect to char subtitles. + foregroundPixelCode = data.readBits(8); + backgroundPixelCode = data.readBits(8); + remainingLength -= 2; + } + + regionObjects.put(objectId, new RegionObject(objectType, objectProvider, + objectHorizontalPosition, objectVerticalPosition, foregroundPixelCode, + backgroundPixelCode)); + } + + return new RegionComposition(id, fillFlag, width, height, levelOfCompatibility, depth, clutId, + pixelCode8Bit, pixelCode4Bit, pixelCode2Bit, regionObjects); + } + + /** + * Parses a CLUT definition segment, as defined by ETSI EN 300 743 7.2.4. + */ + private static ClutDefinition parseClutDefinition(ParsableBitArray data, int length) { + int clutId = data.readBits(8); + data.skipBits(8); // Skip clut_version_number (4), reserved (4) + int remainingLength = length - 2; + + int[] clutEntries2Bit = generateDefault2BitClutEntries(); + int[] clutEntries4Bit = generateDefault4BitClutEntries(); + int[] clutEntries8Bit = generateDefault8BitClutEntries(); + + while (remainingLength > 0) { + int entryId = data.readBits(8); + int entryFlags = data.readBits(8); + remainingLength -= 2; + + int[] clutEntries; + if ((entryFlags & 0x80) != 0) { + clutEntries = clutEntries2Bit; + } else if ((entryFlags & 0x40) != 0) { + clutEntries = clutEntries4Bit; + } else { + clutEntries = clutEntries8Bit; + } + + int y; + int cr; + int cb; + int t; + if ((entryFlags & 0x01) != 0) { + y = data.readBits(8); + cr = data.readBits(8); + cb = data.readBits(8); + t = data.readBits(8); + remainingLength -= 4; + } else { + y = data.readBits(6) << 2; + cr = data.readBits(4) << 4; + cb = data.readBits(4) << 4; + t = data.readBits(2) << 6; + remainingLength -= 2; + } + + if (y == 0x00) { + cr = 0x00; + cb = 0x00; + t = 0xFF; + } + + int a = (byte) (0xFF - (t & 0xFF)); + int r = (int) (y + (1.40200 * (cr - 128))); + int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128))); + int b = (int) (y + (1.77200 * (cb - 128))); + clutEntries[entryId] = getColor(a, Util.constrainValue(r, 0, 255), + Util.constrainValue(g, 0, 255), Util.constrainValue(b, 0, 255)); + } + + return new ClutDefinition(clutId, clutEntries2Bit, clutEntries4Bit, clutEntries8Bit); + } + + /** + * Parses an object data segment, as defined by ETSI EN 300 743 7.2.5. + * + * @return The parsed object data. + */ + private static ObjectData parseObjectData(ParsableBitArray data) { + int objectId = data.readBits(16); + data.skipBits(4); // Skip object_version_number + int objectCodingMethod = data.readBits(2); + boolean nonModifyingColorFlag = data.readBit(); + data.skipBits(1); // Skip reserved. + + byte[] topFieldData = null; + byte[] bottomFieldData = null; + + if (objectCodingMethod == OBJECT_CODING_STRING) { + int numberOfCodes = data.readBits(8); + // TODO: Parse and use character_codes. + data.skipBits(numberOfCodes * 16); // Skip character_codes. + } else if (objectCodingMethod == OBJECT_CODING_PIXELS) { + int topFieldDataLength = data.readBits(16); + int bottomFieldDataLength = data.readBits(16); + if (topFieldDataLength > 0) { + topFieldData = new byte[topFieldDataLength]; + data.readBytes(topFieldData, 0, topFieldDataLength); + } + if (bottomFieldDataLength > 0) { + bottomFieldData = new byte[bottomFieldDataLength]; + data.readBytes(bottomFieldData, 0, bottomFieldDataLength); + } else { + bottomFieldData = topFieldData; + } + } + + return new ObjectData(objectId, nonModifyingColorFlag, topFieldData, bottomFieldData); + } + + private static int[] generateDefault2BitClutEntries() { + int[] entries = new int[4]; + entries[0] = 0x00000000; + entries[1] = 0xFFFFFFFF; + entries[2] = 0xFF000000; + entries[3] = 0xFF7F7F7F; + return entries; + } + + private static int[] generateDefault4BitClutEntries() { + int[] entries = new int[16]; + entries[0] = 0x00000000; + for (int i = 1; i < entries.length; i++) { + if (i < 8) { + entries[i] = getColor( + 0xFF, + ((i & 0x01) != 0 ? 0xFF : 0x00), + ((i & 0x02) != 0 ? 0xFF : 0x00), + ((i & 0x04) != 0 ? 0xFF : 0x00)); + } else { + entries[i] = getColor( + 0xFF, + ((i & 0x01) != 0 ? 0x7F : 0x00), + ((i & 0x02) != 0 ? 0x7F : 0x00), + ((i & 0x04) != 0 ? 0x7F : 0x00)); + } + } + return entries; + } + + private static int[] generateDefault8BitClutEntries() { + int[] entries = new int[256]; + entries[0] = 0x00000000; + for (int i = 0; i < entries.length; i++) { + if (i < 8) { + entries[i] = getColor( + 0x3F, + ((i & 0x01) != 0 ? 0xFF : 0x00), + ((i & 0x02) != 0 ? 0xFF : 0x00), + ((i & 0x04) != 0 ? 0xFF : 0x00)); + } else { + switch (i & 0x88) { + case 0x00: + entries[i] = getColor( + 0xFF, + (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)), + (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)), + (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00))); + break; + case 0x08: + entries[i] = getColor( + 0x7F, + (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)), + (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)), + (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00))); + break; + case 0x80: + entries[i] = getColor( + 0xFF, + (127 + ((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)), + (127 + ((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)), + (127 + ((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00))); + break; + case 0x88: + entries[i] = getColor( + 0xFF, + (((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)), + (((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)), + (((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00))); + break; + } + } + } + return entries; + } + + private static int getColor(int a, int r, int g, int b) { + return (a << 24) | (r << 16) | (g << 8) | b; + } + + // 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) { + int[] clutEntries; + if (regionDepth == REGION_DEPTH_8_BIT) { + clutEntries = clutDefinition.clutEntries8Bit; + } else if (regionDepth == REGION_DEPTH_4_BIT) { + clutEntries = clutDefinition.clutEntries4Bit; + } else { + clutEntries = clutDefinition.clutEntries2Bit; + } + paintPixelDataSubBlock(objectData.topFieldData, clutEntries, regionDepth, horizontalAddress, + verticalAddress, paint, canvas); + paintPixelDataSubBlock(objectData.bottomFieldData, clutEntries, regionDepth, horizontalAddress, + 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) { + ParsableBitArray data = new ParsableBitArray(pixelData); + int column = horizontalAddress; + int line = verticalAddress; + byte[] clutMapTable2To4 = null; + byte[] clutMapTable2To8 = null; + byte[] clutMapTable4To8 = null; + + while (data.bitsLeft() != 0) { + int dataType = data.readBits(8); + switch (dataType) { + case DATA_TYPE_2BP_CODE_STRING: + byte[] clutMapTable2ToX; + if (regionDepth == REGION_DEPTH_8_BIT) { + clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8; + } else if (regionDepth == REGION_DEPTH_4_BIT) { + clutMapTable2ToX = clutMapTable2To4 == null ? defaultMap2To4 : clutMapTable2To4; + } else { + clutMapTable2ToX = null; + } + column = paint2BitPixelCodeString(data, clutEntries, clutMapTable2ToX, column, line, + paint, canvas); + data.byteAlign(); + break; + case DATA_TYPE_4BP_CODE_STRING: + byte[] clutMapTable4ToX; + if (regionDepth == REGION_DEPTH_8_BIT) { + clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8; + } else { + clutMapTable4ToX = null; + } + column = paint4BitPixelCodeString(data, clutEntries, clutMapTable4ToX, column, line, + paint, canvas); + data.byteAlign(); + break; + case DATA_TYPE_8BP_CODE_STRING: + column = paint8BitPixelCodeString(data, clutEntries, null, column, line, paint, canvas); + break; + case DATA_TYPE_24_TABLE_DATA: + clutMapTable2To4 = buildClutMapTable(4, 4, data); + break; + case DATA_TYPE_28_TABLE_DATA: + clutMapTable2To8 = buildClutMapTable(4, 8, data); + break; + case DATA_TYPE_48_TABLE_DATA: + clutMapTable2To8 = buildClutMapTable(16, 8, data); + break; + case DATA_TYPE_END_LINE: + column = horizontalAddress; + line += 2; + break; + default: + // Do nothing. + break; + } + } + } + + /** + * 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) { + boolean endOfPixelCodeString = false; + do { + int runLength = 0; + int clutIndex = 0; + int peek = data.readBits(2); + if (!data.readBit()) { + runLength = 1; + clutIndex = peek; + } else if (data.readBit()) { + runLength = 3 + data.readBits(3); + clutIndex = data.readBits(2); + } else if (!data.readBit()) { + switch (data.readBits(2)) { + case 0x00: + endOfPixelCodeString = true; + break; + case 0x01: + runLength = 2; + break; + case 0x02: + runLength = 12 + data.readBits(4); + clutIndex = data.readBits(2); + break; + case 0x03: + runLength = 29 + data.readBits(8); + clutIndex = data.readBits(2); + break; + } + } + + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); + } + + column += runLength; + } while (!endOfPixelCodeString); + + 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) { + boolean endOfPixelCodeString = false; + do { + int runLength = 0; + int clutIndex = 0; + int peek = data.readBits(4); + if (peek != 0x00) { + runLength = 1; + clutIndex = peek; + } else if (!data.readBit()) { + peek = data.readBits(3); + if (peek != 0x00) { + runLength = 2 + peek; + clutIndex = 0x00; + } else { + endOfPixelCodeString = true; + } + } else if (!data.readBit()) { + runLength = 4 + data.readBits(2); + clutIndex = data.readBits(4); + } else { + switch (data.readBits(2)) { + case 0x00: + runLength = 1; + break; + case 0x01: + runLength = 2; + break; + case 0x02: + runLength = 9 + data.readBits(4); + clutIndex = data.readBits(4); + break; + case 0x03: + runLength = 25 + data.readBits(8); + clutIndex = data.readBits(4); + break; + } + } + + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); + } + + column += runLength; + } while (!endOfPixelCodeString); + + 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) { + boolean endOfPixelCodeString = false; + do { + int runLength = 0; + int clutIndex = 0; + int peek = data.readBits(8); + if (peek != 0x00) { + runLength = 1; + clutIndex = peek; + } else { + if (!data.readBit()) { + peek = data.readBits(7); + if (peek != 0x00) { + runLength = peek; + clutIndex = 0x00; + } else { + endOfPixelCodeString = true; + } + } else { + runLength = data.readBits(7); + clutIndex = data.readBits(8); + } + } + + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); + } + column += runLength; + } while (!endOfPixelCodeString); + + return column; + } + + private static byte[] buildClutMapTable(int length, int bitsPerEntry, ParsableBitArray data) { + byte[] clutMapTable = new byte[length]; + for (int i = 0; i < length; i++) { + clutMapTable[i] = (byte) data.readBits(bitsPerEntry); + } + return clutMapTable; + } + + // Private inner classes. + + /** + * The subtitle service definition. + */ + private static final class SubtitleService { + + 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 DisplayDefinition displayDefinition; + public PageComposition pageComposition; + + public SubtitleService(int subtitlePageId, int ancillaryPageId) { + this.subtitlePageId = subtitlePageId; + this.ancillaryPageId = ancillaryPageId; + } + + public void reset() { + regions.clear(); + cluts.clear(); + objects.clear(); + ancillaryCluts.clear(); + ancillaryObjects.clear(); + displayDefinition = null; + pageComposition = null; + } + + } + + /** + * Contains the geometry and active area of the subtitle service. + *

    + * See ETSI EN 300 743 7.2.1 + */ + private static final class DisplayDefinition { + + public final int width; + public final int height; + + public final int horizontalPositionMinimum; + public final int horizontalPositionMaximum; + public final int verticalPositionMinimum; + public final int verticalPositionMaximum; + + public DisplayDefinition(int width, int height, int horizontalPositionMinimum, + int horizontalPositionMaximum, int verticalPositionMinimum, int verticalPositionMaximum) { + this.width = width; + this.height = height; + this.horizontalPositionMinimum = horizontalPositionMinimum; + this.horizontalPositionMaximum = horizontalPositionMaximum; + this.verticalPositionMinimum = verticalPositionMinimum; + this.verticalPositionMaximum = verticalPositionMaximum; + } + + } + + /** + * The page is the definition and arrangement of regions in the screen. + *

    + * See ETSI EN 300 743 7.2.2 + */ + private static final class PageComposition { + + public final int timeOutSecs; // TODO: Use this or remove it. + public final int version; + public final int state; + public final SparseArray regions; + + public PageComposition(int timeoutSecs, int version, int state, + SparseArray regions) { + this.timeOutSecs = timeoutSecs; + this.version = version; + this.state = state; + this.regions = regions; + } + + } + + /** + * A region within a {@link PageComposition}. + *

    + * See ETSI EN 300 743 7.2.2 + */ + private static final class PageRegion { + + public final int horizontalAddress; + public final int verticalAddress; + + public PageRegion(int horizontalAddress, int verticalAddress) { + this.horizontalAddress = horizontalAddress; + this.verticalAddress = verticalAddress; + } + + } + + /** + * An area of the page composed of a list of objects and a CLUT. + *

    + * See ETSI EN 300 743 7.2.3 + */ + private static final class RegionComposition { + + public final int id; + public final boolean fillFlag; + public final int width; + public final int height; + public final int levelOfCompatibility; // TODO: Use this or remove it. + public final int depth; + public final int clutId; + public final int pixelCode8Bit; + public final int pixelCode4Bit; + public final int pixelCode2Bit; + public final SparseArray regionObjects; + + public RegionComposition(int id, boolean fillFlag, int width, int height, + int levelOfCompatibility, int depth, int clutId, int pixelCode8Bit, int pixelCode4Bit, + int pixelCode2Bit, SparseArray regionObjects) { + this.id = id; + this.fillFlag = fillFlag; + this.width = width; + this.height = height; + this.levelOfCompatibility = levelOfCompatibility; + this.depth = depth; + this.clutId = clutId; + this.pixelCode8Bit = pixelCode8Bit; + this.pixelCode4Bit = pixelCode4Bit; + this.pixelCode2Bit = pixelCode2Bit; + this.regionObjects = regionObjects; + } + + 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)); + } + } + + } + + /** + * An object within a {@link RegionComposition}. + *

    + * See ETSI EN 300 743 7.2.3 + */ + private static final class RegionObject { + + public final int type; // TODO: Use this or remove it. + public final int provider; // TODO: Use this or remove it. + public final int horizontalPosition; + public final int verticalPosition; + public final int foregroundPixelCode; // TODO: Use this or remove it. + public final int backgroundPixelCode; // TODO: Use this or remove it. + + public RegionObject(int type, int provider, int horizontalPosition, + int verticalPosition, int foregroundPixelCode, int backgroundPixelCode) { + this.type = type; + this.provider = provider; + this.horizontalPosition = horizontalPosition; + this.verticalPosition = verticalPosition; + this.foregroundPixelCode = foregroundPixelCode; + this.backgroundPixelCode = backgroundPixelCode; + } + + } + + /** + * CLUT family definition containing the color tables for the three bit depths defined + *

    + * See ETSI EN 300 743 7.2.4 + */ + private static final class ClutDefinition { + + public final int id; + public final int[] clutEntries2Bit; + public final int[] clutEntries4Bit; + public final int[] clutEntries8Bit; + + public ClutDefinition(int id, int[] clutEntries2Bit, int[] clutEntries4Bit, + int[] clutEntries8bit) { + this.id = id; + this.clutEntries2Bit = clutEntries2Bit; + this.clutEntries4Bit = clutEntries4Bit; + this.clutEntries8Bit = clutEntries8bit; + } + + } + + /** + * The textual or graphical representation of an object. + *

    + * See ETSI EN 300 743 7.2.5 + */ + private static final class ObjectData { + + public final int id; + public final boolean nonModifyingColorFlag; + public final byte[] topFieldData; + public final byte[] bottomFieldData; + + public ObjectData(int id, boolean nonModifyingColorFlag, byte[] topFieldData, + byte[] bottomFieldData) { + this.id = id; + this.nonModifyingColorFlag = nonModifyingColorFlag; + this.topFieldData = topFieldData; + this.bottomFieldData = bottomFieldData; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/dvb/DvbSubtitle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/dvb/DvbSubtitle.java new file mode 100755 index 000000000..e7f356c53 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/dvb/DvbSubtitle.java @@ -0,0 +1,54 @@ +/* + * 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 org.telegram.messenger.exoplayer2.text.dvb; + +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.text.Cue; +import org.telegram.messenger.exoplayer2.text.Subtitle; +import java.util.List; + +/** + * A representation of a DVB subtitle. + */ +/* package */ final class DvbSubtitle implements Subtitle { + + private final List cues; + + public DvbSubtitle(List cues) { + this.cues = cues; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + return C.INDEX_UNSET; + } + + @Override + public int getEventTimeCount() { + return 1; + } + + @Override + public long getEventTime(int index) { + return 0; + } + + @Override + public List getCues(long timeUs) { + return cues; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/subrip/SubripDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/subrip/SubripDecoder.java index b5ca9dfe8..1bcc892c8 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/subrip/SubripDecoder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/subrip/SubripDecoder.java @@ -34,9 +34,9 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { private static final String TAG = "SubripDecoder"; - private static final Pattern SUBRIP_TIMING_LINE = Pattern.compile("(\\S*)\\s*-->\\s*(\\S*)"); - private static final Pattern SUBRIP_TIMESTAMP = - Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+),(\\d+)"); + private 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*"); private final StringBuilder textBuilder; @@ -46,11 +46,10 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } @Override - protected SubripSubtitle decode(byte[] bytes, int length) { + protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); - boolean haveEndTimecode; String currentLine; while ((currentLine = subripData.readLine()) != null) { @@ -68,15 +67,14 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } // Read and parse the timing line. - haveEndTimecode = false; + boolean haveEndTimecode = false; currentLine = subripData.readLine(); Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); - if (matcher.find()) { - cueTimesUs.add(parseTimecode(matcher.group(1))); - String endTimecode = matcher.group(2); - if (!TextUtils.isEmpty(endTimecode)) { + if (matcher.matches()) { + cueTimesUs.add(parseTimecode(matcher, 1)); + if (!TextUtils.isEmpty(matcher.group(6))) { haveEndTimecode = true; - cueTimesUs.add(parseTimecode(matcher.group(2))); + cueTimesUs.add(parseTimecode(matcher, 6)); } } else { Log.w(TAG, "Skipping invalid timing: " + currentLine); @@ -105,15 +103,11 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { return new SubripSubtitle(cuesArray, cueTimesUsArray); } - private static long parseTimecode(String s) throws NumberFormatException { - Matcher matcher = SUBRIP_TIMESTAMP.matcher(s); - if (!matcher.matches()) { - throw new NumberFormatException("has invalid format"); - } - long timestampMs = Long.parseLong(matcher.group(1)) * 60 * 60 * 1000; - timestampMs += Long.parseLong(matcher.group(2)) * 60 * 1000; - timestampMs += Long.parseLong(matcher.group(3)) * 1000; - timestampMs += Long.parseLong(matcher.group(4)); + private static long parseTimecode(Matcher matcher, int groupOffset) { + long timestampMs = Long.parseLong(matcher.group(groupOffset + 1)) * 60 * 60 * 1000; + timestampMs += Long.parseLong(matcher.group(groupOffset + 2)) * 60 * 1000; + timestampMs += Long.parseLong(matcher.group(groupOffset + 3)) * 1000; + timestampMs += Long.parseLong(matcher.group(groupOffset + 4)); return timestampMs * 1000; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/ttml/TtmlDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/ttml/TtmlDecoder.java index 4eff4831c..7b71093aa 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/ttml/TtmlDecoder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/ttml/TtmlDecoder.java @@ -94,7 +94,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } @Override - protected TtmlSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException { + protected TtmlSubtitle decode(byte[] bytes, int length, boolean reset) + throws SubtitleDecoderException { try { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); Map globalStyles = new HashMap<>(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/ttml/TtmlStyle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/ttml/TtmlStyle.java index 04ad04a25..3c4e5ecf0 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/ttml/TtmlStyle.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/ttml/TtmlStyle.java @@ -56,16 +56,11 @@ import java.lang.annotation.RetentionPolicy; private boolean hasFontColor; private int backgroundColor; private boolean hasBackgroundColor; - @OptionalBoolean - private int linethrough; - @OptionalBoolean - private int underline; - @OptionalBoolean - private int bold; - @OptionalBoolean - private int italic; - @FontSizeUnit - private int fontSizeUnit; + @OptionalBoolean private int linethrough; + @OptionalBoolean private int underline; + @OptionalBoolean private int bold; + @OptionalBoolean private int italic; + @FontSizeUnit private int fontSizeUnit; private float fontSize; private String id; private TtmlStyle inheritableStyle; @@ -85,8 +80,7 @@ import java.lang.annotation.RetentionPolicy; * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD} * or {@link #STYLE_BOLD_ITALIC}. */ - @StyleFlags - public int getStyle() { + @StyleFlags public int getStyle() { if (bold == UNSPECIFIED && italic == UNSPECIFIED) { return UNSPECIFIED; } @@ -255,8 +249,7 @@ import java.lang.annotation.RetentionPolicy; return this; } - @FontSizeUnit - public int getFontSizeUnit() { + @FontSizeUnit public int getFontSizeUnit() { return fontSizeUnit; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/tx3g/Tx3gDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/tx3g/Tx3gDecoder.java index 587a8206d..eaa3fa1a1 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/tx3g/Tx3gDecoder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -15,34 +15,220 @@ */ package org.telegram.messenger.exoplayer2.text.tx3g; +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; +import android.text.style.UnderlineSpan; +import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.text.Cue; import org.telegram.messenger.exoplayer2.text.SimpleSubtitleDecoder; import org.telegram.messenger.exoplayer2.text.Subtitle; +import org.telegram.messenger.exoplayer2.text.SubtitleDecoderException; import org.telegram.messenger.exoplayer2.util.ParsableByteArray; +import org.telegram.messenger.exoplayer2.util.Util; +import java.nio.charset.Charset; +import java.util.List; /** * A {@link SimpleSubtitleDecoder} for tx3g. *

    - * Currently only supports parsing of a single text track. + * Currently supports parsing of a single text track with embedded styles. */ public final class Tx3gDecoder extends SimpleSubtitleDecoder { - private final ParsableByteArray parsableByteArray; + private static final char BOM_UTF16_BE = '\uFEFF'; + private static final char BOM_UTF16_LE = '\uFFFE'; - public Tx3gDecoder() { + private static final int TYPE_STYL = Util.getIntegerCodeForString("styl"); + private static final int TYPE_TBOX = Util.getIntegerCodeForString("tbox"); + private static final String TX3G_SERIF = "Serif"; + + private static final int SIZE_ATOM_HEADER = 8; + private static final int SIZE_SHORT = 2; + private static final int SIZE_BOM_UTF16 = 2; + private static final int SIZE_STYLE_RECORD = 12; + + private static final int FONT_FACE_BOLD = 0x0001; + private static final int FONT_FACE_ITALIC = 0x0002; + private static final int FONT_FACE_UNDERLINE = 0x0004; + + private static final int SPAN_PRIORITY_LOW = (0xFF << Spanned.SPAN_PRIORITY_SHIFT); + private static final int SPAN_PRIORITY_HIGH = (0x00 << Spanned.SPAN_PRIORITY_SHIFT); + + private static final int DEFAULT_FONT_FACE = 0; + private static final int DEFAULT_COLOR = Color.WHITE; + private static final String DEFAULT_FONT_FAMILY = C.SANS_SERIF_NAME; + private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f; + + private final ParsableByteArray parsableByteArray; + private boolean customVerticalPlacement; + private int defaultFontFace; + private int defaultColorRgba; + private String defaultFontFamily; + private float defaultVerticalPlacement; + private int calculatedVideoTrackHeight; + + /** + * Sets up a new {@link Tx3gDecoder} with default values. + * + * @param initializationData Sample description atom ('stsd') data with default subtitle styles. + */ + public Tx3gDecoder(List initializationData) { super("Tx3gDecoder"); parsableByteArray = new ParsableByteArray(); + decodeInitializationData(initializationData); + } + + private void decodeInitializationData(List initializationData) { + if (initializationData != null && initializationData.size() == 1 + && (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) { + byte[] initializationBytes = initializationData.get(0); + defaultFontFace = initializationBytes[24]; + defaultColorRgba = ((initializationBytes[26] & 0xFF) << 24) + | ((initializationBytes[27] & 0xFF) << 16) + | ((initializationBytes[28] & 0xFF) << 8) + | (initializationBytes[29] & 0xFF); + String fontFamily = new String(initializationBytes, 43, initializationBytes.length - 43); + defaultFontFamily = TX3G_SERIF.equals(fontFamily) ? C.SERIF_NAME : C.SANS_SERIF_NAME; + //font size (initializationBytes[25]) is 5% of video height + calculatedVideoTrackHeight = 20 * initializationBytes[25]; + customVerticalPlacement = (initializationBytes[0] & 0x20) != 0; + if (customVerticalPlacement) { + int requestedVerticalPlacement = ((initializationBytes[10] & 0xFF) << 8) + | (initializationBytes[11] & 0xFF); + defaultVerticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight; + defaultVerticalPlacement = Util.constrainValue(defaultVerticalPlacement, 0.0f, 0.95f); + } else { + defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT; + } + } else { + defaultFontFace = DEFAULT_FONT_FACE; + defaultColorRgba = DEFAULT_COLOR; + defaultFontFamily = DEFAULT_FONT_FAMILY; + customVerticalPlacement = false; + defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT; + } } @Override - protected Subtitle decode(byte[] bytes, int length) { + protected Subtitle decode(byte[] bytes, int length, boolean reset) + throws SubtitleDecoderException { parsableByteArray.reset(bytes, length); - int textLength = parsableByteArray.readUnsignedShort(); - if (textLength == 0) { + String cueTextString = readSubtitleText(parsableByteArray); + if (cueTextString.isEmpty()) { return Tx3gSubtitle.EMPTY; } - String cueText = parsableByteArray.readString(textLength); - return new Tx3gSubtitle(new Cue(cueText)); + // Attach default styles. + SpannableStringBuilder cueText = new SpannableStringBuilder(cueTextString); + attachFontFace(cueText, defaultFontFace, DEFAULT_FONT_FACE, 0, cueText.length(), + SPAN_PRIORITY_LOW); + attachColor(cueText, defaultColorRgba, DEFAULT_COLOR, 0, cueText.length(), + SPAN_PRIORITY_LOW); + attachFontFamily(cueText, defaultFontFamily, DEFAULT_FONT_FAMILY, 0, cueText.length(), + SPAN_PRIORITY_LOW); + float verticalPlacement = defaultVerticalPlacement; + // Find and attach additional styles. + while (parsableByteArray.bytesLeft() >= SIZE_ATOM_HEADER) { + int position = parsableByteArray.getPosition(); + int atomSize = parsableByteArray.readInt(); + int atomType = parsableByteArray.readInt(); + if (atomType == TYPE_STYL) { + assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT); + int styleRecordCount = parsableByteArray.readUnsignedShort(); + for (int i = 0; i < styleRecordCount; i++) { + applyStyleRecord(parsableByteArray, cueText); + } + } else if (atomType == TYPE_TBOX && customVerticalPlacement) { + assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT); + int requestedVerticalPlacement = parsableByteArray.readUnsignedShort(); + verticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight; + verticalPlacement = Util.constrainValue(verticalPlacement, 0.0f, 0.95f); + } + parsableByteArray.setPosition(position + atomSize); + } + return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET)); } + private static String readSubtitleText(ParsableByteArray parsableByteArray) + throws SubtitleDecoderException { + assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT); + int textLength = parsableByteArray.readUnsignedShort(); + if (textLength == 0) { + return ""; + } + if (parsableByteArray.bytesLeft() >= SIZE_BOM_UTF16) { + char firstChar = parsableByteArray.peekChar(); + if (firstChar == BOM_UTF16_BE || firstChar == BOM_UTF16_LE) { + return parsableByteArray.readString(textLength, Charset.forName(C.UTF16_NAME)); + } + } + return parsableByteArray.readString(textLength, Charset.forName(C.UTF8_NAME)); + } + + private void applyStyleRecord(ParsableByteArray parsableByteArray, + SpannableStringBuilder cueText) throws SubtitleDecoderException { + assertTrue(parsableByteArray.bytesLeft() >= SIZE_STYLE_RECORD); + int start = parsableByteArray.readUnsignedShort(); + int end = parsableByteArray.readUnsignedShort(); + parsableByteArray.skipBytes(2); // font identifier + int fontFace = parsableByteArray.readUnsignedByte(); + parsableByteArray.skipBytes(1); // font size + int colorRgba = parsableByteArray.readInt(); + attachFontFace(cueText, fontFace, defaultFontFace, start, end, SPAN_PRIORITY_HIGH); + attachColor(cueText, colorRgba, defaultColorRgba, start, end, SPAN_PRIORITY_HIGH); + } + + private static void attachFontFace(SpannableStringBuilder cueText, int fontFace, + int defaultFontFace, int start, int end, int spanPriority) { + if (fontFace != defaultFontFace) { + final int flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority; + boolean isBold = (fontFace & FONT_FACE_BOLD) != 0; + boolean isItalic = (fontFace & FONT_FACE_ITALIC) != 0; + if (isBold) { + if (isItalic) { + cueText.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flags); + } else { + cueText.setSpan(new StyleSpan(Typeface.BOLD), start, end, flags); + } + } else if (isItalic) { + cueText.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flags); + } + boolean isUnderlined = (fontFace & FONT_FACE_UNDERLINE) != 0; + if (isUnderlined) { + cueText.setSpan(new UnderlineSpan(), start, end, flags); + } + if (!isUnderlined && !isBold && !isItalic) { + cueText.setSpan(new StyleSpan(Typeface.NORMAL), start, end, flags); + } + } + } + + private static void attachColor(SpannableStringBuilder cueText, int colorRgba, + int defaultColorRgba, int start, int end, int spanPriority) { + if (colorRgba != defaultColorRgba) { + int colorArgb = ((colorRgba & 0xFF) << 24) | (colorRgba >>> 8); + cueText.setSpan(new ForegroundColorSpan(colorArgb), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority); + } + } + + @SuppressWarnings("ReferenceEquality") + private static void attachFontFamily(SpannableStringBuilder cueText, String fontFamily, + String defaultFontFamily, int start, int end, int spanPriority) { + if (fontFamily != defaultFontFamily) { + cueText.setSpan(new TypefaceSpan(fontFamily), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority); + } + } + + private static void assertTrue(boolean checkValue) throws SubtitleDecoderException { + if (!checkValue) { + throw new SubtitleDecoderException("Unexpected subtitle format."); + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/Mp4WebvttDecoder.java index b25edbe6c..dc2ccc8cc 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/Mp4WebvttDecoder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/Mp4WebvttDecoder.java @@ -45,7 +45,8 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected Mp4WebvttSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException { + protected Mp4WebvttSubtitle 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. sampleData.reset(bytes, length); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttCssStyle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttCssStyle.java index 112c0d472..eacec5b5e 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -69,16 +69,11 @@ import java.util.List; private boolean hasFontColor; private int backgroundColor; private boolean hasBackgroundColor; - @OptionalBoolean - private int linethrough; - @OptionalBoolean - private int underline; - @OptionalBoolean - private int bold; - @OptionalBoolean - private int italic; - @FontSizeUnit - private int fontSizeUnit; + @OptionalBoolean private int linethrough; + @OptionalBoolean private int underline; + @OptionalBoolean private int bold; + @OptionalBoolean private int italic; + @FontSizeUnit private int fontSizeUnit; private float fontSize; private Layout.Alignment textAlign; @@ -162,8 +157,7 @@ import java.util.List; * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD} * or {@link #STYLE_BOLD_ITALIC}. */ - @StyleFlags - public int getStyle() { + @StyleFlags public int getStyle() { if (bold == UNSPECIFIED && italic == UNSPECIFIED) { return UNSPECIFIED; } @@ -260,8 +254,7 @@ import java.util.List; return this; } - @FontSizeUnit - public int getFontSizeUnit() { + @FontSizeUnit public int getFontSizeUnit() { return fontSizeUnit; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttCueParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttCueParser.java index ba559faa4..7c9903237 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttCueParser.java @@ -16,6 +16,7 @@ package org.telegram.messenger.exoplayer2.text.webvtt; import android.graphics.Typeface; +import android.support.annotation.NonNull; import android.text.Layout.Alignment; import android.text.Spannable; import android.text.SpannableStringBuilder; @@ -256,7 +257,13 @@ import java.util.regex.Pattern; if (s.endsWith("%")) { builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION); } else { - builder.setLine(Integer.parseInt(s)).setLineType(Cue.LINE_TYPE_NUMBER); + int lineNumber = Integer.parseInt(s); + if (lineNumber < 0) { + // WebVTT defines line -1 as last visible row when lineAnchor is ANCHOR_TYPE_START, where-as + // Cue defines it to be the first row that's not visible. + lineNumber--; + } + builder.setLine(lineNumber).setLineType(Cue.LINE_TYPE_NUMBER); } } @@ -470,7 +477,7 @@ import java.util.regex.Pattern; } @Override - public int compareTo(StyleMatch another) { + public int compareTo(@NonNull StyleMatch another) { return this.score - another.score; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttDecoder.java index 50cc43ce9..e5886f119 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttDecoder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/text/webvtt/WebvttDecoder.java @@ -54,7 +54,8 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected WebvttSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException { + protected WebvttSubtitle decode(byte[] bytes, int length, boolean reset) + throws SubtitleDecoderException { parsableWebvttData.reset(bytes, length); // Initialization for consistent starting state. webvttCueBuilder.reset(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/AdaptiveTrackSelection.java similarity index 88% rename from TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/AdaptiveTrackSelection.java index 75e7a09f9..fd2a80965 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -24,13 +24,13 @@ import org.telegram.messenger.exoplayer2.upstream.BandwidthMeter; import java.util.List; /** - * A bandwidth based adaptive {@link TrackSelection} for video, whose selected track is updated to - * be the one of highest quality given the current network conditions and the state of the buffer. + * A bandwidth based adaptive {@link TrackSelection}, whose selected track is updated to be the one + * of highest quality given the current network conditions and the state of the buffer. */ -public class AdaptiveVideoTrackSelection extends BaseTrackSelection { +public class AdaptiveTrackSelection extends BaseTrackSelection { /** - * Factory for {@link AdaptiveVideoTrackSelection} instances. + * Factory for {@link AdaptiveTrackSelection} instances. */ public static final class Factory implements TrackSelection.Factory { @@ -79,8 +79,8 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { } @Override - public AdaptiveVideoTrackSelection createTrackSelection(TrackGroup group, int... tracks) { - return new AdaptiveVideoTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, + public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) { + return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, bandwidthFraction); } @@ -104,12 +104,12 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { private int reason; /** - * @param group The {@link TrackGroup}. Must not be null. + * @param group The {@link TrackGroup}. * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be - * null or empty. May be in any order. + * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. */ - public AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks, + public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter) { this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, @@ -118,9 +118,9 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { } /** - * @param group The {@link TrackGroup}. Must not be null. + * @param group The {@link TrackGroup}. * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be - * null or empty. May be in any order. + * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a * bandwidth estimate is unavailable. @@ -136,7 +136,7 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { * consider available for use. Setting to a value less than 1 is recommended to account * for inaccuracies in the bandwidth estimator. */ - public AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, + public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, int maxInitialBitrate, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, float bandwidthFraction) { @@ -208,15 +208,18 @@ public class AdaptiveVideoTrackSelection extends BaseTrackSelection { } int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime()); Format idealFormat = getFormat(idealSelectedIndex); - // Discard from the first SD chunk beyond minDurationToRetainAfterDiscardUs whose resolution and - // bitrate are both lower than the ideal track. + // If the chunks contain video, discard from the first SD chunk beyond + // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal + // track. for (int i = 0; i < queueSize; i++) { MediaChunk chunk = queue.get(i); + Format format = chunk.trackFormat; long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs; if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs - && chunk.trackFormat.bitrate < idealFormat.bitrate - && chunk.trackFormat.height < idealFormat.height - && chunk.trackFormat.height < 720 && chunk.trackFormat.width < 1280) { + && format.bitrate < idealFormat.bitrate + && format.height != Format.NO_VALUE && format.height < 720 + && format.width != Format.NO_VALUE && format.width < 1280 + && format.height < idealFormat.height) { return i; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/BaseTrackSelection.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/BaseTrackSelection.java index f1352bf71..9cc8454b2 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/BaseTrackSelection.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/BaseTrackSelection.java @@ -148,7 +148,7 @@ public abstract class BaseTrackSelection implements TrackSelection { } /** - * Returns whether the track at the specified index in the selection is blaclisted. + * Returns whether the track at the specified index in the selection is blacklisted. * * @param index The index of the track in the selection. * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/DefaultTrackSelector.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/DefaultTrackSelector.java index ade965617..77e771f96 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/DefaultTrackSelector.java @@ -54,7 +54,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final boolean allowNonSeamlessAdaptiveness; public final int maxVideoWidth; public final int maxVideoHeight; + public final int maxVideoBitrate; public final boolean exceedVideoConstraintsIfNecessary; + public final boolean exceedRendererCapabilitiesIfNecessary; public final int viewportWidth; public final int viewportHeight; public final boolean orientationMayChange; @@ -67,13 +69,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { *

  • Adaptation between different mime types is not allowed.
  • *
  • Non seamless adaptation is allowed.
  • *
  • No max limit for video width/height.
  • - *
  • Video constraints are ignored if no supported selection can be made otherwise.
  • + *
  • No max video bitrate.
  • + *
  • Video constraints are exceeded if no supported selection can be made otherwise.
  • + *
  • Renderer capabilities are exceeded if no supported selection can be made.
  • *
  • No viewport width/height constraints are set.
  • * */ public Parameters() { - this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true, Integer.MAX_VALUE, - Integer.MAX_VALUE, true); + this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, true, + true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); } /** @@ -86,15 +90,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed. * @param maxVideoWidth Maximum allowed video width. * @param maxVideoHeight Maximum allowed video height. - * @param exceedVideoConstraintsIfNecessary True to ignore video constraints when no selections - * can be made otherwise. False to force constraints anyway. + * @param maxVideoBitrate Maximum allowed video bitrate. + * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no + * selection can be made otherwise. + * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no + * selection can be made otherwise. * @param viewportWidth Viewport width in pixels. * @param viewportHeight Viewport height in pixels. * @param orientationMayChange Whether orientation may change during playback. */ public Parameters(String preferredAudioLanguage, String preferredTextLanguage, boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, - int maxVideoWidth, int maxVideoHeight, boolean exceedVideoConstraintsIfNecessary, + int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, + boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, boolean orientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; @@ -102,7 +110,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; this.maxVideoWidth = maxVideoWidth; this.maxVideoHeight = maxVideoHeight; + this.maxVideoBitrate = maxVideoBitrate; this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary; + this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; this.viewportWidth = viewportWidth; this.viewportHeight = viewportHeight; this.orientationMayChange = orientationMayChange; @@ -124,7 +134,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, orientationMayChange); + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -140,9 +151,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -156,9 +167,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -172,9 +183,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -189,9 +200,25 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); + } + + /** + * Returns a {@link Parameters} instance with the provided max video bitrate. + * + * @param maxVideoBitrate The max video bitrate. + * @return A {@link Parameters} instance with the provided max video bitrate. + */ + public Parameters withMaxVideoBitrate(int maxVideoBitrate) { + if (maxVideoBitrate == this.maxVideoBitrate) { + return this; + } + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -216,8 +243,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Returns a {@link Parameters} instance with the provided * {@code exceedVideoConstraintsIfNecessary} value. * - * @param exceedVideoConstraintsIfNecessary True to ignore video constraints when no selections - * can be made otherwise. False to force constraints anyway. + * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no + * selection can be made otherwise. * @return A {@link Parameters} instance with the provided * {@code exceedVideoConstraintsIfNecessary} value. */ @@ -227,9 +254,29 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); + } + + /** + * Returns a {@link Parameters} instance with the provided + * {@code exceedRendererCapabilitiesIfNecessary} value. + * + * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no + * selection can be made otherwise. + * @return A {@link Parameters} instance with the provided + * {@code exceedRendererCapabilitiesIfNecessary} value. + */ + public Parameters withExceedRendererCapabilitiesIfNecessary( + boolean exceedRendererCapabilitiesIfNecessary) { + if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) { + return this; + } + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -247,9 +294,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, - maxVideoHeight, exceedVideoConstraintsIfNecessary, viewportWidth, viewportHeight, - orientationMayChange); + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, orientationMayChange); } /** @@ -289,8 +336,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { && allowNonSeamlessAdaptiveness == other.allowNonSeamlessAdaptiveness && maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary + && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary && orientationMayChange == other.orientationMayChange && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight + && maxVideoBitrate == other.maxVideoBitrate && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage); } @@ -303,7 +352,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + (allowNonSeamlessAdaptiveness ? 1 : 0); result = 31 * result + maxVideoWidth; result = 31 * result + maxVideoHeight; + result = 31 * result + maxVideoBitrate; result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0); + result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); result = 31 * result + (orientationMayChange ? 1 : 0); result = 31 * result + viewportWidth; result = 31 * result + viewportHeight; @@ -319,26 +370,27 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ private static final float FRACTION_TO_CONSIDER_FULLSCREEN = 0.98f; private static final int[] NO_TRACKS = new int[0]; + private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000; - private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory; - private final AtomicReference params; + private final TrackSelection.Factory adaptiveTrackSelectionFactory; + private final AtomicReference paramsReference; /** - * Constructs an instance that does not support adaptive video. + * Constructs an instance that does not support adaptive tracks. */ public DefaultTrackSelector() { this(null); } /** - * Constructs an instance that uses a factory to create adaptive video track selections. + * Constructs an instance that uses a factory to create adaptive track selections. * - * @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s, - * or null if the selector should not support adaptive video. + * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null if + * the selector should not support adaptive tracks. */ - public DefaultTrackSelector(TrackSelection.Factory adaptiveVideoTrackSelectionFactory) { - this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory; - params = new AtomicReference<>(new Parameters()); + public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { + this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; + paramsReference = new AtomicReference<>(new Parameters()); } /** @@ -347,8 +399,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param params The parameters for track selection. */ public void setParameters(Parameters params) { - if (!this.params.get().equals(params)) { - this.params.set(Assertions.checkNotNull(params)); + Assertions.checkNotNull(params); + if (!paramsReference.getAndSet(params).equals(params)) { invalidate(); } } @@ -359,7 +411,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The current selection parameters. */ public Parameters getParameters() { - return params.get(); + return paramsReference.get(); } // MappingTrackSelector implementation. @@ -369,30 +421,43 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) throws ExoPlaybackException { // Make a track selection for each renderer. - TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCapabilities.length]; - Parameters params = this.params.get(); - for (int i = 0; i < rendererCapabilities.length; i++) { + int rendererCount = rendererCapabilities.length; + TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; + Parameters params = paramsReference.get(); + boolean videoTrackAndRendererPresent = false; + + for (int i = 0; i < rendererCount; i++) { + if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) { + rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], + rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, + params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, + params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, + params.orientationMayChange, adaptiveTrackSelectionFactory, + params.exceedVideoConstraintsIfNecessary, params.exceedRendererCapabilitiesIfNecessary); + videoTrackAndRendererPresent |= rendererTrackGroupArrays[i].length > 0; + } + } + + for (int i = 0; i < rendererCount; i++) { switch (rendererCapabilities[i].getTrackType()) { case C.TRACK_TYPE_VIDEO: - rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], - rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, - params.maxVideoHeight, params.allowNonSeamlessAdaptiveness, - params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, - params.orientationMayChange, adaptiveVideoTrackSelectionFactory, - params.exceedVideoConstraintsIfNecessary); + // Already done. Do nothing. break; case C.TRACK_TYPE_AUDIO: rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredAudioLanguage); + rendererFormatSupports[i], params.preferredAudioLanguage, + params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness, + videoTrackAndRendererPresent ? null : adaptiveTrackSelectionFactory); break; case C.TRACK_TYPE_TEXT: rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i], rendererFormatSupports[i], params.preferredTextLanguage, - params.preferredAudioLanguage); + params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary); break; default: rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(), - rendererTrackGroupArrays[i], rendererFormatSupports[i]); + rendererTrackGroupArrays[i], rendererFormatSupports[i], + params.exceedRendererCapabilitiesIfNecessary); break; } } @@ -403,29 +468,30 @@ public class DefaultTrackSelector extends MappingTrackSelector { protected TrackSelection selectVideoTrack(RendererCapabilities rendererCapabilities, TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth, - int viewportHeight, boolean orientationMayChange, - TrackSelection.Factory adaptiveVideoTrackSelectionFactory, - boolean exceedConstraintsIfNecessary) throws ExoPlaybackException { + int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, + int viewportWidth, int viewportHeight, boolean orientationMayChange, + TrackSelection.Factory adaptiveTrackSelectionFactory, boolean exceedConstraintsIfNecessary, + boolean exceedRendererCapabilitiesIfNecessary) throws ExoPlaybackException { TrackSelection selection = null; - if (adaptiveVideoTrackSelectionFactory != null) { + if (adaptiveTrackSelectionFactory != null) { selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport, - maxVideoWidth, maxVideoHeight, allowNonSeamlessAdaptiveness, + maxVideoWidth, maxVideoHeight, maxVideoBitrate, allowNonSeamlessAdaptiveness, allowMixedMimeAdaptiveness, viewportWidth, viewportHeight, - orientationMayChange, adaptiveVideoTrackSelectionFactory); + orientationMayChange, adaptiveTrackSelectionFactory); } if (selection == null) { selection = selectFixedVideoTrack(groups, formatSupport, maxVideoWidth, maxVideoHeight, - viewportWidth, viewportHeight, orientationMayChange, exceedConstraintsIfNecessary); + maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange, + exceedConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary); } return selection; } private static TrackSelection selectAdaptiveVideoTrack(RendererCapabilities rendererCapabilities, TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth, - int viewportHeight, boolean orientationMayChange, - TrackSelection.Factory adaptiveVideoTrackSelectionFactory) throws ExoPlaybackException { + int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, + int viewportWidth, int viewportHeight, boolean orientationMayChange, + TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) : RendererCapabilities.ADAPTIVE_SEAMLESS; @@ -433,19 +499,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { && (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) != 0; for (int i = 0; i < groups.length; i++) { TrackGroup group = groups.get(i); - int[] adaptiveTracks = getAdaptiveTracksForGroup(group, formatSupport[i], + int[] adaptiveTracks = getAdaptiveVideoTracksForGroup(group, formatSupport[i], allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, - viewportWidth, viewportHeight, orientationMayChange); + maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange); if (adaptiveTracks.length > 0) { - return adaptiveVideoTrackSelectionFactory.createTrackSelection(group, adaptiveTracks); + return adaptiveTrackSelectionFactory.createTrackSelection(group, adaptiveTracks); } } return null; } - private static int[] getAdaptiveTracksForGroup(TrackGroup group, int[] formatSupport, + private static int[] getAdaptiveVideoTracksForGroup(TrackGroup group, int[] formatSupport, boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, - int maxVideoHeight, int viewportWidth, int viewportHeight, boolean orientationMayChange) { + int maxVideoHeight, int maxVideoBitrate, int viewportWidth, int viewportHeight, + boolean orientationMayChange) { if (group.length < 2) { return NO_TRACKS; } @@ -464,11 +531,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); String sampleMimeType = group.getFormat(trackIndex).sampleMimeType; - if (!seenMimeTypes.contains(sampleMimeType)) { - seenMimeTypes.add(sampleMimeType); - int countForMimeType = getAdaptiveTrackCountForMimeType(group, formatSupport, + if (seenMimeTypes.add(sampleMimeType)) { + int countForMimeType = getAdaptiveVideoTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport, sampleMimeType, maxVideoWidth, maxVideoHeight, - selectedTrackIndices); + maxVideoBitrate, selectedTrackIndices); if (countForMimeType > selectedMimeTypeTrackCount) { selectedMimeType = sampleMimeType; selectedMimeTypeTrackCount = countForMimeType; @@ -478,79 +544,100 @@ public class DefaultTrackSelector extends MappingTrackSelector { } // Filter by the selected mime type. - filterAdaptiveTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport, - selectedMimeType, maxVideoWidth, maxVideoHeight, selectedTrackIndices); + filterAdaptiveVideoTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport, + selectedMimeType, maxVideoWidth, maxVideoHeight, maxVideoBitrate, selectedTrackIndices); return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices); } - private static int getAdaptiveTrackCountForMimeType(TrackGroup group, int[] formatSupport, + private static int getAdaptiveVideoTrackCountForMimeType(TrackGroup group, int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight, - List selectedTrackIndices) { + int maxVideoBitrate, List selectedTrackIndices) { int adaptiveTrackCount = 0; for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); if (isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType, - formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight)) { + formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, + maxVideoBitrate)) { adaptiveTrackCount++; } } return adaptiveTrackCount; } - private static void filterAdaptiveTrackCountForMimeType(TrackGroup group, int[] formatSupport, - int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight, - List selectedTrackIndices) { + private static void filterAdaptiveVideoTrackCountForMimeType(TrackGroup group, + int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, + int maxVideoHeight, int maxVideoBitrate, List selectedTrackIndices) { for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) { int trackIndex = selectedTrackIndices.get(i); if (!isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType, - formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight)) { + formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, + maxVideoBitrate)) { selectedTrackIndices.remove(i); } } } private static boolean isSupportedAdaptiveVideoTrack(Format format, String mimeType, - int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight) { - return isSupported(formatSupport) && ((formatSupport & requiredAdaptiveSupport) != 0) + int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, + int maxVideoBitrate) { + return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0) && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType)) && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) - && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight); + && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight) + && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); } private static TrackSelection selectFixedVideoTrack(TrackGroupArray groups, - int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int viewportWidth, - int viewportHeight, boolean orientationMayChange, boolean exceedConstraintsIfNecessary) { + int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, + int viewportWidth, int viewportHeight, boolean orientationMayChange, + boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; + int selectedTrackScore = 0; + int selectedBitrate = Format.NO_VALUE; int selectedPixelCount = Format.NO_VALUE; - boolean selectedIsWithinConstraints = false; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { - TrackGroup group = groups.get(groupIndex); - List selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth, - viewportHeight, orientationMayChange); + TrackGroup trackGroup = groups.get(groupIndex); + List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup, + viewportWidth, viewportHeight, orientationMayChange); int[] trackFormatSupport = formatSupport[groupIndex]; - for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex])) { - Format format = group.getFormat(trackIndex); + for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + Format format = trackGroup.getFormat(trackIndex); boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex) && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) - && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight); - int pixelCount = format.getPixelCount(); - boolean selectTrack; - if (selectedIsWithinConstraints) { - selectTrack = isWithinConstraints - && comparePixelCounts(pixelCount, selectedPixelCount) > 0; - } else { - selectTrack = isWithinConstraints || (exceedConstraintsIfNecessary - && (selectedGroup == null - || comparePixelCounts(pixelCount, selectedPixelCount) < 0)); + && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight) + && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); + if (!isWithinConstraints && !exceedConstraintsIfNecessary) { + // Track should not be selected. + continue; + } + int trackScore = isWithinConstraints ? 2 : 1; + if (isSupported(trackFormatSupport[trackIndex], false)) { + trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; + } + boolean selectTrack = trackScore > selectedTrackScore; + if (trackScore == selectedTrackScore) { + // Use the pixel count as a tie breaker (or bitrate if pixel counts are tied). If we're + // within constraints prefer a higher pixel count (or bitrate), else prefer a lower + // count (or bitrate). If still tied then prefer the first track (i.e. the one that's + // already selected). + int comparisonResult; + int formatPixelCount = format.getPixelCount(); + if (formatPixelCount != selectedPixelCount) { + comparisonResult = compareFormatValues(format.getPixelCount(), selectedPixelCount); + } else { + comparisonResult = compareFormatValues(format.bitrate, selectedBitrate); + } + selectTrack = isWithinConstraints ? comparisonResult > 0 : comparisonResult < 0; } if (selectTrack) { - selectedGroup = group; + selectedGroup = trackGroup; selectedTrackIndex = trackIndex; - selectedPixelCount = pixelCount; - selectedIsWithinConstraints = isWithinConstraints; + selectedTrackScore = trackScore; + selectedBitrate = format.bitrate; + selectedPixelCount = format.getPixelCount(); } } } @@ -560,62 +647,139 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Compares two pixel counts for order. A known pixel count is considered greater than + * Compares two format values for order. A known value is considered greater than * {@link Format#NO_VALUE}. * - * @param first The first pixel count. - * @param second The second pixel count. - * @return A negative integer if the first pixel count is less than the second. Zero if they are - * equal. A positive integer if the first pixel count is greater than the second. + * @param first The first value. + * @param second The second value. + * @return A negative integer if the first value is less than the second. Zero if they are equal. + * A positive integer if the first value is greater than the second. */ - private static int comparePixelCounts(int first, int second) { + private static int compareFormatValues(int first, int second) { return first == Format.NO_VALUE ? (second == Format.NO_VALUE ? 0 : -1) : (second == Format.NO_VALUE ? 1 : (first - second)); } - // Audio track selection implementation. protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, - String preferredAudioLanguage) { - TrackGroup selectedGroup = null; - int selectedTrackIndex = 0; + String preferredAudioLanguage, boolean exceedRendererCapabilitiesIfNecessary, + boolean allowMixedMimeAdaptiveness, TrackSelection.Factory adaptiveTrackSelectionFactory) { + int selectedGroupIndex = C.INDEX_UNSET; + int selectedTrackIndex = C.INDEX_UNSET; int selectedTrackScore = 0; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex])) { + if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - int trackScore; - if (formatHasLanguage(format, preferredAudioLanguage)) { - if (isDefault) { - trackScore = 4; - } else { - trackScore = 3; - } - } else if (isDefault) { - trackScore = 2; - } else { - trackScore = 1; - } + int trackScore = getAudioTrackScore(trackFormatSupport[trackIndex], + preferredAudioLanguage, format); if (trackScore > selectedTrackScore) { - selectedGroup = trackGroup; + selectedGroupIndex = groupIndex; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; } } } } - return selectedGroup == null ? null - : new FixedTrackSelection(selectedGroup, selectedTrackIndex); + + if (selectedGroupIndex == C.INDEX_UNSET) { + return null; + } + + TrackGroup selectedGroup = groups.get(selectedGroupIndex); + if (adaptiveTrackSelectionFactory != null) { + // If the group of the track with the highest score allows it, try to enable adaptation. + int[] adaptiveTracks = getAdaptiveAudioTracks(selectedGroup, + formatSupport[selectedGroupIndex], allowMixedMimeAdaptiveness); + if (adaptiveTracks.length > 0) { + return adaptiveTrackSelectionFactory.createTrackSelection(selectedGroup, + adaptiveTracks); + } + } + return new FixedTrackSelection(selectedGroup, selectedTrackIndex); + } + + private static int getAudioTrackScore(int formatSupport, String preferredLanguage, + Format format) { + boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + int trackScore; + if (formatHasLanguage(format, preferredLanguage)) { + if (isDefault) { + trackScore = 4; + } else { + trackScore = 3; + } + } else if (isDefault) { + trackScore = 2; + } else { + trackScore = 1; + } + if (isSupported(formatSupport, false)) { + trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; + } + return trackScore; + } + + private static int[] getAdaptiveAudioTracks(TrackGroup group, int[] formatSupport, + boolean allowMixedMimeTypes) { + int selectedConfigurationTrackCount = 0; + AudioConfigurationTuple selectedConfiguration = null; + HashSet seenConfigurationTuples = new HashSet<>(); + for (int i = 0; i < group.length; i++) { + Format format = group.getFormat(i); + AudioConfigurationTuple configuration = new AudioConfigurationTuple( + format.channelCount, format.sampleRate, + allowMixedMimeTypes ? null : format.sampleMimeType); + if (seenConfigurationTuples.add(configuration)) { + int configurationCount = getAdaptiveAudioTrackCount(group, formatSupport, configuration); + if (configurationCount > selectedConfigurationTrackCount) { + selectedConfiguration = configuration; + selectedConfigurationTrackCount = configurationCount; + } + } + } + + if (selectedConfigurationTrackCount > 1) { + int[] adaptiveIndices = new int[selectedConfigurationTrackCount]; + int index = 0; + for (int i = 0; i < group.length; i++) { + if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i], + selectedConfiguration)) { + adaptiveIndices[index++] = i; + } + } + return adaptiveIndices; + } + return NO_TRACKS; + } + + private static int getAdaptiveAudioTrackCount(TrackGroup group, int[] formatSupport, + AudioConfigurationTuple configuration) { + int count = 0; + for (int i = 0; i < group.length; i++) { + if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i], configuration)) { + count++; + } + } + return count; + } + + private static boolean isSupportedAdaptiveAudioTrack(Format format, int formatSupport, + AudioConfigurationTuple configuration) { + return isSupported(formatSupport, false) && format.channelCount == configuration.channelCount + && format.sampleRate == configuration.sampleRate + && (configuration.mimeType == null + || TextUtils.equals(configuration.mimeType, format.sampleMimeType)); } // Text track selection implementation. protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, - String preferredTextLanguage, String preferredAudioLanguage) { + String preferredTextLanguage, String preferredAudioLanguage, + boolean exceedRendererCapabilitiesIfNecessary) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -623,7 +787,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex])) { + if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; @@ -648,7 +812,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { trackScore = 1; } } else { - trackScore = 0; + // Track should not be selected. + continue; + } + if (isSupported(trackFormatSupport[trackIndex], false)) { + trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; } if (trackScore > selectedTrackScore) { selectedGroup = trackGroup; @@ -665,7 +833,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { // General track selection methods. protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups, - int[][] formatSupport) { + int[][] formatSupport, boolean exceedRendererCapabilitiesIfNecessary) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -673,10 +841,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex])) { + if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; int trackScore = isDefault ? 2 : 1; + if (isSupported(trackFormatSupport[trackIndex], false)) { + trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; + } if (trackScore > selectedTrackScore) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; @@ -689,13 +860,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { : new FixedTrackSelection(selectedGroup, selectedTrackIndex); } - private static boolean isSupported(int formatSupport) { - return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) - == RendererCapabilities.FORMAT_HANDLED; + protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) { + int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK; + return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities + && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } - private static boolean formatHasLanguage(Format format, String language) { - return language != null && language.equals(Util.normalizeLanguageCode(format.language)); + protected static boolean formatHasLanguage(Format format, String language) { + return TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); } // Viewport size util methods. @@ -769,4 +941,39 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } + private static final class AudioConfigurationTuple { + + public final int channelCount; + public final int sampleRate; + public final String mimeType; + + public AudioConfigurationTuple(int channelCount, int sampleRate, String mimeType) { + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.mimeType = mimeType; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + AudioConfigurationTuple other = (AudioConfigurationTuple) obj; + return channelCount == other.channelCount && sampleRate == other.sampleRate + && TextUtils.equals(mimeType, other.mimeType); + } + + @Override + public int hashCode() { + int result = channelCount; + result = 31 * result + sampleRate; + result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0); + return result; + } + + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/MappingTrackSelector.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/MappingTrackSelector.java index cff62611a..4ad1790ee 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/MappingTrackSelector.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/MappingTrackSelector.java @@ -15,11 +15,13 @@ */ package org.telegram.messenger.exoplayer2.trackselection; -import android.util.Pair; +import android.content.Context; import android.util.SparseArray; import android.util.SparseBooleanArray; +import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.ExoPlaybackException; import org.telegram.messenger.exoplayer2.RendererCapabilities; +import org.telegram.messenger.exoplayer2.RendererConfiguration; import org.telegram.messenger.exoplayer2.source.TrackGroup; import org.telegram.messenger.exoplayer2.source.TrackGroupArray; import org.telegram.messenger.exoplayer2.util.Util; @@ -81,12 +83,14 @@ public abstract class MappingTrackSelector extends TrackSelector { private final SparseArray> selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; + private int tunnelingAudioSessionId; private MappedTrackInfo currentMappedTrackInfo; public MappingTrackSelector() { selectionOverrides = new SparseArray<>(); rendererDisabledFlags = new SparseBooleanArray(); + tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET; } /** @@ -138,8 +142,6 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param groups The {@link TrackGroupArray} for which the override should be applied. * @param override The override. */ - // TODO - Don't allow overrides that select unsupported tracks, unless some flag has been - // explicitly set by the user to indicate that they want this. public final void setSelectionOverride(int rendererIndex, TrackGroupArray groups, SelectionOverride override) { Map overrides = selectionOverrides.get(rendererIndex); @@ -225,12 +227,28 @@ public abstract class MappingTrackSelector extends TrackSelector { invalidate(); } + /** + * Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in + * tunneling mode. Session ids can be generated using + * {@link C#generateAudioSessionIdV21(Context)}. To disable tunneling pass + * {@link C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and + * supported by the audio and video renderers for the selected tracks. + * + * @param tunnelingAudioSessionId The audio session id to use when tunneling, or + * {@link C#AUDIO_SESSION_ID_UNSET} to disable tunneling. + */ + public void setTunnelingAudioSessionId(int tunnelingAudioSessionId) { + if (this.tunnelingAudioSessionId != tunnelingAudioSessionId) { + this.tunnelingAudioSessionId = tunnelingAudioSessionId; + invalidate(); + } + } + // TrackSelector implementation. @Override - public final Pair selectTracks( - RendererCapabilities[] rendererCapabilities, TrackGroupArray trackGroups) - throws ExoPlaybackException { + public final TrackSelectorResult selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray trackGroups) throws ExoPlaybackException { // Structures into which data will be written during the selection. The extra item at the end // of each array is to store data associated with track groups that cannot be associated with // any renderer. @@ -298,8 +316,20 @@ public abstract class MappingTrackSelector extends TrackSelector { MappedTrackInfo mappedTrackInfo = new MappedTrackInfo(rendererTrackTypes, rendererTrackGroupArrays, mixedMimeTypeAdaptationSupport, rendererFormatSupports, unassociatedTrackGroupArray); - return Pair.create(new TrackSelectionArray(trackSelections), - mappedTrackInfo); + + // Initialize the renderer configurations to the default configuration for all renderers with + // selections, and null otherwise. + RendererConfiguration[] rendererConfigurations = + new RendererConfiguration[rendererCapabilities.length]; + for (int i = 0; i < rendererCapabilities.length; i++) { + rendererConfigurations[i] = trackSelections[i] != null ? RendererConfiguration.DEFAULT : null; + } + // Configure audio and video renderers to use tunneling if appropriate. + maybeConfigureRenderersForTunneling(rendererCapabilities, rendererTrackGroupArrays, + rendererFormatSupports, rendererConfigurations, trackSelections, tunnelingAudioSessionId); + + return new TrackSelectorResult(trackGroups, new TrackSelectionArray(trackSelections), + mappedTrackInfo, rendererConfigurations); } @Override @@ -346,15 +376,16 @@ public abstract class MappingTrackSelector extends TrackSelector { private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group) throws ExoPlaybackException { int bestRendererIndex = rendererCapabilities.length; - int bestSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; + int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; for (int rendererIndex = 0; rendererIndex < rendererCapabilities.length; rendererIndex++) { RendererCapabilities rendererCapability = rendererCapabilities[rendererIndex]; for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - int trackSupportLevel = rendererCapability.supportsFormat(group.getFormat(trackIndex)); - if (trackSupportLevel > bestSupportLevel) { + int formatSupportLevel = rendererCapability.supportsFormat(group.getFormat(trackIndex)) + & RendererCapabilities.FORMAT_SUPPORT_MASK; + if (formatSupportLevel > bestFormatSupportLevel) { bestRendererIndex = rendererIndex; - bestSupportLevel = trackSupportLevel; - if (bestSupportLevel == RendererCapabilities.FORMAT_HANDLED) { + bestFormatSupportLevel = formatSupportLevel; + if (bestFormatSupportLevel == RendererCapabilities.FORMAT_HANDLED) { // We can't do better. return bestRendererIndex; } @@ -401,6 +432,94 @@ public abstract class MappingTrackSelector extends TrackSelector { return mixedMimeTypeAdaptationSupport; } + /** + * Determines whether tunneling should be enabled, replacing {@link RendererConfiguration}s in + * {@code rendererConfigurations} with configurations that enable tunneling on the appropriate + * renderers if so. + * + * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which + * {@link TrackSelection}s are to be generated. + * @param rendererTrackGroupArrays An array of {@link TrackGroupArray}s where each entry + * corresponds to the renderer of equal index in {@code renderers}. + * @param rendererFormatSupports Maps every available track to a specific level of support as + * defined by the renderer {@code FORMAT_*} constants. + * @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. + * @param tunnelingAudioSessionId The audio session id to use when tunneling, or + * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. + */ + private static void maybeConfigureRenderersForTunneling( + RendererCapabilities[] rendererCapabilities, TrackGroupArray[] rendererTrackGroupArrays, + int[][][] rendererFormatSupports, RendererConfiguration[] rendererConfigurations, + TrackSelection[] trackSelections, int tunnelingAudioSessionId) { + if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) { + return; + } + // Check whether we can enable tunneling. To enable tunneling we require exactly one audio and + // one video renderer to support tunneling and have a selection. + int tunnelingAudioRendererIndex = -1; + int tunnelingVideoRendererIndex = -1; + boolean enableTunneling = true; + for (int i = 0; i < rendererCapabilities.length; i++) { + int rendererType = rendererCapabilities[i].getTrackType(); + TrackSelection trackSelection = trackSelections[i]; + if ((rendererType == C.TRACK_TYPE_AUDIO || rendererType == C.TRACK_TYPE_VIDEO) + && trackSelection != null) { + if (rendererSupportsTunneling(rendererFormatSupports[i], rendererTrackGroupArrays[i], + trackSelection)) { + if (rendererType == C.TRACK_TYPE_AUDIO) { + if (tunnelingAudioRendererIndex != -1) { + enableTunneling = false; + break; + } else { + tunnelingAudioRendererIndex = i; + } + } else { + if (tunnelingVideoRendererIndex != -1) { + enableTunneling = false; + break; + } else { + tunnelingVideoRendererIndex = i; + } + } + } + } + } + enableTunneling &= tunnelingAudioRendererIndex != -1 && tunnelingVideoRendererIndex != -1; + if (enableTunneling) { + RendererConfiguration tunnelingRendererConfiguration = + new RendererConfiguration(tunnelingAudioSessionId); + rendererConfigurations[tunnelingAudioRendererIndex] = tunnelingRendererConfiguration; + rendererConfigurations[tunnelingVideoRendererIndex] = tunnelingRendererConfiguration; + } + } + + /** + * Returns whether a renderer supports tunneling for a {@link TrackSelection}. + * + * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} 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[][] formatSupport, + TrackGroupArray trackGroups, TrackSelection selection) { + if (selection == null) { + return false; + } + int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); + for (int i = 0; i < selection.length(); i++) { + int trackFormatSupport = formatSupport[trackGroupIndex][selection.getIndexInTrackGroup(i)]; + if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK) + != RendererCapabilities.TUNNELING_SUPPORTED) { + return false; + } + } + return true; + } + /** * Provides track information for each renderer. */ @@ -411,13 +530,18 @@ public abstract class MappingTrackSelector extends TrackSelector { */ public static final int RENDERER_SUPPORT_NO_TRACKS = 0; /** - * The renderer has associated tracks, but cannot play any of them. + * The renderer has associated tracks, but all are of unsupported types. */ - public static final int RENDERER_SUPPORT_UNPLAYABLE_TRACKS = 1; + public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1; /** - * The renderer has associated tracks, and can play at least one of them. + * The renderer has associated tracks and at least one is of a supported type, but all of the + * tracks whose types are supported exceed the renderer's capabilities. */ - public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 2; + public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2; + /** + * The renderer has associated tracks and can play at least one of them. + */ + public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3; /** * The number of renderers to which tracks are mapped. @@ -465,21 +589,49 @@ public abstract class MappingTrackSelector extends TrackSelector { * * @param rendererIndex The renderer index. * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, - * {@link #RENDERER_SUPPORT_UNPLAYABLE_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, + * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. */ public int getRendererSupport(int rendererIndex) { - boolean hasTracks = false; + int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; int[][] rendererFormatSupport = formatSupport[rendererIndex]; for (int i = 0; i < rendererFormatSupport.length; i++) { for (int j = 0; j < rendererFormatSupport[i].length; j++) { - hasTracks = true; - if ((rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) - == RendererCapabilities.FORMAT_HANDLED) { - return RENDERER_SUPPORT_PLAYABLE_TRACKS; + int trackRendererSupport; + switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) { + case RendererCapabilities.FORMAT_HANDLED: + return RENDERER_SUPPORT_PLAYABLE_TRACKS; + case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: + trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS; + break; + default: + trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS; + break; } + bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport); } } - return hasTracks ? RENDERER_SUPPORT_UNPLAYABLE_TRACKS : RENDERER_SUPPORT_NO_TRACKS; + return bestRendererSupport; + } + + /** + * Returns the best level of support obtained from {@link #getRendererSupport(int)} for all + * renderers of the specified track type. If no renderers exist for the specified type then + * {@link #RENDERER_SUPPORT_NO_TRACKS} is 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}. + */ + public int getTrackTypeRendererSupport(int trackType) { + int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + for (int i = 0; i < length; i++) { + if (rendererTrackTypes[i] == trackType) { + bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); + } + } + return bestRendererSupport; } /** @@ -576,25 +728,6 @@ public abstract class MappingTrackSelector extends TrackSelector { return unassociatedTrackGroups; } - /** - * Returns true if tracks of the specified type exist and have been associated with renderers, - * but are all unplayable. Returns false in all other cases. - * - * @param trackType The track type. - * @return True if tracks of the specified type exist, if at least one renderer exists that - * handles tracks of the specified type, and if all of the tracks if the specified type are - * unplayable. False in all other cases. - */ - public boolean hasOnlyUnplayableTracks(int trackType) { - int rendererSupport = RENDERER_SUPPORT_NO_TRACKS; - for (int i = 0; i < length; i++) { - if (rendererTrackTypes[i] == trackType) { - rendererSupport = Math.max(rendererSupport, getRendererSupport(i)); - } - } - return rendererSupport == RENDERER_SUPPORT_UNPLAYABLE_TRACKS; - } - } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/TrackSelector.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/TrackSelector.java index d380907b7..7bb92ad64 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/TrackSelector.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/TrackSelector.java @@ -15,7 +15,6 @@ */ package org.telegram.messenger.exoplayer2.trackselection; -import android.util.Pair; import org.telegram.messenger.exoplayer2.ExoPlaybackException; import org.telegram.messenger.exoplayer2.RendererCapabilities; import org.telegram.messenger.exoplayer2.source.TrackGroupArray; @@ -47,25 +46,22 @@ public abstract class TrackSelector { } /** - * Generates {@link TrackSelectionArray} for the renderers. + * Performs a track selection for renderers. * - * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which - * {@link TrackSelection}s are to be generated. + * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks + * are to be selected. * @param trackGroups The available track groups. - * @return The track selections, and an implementation specific object that will be returned to - * the selector via {@link #onSelectionActivated(Object)} should the selections be activated. + * @return A {@link TrackSelectorResult} describing the track selections. * @throws ExoPlaybackException If an error occurs selecting tracks. */ - public abstract Pair selectTracks( - RendererCapabilities[] rendererCapabilities, TrackGroupArray trackGroups) - throws ExoPlaybackException; + public abstract TrackSelectorResult selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray trackGroups) throws ExoPlaybackException; /** - * Called when {@link TrackSelectionArray} previously generated by - * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} are activated. + * Called when a {@link TrackSelectorResult} previously generated by + * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} is activated. * - * @param info The information associated with the selections, or null if the selected tracks are - * being cleared. + * @param info The value of {@link TrackSelectorResult#info} in the activated result. */ public abstract void onSelectionActivated(Object info); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/TrackSelectorResult.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/TrackSelectorResult.java new file mode 100755 index 000000000..1f2c38cf6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/trackselection/TrackSelectorResult.java @@ -0,0 +1,98 @@ +/* + * 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 org.telegram.messenger.exoplayer2.trackselection; + +import org.telegram.messenger.exoplayer2.RendererConfiguration; +import org.telegram.messenger.exoplayer2.source.TrackGroupArray; +import org.telegram.messenger.exoplayer2.util.Util; + +/** + * The result of a {@link TrackSelector} operation. + */ +public final class TrackSelectorResult { + + /** + * The groups provided to the {@link TrackSelector}. + */ + public final TrackGroupArray groups; + /** + * A {@link TrackSelectionArray} containing the selection for each renderer. + */ + public final TrackSelectionArray selections; + /** + * An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)} + * should the selections be activated. + */ + public final Object info; + /** + * A {@link RendererConfiguration} for each renderer, to be used with the selections. + */ + public final RendererConfiguration[] rendererConfigurations; + + /** + * @param groups The groups provided to the {@link TrackSelector}. + * @param selections A {@link TrackSelectionArray} containing the selection for each renderer. + * @param info An opaque object that will be returned to + * {@link TrackSelector#onSelectionActivated(Object)} should the selections be activated. + * @param rendererConfigurations A {@link RendererConfiguration} for each renderer, to be used + * with the selections. + */ + public TrackSelectorResult(TrackGroupArray groups, TrackSelectionArray selections, Object info, + RendererConfiguration[] rendererConfigurations) { + this.groups = groups; + this.selections = selections; + this.info = info; + this.rendererConfigurations = rendererConfigurations; + } + + /** + * Returns whether this result is equivalent to {@code other} for all renderers. + * + * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} + * will be returned in all cases. + * @return Whether this result is equivalent to {@code other} for all renderers. + */ + public boolean isEquivalent(TrackSelectorResult other) { + if (other == null) { + return false; + } + for (int i = 0; i < selections.length; i++) { + if (!isEquivalent(other, i)) { + return false; + } + } + return true; + } + + /** + * Returns whether this result is equivalent to {@code other} for the renderer at the given index. + * 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 in all cases. + * @param index The renderer index to check for equivalence. + * @return Whether this result is equivalent to {@code other} for all renderers. + */ + public boolean isEquivalent(TrackSelectorResult other, int index) { + if (other == null) { + return false; + } + return Util.areEqual(selections.get(index), other.selections.get(index)) + && Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index]); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/AspectRatioFrameLayout.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/AspectRatioFrameLayout.java index 4e8b86187..719fe519a 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/AspectRatioFrameLayout.java @@ -31,136 +31,148 @@ import java.lang.annotation.RetentionPolicy; */ public class AspectRatioFrameLayout extends FrameLayout { - /** - * Resize modes for {@link AspectRatioFrameLayout}. - */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL}) - public @interface ResizeMode {} + /** + * Resize modes for {@link AspectRatioFrameLayout}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL}) + public @interface ResizeMode {} - /** - * Either the width or height is decreased to obtain the desired aspect ratio. - */ - public static final int RESIZE_MODE_FIT = 0; - /** - * The width is fixed and the height is increased or decreased to obtain the desired aspect ratio. - */ - public static final int RESIZE_MODE_FIXED_WIDTH = 1; - /** - * The height is fixed and the width is increased or decreased to obtain the desired aspect ratio. - */ - public static final int RESIZE_MODE_FIXED_HEIGHT = 2; - /** - * The specified aspect ratio is ignored. - */ - public static final int RESIZE_MODE_FILL = 3; + /** + * Either the width or height is decreased to obtain the desired aspect ratio. + */ + public static final int RESIZE_MODE_FIT = 0; + /** + * The width is fixed and the height is increased or decreased to obtain the desired aspect ratio. + */ + public static final int RESIZE_MODE_FIXED_WIDTH = 1; + /** + * The height is fixed and the width is increased or decreased to obtain the desired aspect ratio. + */ + public static final int RESIZE_MODE_FIXED_HEIGHT = 2; + /** + * The specified aspect ratio is ignored. + */ + public static final int RESIZE_MODE_FILL = 3; - /** - * The {@link FrameLayout} will not resize itself if the fractional difference between its natural - * aspect ratio and the requested aspect ratio falls below this threshold. - *

    - * This tolerance allows the view to occupy the whole of the screen when the requested aspect - * ratio is very close, but not exactly equal to, the aspect ratio of the screen. This may reduce - * the number of view layers that need to be composited by the underlying system, which can help - * to reduce power consumption. - */ - private static final float MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f; + /** + * The {@link FrameLayout} will not resize itself if the fractional difference between its natural + * aspect ratio and the requested aspect ratio falls below this threshold. + *

    + * This tolerance allows the view to occupy the whole of the screen when the requested aspect + * ratio is very close, but not exactly equal to, the aspect ratio of the screen. This may reduce + * the number of view layers that need to be composited by the underlying system, which can help + * to reduce power consumption. + */ + private static final float MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f; - private float videoAspectRatio; - private int resizeMode; + private float videoAspectRatio; + private int resizeMode; + private boolean drawingReady; private int rotation; private Matrix matrix = new Matrix(); - public AspectRatioFrameLayout(Context context) { - this(context, null); - } + public AspectRatioFrameLayout(Context context) { + this(context, null); + } - public AspectRatioFrameLayout(Context context, AttributeSet attrs) { - super(context, attrs); - resizeMode = RESIZE_MODE_FIT; - } + public AspectRatioFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + resizeMode = RESIZE_MODE_FIT; + } - /** - * Set the aspect ratio that this view should satisfy. - * - * @param widthHeightRatio The width to height ratio. - */ - public void setAspectRatio(float widthHeightRatio, int rotation) { - if (this.videoAspectRatio != widthHeightRatio || this.rotation != rotation) { - this.videoAspectRatio = widthHeightRatio; - this.rotation = rotation; - requestLayout(); - } - } + public void setDrawingReady(boolean value) { + if (drawingReady == value) { + return; + } + drawingReady = value; + } - public float getAspectRatio() { - return videoAspectRatio; - } + public boolean isDrawingReady() { + return drawingReady; + } - public int getVideoRotation() { - return rotation; - } + /** + * Set the aspect ratio that this view should satisfy. + * + * @param widthHeightRatio The width to height ratio. + */ + public void setAspectRatio(float widthHeightRatio, int rotation) { + if (this.videoAspectRatio != widthHeightRatio || this.rotation != rotation) { + this.videoAspectRatio = widthHeightRatio; + this.rotation = rotation; + requestLayout(); + } + } - /** - * Sets the resize mode. - * - * @param resizeMode The resize mode. - */ - public void setResizeMode(@ResizeMode int resizeMode) { - if (this.resizeMode != resizeMode) { - this.resizeMode = resizeMode; - requestLayout(); - } - } + public float getAspectRatio() { + return videoAspectRatio; + } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (resizeMode == RESIZE_MODE_FILL || videoAspectRatio <= 0) { - // Aspect ratio not set. - return; - } + public int getVideoRotation() { + return rotation; + } - int width = getMeasuredWidth(); - int height = getMeasuredHeight(); - float viewAspectRatio = (float) width / height; - float aspectDeformation = videoAspectRatio / viewAspectRatio - 1; - if (Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) { - // We're within the allowed tolerance. - return; - } + /** + * Sets the resize mode. + * + * @param resizeMode The resize mode. + */ + public void setResizeMode(@ResizeMode int resizeMode) { + if (this.resizeMode != resizeMode) { + this.resizeMode = resizeMode; + requestLayout(); + } + } - switch (resizeMode) { - case RESIZE_MODE_FIXED_WIDTH: - height = (int) (width / videoAspectRatio); - break; - case RESIZE_MODE_FIXED_HEIGHT: - width = (int) (height * videoAspectRatio); - break; - default: - if (aspectDeformation > 0) { - height = (int) (width / videoAspectRatio); - } else { - width = (int) (height * videoAspectRatio); - } - break; - } - super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); - int count = getChildCount(); - for (int a = 0; a < count; a++) { - View child = getChildAt(a); - if (child instanceof TextureView) { - matrix.reset(); - int px = getWidth() / 2; - int py = getHeight() / 2; - matrix.postRotate(rotation, px, py); - if (rotation == 90 || rotation == 270) { - float ratio = (float) getHeight() / getWidth(); - matrix.postScale(1 / ratio, ratio, px, py); - } - ((TextureView) child).setTransform(matrix); - break; - } - } - } + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (resizeMode == RESIZE_MODE_FILL || videoAspectRatio <= 0) { + // Aspect ratio not set. + return; + } + + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + float viewAspectRatio = (float) width / height; + float aspectDeformation = videoAspectRatio / viewAspectRatio - 1; + if (Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) { + // We're within the allowed tolerance. + return; + } + + switch (resizeMode) { + case RESIZE_MODE_FIXED_WIDTH: + height = (int) (width / videoAspectRatio); + break; + case RESIZE_MODE_FIXED_HEIGHT: + width = (int) (height * videoAspectRatio); + break; + default: + if (aspectDeformation > 0) { + height = (int) (width / videoAspectRatio); + } else { + width = (int) (height * videoAspectRatio); + } + break; + } + super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + int count = getChildCount(); + for (int a = 0; a < count; a++) { + View child = getChildAt(a); + if (child instanceof TextureView) { + matrix.reset(); + int px = getWidth() / 2; + int py = getHeight() / 2; + matrix.postRotate(rotation, px, py); + if (rotation == 90 || rotation == 270) { + float ratio = (float) getHeight() / getWidth(); + matrix.postScale(1 / ratio, ratio, px, py); + } + ((TextureView) child).setTransform(matrix); + break; + } + } + } } \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/DebugTextViewHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/DebugTextViewHelper.java index 7b113d9f9..95530722b 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/DebugTextViewHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/DebugTextViewHelper.java @@ -19,6 +19,7 @@ import android.widget.TextView; import org.telegram.messenger.exoplayer2.ExoPlaybackException; import org.telegram.messenger.exoplayer2.ExoPlayer; import org.telegram.messenger.exoplayer2.Format; +import org.telegram.messenger.exoplayer2.PlaybackParameters; import org.telegram.messenger.exoplayer2.SimpleExoPlayer; import org.telegram.messenger.exoplayer2.Timeline; import org.telegram.messenger.exoplayer2.decoder.DecoderCounters; @@ -90,6 +91,11 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe updateAndPost(); } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { // Do nothing. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/SubtitlePainter.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/SubtitlePainter.java index 673adbceb..45522d768 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/SubtitlePainter.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/SubtitlePainter.java @@ -18,16 +18,21 @@ package org.telegram.messenger.exoplayer2.ui; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Join; import android.graphics.Paint.Style; +import android.graphics.Rect; import android.graphics.RectF; import android.text.Layout.Alignment; +import android.text.SpannableStringBuilder; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.RelativeSizeSpan; import android.util.DisplayMetrics; import android.util.Log; import org.telegram.messenger.exoplayer2.text.CaptionStyleCompat; @@ -65,6 +70,7 @@ import org.telegram.messenger.exoplayer2.util.Util; // Previous input variables. private CharSequence cueText; private Alignment cueTextAlignment; + private Bitmap cueBitmap; private float cueLine; @Cue.LineType private int cueLineType; @@ -74,7 +80,9 @@ import org.telegram.messenger.exoplayer2.util.Util; @Cue.AnchorType private int cuePositionAnchor; private float cueSize; + private float cueBitmapHeight; private boolean applyEmbeddedStyles; + private boolean applyEmbeddedFontSizes; private int foregroundColor; private int backgroundColor; private int windowColor; @@ -93,6 +101,7 @@ import org.telegram.messenger.exoplayer2.util.Util; private int textLeft; private int textTop; private int textPaddingX; + private Rect bitmapRect; @SuppressWarnings("ResourceType") public SubtitlePainter(Context context) { @@ -128,6 +137,8 @@ import org.telegram.messenger.exoplayer2.util.Util; * * @param cue The cue to draw. * @param applyEmbeddedStyles Whether styling embedded within the cue should be applied. + * @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font + * sizes embedded within the cue should be applied. Otherwise, it is ignored. * @param style The style to use when drawing the cue text. * @param textSizePx The text size to use when drawing the cue text, in pixels. * @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is @@ -138,30 +149,34 @@ import org.telegram.messenger.exoplayer2.util.Util; * @param cueBoxRight The right position of the enclosing cue box. * @param cueBoxBottom The bottom position of the enclosing cue box. */ - public void draw(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, float textSizePx, - float bottomPaddingFraction, Canvas canvas, int cueBoxLeft, int cueBoxTop, int cueBoxRight, - int cueBoxBottom) { - CharSequence cueText = cue.text; - if (TextUtils.isEmpty(cueText)) { - // Nothing to draw. - return; + public void draw(Cue cue, boolean applyEmbeddedStyles, boolean applyEmbeddedFontSizes, + CaptionStyleCompat style, float textSizePx, float bottomPaddingFraction, Canvas canvas, + int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { + boolean isTextCue = cue.bitmap == null; + int windowColor = Color.BLACK; + if (isTextCue) { + if (TextUtils.isEmpty(cue.text)) { + // Nothing to draw. + return; + } + windowColor = (cue.windowColorSet && applyEmbeddedStyles) + ? cue.windowColor : style.windowColor; } - if (!applyEmbeddedStyles) { - // Strip out any embedded styling. - cueText = cueText.toString(); - } - if (areCharSequencesEqual(this.cueText, cueText) + if (areCharSequencesEqual(this.cueText, cue.text) && Util.areEqual(this.cueTextAlignment, cue.textAlignment) + && this.cueBitmap == cue.bitmap && this.cueLine == cue.line && this.cueLineType == cue.lineType && Util.areEqual(this.cueLineAnchor, cue.lineAnchor) && this.cuePosition == cue.position && Util.areEqual(this.cuePositionAnchor, cue.positionAnchor) && this.cueSize == cue.size + && this.cueBitmapHeight == cue.bitmapHeight && this.applyEmbeddedStyles == applyEmbeddedStyles + && this.applyEmbeddedFontSizes == applyEmbeddedFontSizes && this.foregroundColor == style.foregroundColor && this.backgroundColor == style.backgroundColor - && this.windowColor == style.windowColor + && this.windowColor == windowColor && this.edgeType == style.edgeType && this.edgeColor == style.edgeColor && Util.areEqual(this.textPaint.getTypeface(), style.typeface) @@ -172,22 +187,25 @@ import org.telegram.messenger.exoplayer2.util.Util; && this.parentRight == cueBoxRight && this.parentBottom == cueBoxBottom) { // We can use the cached layout. - drawLayout(canvas); + drawLayout(canvas, isTextCue); return; } - this.cueText = cueText; + this.cueText = cue.text; this.cueTextAlignment = cue.textAlignment; + this.cueBitmap = cue.bitmap; this.cueLine = cue.line; this.cueLineType = cue.lineType; this.cueLineAnchor = cue.lineAnchor; this.cuePosition = cue.position; this.cuePositionAnchor = cue.positionAnchor; this.cueSize = cue.size; + this.cueBitmapHeight = cue.bitmapHeight; this.applyEmbeddedStyles = applyEmbeddedStyles; + this.applyEmbeddedFontSizes = applyEmbeddedFontSizes; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; - this.windowColor = style.windowColor; + this.windowColor = windowColor; this.edgeType = style.edgeType; this.edgeColor = style.edgeColor; this.textPaint.setTypeface(style.typeface); @@ -198,6 +216,15 @@ import org.telegram.messenger.exoplayer2.util.Util; this.parentRight = cueBoxRight; this.parentBottom = cueBoxBottom; + if (isTextCue) { + setupTextLayout(); + } else { + setupBitmapLayout(); + } + drawLayout(canvas, isTextCue); + } + + private void setupTextLayout() { int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; @@ -213,6 +240,26 @@ import org.telegram.messenger.exoplayer2.util.Util; return; } + // Remove embedded styling or font size if requested. + CharSequence cueText; + if (applyEmbeddedFontSizes && applyEmbeddedStyles) { + cueText = this.cueText; + } else if (!applyEmbeddedStyles) { + cueText = this.cueText.toString(); // Equivalent to erasing all spans. + } else { + SpannableStringBuilder newCueText = new SpannableStringBuilder(this.cueText); + int cueLength = newCueText.length(); + AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class); + RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class); + for (AbsoluteSizeSpan absSpan : absSpans) { + newCueText.removeSpan(absSpan); + } + for (RelativeSizeSpan relSpan : relSpans) { + newCueText.removeSpan(relSpan); + } + cueText = newCueText; + } + Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment; textLayout = new StaticLayout(cueText, textPaint, availableWidth, textAlignment, spacingMult, spacingAdd, true); @@ -233,7 +280,7 @@ import org.telegram.messenger.exoplayer2.util.Util; int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft; textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2 - : anchorPosition; + : anchorPosition; textLeft = Math.max(textLeft, parentLeft); textRight = Math.min(textLeft + textWidth, parentRight); } else { @@ -241,6 +288,12 @@ import org.telegram.messenger.exoplayer2.util.Util; textRight = textLeft + textWidth; } + textWidth = textRight - textLeft; + if (textWidth <= 0) { + Log.w(TAG, "Skipped drawing subtitle cue (invalid horizontal positioning)"); + return; + } + int textTop; if (cueLine != Cue.DIMEN_UNSET) { int anchorPosition; @@ -252,12 +305,12 @@ import org.telegram.messenger.exoplayer2.util.Util; if (cueLine >= 0) { anchorPosition = Math.round(cueLine * firstLineHeight) + parentTop; } else { - anchorPosition = Math.round(cueLine * firstLineHeight) + parentBottom; + anchorPosition = Math.round((cueLine + 1) * firstLineHeight) + parentBottom; } } textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2 - : anchorPosition; + : anchorPosition; if (textTop + textHeight > parentBottom) { textTop = parentBottom - textHeight; } else if (textTop < parentTop) { @@ -267,25 +320,39 @@ import org.telegram.messenger.exoplayer2.util.Util; textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction); } - textWidth = textRight - textLeft; - // Update the derived drawing variables. this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult, spacingAdd, true); this.textLeft = textLeft; this.textTop = textTop; this.textPaddingX = textPaddingX; - - drawLayout(canvas); } - /** - * Draws {@link #textLayout} into the provided canvas. - * - * @param canvas The canvas into which to draw. - */ - private void drawLayout(Canvas canvas) { - final StaticLayout layout = textLayout; + private void setupBitmapLayout() { + int parentWidth = parentRight - parentLeft; + int parentHeight = parentBottom - parentTop; + float anchorX = parentLeft + (parentWidth * cuePosition); + float anchorY = parentTop + (parentHeight * cueLine); + int width = Math.round(parentWidth * cueSize); + int height = cueBitmapHeight != Cue.DIMEN_UNSET ? Math.round(parentHeight * cueBitmapHeight) + : Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); + int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) + : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); + int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) + : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); + bitmapRect = new Rect(x, y, x + width, y + height); + } + + private void drawLayout(Canvas canvas, boolean isTextCue) { + if (isTextCue) { + drawTextLayout(canvas); + } else { + drawBitmapLayout(canvas); + } + } + + private void drawTextLayout(Canvas canvas) { + StaticLayout layout = textLayout; if (layout == null) { // Nothing to draw. return; @@ -343,6 +410,10 @@ import org.telegram.messenger.exoplayer2.util.Util; canvas.restoreToCount(saveCount); } + private void drawBitmapLayout(Canvas canvas) { + canvas.drawBitmap(cueBitmap, null, bitmapRect, null); + } + /** * This method is used instead of {@link TextUtils#equals(CharSequence, CharSequence)} because the * latter only checks the text of each sequence, and does not check for equality of styling that diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/SubtitleView.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/SubtitleView.java index ee7ae0813..8a086652b 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/SubtitleView.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/SubtitleView.java @@ -60,6 +60,7 @@ public final class SubtitleView extends View implements TextRenderer.Output { private int textSizeType; private float textSize; private boolean applyEmbeddedStyles; + private boolean applyEmbeddedFontSizes; private CaptionStyleCompat style; private float bottomPaddingFraction; @@ -73,6 +74,7 @@ public final class SubtitleView extends View implements TextRenderer.Output { textSizeType = FRACTIONAL; textSize = DEFAULT_TEXT_SIZE_FRACTION; applyEmbeddedStyles = true; + applyEmbeddedFontSizes = true; style = CaptionStyleCompat.DEFAULT; bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION; } @@ -166,14 +168,32 @@ public final class SubtitleView extends View implements TextRenderer.Output { /** * Sets whether styling embedded within the cues should be applied. Enabled by default. + * Overrides any setting made with {@link SubtitleView#setApplyEmbeddedFontSizes}. * * @param applyEmbeddedStyles Whether styling embedded within the cues should be applied. */ public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) { - if (this.applyEmbeddedStyles == applyEmbeddedStyles) { + if (this.applyEmbeddedStyles == applyEmbeddedStyles + && this.applyEmbeddedFontSizes == applyEmbeddedStyles) { return; } this.applyEmbeddedStyles = applyEmbeddedStyles; + this.applyEmbeddedFontSizes = applyEmbeddedStyles; + // Invalidate to trigger drawing. + invalidate(); + } + + /** + * Sets whether font sizes embedded within the cues should be applied. Enabled by default. + * Only takes effect if {@link SubtitleView#setApplyEmbeddedStyles} is set to true. + * + * @param applyEmbeddedFontSizes Whether font sizes embedded within the cues should be applied. + */ + public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) { + if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) { + return; + } + this.applyEmbeddedFontSizes = applyEmbeddedFontSizes; // Invalidate to trigger drawing. invalidate(); } @@ -243,8 +263,8 @@ public final class SubtitleView extends View implements TextRenderer.Output { } for (int i = 0; i < cueCount; i++) { - painters.get(i).draw(cues.get(i), applyEmbeddedStyles, style, textSizePx, - bottomPaddingFraction, canvas, left, top, right, bottom); + painters.get(i).draw(cues.get(i), applyEmbeddedStyles, applyEmbeddedFontSizes, style, + textSizePx, bottomPaddingFraction, canvas, left, top, right, bottom); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/TimeBar.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/TimeBar.java new file mode 100755 index 000000000..099c41280 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/ui/TimeBar.java @@ -0,0 +1,120 @@ +/* + * 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 org.telegram.messenger.exoplayer2.ui; + +import android.support.annotation.Nullable; +import android.view.View; + +/** + * Interface for time bar views that can display a playback position, buffered position, duration + * and ad markers, and that have a listener for scrubbing (seeking) events. + */ +public interface TimeBar { + + /** + * @see View#isEnabled() + */ + void setEnabled(boolean enabled); + + /** + * Sets the listener for the scrubbing events. + * + * @param listener The listener for scrubbing events. + */ + void setListener(OnScrubListener listener); + + /** + * Sets the position increment for key presses and accessibility actions, in milliseconds. + *

    + * Clears any increment specified in a preceding call to {@link #setKeyCountIncrement(int)}. + * + * @param time The time increment, in milliseconds. + */ + void setKeyTimeIncrement(long time); + + /** + * Sets the position increment for key presses and accessibility actions, as a number of + * increments that divide the duration of the media. For example, passing 20 will cause key + * presses to increment/decrement the position by 1/20th of the duration (if known). + *

    + * Clears any increment specified in a preceding call to {@link #setKeyTimeIncrement(long)}. + * + * @param count The number of increments that divide the duration of the media. + */ + void setKeyCountIncrement(int count); + + /** + * Sets the current position. + * + * @param position The current position to show, in milliseconds. + */ + void setPosition(long position); + + /** + * Sets the buffered position. + * + * @param bufferedPosition The current buffered position to show, in milliseconds. + */ + void setBufferedPosition(long bufferedPosition); + + /** + * Sets the duration. + * + * @param duration The duration to show, in milliseconds. + */ + void setDuration(long duration); + + /** + * Sets the times of ad breaks. + * + * @param adBreakTimesMs An array where the first {@code adBreakCount} elements are the times of + * ad breaks in milliseconds. May be {@code null} if there are no ad breaks. + * @param adBreakCount The number of ad breaks. + */ + void setAdBreakTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount); + + /** + * Listener for scrubbing events. + */ + interface OnScrubListener { + + /** + * Called when the user starts moving the scrubber. + * + * @param timeBar The time bar. + */ + void onScrubStart(TimeBar timeBar); + + /** + * Called when the user moves the scrubber. + * + * @param timeBar The time bar. + * @param position The position of the scrubber, in milliseconds. + */ + void onScrubMove(TimeBar timeBar, long position); + + /** + * Called when the user stops moving the scrubber. + * + * @param timeBar The time bar. + * @param position The position of the scrubber, in milliseconds. + * @param canceled Whether scrubbing was canceled. + */ + void onScrubStop(TimeBar timeBar, long position, boolean canceled); + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSource.java index ed7b8431b..452a8c273 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSource.java @@ -65,7 +65,7 @@ public interface DataSource { * @param buffer The buffer into which the read data should be stored. * @param offset The start offset into {@code buffer} at which data should be written. * @param readLength The maximum number of bytes to read. - * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is avaliable + * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available * because the end of the opened range has been reached. * @throws IOException If an error occurs reading from the source. */ diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSourceInputStream.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSourceInputStream.java index 474eb672b..2771e8541 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSourceInputStream.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSourceInputStream.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.upstream; +import android.support.annotation.NonNull; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.util.Assertions; import java.io.IOException; @@ -67,24 +68,16 @@ public final class DataSourceInputStream extends InputStream { @Override public int read() throws IOException { int length = read(singleByteArray); - if (length == -1) { - return -1; - } - totalBytesRead++; - return singleByteArray[0] & 0xFF; + return length == -1 ? -1 : (singleByteArray[0] & 0xFF); } @Override - public int read(byte[] buffer) throws IOException { - int bytesRead = read(buffer, 0, buffer.length); - if (bytesRead != -1) { - totalBytesRead += bytesRead; - } - return bytesRead; + public int read(@NonNull byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); } @Override - public int read(byte[] buffer, int offset, int length) throws IOException { + public int read(@NonNull byte[] buffer, int offset, int length) throws IOException { Assertions.checkState(!closed); checkOpened(); int bytesRead = dataSource.read(buffer, offset, length); @@ -96,15 +89,6 @@ public final class DataSourceInputStream extends InputStream { } } - @Override - public long skip(long byteCount) throws IOException { - Assertions.checkState(!closed); - checkOpened(); - long bytesSkipped = super.skip(byteCount); - totalBytesRead += bytesSkipped; - return bytesSkipped; - } - @Override public void close() throws IOException { if (!closed) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSpec.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSpec.java index b76c724b0..ebc88d6da 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSpec.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DataSpec.java @@ -32,7 +32,7 @@ public final class DataSpec { * The flags that apply to any request for data. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_ALLOW_GZIP}) + @IntDef(flag = true, value = {FLAG_ALLOW_GZIP, FLAG_ALLOW_CACHING_UNKNOWN_LENGTH}) public @interface Flags {} /** * Permits an underlying network stack to request that the server use gzip compression. @@ -45,7 +45,12 @@ public final class DataSpec { * {@link DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNSET}. The data read from * {@link DataSource#read(byte[], int, int)} will be the decompressed data. */ - public static final int FLAG_ALLOW_GZIP = 1; + public static final int FLAG_ALLOW_GZIP = 1 << 0; + + /** + * Permits content to be cached even if its length can not be resolved. + */ + public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; /** * The source from which data should be read. @@ -76,10 +81,10 @@ public final class DataSpec { */ public final String key; /** - * Request flags. Currently {@link #FLAG_ALLOW_GZIP} is the only supported flag. + * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and + * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. */ - @Flags - public final int flags; + @Flags public final int flags; /** * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. @@ -167,6 +172,15 @@ public final class DataSpec { this.flags = flags; } + /** + * Returns whether the given flag is set. + * + * @param flag Flag to be checked if it is set. + */ + public boolean isFlagSet(@Flags int flag) { + return (this.flags & flag) == flag; + } + @Override public String toString() { return "DataSpec[" + uri + ", " + Arrays.toString(postBody) + ", " + absoluteStreamPosition diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultDataSource.java index 44ff80f1e..2a13b5421 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultDataSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultDataSource.java @@ -81,7 +81,7 @@ public final class DefaultDataSource implements DataSource { boolean allowCrossProtocolRedirects) { this(context, listener, new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis, - readTimeoutMillis, allowCrossProtocolRedirects)); + readTimeoutMillis, allowCrossProtocolRedirects, null)); } /** diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultHttpDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultHttpDataSource.java index 3591f6198..6c041917a 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultHttpDataSource.java @@ -32,7 +32,6 @@ import java.net.HttpURLConnection; import java.net.NoRouteToHostException; import java.net.ProtocolException; import java.net.URL; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -44,8 +43,8 @@ import java.util.regex.Pattern; *

    * By default this implementation will not follow cross-protocol redirects (i.e. redirects from * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the - * {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean)} - * constructor and passing {@code true} as the final argument. + * {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean, + * RequestProperties)} constructor and passing {@code true} as the second last argument. */ public class DefaultHttpDataSource implements HttpDataSource { @@ -70,7 +69,8 @@ public class DefaultHttpDataSource implements HttpDataSource { private final int readTimeoutMillis; private final String userAgent; private final Predicate contentTypePredicate; - private final HashMap requestProperties; + private final RequestProperties defaultRequestProperties; + private final RequestProperties requestProperties; private final TransferListener listener; private DataSpec dataSpec; @@ -121,7 +121,8 @@ public class DefaultHttpDataSource implements HttpDataSource { public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) { - this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false); + this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false, + null); } /** @@ -137,17 +138,21 @@ public class DefaultHttpDataSource implements HttpDataSource { * 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, Predicate contentTypePredicate, TransferListener listener, int connectTimeoutMillis, - int readTimeoutMillis, boolean allowCrossProtocolRedirects) { + int readTimeoutMillis, boolean allowCrossProtocolRedirects, + RequestProperties defaultRequestProperties) { this.userAgent = Assertions.checkNotEmpty(userAgent); this.contentTypePredicate = contentTypePredicate; this.listener = listener; - this.requestProperties = new HashMap<>(); + this.requestProperties = new RequestProperties(); this.connectTimeoutMillis = connectTimeoutMillis; this.readTimeoutMillis = readTimeoutMillis; this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; + this.defaultRequestProperties = defaultRequestProperties; } @Override @@ -164,24 +169,18 @@ public class DefaultHttpDataSource implements HttpDataSource { public void setRequestProperty(String name, String value) { Assertions.checkNotNull(name); Assertions.checkNotNull(value); - synchronized (requestProperties) { - requestProperties.put(name, value); - } + requestProperties.set(name, value); } @Override public void clearRequestProperty(String name) { Assertions.checkNotNull(name); - synchronized (requestProperties) { - requestProperties.remove(name); - } + requestProperties.remove(name); } @Override public void clearAllRequestProperties() { - synchronized (requestProperties) { - requestProperties.clear(); - } + requestProperties.clear(); } @Override @@ -230,7 +229,7 @@ public class DefaultHttpDataSource implements HttpDataSource { bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; // Determine the length of the data to be read, after skipping. - if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) { + if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) { if (dataSpec.length != C.LENGTH_UNSET) { bytesToRead = dataSpec.length; } else { @@ -343,7 +342,7 @@ public class DefaultHttpDataSource implements HttpDataSource { byte[] postBody = dataSpec.postBody; long position = dataSpec.position; long length = dataSpec.length; - boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0; + boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP); if (!allowCrossProtocolRedirects) { // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection @@ -394,11 +393,14 @@ public class DefaultHttpDataSource implements HttpDataSource { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(connectTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); - synchronized (requestProperties) { - for (Map.Entry property : requestProperties.entrySet()) { + if (defaultRequestProperties != null) { + for (Map.Entry property : defaultRequestProperties.getSnapshot().entrySet()) { connection.setRequestProperty(property.getKey(), property.getValue()); } } + for (Map.Entry property : requestProperties.getSnapshot().entrySet()) { + connection.setRequestProperty(property.getKey(), property.getValue()); + } if (!(position == 0 && length == C.LENGTH_UNSET)) { String rangeRequest = "bytes=" + position + "-"; if (length != C.LENGTH_UNSET) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultHttpDataSourceFactory.java index 1b0c06910..de991cc31 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultHttpDataSourceFactory.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DefaultHttpDataSourceFactory.java @@ -15,10 +15,11 @@ */ package org.telegram.messenger.exoplayer2.upstream; +import org.telegram.messenger.exoplayer2.upstream.HttpDataSource.BaseFactory; import org.telegram.messenger.exoplayer2.upstream.HttpDataSource.Factory; /** A {@link Factory} that produces {@link DefaultHttpDataSource} instances. */ -public final class DefaultHttpDataSourceFactory implements Factory { +public final class DefaultHttpDataSourceFactory extends BaseFactory { private final String userAgent; private final TransferListener listener; @@ -75,8 +76,10 @@ public final class DefaultHttpDataSourceFactory implements Factory { } @Override - public DefaultHttpDataSource createDataSource() { + protected DefaultHttpDataSource createDataSourceInternal( + HttpDataSource.RequestProperties defaultRequestProperties) { return new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis, - readTimeoutMillis, allowCrossProtocolRedirects); + readTimeoutMillis, allowCrossProtocolRedirects, defaultRequestProperties); } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DummyDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DummyDataSource.java new file mode 100755 index 000000000..36676dd96 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/DummyDataSource.java @@ -0,0 +1,58 @@ +/* + * 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 org.telegram.messenger.exoplayer2.upstream; + +import android.net.Uri; +import java.io.IOException; + +/** + * A dummy DataSource which provides no data. {@link #open(DataSpec)} throws {@link IOException}. + */ +public final class DummyDataSource implements DataSource { + + public static final DummyDataSource INSTANCE = new DummyDataSource(); + + /** A factory that that produces {@link DummyDataSource}. */ + public static final Factory FACTORY = new Factory() { + @Override + public DataSource createDataSource() { + return new DummyDataSource(); + } + }; + + private DummyDataSource() {} + + @Override + public long open(DataSpec dataSpec) throws IOException { + throw new IOException("Dummy source"); + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public Uri getUri() { + return null; + } + + @Override + public void close() throws IOException { + // do nothing. + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/HttpDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/HttpDataSource.java index 1e2a33ac8..9fbd024a8 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/HttpDataSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/HttpDataSource.java @@ -22,6 +22,8 @@ import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,6 +40,175 @@ public interface HttpDataSource extends DataSource { @Override HttpDataSource createDataSource(); + /** + * Gets the default request properties used by all {@link HttpDataSource}s created by the + * factory. Changes to the properties will be reflected in any future requests made by + * {@link HttpDataSource}s created by the factory. + * + * @return The default request properties of the factory. + */ + RequestProperties getDefaultRequestProperties(); + + /** + * Sets a default request header for {@link HttpDataSource} instances created by the factory. + * + * @deprecated Use {@link #getDefaultRequestProperties} instead. + * @param name The name of the header field. + * @param value The value of the field. + */ + @Deprecated + void setDefaultRequestProperty(String name, String value); + + /** + * Clears a default request header for {@link HttpDataSource} instances created by the factory. + * + * @deprecated Use {@link #getDefaultRequestProperties} instead. + * @param name The name of the header field. + */ + @Deprecated + void clearDefaultRequestProperty(String name); + + /** + * Clears all default request headers for all {@link HttpDataSource} instances created by the + * factory. + * + * @deprecated Use {@link #getDefaultRequestProperties} instead. + */ + @Deprecated + void clearAllDefaultRequestProperties(); + + } + + /** + * Stores HTTP request properties (aka HTTP headers) and provides methods to modify the headers + * in a thread safe way to avoid the potential of creating snapshots of an inconsistent or + * unintended state. + */ + final class RequestProperties { + + private final Map requestProperties; + private Map requestPropertiesSnapshot; + + public RequestProperties() { + requestProperties = new HashMap<>(); + } + + /** + * Sets the specified property {@code value} for the specified {@code name}. If a property for + * this name previously existed, the old value is replaced by the specified value. + * + * @param name The name of the request property. + * @param value The value of the request property. + */ + public synchronized void set(String name, String value) { + requestPropertiesSnapshot = null; + requestProperties.put(name, value); + } + + /** + * Sets the keys and values contained in the map. If a property previously existed, the old + * value is replaced by the specified value. If a property previously existed and is not in the + * map, the property is left unchanged. + * + * @param properties The request properties. + */ + public synchronized void set(Map properties) { + requestPropertiesSnapshot = null; + requestProperties.putAll(properties); + } + + /** + * Removes all properties previously existing and sets the keys and values of the map. + * + * @param properties The request properties. + */ + public synchronized void clearAndSet(Map properties) { + requestPropertiesSnapshot = null; + requestProperties.clear(); + requestProperties.putAll(properties); + } + + /** + * Removes a request property by name. + * + * @param name The name of the request property to remove. + */ + public synchronized void remove(String name) { + requestPropertiesSnapshot = null; + requestProperties.remove(name); + } + + /** + * Clears all request properties. + */ + public synchronized void clear() { + requestPropertiesSnapshot = null; + requestProperties.clear(); + } + + /** + * Gets a snapshot of the request properties. + * + * @return A snapshot of the request properties. + */ + public synchronized Map getSnapshot() { + if (requestPropertiesSnapshot == null) { + requestPropertiesSnapshot = Collections.unmodifiableMap(new HashMap<>(requestProperties)); + } + return requestPropertiesSnapshot; + } + + } + + /** + * Base implementation of {@link Factory} that sets default request properties. + */ + abstract class BaseFactory implements Factory { + + private final RequestProperties defaultRequestProperties; + + public BaseFactory() { + defaultRequestProperties = new RequestProperties(); + } + + @Override + public final HttpDataSource createDataSource() { + return createDataSourceInternal(defaultRequestProperties); + } + + @Override + public final RequestProperties getDefaultRequestProperties() { + return defaultRequestProperties; + } + + @Deprecated + @Override + public final void setDefaultRequestProperty(String name, String value) { + defaultRequestProperties.set(name, value); + } + + @Deprecated + @Override + public final void clearDefaultRequestProperty(String name) { + defaultRequestProperties.remove(name); + } + + @Deprecated + @Override + public final void clearAllDefaultRequestProperties() { + defaultRequestProperties.clear(); + } + + /** + * Called by {@link #createDataSource()} to create a {@link HttpDataSource} instance. + * + * @param defaultRequestProperties The default {@code RequestProperties} to be used by the + * {@link HttpDataSource} instance. + * @return A {@link HttpDataSource} instance. + */ + protected abstract HttpDataSource createDataSourceInternal(RequestProperties + defaultRequestProperties); + } /** @@ -67,8 +238,7 @@ public interface HttpDataSource extends DataSource { public static final int TYPE_READ = 2; public static final int TYPE_CLOSE = 3; - @Type - public final int type; + @Type public final int type; /** * The {@link DataSpec} associated with the current connection. @@ -150,7 +320,7 @@ public interface HttpDataSource extends DataSource { int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException; /** - * Sets the value of a request header field. The value will be used for subsequent connections + * Sets the value of a request header. The value will be used for subsequent connections * established by the source. * * @param name The name of the header field. @@ -159,7 +329,7 @@ public interface HttpDataSource extends DataSource { void setRequestProperty(String name, String value); /** - * Clears the value of a request header field. The change will apply to subsequent connections + * Clears the value of a request header. The change will apply to subsequent connections * established by the source. * * @param name The name of the header field. @@ -167,7 +337,7 @@ public interface HttpDataSource extends DataSource { void clearRequestProperty(String name); /** - * Clears all request header fields that were set by {@link #setRequestProperty(String, String)}. + * Clears all request headers that were set by {@link #setRequestProperty(String, String)}. */ void clearAllRequestProperties(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/Loader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/Loader.java index 86a76f313..3ed8b84f9 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/Loader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/Loader.java @@ -199,7 +199,7 @@ public final class Loader implements LoaderErrorThrower { currentTask.cancel(true); } if (postLoadAction != null) { - downloadExecutorService.submit(postLoadAction); + downloadExecutorService.execute(postLoadAction); } downloadExecutorService.shutdown(); } @@ -260,7 +260,7 @@ public final class Loader implements LoaderErrorThrower { if (delayMillis > 0) { sendEmptyMessageDelayed(MSG_START, delayMillis); } else { - submitToExecutor(); + execute(); } } @@ -334,7 +334,7 @@ public final class Loader implements LoaderErrorThrower { return; } if (msg.what == MSG_START) { - submitToExecutor(); + execute(); return; } if (msg.what == MSG_FATAL_ERROR) { @@ -367,9 +367,9 @@ public final class Loader implements LoaderErrorThrower { } } - private void submitToExecutor() { + private void execute() { currentError = null; - downloadExecutorService.submit(currentTask); + downloadExecutorService.execute(currentTask); } private void finish() { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/ParsingLoadable.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/ParsingLoadable.java index 92f2838e9..0ecc65cd8 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/ParsingLoadable.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/ParsingLoadable.java @@ -19,6 +19,7 @@ import android.net.Uri; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.ParserException; import org.telegram.messenger.exoplayer2.upstream.Loader.Loadable; +import org.telegram.messenger.exoplayer2.util.Util; import java.io.IOException; import java.io.InputStream; @@ -58,7 +59,7 @@ public final class ParsingLoadable implements Loadable { public final int type; private final DataSource dataSource; - private final Parser parser; + private final Parser parser; private volatile T result; private volatile boolean isCanceled; @@ -70,7 +71,7 @@ public final class ParsingLoadable implements Loadable { * @param type See {@link #type}. * @param parser Parses the object from the response. */ - public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser parser) { + public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser parser) { this.dataSource = dataSource; this.dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); this.type = type; @@ -114,7 +115,7 @@ public final class ParsingLoadable implements Loadable { result = parser.parse(dataSource.getUri(), inputStream); } finally { bytesLoaded = inputStream.bytesRead(); - inputStream.close(); + Util.closeQuietly(inputStream); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/PriorityDataSourceFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/PriorityDataSourceFactory.java new file mode 100755 index 000000000..055a80216 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/PriorityDataSourceFactory.java @@ -0,0 +1,49 @@ +/* + * 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 org.telegram.messenger.exoplayer2.upstream; + +import org.telegram.messenger.exoplayer2.upstream.DataSource.Factory; +import org.telegram.messenger.exoplayer2.util.PriorityTaskManager; + +/** + * A {@link DataSource.Factory} that produces {@link PriorityDataSource} instances. + */ +public final class PriorityDataSourceFactory implements Factory { + + private final Factory upstreamFactory; + private final PriorityTaskManager priorityTaskManager; + private final int priority; + + /** + * @param upstreamFactory A {@link DataSource.Factory} to be used to create an upstream {@link + * DataSource} for {@link PriorityDataSource}. + * @param priorityTaskManager The priority manager to which PriorityDataSource task is registered. + * @param priority The priority of PriorityDataSource task. + */ + public PriorityDataSourceFactory(Factory upstreamFactory, PriorityTaskManager priorityTaskManager, + int priority) { + this.upstreamFactory = upstreamFactory; + this.priorityTaskManager = priorityTaskManager; + this.priority = priority; + } + + @Override + public PriorityDataSource createDataSource() { + return new PriorityDataSource(upstreamFactory.createDataSource(), priorityTaskManager, + priority); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/Cache.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/Cache.java index eb7bdb96d..4e6c9f589 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/Cache.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/Cache.java @@ -198,6 +198,18 @@ public interface Cache { */ boolean isCached(String key, long position, long length); + /** + * Returns the length of the cached data block starting from the {@code position} to the block end + * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap + * to the next cached data up to {@code length} bytes) is returned. + * + * @param key The cache key for the data. + * @param position The starting position of the data. + * @param length The maximum length of the data to be returned. + * @return the length of the cached or not cached data block length. + */ + long getCachedBytes(String key, long position, long length); + /** * Sets the content length for the given key. * diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSink.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSink.java index 1907a91ae..611835519 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSink.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSink.java @@ -32,6 +32,9 @@ import java.io.OutputStream; */ public final class CacheDataSink implements DataSink { + /** Default buffer size. */ + public static final int DEFAULT_BUFFER_SIZE = 20480; + private final Cache cache; private final long maxCacheFileSize; private final int bufferSize; @@ -56,13 +59,15 @@ public final class CacheDataSink implements DataSink { } /** + * Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}. + * * @param cache The cache into which data should be written. * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into * multiple cache files. */ public CacheDataSink(Cache cache, long maxCacheFileSize) { - this(cache, maxCacheFileSize, 0); + this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE); } /** @@ -81,10 +86,12 @@ public final class CacheDataSink implements DataSink { @Override public void open(DataSpec dataSpec) throws CacheDataSinkException { - this.dataSpec = dataSpec; - if (dataSpec.length == C.LENGTH_UNSET) { + if (dataSpec.length == C.LENGTH_UNSET + && !dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)) { + this.dataSpec = null; return; } + this.dataSpec = dataSpec; dataSpecBytesWritten = 0; try { openNextOutputStream(); @@ -95,7 +102,7 @@ public final class CacheDataSink implements DataSink { @Override public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException { - if (dataSpec.length == C.LENGTH_UNSET) { + if (dataSpec == null) { return; } try { @@ -119,7 +126,7 @@ public final class CacheDataSink implements DataSink { @Override public void close() throws CacheDataSinkException { - if (dataSpec == null || dataSpec.length == C.LENGTH_UNSET) { + if (dataSpec == null) { return; } try { @@ -130,8 +137,10 @@ public final class CacheDataSink implements DataSink { } private void openNextOutputStream() throws IOException { + long maxLength = dataSpec.length == C.LENGTH_UNSET ? maxCacheFileSize + : Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize); file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, - Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize)); + maxLength); underlyingFileOutputStream = new FileOutputStream(file); if (bufferSize > 0) { if (bufferedOutputStream == null) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSinkFactory.java index 6daf7470a..ea98c6b42 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Sink Project + * 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. @@ -24,18 +24,27 @@ public final class CacheDataSinkFactory implements DataSink.Factory { private final Cache cache; private final long maxCacheFileSize; + private final int bufferSize; /** * @see CacheDataSink#CacheDataSink(Cache, long) */ public CacheDataSinkFactory(Cache cache, long maxCacheFileSize) { + this(cache, maxCacheFileSize, CacheDataSink.DEFAULT_BUFFER_SIZE); + } + + /** + * @see CacheDataSink#CacheDataSink(Cache, long, int) + */ + public CacheDataSinkFactory(Cache cache, long maxCacheFileSize, int bufferSize) { this.cache = cache; this.maxCacheFileSize = maxCacheFileSize; + this.bufferSize = bufferSize; } @Override public DataSink createDataSink() { - return new CacheDataSink(cache, maxCacheFileSize); + return new CacheDataSink(cache, maxCacheFileSize, bufferSize); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSource.java index 7e81516e1..f55aba193 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSource.java @@ -17,6 +17,7 @@ package org.telegram.messenger.exoplayer2.upstream.cache; import android.net.Uri; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.upstream.DataSink; import org.telegram.messenger.exoplayer2.upstream.DataSource; @@ -89,7 +90,7 @@ public final class CacheDataSource implements DataSource { private final DataSource cacheReadDataSource; private final DataSource cacheWriteDataSource; private final DataSource upstreamDataSource; - private final EventListener eventListener; + @Nullable private final EventListener eventListener; private final boolean blockOnCache; private final boolean ignoreCacheOnError; @@ -142,13 +143,14 @@ public final class CacheDataSource implements DataSource { * @param cache The cache. * @param upstream A {@link DataSource} for reading data not in the cache. * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. - * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. + * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is + * accessed read-only. * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. * @param eventListener An optional {@link EventListener} to receive events. */ public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, - DataSink cacheWriteDataSink, @Flags int flags, EventListener eventListener) { + DataSink cacheWriteDataSink, @Flags int flags, @Nullable EventListener eventListener) { this.cache = cache; this.cacheReadDataSource = cacheReadDataSource; this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0; @@ -169,7 +171,7 @@ public final class CacheDataSource implements DataSource { try { uri = dataSpec.uri; flags = dataSpec.flags; - key = dataSpec.key != null ? dataSpec.key : uri.toString(); + key = CacheUtil.getKey(dataSpec); readPosition = dataSpec.position; currentRequestIgnoresCache = (ignoreCacheOnError && seenCacheError) || (dataSpec.length == C.LENGTH_UNSET && ignoreCacheForUnsetLengthRequests); @@ -179,6 +181,9 @@ public final class CacheDataSource implements DataSource { bytesRemaining = cache.getContentLength(key); if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= dataSpec.position; + if (bytesRemaining <= 0) { + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); + } } } openNextSource(true); @@ -283,7 +288,6 @@ public final class CacheDataSource implements DataSource { currentDataSource = cacheReadDataSource; } else { // Data is not cached, and data is not locked, read from upstream with cache backing. - lockedSpan = span; long length; if (span.isOpenEnded()) { length = bytesRemaining; @@ -294,8 +298,13 @@ public final class CacheDataSource implements DataSource { } } dataSpec = new DataSpec(uri, readPosition, length, key, flags); - currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource - : upstreamDataSource; + if (cacheWriteDataSource != null) { + currentDataSource = cacheWriteDataSource; + lockedSpan = span; + } else { + currentDataSource = upstreamDataSource; + cache.releaseHoleSpan(span); + } } currentRequestUnbounded = dataSpec.length == C.LENGTH_UNSET; @@ -330,16 +339,16 @@ public final class CacheDataSource implements DataSource { // bytesRemaining == C.LENGTH_UNSET) and got a resolved length from open() request if (currentRequestUnbounded && currentBytesRemaining != C.LENGTH_UNSET) { bytesRemaining = currentBytesRemaining; - // If writing into cache - if (lockedSpan != null) { - setContentLength(dataSpec.position + bytesRemaining); - } + setContentLength(dataSpec.position + bytesRemaining); } return successful; } private void setContentLength(long length) throws IOException { - cache.setContentLength(key, length); + // If writing into cache + if (currentDataSource == cacheWriteDataSource) { + cache.setContentLength(key, length); + } } private void closeCurrentSource() throws IOException { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSourceFactory.java index 983dd3129..173099e21 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSourceFactory.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheDataSourceFactory.java @@ -65,10 +65,11 @@ public final class CacheDataSourceFactory implements DataSource.Factory { } @Override - public DataSource createDataSource() { + public CacheDataSource createDataSource() { return new CacheDataSource(cache, upstreamFactory.createDataSource(), cacheReadDataSourceFactory.createDataSource(), - cacheWriteDataSinkFactory.createDataSink(), flags, eventListener); + cacheWriteDataSinkFactory != null ? cacheWriteDataSinkFactory.createDataSink() : null, + flags, eventListener); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheSpan.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheSpan.java index daf042b9b..78f436a0a 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheSpan.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheSpan.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.upstream.cache; +import android.support.annotation.NonNull; import org.telegram.messenger.exoplayer2.C; import java.io.File; @@ -95,7 +96,7 @@ public class CacheSpan implements Comparable { } @Override - public int compareTo(CacheSpan another) { + public int compareTo(@NonNull CacheSpan another) { if (!key.equals(another.key)) { return key.compareTo(another.key); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheUtil.java new file mode 100755 index 000000000..91e36336e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CacheUtil.java @@ -0,0 +1,237 @@ +/* + * 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 org.telegram.messenger.exoplayer2.upstream.cache; + +import android.net.Uri; +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.upstream.DataSource; +import org.telegram.messenger.exoplayer2.upstream.DataSpec; +import org.telegram.messenger.exoplayer2.util.Assertions; +import org.telegram.messenger.exoplayer2.util.PriorityTaskManager; +import org.telegram.messenger.exoplayer2.util.Util; +import java.io.IOException; +import java.util.NavigableSet; + +/** + * Caching related utility methods. + */ +public final class CacheUtil { + + /** Holds the counters used during caching. */ + public static class CachingCounters { + /** Total number of already cached bytes. */ + public long alreadyCachedBytes; + /** + * Total number of downloaded bytes. + * + *

    {@link #getCached(DataSpec, Cache, CachingCounters)} sets it to the count of the missing + * bytes or to {@link C#LENGTH_UNSET} if {@code dataSpec} is unbounded and content length isn't + * available in the {@code cache}. + */ + public long downloadedBytes; + } + + /** + * Generates a cache key out of the given {@link Uri}. + * + * @param uri Uri of a content which the requested key is for. + */ + public static String generateKey(Uri uri) { + return uri.toString(); + } + + /** + * Returns the {@code dataSpec.key} if not null, otherwise generates a cache key out of {@code + * dataSpec.uri} + * + * @param dataSpec Defines a content which the requested key is for. + */ + public static String getKey(DataSpec dataSpec) { + return dataSpec.key != null ? dataSpec.key : generateKey(dataSpec.uri); + } + + /** + * Returns already cached and missing bytes in the {@cache} for the data defined by {@code + * dataSpec}. + * + * @param dataSpec Defines the data to be checked. + * @param cache A {@link Cache} which has the data. + * @param counters The counters to be set. If null a new {@link CachingCounters} is created and + * used. + * @return The used {@link CachingCounters} instance. + */ + public static CachingCounters getCached(DataSpec dataSpec, Cache cache, + CachingCounters counters) { + try { + return internalCache(dataSpec, cache, null, null, null, 0, counters); + } catch (IOException | InterruptedException e) { + throw new IllegalStateException(e); + } + } + + /** + * Caches the data defined by {@code dataSpec} while skipping already cached data. + * + * @param dataSpec Defines the data to be cached. + * @param cache A {@link Cache} to store the data. + * @param dataSource A {@link CacheDataSource} that works on the {@code cache}. + * @param buffer The buffer to be used while caching. + * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with + * caching. + * @param priority The priority of this task. Used with {@code priorityTaskManager}. + * @param counters The counters to be set during caching. If not null its values reset to + * zero before using. If null a new {@link CachingCounters} is created and used. + * @return The used {@link CachingCounters} instance. + * @throws IOException If an error occurs reading from the source. + * @throws InterruptedException If the thread was interrupted. + */ + public static CachingCounters cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource, + byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, + CachingCounters counters) throws IOException, InterruptedException { + Assertions.checkNotNull(dataSource); + Assertions.checkNotNull(buffer); + return internalCache(dataSpec, cache, dataSource, buffer, priorityTaskManager, priority, + counters); + } + + /** + * Caches the data defined by {@code dataSpec} while skipping already cached data. If {@code + * dataSource} or {@code buffer} is null performs a dry run. + * + * @param dataSpec Defines the data to be cached. + * @param cache A {@link Cache} to store the data. + * @param dataSource A {@link CacheDataSource} that works on the {@code cache}. If null a dry run + * is performed. + * @param buffer The buffer to be used while caching. If null a dry run is performed. + * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with + * caching. + * @param priority The priority of this task. Used with {@code priorityTaskManager}. + * @param counters The counters to be set during caching. If not null its values reset to + * zero before using. If null a new {@link CachingCounters} is created and used. + * @return The used {@link CachingCounters} instance. + * @throws IOException If not dry run and an error occurs reading from the source. + * @throws InterruptedException If not dry run and the thread was interrupted. + */ + private static CachingCounters internalCache(DataSpec dataSpec, Cache cache, + CacheDataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, + int priority, CachingCounters counters) throws IOException, InterruptedException { + long start = dataSpec.position; + long left = dataSpec.length; + String key = getKey(dataSpec); + if (left == C.LENGTH_UNSET) { + left = cache.getContentLength(key); + if (left == C.LENGTH_UNSET) { + left = Long.MAX_VALUE; + } + } + if (counters == null) { + counters = new CachingCounters(); + } else { + counters.alreadyCachedBytes = 0; + counters.downloadedBytes = 0; + } + while (left > 0) { + long blockLength = cache.getCachedBytes(key, start, left); + // Skip already cached data + if (blockLength > 0) { + counters.alreadyCachedBytes += blockLength; + } else { + // There is a hole in the cache which is at least "-blockLength" long. + blockLength = -blockLength; + if (dataSource != null && buffer != null) { + DataSpec subDataSpec = new DataSpec(dataSpec.uri, start, + blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength, key); + long read = readAndDiscard(subDataSpec, dataSource, buffer, priorityTaskManager, + priority); + counters.downloadedBytes += read; + if (read < blockLength) { + // Reached end of data. + break; + } + } else if (blockLength == Long.MAX_VALUE) { + counters.downloadedBytes = C.LENGTH_UNSET; + break; + } else { + counters.downloadedBytes += blockLength; + } + } + start += blockLength; + if (left != Long.MAX_VALUE) { + left -= blockLength; + } + } + return counters; + } + + /** + * Reads and discards all data specified by the {@code dataSpec}. + * + * @param dataSpec Defines the data to be read. + * @param dataSource The {@link DataSource} to read the data from. + * @param buffer The buffer to be used while downloading. + * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with + * caching. + * @param priority The priority of this task. + * @return Number of read bytes, or 0 if no data is available because the end of the opened range + * has been reached. + */ + private static long readAndDiscard(DataSpec dataSpec, DataSource dataSource, byte[] buffer, + PriorityTaskManager priorityTaskManager, int priority) + throws IOException, InterruptedException { + while (true) { + if (priorityTaskManager != null) { + // Wait for any other thread with higher priority to finish its job. + priorityTaskManager.proceed(priority); + } + try { + dataSource.open(dataSpec); + long totalRead = 0; + while (true) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + int read = dataSource.read(buffer, 0, buffer.length); + if (read == C.RESULT_END_OF_INPUT) { + return totalRead; + } + totalRead += read; + } + } catch (PriorityTaskManager.PriorityTooLowException exception) { + // catch and try again + } finally { + Util.closeQuietly(dataSource); + } + } + } + + /** Removes all of the data in the {@code cache} pointed by the {@code key}. */ + public static void remove(Cache cache, String key) { + NavigableSet cachedSpans = cache.getCachedSpans(key); + if (cachedSpans == null) { + return; + } + for (CacheSpan cachedSpan : cachedSpans) { + try { + cache.removeSpan(cachedSpan); + } catch (Cache.CacheException e) { + // do nothing + } + } + } + + private CacheUtil() {} + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CachedContent.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CachedContent.java index d41a992f4..ab152a21c 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CachedContent.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CachedContent.java @@ -106,43 +106,49 @@ import java.util.TreeSet; * which defines the maximum extents of the hole in the cache. */ public SimpleCacheSpan getSpan(long position) { - SimpleCacheSpan span = getSpanInternal(position); - if (!span.isCached) { - SimpleCacheSpan ceilEntry = cachedSpans.ceiling(span); - return ceilEntry == null ? SimpleCacheSpan.createOpenHole(key, position) - : SimpleCacheSpan.createClosedHole(key, position, ceilEntry.position - position); + SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position); + SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan); + if (floorSpan != null && floorSpan.position + floorSpan.length > position) { + return floorSpan; } - return span; + SimpleCacheSpan ceilSpan = cachedSpans.ceiling(lookupSpan); + return ceilSpan == null ? SimpleCacheSpan.createOpenHole(key, position) + : SimpleCacheSpan.createClosedHole(key, position, ceilSpan.position - position); } - /** Queries if a range is entirely available in the cache. */ - public boolean isCached(long position, long length) { - SimpleCacheSpan floorSpan = getSpanInternal(position); - if (!floorSpan.isCached) { + /** + * Returns the length of the cached data block starting from the {@code position} to the block end + * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap + * to the next cached data up to {@code length} bytes) is returned. + * + * @param position The starting position of the data. + * @param length The maximum length of the data to be returned. + * @return the length of the cached or not cached data block length. + */ + public long getCachedBytes(long position, long length) { + SimpleCacheSpan span = getSpan(position); + if (span.isHoleSpan()) { // We don't have a span covering the start of the queried region. - return false; + return -Math.min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length); } long queryEndPosition = position + length; - long currentEndPosition = floorSpan.position + floorSpan.length; - if (currentEndPosition >= queryEndPosition) { - // floorSpan covers the queried region. - return true; - } - for (SimpleCacheSpan next : cachedSpans.tailSet(floorSpan, false)) { - if (next.position > currentEndPosition) { - // There's a hole in the cache within the queried region. - return false; - } - // We expect currentEndPosition to always equal (next.position + next.length), but - // perform a max check anyway to guard against the existence of overlapping spans. - currentEndPosition = Math.max(currentEndPosition, next.position + next.length); - if (currentEndPosition >= queryEndPosition) { - // We've found spans covering the queried region. - return true; + long currentEndPosition = span.position + span.length; + if (currentEndPosition < queryEndPosition) { + for (SimpleCacheSpan next : cachedSpans.tailSet(span, false)) { + if (next.position > currentEndPosition) { + // There's a hole in the cache within the queried region. + break; + } + // We expect currentEndPosition to always equal (next.position + next.length), but + // perform a max check anyway to guard against the existence of overlapping spans. + currentEndPosition = Math.max(currentEndPosition, next.position + next.length); + if (currentEndPosition >= queryEndPosition) { + // We've found spans covering the queried region. + break; + } } } - // We ran out of spans before covering the queried region. - return false; + return Math.min(currentEndPosition - position, length); } /** @@ -190,15 +196,4 @@ import java.util.TreeSet; return result; } - /** - * Returns the span containing the position. If there isn't one, it returns the lookup span it - * used for searching. - */ - private SimpleCacheSpan getSpanInternal(long position) { - SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position); - SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan); - return floorSpan == null || floorSpan.position + floorSpan.length <= position ? lookupSpan - : floorSpan; - } - } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CachedContentIndex.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CachedContentIndex.java index 63a079991..256d5bc5a 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CachedContentIndex.java @@ -15,6 +15,7 @@ */ package org.telegram.messenger.exoplayer2.upstream.cache; +import android.util.Log; import android.util.SparseArray; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.upstream.cache.Cache.CacheException; @@ -26,6 +27,7 @@ import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -55,6 +57,8 @@ import javax.crypto.spec.SecretKeySpec; private static final int FLAG_ENCRYPTED_INDEX = 1; + private static final String TAG = "CachedContentIndex"; + private final HashMap keyToContent; private final SparseArray idToKey; private final AtomicFile atomicFile; @@ -63,14 +67,25 @@ import javax.crypto.spec.SecretKeySpec; private boolean changed; private ReusableBufferedOutputStream bufferedOutputStream; - /** Creates a CachedContentIndex which works on the index file in the given cacheDir. */ + /** + * Creates a CachedContentIndex which works on the index file in the given cacheDir. + * + * @param cacheDir Directory where the index file is kept. + */ public CachedContentIndex(File cacheDir) { this(cacheDir, null); } - /** Creates a CachedContentIndex which works on the index file in the given cacheDir. */ + /** + * Creates a CachedContentIndex which works on the index file in the given cacheDir. + * + * @param cacheDir Directory where the index file is kept. + * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC. + * The key must be 16 bytes long. + */ public CachedContentIndex(File cacheDir, byte[] secretKey) { if (secretKey != null) { + Assertions.checkArgument(secretKey.length == 16); try { cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); secretKeySpec = new SecretKeySpec(secretKey, "AES"); @@ -224,7 +239,7 @@ import javax.crypto.spec.SecretKeySpec; return false; } byte[] initializationVector = new byte[16]; - input.read(initializationVector); + input.readFully(initializationVector); IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); try { cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); @@ -232,19 +247,26 @@ import javax.crypto.spec.SecretKeySpec; throw new IllegalStateException(e); } input = new DataInputStream(new CipherInputStream(inputStream, cipher)); + } else { + if (cipher != null) { + changed = true; // Force index to be rewritten encrypted after read. + } } int count = input.readInt(); int hashCode = 0; for (int i = 0; i < count; i++) { CachedContent cachedContent = new CachedContent(input); - addNew(cachedContent); + add(cachedContent); hashCode += cachedContent.headerHashCode(); } if (input.readInt() != hashCode) { return false; } + } catch (FileNotFoundException e) { + return false; } catch (IOException e) { + Log.e(TAG, "Error reading cache content index file.", e); return false; } finally { if (input != null) { @@ -291,6 +313,9 @@ import javax.crypto.spec.SecretKeySpec; } output.writeInt(hashCode); atomicFile.endWrite(output); + // Avoid calling close twice. Duplicate CipherOutputStream.close calls did + // not used to be no-ops: https://android-review.googlesource.com/#/c/272799/ + output = null; } catch (IOException e) { throw new CacheException(e); } finally { @@ -298,10 +323,14 @@ import javax.crypto.spec.SecretKeySpec; } } - /** Adds the given CachedContent to the index. */ - /*package*/ void addNew(CachedContent cachedContent) { + private void add(CachedContent cachedContent) { keyToContent.put(cachedContent.key, cachedContent); idToKey.put(cachedContent.id, cachedContent.key); + } + + /** Adds the given CachedContent to the index. */ + /*package*/ void addNew(CachedContent cachedContent) { + add(cachedContent); changed = true; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CachedRegionTracker.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CachedRegionTracker.java new file mode 100755 index 000000000..fbafb84c3 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/CachedRegionTracker.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2.upstream.cache; + +import android.support.annotation.NonNull; +import android.util.Log; +import org.telegram.messenger.exoplayer2.extractor.ChunkIndex; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NavigableSet; +import java.util.TreeSet; + +/** + * Utility class for efficiently tracking regions of data that are stored in a {@link Cache} + * for a given cache key. + */ +public final class CachedRegionTracker implements Cache.Listener { + + private static final String TAG = "CachedRegionTracker"; + + public static final int NOT_CACHED = -1; + public static final int CACHED_TO_END = -2; + + private final Cache cache; + private final String cacheKey; + private final ChunkIndex chunkIndex; + + private final TreeSet regions; + private final Region lookupRegion; + + public CachedRegionTracker(Cache cache, String cacheKey, ChunkIndex chunkIndex) { + this.cache = cache; + this.cacheKey = cacheKey; + this.chunkIndex = chunkIndex; + this.regions = new TreeSet<>(); + this.lookupRegion = new Region(0, 0); + + synchronized (this) { + NavigableSet cacheSpans = cache.addListener(cacheKey, this); + if (cacheSpans != null) { + // Merge the spans into regions. mergeSpan is more efficient when merging from high to low, + // which is why a descending iterator is used here. + Iterator spanIterator = cacheSpans.descendingIterator(); + while (spanIterator.hasNext()) { + CacheSpan span = spanIterator.next(); + mergeSpan(span); + } + } + } + } + + public void release() { + cache.removeListener(cacheKey, this); + } + + /** + * When provided with a byte offset, this method locates the cached region within which the + * offset falls, and returns the approximate end position in milliseconds of that region. If the + * byte offset does not fall within a cached region then {@link #NOT_CACHED} is returned. + * If the cached region extends to the end of the stream, {@link #CACHED_TO_END} is returned. + * + * @param byteOffset The byte offset in the underlying stream. + * @return The end position of the corresponding cache region, {@link #NOT_CACHED}, or + * {@link #CACHED_TO_END}. + */ + public synchronized int getRegionEndTimeMs(long byteOffset) { + lookupRegion.startOffset = byteOffset; + Region floorRegion = regions.floor(lookupRegion); + if (floorRegion == null || byteOffset > floorRegion.endOffset + || floorRegion.endOffsetIndex == -1) { + return NOT_CACHED; + } + int index = floorRegion.endOffsetIndex; + if (index == chunkIndex.length - 1 + && floorRegion.endOffset == (chunkIndex.offsets[index] + chunkIndex.sizes[index])) { + return CACHED_TO_END; + } + long segmentFractionUs = (chunkIndex.durationsUs[index] + * (floorRegion.endOffset - chunkIndex.offsets[index])) / chunkIndex.sizes[index]; + return (int) ((chunkIndex.timesUs[index] + segmentFractionUs) / 1000); + } + + @Override + public synchronized void onSpanAdded(Cache cache, CacheSpan span) { + mergeSpan(span); + } + + @Override + public synchronized void onSpanRemoved(Cache cache, CacheSpan span) { + Region removedRegion = new Region(span.position, span.position + span.length); + + // Look up a region this span falls into. + Region floorRegion = regions.floor(removedRegion); + if (floorRegion == null) { + Log.e(TAG, "Removed a span we were not aware of"); + return; + } + + // Remove it. + regions.remove(floorRegion); + + // Add new floor and ceiling regions, if necessary. + if (floorRegion.startOffset < removedRegion.startOffset) { + Region newFloorRegion = new Region(floorRegion.startOffset, removedRegion.startOffset); + + int index = Arrays.binarySearch(chunkIndex.offsets, newFloorRegion.endOffset); + newFloorRegion.endOffsetIndex = index < 0 ? -index - 2 : index; + regions.add(newFloorRegion); + } + + if (floorRegion.endOffset > removedRegion.endOffset) { + Region newCeilingRegion = new Region(removedRegion.endOffset + 1, floorRegion.endOffset); + newCeilingRegion.endOffsetIndex = floorRegion.endOffsetIndex; + regions.add(newCeilingRegion); + } + } + + @Override + public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) { + // Do nothing. + } + + private void mergeSpan(CacheSpan span) { + Region newRegion = new Region(span.position, span.position + span.length); + Region floorRegion = regions.floor(newRegion); + Region ceilingRegion = regions.ceiling(newRegion); + boolean floorConnects = regionsConnect(floorRegion, newRegion); + boolean ceilingConnects = regionsConnect(newRegion, ceilingRegion); + + if (ceilingConnects) { + if (floorConnects) { + // Extend floorRegion to cover both newRegion and ceilingRegion. + floorRegion.endOffset = ceilingRegion.endOffset; + floorRegion.endOffsetIndex = ceilingRegion.endOffsetIndex; + } else { + // Extend newRegion to cover ceilingRegion. Add it. + newRegion.endOffset = ceilingRegion.endOffset; + newRegion.endOffsetIndex = ceilingRegion.endOffsetIndex; + regions.add(newRegion); + } + regions.remove(ceilingRegion); + } else if (floorConnects) { + // Extend floorRegion to the right to cover newRegion. + floorRegion.endOffset = newRegion.endOffset; + int index = floorRegion.endOffsetIndex; + while (index < chunkIndex.length - 1 + && (chunkIndex.offsets[index + 1] <= floorRegion.endOffset)) { + index++; + } + floorRegion.endOffsetIndex = index; + } else { + // This is a new region. + int index = Arrays.binarySearch(chunkIndex.offsets, newRegion.endOffset); + newRegion.endOffsetIndex = index < 0 ? -index - 2 : index; + regions.add(newRegion); + } + } + + private boolean regionsConnect(Region lower, Region upper) { + return lower != null && upper != null && lower.endOffset == upper.startOffset; + } + + private static class Region implements Comparable { + + /** + * The first byte of the region (inclusive). + */ + public long startOffset; + /** + * End offset of the region (exclusive). + */ + public long endOffset; + /** + * The index in chunkIndex that contains the end offset. May be -1 if the end offset comes + * before the start of the first media chunk (i.e. if the end offset is within the stream + * header). + */ + public int endOffsetIndex; + + public Region(long position, long endOffset) { + this.startOffset = position; + this.endOffset = endOffset; + } + + @Override + public int compareTo(@NonNull Region another) { + return startOffset < another.startOffset ? -1 + : startOffset == another.startOffset ? 0 : 1; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/SimpleCache.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/SimpleCache.java index 2f03e9fa8..c24d5d580 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/SimpleCache.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/cache/SimpleCache.java @@ -354,7 +354,13 @@ public final class SimpleCache implements Cache { @Override public synchronized boolean isCached(String key, long position, long length) { CachedContent cachedContent = index.get(key); - return cachedContent != null && cachedContent.isCached(position, length); + return cachedContent != null && cachedContent.getCachedBytes(position, length) >= length; + } + + @Override + public synchronized long getCachedBytes(String key, long position, long length) { + CachedContent cachedContent = index.get(key); + return cachedContent != null ? cachedContent.getCachedBytes(position, length) : -length; } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/AesCipherDataSink.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/AesCipherDataSink.java new file mode 100755 index 000000000..86d686360 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/AesCipherDataSink.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2.upstream.crypto; + +import org.telegram.messenger.exoplayer2.upstream.DataSink; +import org.telegram.messenger.exoplayer2.upstream.DataSpec; +import java.io.IOException; +import javax.crypto.Cipher; + +/** + * A wrapping {@link DataSink} that encrypts the data being consumed. + */ +public final class AesCipherDataSink implements DataSink { + + private final DataSink wrappedDataSink; + private final byte[] secretKey; + private final byte[] scratch; + + private AesFlushingCipher cipher; + + /** + * Create an instance whose {@code write} methods have the side effect of overwriting the input + * {@code data}. Use this constructor for maximum efficiency in the case that there is no + * requirement for the input data arrays to remain unchanged. + * + * @param secretKey The key data. + * @param wrappedDataSink The wrapped {@link DataSink}. + */ + public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink) { + this(secretKey, wrappedDataSink, null); + } + + /** + * Create an instance whose {@code write} methods are free of side effects. Use this constructor + * when the input data arrays are required to remain unchanged. + * + * @param secretKey The key data. + * @param wrappedDataSink The wrapped {@link DataSink}. + * @param scratch Scratch space. Data is decrypted into this array before being written to the + * wrapped {@link DataSink}. It should be of appropriate size for the expected writes. If a + * write is larger than the size of this array the write will still succeed, but multiple + * cipher calls will be required to complete the operation. + */ + public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, byte[] scratch) { + this.wrappedDataSink = wrappedDataSink; + this.secretKey = secretKey; + this.scratch = scratch; + } + + @Override + public void open(DataSpec dataSpec) throws IOException { + wrappedDataSink.open(dataSpec); + long nonce = CryptoUtil.getFNV64Hash(dataSpec.key); + cipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, secretKey, nonce, + dataSpec.absoluteStreamPosition); + } + + @Override + 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); + 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); + bytesProcessed += bytesToProcess; + } + } + } + + @Override + public void close() throws IOException { + cipher = null; + wrappedDataSink.close(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/AesCipherDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/AesCipherDataSource.java new file mode 100755 index 000000000..087aa727b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/AesCipherDataSource.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2.upstream.crypto; + +import android.net.Uri; +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.upstream.DataSource; +import org.telegram.messenger.exoplayer2.upstream.DataSpec; +import java.io.IOException; +import javax.crypto.Cipher; + +/** + * A {@link DataSource} that decrypts the data read from an upstream source. + */ +public final class AesCipherDataSource implements DataSource { + + private final DataSource upstream; + private final byte[] secretKey; + + private AesFlushingCipher cipher; + + public AesCipherDataSource(byte[] secretKey, DataSource upstream) { + this.upstream = upstream; + this.secretKey = secretKey; + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + long dataLength = upstream.open(dataSpec); + long nonce = CryptoUtil.getFNV64Hash(dataSpec.key); + cipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, secretKey, nonce, + dataSpec.absoluteStreamPosition); + return dataLength; + } + + @Override + public int read(byte[] data, int offset, int readLength) throws IOException { + if (readLength == 0) { + return 0; + } + int read = upstream.read(data, offset, readLength); + if (read == C.RESULT_END_OF_INPUT) { + return C.RESULT_END_OF_INPUT; + } + cipher.updateInPlace(data, offset, read); + return read; + } + + @Override + public void close() throws IOException { + cipher = null; + upstream.close(); + } + + @Override + public Uri getUri() { + return upstream.getUri(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/AesFlushingCipher.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/AesFlushingCipher.java new file mode 100755 index 000000000..a386b7324 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/AesFlushingCipher.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2.upstream.crypto; + +import org.telegram.messenger.exoplayer2.util.Assertions; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * A flushing variant of a AES/CTR/NoPadding {@link Cipher}. + * + * Unlike a regular {@link Cipher}, the update methods of this class are guaranteed to process all + * of the bytes input (and hence output the same number of bytes). + */ +public final class AesFlushingCipher { + + private final Cipher cipher; + private final int blockSize; + private final byte[] zerosBlock; + private final byte[] flushedBlock; + + private int pendingXorBytes; + + public AesFlushingCipher(int mode, byte[] secretKey, long nonce, long offset) { + try { + cipher = Cipher.getInstance("AES/CTR/NoPadding"); + blockSize = cipher.getBlockSize(); + zerosBlock = new byte[blockSize]; + flushedBlock = new byte[blockSize]; + long counter = offset / blockSize; + int startPadding = (int) (offset % blockSize); + cipher.init(mode, new SecretKeySpec(secretKey, cipher.getAlgorithm().split("/")[0]), + new IvParameterSpec(getInitializationVector(nonce, counter))); + if (startPadding != 0) { + updateInPlace(new byte[startPadding], 0, startPadding); + } + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException e) { + // Should never happen. + throw new RuntimeException(e); + } + } + + public void updateInPlace(byte[] data, int offset, int length) { + update(data, offset, length, data, offset); + } + + public void update(byte[] in, int inOffset, int length, byte[] out, int outOffset) { + // If we previously flushed the cipher by inputting zeros up to a block boundary, then we need + // to manually transform the data that actually ended the block. See the comment below for more + // details. + while (pendingXorBytes > 0) { + out[outOffset] = (byte) (in[inOffset] ^ flushedBlock[blockSize - pendingXorBytes]); + outOffset++; + inOffset++; + pendingXorBytes--; + length--; + if (length == 0) { + return; + } + } + + // Do the bulk of the update. + int written = nonFlushingUpdate(in, inOffset, length, out, outOffset); + if (length == written) { + return; + } + + // We need to finish the block to flush out the remaining bytes. We do so by inputting zeros, + // so that the corresponding bytes output by the cipher are those that would have been XORed + // against the real end-of-block data to transform it. We store these bytes so that we can + // perform the transformation manually in the case of a subsequent call to this method with + // the real data. + int bytesToFlush = length - written; + Assertions.checkState(bytesToFlush < blockSize); + outOffset += written; + pendingXorBytes = blockSize - bytesToFlush; + written = nonFlushingUpdate(zerosBlock, 0, pendingXorBytes, flushedBlock, 0); + Assertions.checkState(written == blockSize); + // The first part of xorBytes contains the flushed data, which we copy out. The remainder + // contains the bytes that will be needed for manual transformation in a subsequent call. + for (int i = 0; i < bytesToFlush; i++) { + out[outOffset++] = flushedBlock[i]; + } + } + + private int nonFlushingUpdate(byte[] in, int inOffset, int length, byte[] out, int outOffset) { + try { + return cipher.update(in, inOffset, length, out, outOffset); + } catch (ShortBufferException e) { + // Should never happen. + throw new RuntimeException(e); + } + } + + private byte[] getInitializationVector(long nonce, long counter) { + return ByteBuffer.allocate(16).putLong(nonce).putLong(counter).array(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/CryptoUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/CryptoUtil.java new file mode 100755 index 000000000..b05577b0b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/upstream/crypto/CryptoUtil.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer2.upstream.crypto; + +/** + * Utility functions for the crypto package. + */ +/* package */ final class CryptoUtil { + + private CryptoUtil() {} + + /** + * 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()}. + */ + public static long getFNV64Hash(String input) { + if (input == null) { + return 0; + } + + long hash = 0; + for (int i = 0; i < input.length(); i++) { + hash ^= input.charAt(i); + // This is equivalent to hash *= 0x100000001b3 (the FNV magic prime number). + hash += (hash << 1) + (hash << 4) + (hash << 5) + (hash << 7) + (hash << 8) + (hash << 40); + } + return hash; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/AtomicFile.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/AtomicFile.java index a2251713f..e7e369007 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/AtomicFile.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/AtomicFile.java @@ -16,6 +16,7 @@ package org.telegram.messenger.exoplayer2.util; +import android.support.annotation.NonNull; import android.util.Log; import java.io.File; import java.io.FileInputStream; @@ -185,12 +186,12 @@ public final class AtomicFile { } @Override - public void write(byte[] b) throws IOException { + public void write(@NonNull byte[] b) throws IOException { fileOutputStream.write(b); } @Override - public void write(byte[] b, int off, int len) throws IOException { + public void write(@NonNull byte[] b, int off, int len) throws IOException { fileOutputStream.write(b, off, len); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/CodecSpecificDataUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/CodecSpecificDataUtil.java index 8c7566ac7..7febfcbc9 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/CodecSpecificDataUtil.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/CodecSpecificDataUtil.java @@ -75,6 +75,8 @@ public final class CodecSpecificDataUtil { private static final int AUDIO_OBJECT_TYPE_ER_BSAC = 22; // Parametric Stereo. private static final int AUDIO_OBJECT_TYPE_PS = 29; + // Escape code for extended audio object types. + private static final int AUDIO_OBJECT_TYPE_ESCAPE = 31; private CodecSpecificDataUtil() {} @@ -86,15 +88,8 @@ public final class CodecSpecificDataUtil { */ public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) { ParsableBitArray bitArray = new ParsableBitArray(audioSpecificConfig); - int audioObjectType = bitArray.readBits(5); - int frequencyIndex = bitArray.readBits(4); - int sampleRate; - if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) { - sampleRate = bitArray.readBits(24); - } else { - Assertions.checkArgument(frequencyIndex < 13); - sampleRate = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex]; - } + int audioObjectType = getAacAudioObjectType(bitArray); + int sampleRate = getAacSamplingFrequency(bitArray); int channelConfiguration = bitArray.readBits(4); if (audioObjectType == AUDIO_OBJECT_TYPE_SBR || audioObjectType == AUDIO_OBJECT_TYPE_PS) { // For an AAC bitstream using spectral band replication (SBR) or parametric stereo (PS) with @@ -102,14 +97,8 @@ public final class CodecSpecificDataUtil { // content; this is identical to the sample rate of the decoded output but may differ from // the sample rate set above. // Use the extensionSamplingFrequencyIndex. - frequencyIndex = bitArray.readBits(4); - if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) { - sampleRate = bitArray.readBits(24); - } else { - Assertions.checkArgument(frequencyIndex < 13); - sampleRate = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex]; - } - audioObjectType = bitArray.readBits(5); + sampleRate = getAacSamplingFrequency(bitArray); + audioObjectType = getAacAudioObjectType(bitArray); if (audioObjectType == AUDIO_OBJECT_TYPE_ER_BSAC) { // Use the extensionChannelConfiguration. channelConfiguration = bitArray.readBits(4); @@ -247,4 +236,37 @@ public final class CodecSpecificDataUtil { return true; } + /** + * Returns the AAC audio object type as specified in 14496-3 (2005) Table 1.14. + * + * @param bitArray The bit array containing the audio specific configuration. + * @return The audio object type. + */ + private static int getAacAudioObjectType(ParsableBitArray bitArray) { + int audioObjectType = bitArray.readBits(5); + if (audioObjectType == AUDIO_OBJECT_TYPE_ESCAPE) { + audioObjectType = 32 + bitArray.readBits(6); + } + return audioObjectType; + } + + /** + * Returns the AAC sampling frequency (or extension sampling frequency) as specified in 14496-3 + * (2005) Table 1.13. + * + * @param bitArray The bit array containing the audio specific configuration. + * @return The sampling frequency. + */ + private static int getAacSamplingFrequency(ParsableBitArray bitArray) { + int samplingFrequency; + int frequencyIndex = bitArray.readBits(4); + if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) { + samplingFrequency = bitArray.readBits(24); + } else { + Assertions.checkArgument(frequencyIndex < 13); + samplingFrequency = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex]; + } + return samplingFrequency; + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/MediaClock.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/MediaClock.java index 684a18e1c..3adc0048d 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/MediaClock.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/MediaClock.java @@ -15,6 +15,8 @@ */ package org.telegram.messenger.exoplayer2.util; +import org.telegram.messenger.exoplayer2.PlaybackParameters; + /** * Tracks the progression of media time. */ @@ -25,4 +27,18 @@ public interface MediaClock { */ long getPositionUs(); + /** + * Attempts to set the playback parameters and returns the active playback parameters, which may + * differ from those passed in. + * + * @param playbackParameters The playback parameters. + * @return The active playback parameters. + */ + PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters); + + /** + * Returns the active playback parameters. + */ + PlaybackParameters getPlaybackParameters(); + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/MimeTypes.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/MimeTypes.java index 486dfad0a..7e5e114d0 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/MimeTypes.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/MimeTypes.java @@ -47,6 +47,8 @@ public final class MimeTypes { public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1"; public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2"; public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw"; + public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + "/g711-alaw"; + public static final String AUDIO_ULAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; @@ -58,24 +60,28 @@ public final class MimeTypes { public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp"; public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac"; + public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4"; public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; + public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608"; public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + "/cea-708"; public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip"; public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; - public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; public static final String APPLICATION_TX3G = BASE_TYPE_APPLICATION + "/x-quicktime-tx3g"; - public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + "/x-mp4vtt"; + public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + "/x-mp4-vtt"; + public static final String APPLICATION_MP4CEA608 = BASE_TYPE_APPLICATION + "/x-mp4-cea-608"; public static final String APPLICATION_RAWCC = BASE_TYPE_APPLICATION + "/x-rawcc"; public static final String APPLICATION_VOBSUB = BASE_TYPE_APPLICATION + "/vobsub"; public static final String APPLICATION_PGS = BASE_TYPE_APPLICATION + "/pgs"; public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + "/x-scte35"; public static final String APPLICATION_CAMERA_MOTION = BASE_TYPE_APPLICATION + "/x-camera-motion"; + public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + "/x-emsg"; + public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs"; private MimeTypes() {} @@ -213,12 +219,16 @@ public final class MimeTypes { } else if (isVideo(mimeType)) { return C.TRACK_TYPE_VIDEO; } else if (isText(mimeType) || APPLICATION_CEA608.equals(mimeType) - || APPLICATION_CEA708.equals(mimeType) || APPLICATION_SUBRIP.equals(mimeType) - || APPLICATION_TTML.equals(mimeType) || APPLICATION_TX3G.equals(mimeType) - || APPLICATION_MP4VTT.equals(mimeType) || APPLICATION_RAWCC.equals(mimeType) - || APPLICATION_VOBSUB.equals(mimeType) || APPLICATION_PGS.equals(mimeType)) { + || APPLICATION_CEA708.equals(mimeType) || APPLICATION_MP4CEA608.equals(mimeType) + || APPLICATION_SUBRIP.equals(mimeType) || APPLICATION_TTML.equals(mimeType) + || APPLICATION_TX3G.equals(mimeType) || APPLICATION_MP4VTT.equals(mimeType) + || APPLICATION_RAWCC.equals(mimeType) || APPLICATION_VOBSUB.equals(mimeType) + || APPLICATION_PGS.equals(mimeType) || APPLICATION_DVBSUBS.equals(mimeType)) { return C.TRACK_TYPE_TEXT; - } else if (APPLICATION_ID3.equals(mimeType)) { + } else if (APPLICATION_ID3.equals(mimeType) + || APPLICATION_EMSG.equals(mimeType) + || APPLICATION_SCTE35.equals(mimeType) + || APPLICATION_CAMERA_MOTION.equals(mimeType)) { return C.TRACK_TYPE_METADATA; } else { return C.TRACK_TYPE_UNKNOWN; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/NalUnitUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/NalUnitUtil.java index 1960678b9..097b6bda6 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/NalUnitUtil.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/NalUnitUtil.java @@ -103,7 +103,9 @@ public final class NalUnitUtil { 2f }; - private static final int NAL_UNIT_TYPE_SPS = 7; + private static final int H264_NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information + private static final int H264_NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set + private static final int H265_NAL_UNIT_TYPE_PREFIX_SEI = 39; private static final Object scratchEscapePositionsLock = new Object(); @@ -176,7 +178,7 @@ public final class NalUnitUtil { while (offset + 1 < length) { int value = data.get(offset) & 0xFF; if (consecutiveZeros == 3) { - if (value == 1 && (data.get(offset + 1) & 0x1F) == NAL_UNIT_TYPE_SPS) { + if (value == 1 && (data.get(offset + 1) & 0x1F) == H264_NAL_UNIT_TYPE_SPS) { // Copy from this NAL unit onwards to the start of the buffer. ByteBuffer offsetData = data.duplicate(); offsetData.position(offset - 3); @@ -197,6 +199,21 @@ public final class NalUnitUtil { data.clear(); } + /** + * Returns whether the NAL unit with the specified header contains supplemental enhancement + * information. + * + * @param mimeType The sample MIME type. + * @param nalUnitHeaderFirstByte The first byte of nal_unit(). + * @return Whether the NAL unit with the specified header is an SEI NAL unit. + */ + public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) { + return (MimeTypes.VIDEO_H264.equals(mimeType) + && (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI) + || (MimeTypes.VIDEO_H265.equals(mimeType) + && ((nalUnitHeaderFirstByte & 0x7E) >> 1) == H265_NAL_UNIT_TYPE_PREFIX_SEI); + } + /** * Returns the type of the NAL unit in {@code data} that starts at {@code offset}. * @@ -297,7 +314,8 @@ public final class NalUnitUtil { int frameCropRightOffset = data.readUnsignedExpGolombCodedInt(); int frameCropTopOffset = data.readUnsignedExpGolombCodedInt(); int frameCropBottomOffset = data.readUnsignedExpGolombCodedInt(); - int cropUnitX, cropUnitY; + int cropUnitX; + int cropUnitY; if (chromaFormatIdc == 0) { cropUnitX = 1; cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ParsableBitArray.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ParsableBitArray.java index 071556a46..ae3a87476 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ParsableBitArray.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ParsableBitArray.java @@ -89,6 +89,16 @@ public final class ParsableBitArray { return byteOffset * 8 + bitOffset; } + /** + * Returns the current byte offset. Must only be called when the position is byte aligned. + * + * @throws IllegalStateException If the position isn't byte aligned. + */ + public int getBytePosition() { + Assertions.checkState(bitOffset == 0); + return byteOffset; + } + /** * Sets the current bit offset. * @@ -177,6 +187,47 @@ public final class ParsableBitArray { return returnValue; } + /** + * Aligns the position to the next byte boundary. Does nothing if the position is already aligned. + */ + public void byteAlign() { + if (bitOffset == 0) { + return; + } + bitOffset = 0; + byteOffset++; + assertValidOffset(); + } + + /** + * Reads the next {@code length} bytes into {@code buffer}. Must only be called when the position + * is byte aligned. + * + * @see System#arraycopy(Object, int, Object, int, int) + * @param buffer The array into which the read data should be written. + * @param offset The offset in {@code buffer} at which the read data should be written. + * @param length The number of bytes to read. + * @throws IllegalStateException If the position isn't byte aligned. + */ + public void readBytes(byte[] buffer, int offset, int length) { + Assertions.checkState(bitOffset == 0); + System.arraycopy(data, byteOffset, buffer, offset, length); + byteOffset += length; + assertValidOffset(); + } + + /** + * Skips the next {@code length} bytes. Must only be called when the position is byte aligned. + * + * @param length The number of bytes to read. + * @throws IllegalStateException If the position isn't byte aligned. + */ + public void skipBytes(int length) { + Assertions.checkState(bitOffset == 0); + byteOffset += length; + assertValidOffset(); + } + private void assertValidOffset() { // It is fine for position to be at the end of the array, but no further. Assertions.checkState(byteOffset >= 0 diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ParsableByteArray.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ParsableByteArray.java index b4a1eff3a..6d93c4d8b 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ParsableByteArray.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ParsableByteArray.java @@ -201,6 +201,14 @@ public final class ParsableByteArray { return (data[position] & 0xFF); } + /** + * Peeks at the next char. + */ + public char peekChar() { + return (char) ((data[position] & 0xFF) << 8 + | (data[position + 1] & 0xFF)); + } + /** * Reads the next byte as an unsigned value. */ @@ -423,24 +431,6 @@ public final class ParsableByteArray { return readString(length, Charset.defaultCharset()); } - /** - * Reads the next {@code length} bytes as UTF-8 characters. A terminating NUL byte is ignored, - * if present. - * - * @param length The number of bytes to read. - * @return The string encoded by the bytes. - */ - public String readNullTerminatedString(int length) { - int stringLength = length; - int lastIndex = position + length - 1; - if (lastIndex < limit && data[lastIndex] == 0) { - stringLength--; - } - String result = new String(data, position, stringLength, Charset.defaultCharset()); - position += length; - return result; - } - /** * Reads the next {@code length} bytes as characters in the specified {@link Charset}. * @@ -454,6 +444,49 @@ public final class ParsableByteArray { return result; } + /** + * Reads the next {@code length} bytes as UTF-8 characters. A terminating NUL byte is discarded, + * if present. + * + * @param length The number of bytes to read. + * @return The string, not including any terminating NUL byte. + */ + public String readNullTerminatedString(int length) { + if (length == 0) { + return ""; + } + int stringLength = length; + int lastIndex = position + length - 1; + if (lastIndex < limit && data[lastIndex] == 0) { + stringLength--; + } + String result = new String(data, position, stringLength); + position += length; + return result; + } + + /** + * Reads up to the next NUL byte (or the limit) as UTF-8 characters. + * + * @return The string not including any terminating NUL byte, or null if the end of the data has + * already been reached. + */ + public String readNullTerminatedString() { + if (bytesLeft() == 0) { + return null; + } + int stringLimit = position; + while (stringLimit < limit && data[stringLimit] != 0) { + stringLimit++; + } + String string = new String(data, position, stringLimit - position); + position = stringLimit; + if (position < limit) { + position++; + } + return string; + } + /** * Reads a line of text. *

    @@ -461,15 +494,15 @@ public final class ParsableByteArray { * ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The system's default * charset (UTF-8) is used. * - * @return A String containing the contents of the line, not including any line-termination - * characters, or null if the end of the stream has been reached. + * @return The line not including any line-termination characters, or null if the end of the data + * has already been reached. */ public String readLine() { if (bytesLeft() == 0) { return null; } int lineLimit = position; - while (lineLimit < limit && data[lineLimit] != '\n' && data[lineLimit] != '\r') { + while (lineLimit < limit && !Util.isLinebreak(data[lineLimit])) { lineLimit++; } if (lineLimit - position >= 3 && data[position] == (byte) 0xEF diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/PriorityTaskManager.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/PriorityTaskManager.java index 296c4173f..c8cffacd8 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/PriorityTaskManager.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/PriorityTaskManager.java @@ -54,7 +54,7 @@ public final class PriorityTaskManager { /** * Register a new task. The task must call {@link #remove(int)} when done. * - * @param priority The priority of the task. + * @param priority The priority of the task. Larger values indicate higher priorities. */ public void add(int priority) { synchronized (lock) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ReusableBufferedOutputStream.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ReusableBufferedOutputStream.java index fc8afbb34..21880857f 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ReusableBufferedOutputStream.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/ReusableBufferedOutputStream.java @@ -67,6 +67,7 @@ public final class ReusableBufferedOutputStream extends BufferedOutputStream { public void reset(OutputStream out) { Assertions.checkState(closed); this.out = out; + count = 0; closed = false; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/SlidingPercentile.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/SlidingPercentile.java index eb1e38436..27cf56521 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/SlidingPercentile.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/SlidingPercentile.java @@ -32,7 +32,7 @@ import java.util.Comparator; * @see Wiki: Moving average * @see Wiki: Selection algorithm */ -public final class SlidingPercentile { +public class SlidingPercentile { // Orderings. private static final Comparator INDEX_COMPARATOR = new Comparator() { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/StandaloneMediaClock.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/StandaloneMediaClock.java index 1742e2c55..7cb1c2e33 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/StandaloneMediaClock.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/StandaloneMediaClock.java @@ -16,33 +16,34 @@ package org.telegram.messenger.exoplayer2.util; import android.os.SystemClock; +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.PlaybackParameters; /** - * A standalone {@link MediaClock}. The clock can be started, stopped and its time can be set and - * retrieved. When started, this clock is based on {@link SystemClock#elapsedRealtime()}. + * A {@link MediaClock} whose position advances with real time based on the playback parameters when + * started. */ public final class StandaloneMediaClock implements MediaClock { private boolean started; + private long baseUs; + private long baseElapsedMs; + private PlaybackParameters playbackParameters; /** - * The media time when the clock was last set or stopped. + * Creates a new standalone media clock. */ - private long positionUs; - - /** - * The difference between {@link SystemClock#elapsedRealtime()} and {@link #positionUs} - * when the clock was last set or started. - */ - private long deltaUs; + public StandaloneMediaClock() { + playbackParameters = PlaybackParameters.DEFAULT; + } /** * Starts the clock. Does nothing if the clock is already started. */ public void start() { if (!started) { + baseElapsedMs = SystemClock.elapsedRealtime(); started = true; - deltaUs = elapsedRealtimeMinus(positionUs); } } @@ -51,26 +52,60 @@ public final class StandaloneMediaClock implements MediaClock { */ public void stop() { if (started) { - positionUs = elapsedRealtimeMinus(deltaUs); + setPositionUs(getPositionUs()); started = false; } } /** - * @param timeUs The position to set in microseconds. + * Sets the clock's position. + * + * @param positionUs The position to set in microseconds. */ - public void setPositionUs(long timeUs) { - this.positionUs = timeUs; - deltaUs = elapsedRealtimeMinus(timeUs); + public void setPositionUs(long positionUs) { + baseUs = positionUs; + if (started) { + baseElapsedMs = SystemClock.elapsedRealtime(); + } + } + + /** + * Synchronizes this clock with the current state of {@code clock}. + * + * @param clock The clock with which to synchronize. + */ + public void synchronize(MediaClock clock) { + setPositionUs(clock.getPositionUs()); + playbackParameters = clock.getPlaybackParameters(); } @Override public long getPositionUs() { - return started ? elapsedRealtimeMinus(deltaUs) : positionUs; + long positionUs = baseUs; + if (started) { + long elapsedSinceBaseMs = SystemClock.elapsedRealtime() - baseElapsedMs; + if (playbackParameters.speed == 1f) { + positionUs += C.msToUs(elapsedSinceBaseMs); + } else { + positionUs += playbackParameters.getSpeedAdjustedDurationUs(elapsedSinceBaseMs); + } + } + return positionUs; } - private long elapsedRealtimeMinus(long toSubtractUs) { - return SystemClock.elapsedRealtime() * 1000 - toSubtractUs; + @Override + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + // Store the current position as the new base, in case the playback speed has changed. + if (started) { + setPositionUs(getPositionUs()); + } + this.playbackParameters = playbackParameters; + return playbackParameters; + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return playbackParameters; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/TimestampAdjuster.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/TimestampAdjuster.java similarity index 64% rename from TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/TimestampAdjuster.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/TimestampAdjuster.java index da28c5b60..2e68b682a 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/extractor/TimestampAdjuster.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/TimestampAdjuster.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.telegram.messenger.exoplayer2.extractor; +package org.telegram.messenger.exoplayer2.util; import org.telegram.messenger.exoplayer2.C; @@ -34,21 +34,67 @@ public final class TimestampAdjuster { */ private static final long MAX_PTS_PLUS_ONE = 0x200000000L; - private final long firstSampleTimestampUs; - + private long firstSampleTimestampUs; private long timestampOffsetUs; // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp. private volatile long lastSampleTimestamp; /** - * @param firstSampleTimestampUs The desired result of the first call to - * {@link #adjustSampleTimestamp(long)}, or {@link #DO_NOT_OFFSET} if presentation timestamps - * should not be offset. + * @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}. */ public TimestampAdjuster(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; lastSampleTimestamp = C.TIME_UNSET; + setFirstSampleTimestampUs(firstSampleTimestampUs); + } + + /** + * Sets the desired result of the first call to {@link #adjustSampleTimestamp(long)}. Can only be + * called before any timestamps have been adjusted. + * + * @param firstSampleTimestampUs The first adjusted sample timestamp in microseconds, or + * {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset. + */ + public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) { + Assertions.checkState(lastSampleTimestamp == C.TIME_UNSET); + this.firstSampleTimestampUs = firstSampleTimestampUs; + } + + /** + * Returns the first adjusted sample timestamp in microseconds. + * + * @return The first adjusted sample timestamp in microseconds. + */ + public long getFirstSampleTimestampUs() { + return firstSampleTimestampUs; + } + + /** + * Returns the last adjusted timestamp. If no timestamp has been adjusted, returns + * {@code firstSampleTimestampUs} as provided to the constructor. If this value is + * {@link #DO_NOT_OFFSET}, returns {@link C#TIME_UNSET}. + * + * @return The last adjusted timestamp. If not present, {@code firstSampleTimestampUs} is + * returned unless equal to {@link #DO_NOT_OFFSET}, in which case {@link C#TIME_UNSET} is + * returned. + */ + public long getLastAdjustedTimestampUs() { + return lastSampleTimestamp != C.TIME_UNSET ? lastSampleTimestamp + : firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET; + } + + /** + * Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output. + * If {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp + * adjuster is yet not initialized, {@link C#TIME_UNSET} is returned. + * + * @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output. + * {@link C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not + * be offset. + */ + public long getTimestampOffsetUs() { + return firstSampleTimestampUs == DO_NOT_OFFSET ? 0 + : lastSampleTimestamp == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs; } /** @@ -65,6 +111,9 @@ public final class TimestampAdjuster { * @return The adjusted timestamp in microseconds. */ public long adjustTsTimestamp(long pts) { + if (pts == C.TIME_UNSET) { + return C.TIME_UNSET; + } if (lastSampleTimestamp != C.TIME_UNSET) { // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), // and we need to snap to the one closest to lastSampleTimestamp. @@ -85,6 +134,9 @@ public final class TimestampAdjuster { * @return The adjusted timestamp in microseconds. */ public long adjustSampleTimestamp(long timeUs) { + if (timeUs == C.TIME_UNSET) { + return C.TIME_UNSET; + } // Record the adjusted PTS to adjust for wraparound next time. if (lastSampleTimestamp != C.TIME_UNSET) { lastSampleTimestamp = timeUs; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/Util.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/Util.java index 77236bbe9..387ccb9e4 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/Util.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/util/Util.java @@ -25,6 +25,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Point; import android.net.Uri; import android.os.Build; +import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import android.view.Display; @@ -36,6 +37,7 @@ import org.telegram.messenger.exoplayer2.upstream.DataSource; import org.telegram.messenger.exoplayer2.upstream.DataSpec; import java.io.ByteArrayOutputStream; import java.io.Closeable; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; @@ -44,6 +46,7 @@ import java.nio.charset.Charset; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; +import java.util.Formatter; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; @@ -64,7 +67,7 @@ public final class Util { * overridden for local testing. */ public static final int SDK_INT = - (Build.VERSION.SDK_INT == 23 && Build.VERSION.CODENAME.charAt(0) == 'N') ? 24 + (Build.VERSION.SDK_INT == 25 && Build.VERSION.CODENAME.charAt(0) == 'O') ? 26 : Build.VERSION.SDK_INT; /** @@ -94,8 +97,8 @@ public final class Util { private static final String TAG = "Util"; private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" - + "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?" - + "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?"); + + "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?" + + "([Zz]|((\\+|\\-)(\\d\\d):?(\\d\\d)))?"); private static final Pattern XS_DURATION_PATTERN = Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); @@ -197,7 +200,7 @@ public final class Util { public static ExecutorService newSingleThreadExecutor(final String threadName) { return Executors.newSingleThreadExecutor(new ThreadFactory() { @Override - public Thread newThread(Runnable r) { + public Thread newThread(@NonNull Runnable r) { return new Thread(r, threadName); } }); @@ -254,6 +257,16 @@ public final class Util { return value.getBytes(Charset.defaultCharset()); // UTF-8 is the default on Android. } + /** + * Returns whether the given character is a carriage return ('\r') or a line feed ('\n'). + * + * @param c The character. + * @return Whether the given character is a linebreak. + */ + public static boolean isLinebreak(int c) { + return c == '\n' || c == '\r'; + } + /** * Converts text to lower case using {@link Locale#US}. * @@ -299,110 +312,191 @@ public final class Util { } /** - * Returns the index of the largest value in an array that is less than (or optionally equal to) - * a specified value. - *

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

    - * The search is performed using a binary search algorithm, so the array must be sorted. + * Constrains a value to the specified bounds. * - * @param a The array to search. + * @param value The value to constrain. + * @param min The lower bound. + * @param max The upper bound. + * @return The constrained value {@code Math.max(min, Math.min(value, max))}. + */ + public static float constrainValue(float value, float min, float max) { + return Math.max(min, Math.min(value, max)); + } + + /** + * 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 + * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the + * index of the first one will be returned. + * + * @param array The array to search. * @param value The value being searched for. * @param inclusive If the value is present in the array, whether to return the corresponding - * index. If false then the returned index corresponds to the largest value in the array that - * is strictly less than the value. + * index. If false then the returned index corresponds to the largest element strictly less + * than the value. * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than - * the smallest value in the array. If false then -1 will be returned. + * the smallest element in the array. If false then -1 will be returned. + * @return The index of the largest element in {@code array} that is less than (or optionally + * equal to) {@code value}. */ - public static int binarySearchFloor(long[] a, long value, boolean inclusive, + public static int binarySearchFloor(int[] array, int value, boolean inclusive, boolean stayInBounds) { - int index = Arrays.binarySearch(a, value); - index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1)); + int index = Arrays.binarySearch(array, value); + if (index < 0) { + index = -(index + 2); + } else { + while ((--index) >= 0 && array[index] == value) {} + if (inclusive) { + index++; + } + } return stayInBounds ? Math.max(0, index) : index; } /** - * Returns the index of the smallest value in an array that is greater than (or optionally equal - * to) a specified value. + * 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. + * 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. * - * @param a The array to search. + * @param array The array to search. * @param value The value being searched for. * @param inclusive If the value is present in the array, whether to return the corresponding - * index. If false then the returned index corresponds to the largest value in the array that - * is strictly less than the value. + * index. If false then the returned index corresponds to the largest element strictly less + * than the value. + * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than + * the smallest element in the array. If false then -1 will be returned. + * @return The index of the largest element in {@code array} that is less than (or optionally + * equal to) {@code value}. + */ + public static int binarySearchFloor(long[] array, long value, boolean inclusive, + boolean stayInBounds) { + int index = Arrays.binarySearch(array, value); + if (index < 0) { + index = -(index + 2); + } else { + while ((--index) >= 0 && array[index] == value) {} + if (inclusive) { + index++; + } + } + return stayInBounds ? Math.max(0, index) : index; + } + + /** + * Returns the index of the smallest element in {@code array} that is greater 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 array contains multiple elements equal to {@code value} and {@code inclusive} is true, the + * index of the last one will be returned. + * + * @param array The array to search. + * @param value The value being searched for. + * @param inclusive If the value is present in the array, whether to return the corresponding + * index. If false then the returned index corresponds to the smallest element strictly + * greater than the value. * @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the - * value is greater than the largest value in the array. If false then {@code a.length} will + * value is greater than the largest element in the array. If false then {@code a.length} will * be returned. + * @return The index of the smallest element in {@code array} that is greater than (or optionally + * equal to) {@code value}. */ - public static int binarySearchCeil(long[] a, long value, boolean inclusive, + public static int binarySearchCeil(long[] array, long value, boolean inclusive, boolean stayInBounds) { - int index = Arrays.binarySearch(a, value); - index = index < 0 ? ~index : (inclusive ? index : (index + 1)); - return stayInBounds ? Math.min(a.length - 1, index) : index; + int index = Arrays.binarySearch(array, value); + if (index < 0) { + index = ~index; + } else { + while ((++index) < array.length && array[index] == value) {} + if (inclusive) { + index--; + } + } + return stayInBounds ? Math.min(array.length - 1, index) : index; } /** - * Returns the index of the largest value in an list that is less than (or optionally equal to) - * a specified value. + * Returns the index of the largest element in {@code list} that is less than (or optionally equal + * to) a specified {@code value}. *

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

    - * The search is performed using a binary search algorithm, so the list must be sorted. + * The search is performed using a binary search algorithm, so the list must be sorted. If the + * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the + * index of the last one will be returned. * * @param The type of values being searched. * @param list The list to search. * @param value The value being searched for. * @param inclusive If the value is present in the list, whether to return the corresponding - * index. If false then the returned index corresponds to the smallest value in the list that - * is strictly greater than the value. + * index. If false then the returned index corresponds to the smallest element strictly + * greater than the value. * @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that - * the value is greater than the largest value in the list. If false then {@code list.size()} - * will be returned. + * the value is greater than the largest element in the list. If false then + * {@code list.size()} will be returned. + * @return The index of the smallest element in {@code list} that is greater than (or optionally + * equal to) {@code value}. */ public static int binarySearchCeil(List> list, T value, boolean inclusive, boolean stayInBounds) { int index = Collections.binarySearch(list, value); - index = index < 0 ? ~index : (inclusive ? index : (index + 1)); + if (index < 0) { + index = ~index; + } else { + int listSize = list.size(); + while ((++index) < listSize && list.get(index).compareTo(value) == 0) {} + if (inclusive) { + index--; + } + } return stayInBounds ? Math.min(list.size() - 1, index) : index; } @@ -672,7 +766,7 @@ public final class Util { versionName = "?"; } return applicationName + "/" + versionName + " (Linux;Android " + Build.VERSION.RELEASE - + ") " + "ExoPlayerLib/" + ExoPlayerLibraryInfo.VERSION; + + ") " + ExoPlayerLibraryInfo.VERSION_SLASHY; } /** @@ -700,6 +794,40 @@ public final class Util { } } + /** + * Returns the frame size for audio with {@code channelCount} channels in the specified encoding. + * + * @param pcmEncoding The encoding of the audio data. + * @param channelCount The channel count. + * @return The size of one audio frame in bytes. + */ + public static int getPcmFrameSize(@C.PcmEncoding int pcmEncoding, int channelCount) { + switch (pcmEncoding) { + case C.ENCODING_PCM_8BIT: + return channelCount; + case C.ENCODING_PCM_16BIT: + return channelCount * 2; + case C.ENCODING_PCM_24BIT: + return channelCount * 3; + case C.ENCODING_PCM_32BIT: + return channelCount * 4; + default: + throw new IllegalArgumentException(); + } + } + + /** + * Makes a best guess to infer the type from a {@link Uri}. + * + * @param uri The {@link Uri}. + * @return The content type. + */ + @C.ContentType + public static int inferContentType(Uri uri) { + String path = uri.getPath(); + return path == null ? C.TYPE_OTHER : inferContentType(path); + } + /** * Makes a best guess to infer the type from a file name. * @@ -708,19 +836,40 @@ public final class Util { */ @C.ContentType public static int inferContentType(String fileName) { - if (fileName == null) { - return C.TYPE_OTHER; - } else if (fileName.endsWith(".mpd")) { + fileName = fileName.toLowerCase(); + if (fileName.endsWith(".mpd")) { return C.TYPE_DASH; - } else if (fileName.endsWith(".ism") || fileName.endsWith(".isml")) { - return C.TYPE_SS; } else if (fileName.endsWith(".m3u8")) { return C.TYPE_HLS; + } else if (fileName.endsWith(".ism") || fileName.endsWith(".isml") + || fileName.endsWith(".ism/manifest") || fileName.endsWith(".isml/manifest")) { + return C.TYPE_SS; } else { return C.TYPE_OTHER; } } + /** + * Returns the specified millisecond time formatted as a string. + * + * @param builder The builder that {@code formatter} will write to. + * @param formatter The formatter. + * @param timeMs The time to format as a string, in milliseconds. + * @return The time formatted as a string. + */ + public static String getStringForTime(StringBuilder builder, Formatter formatter, long timeMs) { + if (timeMs == C.TIME_UNSET) { + timeMs = 0; + } + long totalSeconds = (timeMs + 500) / 1000; + long seconds = totalSeconds % 60; + long minutes = (totalSeconds / 60) % 60; + long hours = totalSeconds / 3600; + builder.setLength(0); + return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() + : formatter.format("%02d:%02d", minutes, seconds).toString(); + } + /** * Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C} * {@code DEFAULT_*_BUFFER_SIZE} constant. @@ -857,6 +1006,24 @@ public final class Util { throw (T) t; } + /** Recursively deletes a directory and its content. */ + public static void recursiveDelete(File fileOrDirectory) { + if (fileOrDirectory.isDirectory()) { + for (File child : fileOrDirectory.listFiles()) { + recursiveDelete(child); + } + } + fileOrDirectory.delete(); + } + + /** Creates an empty directory in the directory returned by {@link Context#getCacheDir()}. */ + public static File createTempDirectory(Context context, String prefix) throws IOException { + File tempFile = File.createTempFile(prefix, null, context.getCacheDir()); + tempFile.delete(); // Delete the temp file. + tempFile.mkdir(); // Create a directory with the same name. + return tempFile; + } + /** * Returns the result of updating a CRC with the specified bytes in a "most significant bit first" * order. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/video/ColorInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/video/ColorInfo.java new file mode 100755 index 000000000..862287511 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/video/ColorInfo.java @@ -0,0 +1,149 @@ +/* + * 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 org.telegram.messenger.exoplayer2.video; + +import android.os.Parcel; +import android.os.Parcelable; +import org.telegram.messenger.exoplayer2.C; +import org.telegram.messenger.exoplayer2.Format; +import java.util.Arrays; + +/** + * Stores color info. + */ +public final class ColorInfo implements Parcelable { + + /** + * The color space of the video. Valid values are {@link C#COLOR_SPACE_BT601}, {@link + * C#COLOR_SPACE_BT709}, {@link C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown. + */ + @C.ColorSpace + public final int colorSpace; + + /** + * The color range of the video. Valid values are {@link C#COLOR_RANGE_LIMITED}, {@link + * C#COLOR_RANGE_FULL} or {@link Format#NO_VALUE} if unknown. + */ + @C.ColorRange + public final int colorRange; + + /** + * The color transfer characteristicks of the video. Valid values are {@link + * C#COLOR_TRANSFER_HLG}, {@link C#COLOR_TRANSFER_ST2084}, {@link C#COLOR_TRANSFER_SDR} or {@link + * Format#NO_VALUE} if unknown. + */ + @C.ColorTransfer + public final int colorTransfer; + + /** + * HdrStaticInfo as defined in CTA-861.3. + */ + public final byte[] hdrStaticInfo; + + // Lazily initialized hashcode. + private int hashCode; + + /** + * Constructs the ColorInfo. + * + * @param colorSpace The color space of the video. + * @param colorRange The color range of the video. + * @param colorTransfer The color transfer characteristics of the video. + * @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3. + */ + public ColorInfo(@C.ColorSpace int colorSpace, @C.ColorRange int colorRange, + @C.ColorTransfer int colorTransfer, byte[] hdrStaticInfo) { + this.colorSpace = colorSpace; + this.colorRange = colorRange; + this.colorTransfer = colorTransfer; + this.hdrStaticInfo = hdrStaticInfo; + } + + @SuppressWarnings("ResourceType") + /* package */ ColorInfo(Parcel in) { + colorSpace = in.readInt(); + colorRange = in.readInt(); + colorTransfer = in.readInt(); + boolean hasHdrStaticInfo = in.readInt() != 0; + hdrStaticInfo = hasHdrStaticInfo ? in.createByteArray() : null; + } + + // Parcelable implementation. + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ColorInfo other = (ColorInfo) obj; + if (colorSpace != other.colorSpace || colorRange != other.colorRange + || colorTransfer != other.colorTransfer + || !Arrays.equals(hdrStaticInfo, other.hdrStaticInfo)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "ColorInfo(" + colorSpace + ", " + colorRange + ", " + colorTransfer + + ", " + (hdrStaticInfo != null) + ")"; + } + + @Override + public int hashCode() { + if (hashCode == 0) { + int result = 17; + result = 31 * result + colorSpace; + result = 31 * result + colorRange; + result = 31 * result + colorTransfer; + result = 31 * result + Arrays.hashCode(hdrStaticInfo); + hashCode = result; + } + return hashCode; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(colorSpace); + dest.writeInt(colorRange); + dest.writeInt(colorTransfer); + dest.writeInt(hdrStaticInfo != null ? 1 : 0); + if (hdrStaticInfo != null) { + dest.writeByteArray(hdrStaticInfo); + } + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public ColorInfo createFromParcel(Parcel in) { + return new ColorInfo(in); + } + + @Override + public ColorInfo[] newArray(int size) { + return new ColorInfo[0]; + } + }; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/video/MediaCodecVideoRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/video/MediaCodecVideoRenderer.java index e81e5c637..833a607a9 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer2/video/MediaCodecVideoRenderer.java @@ -18,16 +18,20 @@ package org.telegram.messenger.exoplayer2.video; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; +import android.graphics.Point; import android.media.MediaCodec; +import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Handler; import android.os.SystemClock; +import android.support.annotation.NonNull; import android.util.Log; import android.view.Surface; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.ExoPlaybackException; import org.telegram.messenger.exoplayer2.Format; +import org.telegram.messenger.exoplayer2.decoder.DecoderInputBuffer; import org.telegram.messenger.exoplayer2.drm.DrmInitData; import org.telegram.messenger.exoplayer2.drm.DrmSessionManager; import org.telegram.messenger.exoplayer2.drm.FrameworkMediaCrypto; @@ -54,6 +58,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private static final String KEY_CROP_BOTTOM = "crop-bottom"; private static final String KEY_CROP_TOP = "crop-top"; + // Long edge length in pixels for standard video formats, in decreasing in order. + private static final int[] STANDARD_LONG_EDGE_VIDEO_PX = new int[] { + 1920, 1600, 1440, 1280, 960, 854, 640, 540, 480}; + private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; private final EventDispatcher eventDispatcher; private final long allowedJoiningTimeMs; @@ -78,10 +86,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private int currentHeight; private int currentUnappliedRotationDegrees; private float currentPixelWidthHeightRatio; - private int lastReportedWidth; - private int lastReportedHeight; - private int lastReportedUnappliedRotationDegrees; - private float lastReportedPixelWidthHeightRatio; + private int reportedWidth; + private int reportedHeight; + private int reportedUnappliedRotationDegrees; + private float reportedPixelWidthHeightRatio; + + private boolean tunneling; + private int tunnelingAudioSessionId; + /* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; /** * @param context A context. @@ -154,7 +166,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentPixelWidthHeightRatio = Format.NO_VALUE; pendingPixelWidthHeightRatio = Format.NO_VALUE; scalingMode = C.VIDEO_SCALING_MODE_DEFAULT; - clearLastReportedVideoSize(); + clearReportedVideoSize(); } @Override @@ -180,12 +192,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { boolean decoderCapable = decoderInfo.isCodecSupported(format.codecs); if (decoderCapable && format.width > 0 && format.height > 0) { if (Util.SDK_INT >= 21) { - if (format.frameRate > 0) { - decoderCapable = decoderInfo.isVideoSizeAndRateSupportedV21(format.width, format.height, - format.frameRate); - } else { - decoderCapable = decoderInfo.isVideoSizeSupportedV21(format.width, format.height); - } + decoderCapable = decoderInfo.isVideoSizeAndRateSupportedV21(format.width, format.height, + format.frameRate); } else { decoderCapable = format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize(); if (!decoderCapable) { @@ -196,13 +204,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } int adaptiveSupport = decoderInfo.adaptive ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; + int tunnelingSupport = decoderInfo.tunneling ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return adaptiveSupport | formatSupport; + return adaptiveSupport | tunnelingSupport | formatSupport; } @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { super.onEnabled(joining); + tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; + tunneling = tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET; eventDispatcher.enabled(decoderCounters); frameReleaseTimeHelper.enable(); } @@ -216,10 +227,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { super.onPositionReset(positionUs, joining); - renderedFirstFrame = false; + clearRenderedFirstFrame(); consecutiveDroppedFrameCount = 0; - joiningDeadlineMs = joining && allowedJoiningTimeMs > 0 - ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; + if (joining) { + setJoiningDeadlineMs(); + } else { + joiningDeadlineMs = C.TIME_UNSET; + } } @Override @@ -246,11 +260,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { super.onStarted(); droppedFrames = 0; droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); + joiningDeadlineMs = C.TIME_UNSET; } @Override protected void onStopped() { - joiningDeadlineMs = C.TIME_UNSET; maybeNotifyDroppedFrames(); super.onStopped(); } @@ -261,8 +275,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentHeight = Format.NO_VALUE; currentPixelWidthHeightRatio = Format.NO_VALUE; pendingPixelWidthHeightRatio = Format.NO_VALUE; - clearLastReportedVideoSize(); + clearReportedVideoSize(); + clearRenderedFirstFrame(); frameReleaseTimeHelper.disable(); + tunnelingOnFrameRenderedListener = null; try { super.onDisabled(); } finally { @@ -287,18 +303,37 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private void setSurface(Surface surface) throws ExoPlaybackException { - // Clear state so that we always call the event listener with the video size and when a frame - // is rendered, even if the surface hasn't changed. - renderedFirstFrame = false; - clearLastReportedVideoSize(); - // We only need to actually release and reinitialize the codec if the surface has changed. + // We only need to update the codec if the surface has changed. if (this.surface != surface) { this.surface = surface; int state = getState(); if (state == STATE_ENABLED || state == STATE_STARTED) { - releaseCodec(); - maybeInitCodec(); + MediaCodec codec = getCodec(); + if (Util.SDK_INT >= 23 && codec != null && surface != null) { + setOutputSurfaceV23(codec, surface); + } else { + releaseCodec(); + maybeInitCodec(); + } } + if (surface != null) { + // If we know the video size, report it again immediately. + maybeRenotifyVideoSizeChanged(); + // We haven't rendered to the new surface yet. + clearRenderedFirstFrame(); + if (state == STATE_STARTED) { + setJoiningDeadlineMs(); + } + } else { + // The surface has been removed. + clearReportedVideoSize(); + clearRenderedFirstFrame(); + } + } else if (surface != null) { + // The surface is unchanged and non-null. If we know the video size and/or have already + // rendered to the surface, report these again immediately. + maybeRenotifyVideoSizeChanged(); + maybeRenotifyRenderedFirstFrame(); } } @@ -308,10 +343,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto) { - codecMaxValues = getCodecMaxValues(format, streamFormats); - MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround); + protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, + MediaCrypto crypto) throws DecoderQueryException { + codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats); + MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround, + tunnelingAudioSessionId); codec.configure(mediaFormat, surface, crypto, 0); + if (Util.SDK_INT >= 23 && tunneling) { + tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec); + } } @Override @@ -328,6 +368,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { pendingRotationDegrees = getRotationDegrees(newFormat); } + @Override + protected void onQueueInputBuffer(DecoderInputBuffer buffer) { + if (Util.SDK_INT < 23 && tunneling) { + maybeNotifyRenderedFirstFrame(); + } + } + @Override protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) { boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT) @@ -403,7 +450,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs); earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; - if (earlyUs < -30000) { + if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { // We're more than 30ms late rendering the frame. dropOutputBuffer(codec, bufferIndex); return true; @@ -437,6 +484,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return false; } + /** + * 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) { + // Drop the frame if we're more than 30ms late rendering the frame. + return earlyUs < -30000; + } + private void skipOutputBuffer(MediaCodec codec, int bufferIndex) { TraceUtil.beginSection("skipVideoBuffer"); codec.releaseOutputBuffer(bufferIndex, false); @@ -465,10 +525,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { TraceUtil.endSection(); decoderCounters.renderedOutputBufferCount++; consecutiveDroppedFrameCount = 0; - if (!renderedFirstFrame) { - renderedFirstFrame = true; - eventDispatcher.renderedFirstFrame(surface); - } + maybeNotifyRenderedFirstFrame(); } @TargetApi(21) @@ -479,29 +536,66 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { TraceUtil.endSection(); decoderCounters.renderedOutputBufferCount++; consecutiveDroppedFrameCount = 0; + maybeNotifyRenderedFirstFrame(); + } + + private void setJoiningDeadlineMs() { + joiningDeadlineMs = allowedJoiningTimeMs > 0 + ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; + } + + private void clearRenderedFirstFrame() { + renderedFirstFrame = false; + // The first frame notification is triggered by renderOutputBuffer or renderOutputBufferV21 for + // non-tunneled playback, onQueueInputBuffer for tunneled playback prior to API level 23, and + // OnFrameRenderedListenerV23.onFrameRenderedListener for tunneled playback on API level 23 and + // above. + if (Util.SDK_INT >= 23 && tunneling) { + MediaCodec codec = getCodec(); + // If codec is null then the listener will be instantiated in configureCodec. + if (codec != null) { + tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec); + } + } + } + + /* package */ void maybeNotifyRenderedFirstFrame() { if (!renderedFirstFrame) { renderedFirstFrame = true; eventDispatcher.renderedFirstFrame(surface); } } - private void clearLastReportedVideoSize() { - lastReportedWidth = Format.NO_VALUE; - lastReportedHeight = Format.NO_VALUE; - lastReportedPixelWidthHeightRatio = Format.NO_VALUE; - lastReportedUnappliedRotationDegrees = Format.NO_VALUE; + private void maybeRenotifyRenderedFirstFrame() { + if (renderedFirstFrame) { + eventDispatcher.renderedFirstFrame(surface); + } + } + + private void clearReportedVideoSize() { + reportedWidth = Format.NO_VALUE; + reportedHeight = Format.NO_VALUE; + reportedPixelWidthHeightRatio = Format.NO_VALUE; + reportedUnappliedRotationDegrees = Format.NO_VALUE; } private void maybeNotifyVideoSizeChanged() { - if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight - || lastReportedUnappliedRotationDegrees != currentUnappliedRotationDegrees - || lastReportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { + if (reportedWidth != currentWidth || reportedHeight != currentHeight + || reportedUnappliedRotationDegrees != currentUnappliedRotationDegrees + || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { + eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, + currentPixelWidthHeightRatio); + reportedWidth = currentWidth; + reportedHeight = currentHeight; + reportedUnappliedRotationDegrees = currentUnappliedRotationDegrees; + reportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; + } + } + + private void maybeRenotifyVideoSizeChanged() { + if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, currentPixelWidthHeightRatio); - lastReportedWidth = currentWidth; - lastReportedHeight = currentHeight; - lastReportedUnappliedRotationDegrees = currentUnappliedRotationDegrees; - lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; } } @@ -517,7 +611,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @SuppressLint("InlinedApi") private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, - boolean deviceNeedsAutoFrcWorkaround) { + boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); // Set the maximum adaptive video dimensions. frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); @@ -530,36 +624,114 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (deviceNeedsAutoFrcWorkaround) { frameworkMediaFormat.setInteger("auto-frc", 0); } + // Configure tunneling if enabled. + if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { + configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); + } return frameworkMediaFormat; } + @TargetApi(23) + private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { + codec.setOutputSurface(surface); + } + + @TargetApi(21) + private static void configureTunnelingV21(MediaFormat mediaFormat, int tunnelingAudioSessionId) { + mediaFormat.setFeatureEnabled(CodecCapabilities.FEATURE_TunneledPlayback, true); + mediaFormat.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, tunnelingAudioSessionId); + } + /** * Returns {@link CodecMaxValues} suitable for configuring a codec for {@code format} in a way * 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 streamFormats The possible stream formats. * @return Suitable {@link CodecMaxValues}. + * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - private static CodecMaxValues getCodecMaxValues(Format format, Format[] streamFormats) { + private static CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format, + Format[] streamFormats) throws DecoderQueryException { int maxWidth = format.width; int maxHeight = format.height; int maxInputSize = getMaxInputSize(format); + if (streamFormats.length == 1) { + // The single entry in streamFormats must correspond to the format for which the codec is + // being configured. + return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); + } + boolean haveUnknownDimensions = false; for (Format streamFormat : streamFormats) { if (areAdaptationCompatible(format, streamFormat)) { + haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE + || streamFormat.height == Format.NO_VALUE); maxWidth = Math.max(maxWidth, streamFormat.width); maxHeight = Math.max(maxHeight, streamFormat.height); maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat)); } } + if (haveUnknownDimensions) { + Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight); + Point codecMaxSize = getCodecMaxSize(codecInfo, format); + if (codecMaxSize != null) { + maxWidth = Math.max(maxWidth, codecMaxSize.x); + maxHeight = Math.max(maxHeight, codecMaxSize.y); + maxInputSize = Math.max(maxInputSize, + getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); + Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); + } + } return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } + /** + * 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. + * @return The maximum video size to use, or null if the size of {@code format} should be used. + * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. + */ + private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) + throws DecoderQueryException { + boolean isVerticalVideo = format.height > format.width; + int formatLongEdgePx = isVerticalVideo ? format.height : format.width; + int formatShortEdgePx = isVerticalVideo ? format.width : format.height; + float aspectRatio = (float) formatShortEdgePx / formatLongEdgePx; + for (int longEdgePx : STANDARD_LONG_EDGE_VIDEO_PX) { + int shortEdgePx = (int) (longEdgePx * aspectRatio); + if (longEdgePx <= formatLongEdgePx || shortEdgePx <= formatShortEdgePx) { + // Don't return a size not larger than the format for which the codec is being configured. + return null; + } else if (Util.SDK_INT >= 21) { + Point alignedSize = codecInfo.alignVideoSizeV21(isVerticalVideo ? shortEdgePx : longEdgePx, + isVerticalVideo ? longEdgePx : shortEdgePx); + float frameRate = format.frameRate; + if (codecInfo.isVideoSizeAndRateSupportedV21(alignedSize.x, alignedSize.y, frameRate)) { + return alignedSize; + } + } else { + // Conservatively assume the codec requires 16px width and height alignment. + longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16; + shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16; + if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) { + return new Point(isVerticalVideo ? shortEdgePx : longEdgePx, + isVerticalVideo ? longEdgePx : shortEdgePx); + } + } + } + return null; + } + /** * Returns a maximum input size for a given format. * * @param format The format. - * @return An maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be + * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be * determined. */ private static int getMaxInputSize(Format format) { @@ -567,8 +739,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // The format defines an explicit maximum input size. return format.maxInputSize; } + return getMaxInputSize(format.sampleMimeType, format.width, format.height); + } - if (format.width == Format.NO_VALUE || format.height == Format.NO_VALUE) { + /** + * Returns a maximum input size for a given mime type, width and height. + * + * @param sampleMimeType The format mime type. + * @param width The width in pixels. + * @param height The height in pixels. + * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be + * determined. + */ + private static int getMaxInputSize(String sampleMimeType, int width, int height) { + if (width == Format.NO_VALUE || height == Format.NO_VALUE) { // We can't infer a maximum input size without video dimensions. return Format.NO_VALUE; } @@ -576,10 +760,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // Attempt to infer a maximum input size from the format. int maxPixels; int minCompressionRatio; - switch (format.sampleMimeType) { + switch (sampleMimeType) { case MimeTypes.VIDEO_H263: case MimeTypes.VIDEO_MP4V: - maxPixels = format.width * format.height; + maxPixels = width * height; minCompressionRatio = 2; break; case MimeTypes.VIDEO_H264: @@ -589,17 +773,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Format.NO_VALUE; } // Round up width/height to an integer number of macroblocks. - maxPixels = ((format.width + 15) / 16) * ((format.height + 15) / 16) * 16 * 16; + maxPixels = Util.ceilDivide(width, 16) * Util.ceilDivide(height, 16) * 16 * 16; minCompressionRatio = 2; break; case MimeTypes.VIDEO_VP8: // VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp. - maxPixels = format.width * format.height; + maxPixels = width * height; minCompressionRatio = 2; break; case MimeTypes.VIDEO_H265: case MimeTypes.VIDEO_VP9: - maxPixels = format.width * format.height; + maxPixels = width * height; minCompressionRatio = 4; break; default: @@ -668,4 +852,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } + @TargetApi(23) + private final class OnFrameRenderedListenerV23 implements MediaCodec.OnFrameRenderedListener { + + private OnFrameRenderedListenerV23(MediaCodec codec) { + codec.setOnFrameRenderedListener(this, new Handler()); + } + + @Override + public void onFrameRendered(@NonNull MediaCodec codec, long presentationTimeUs, long nanoTime) { + if (this != tunnelingOnFrameRenderedListener) { + // Stale event. + return; + } + maybeNotifyRenderedFirstFrame(); + } + + } + } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/GapWorker.java b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/GapWorker.java index 23e8d4565..7fea31196 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/GapWorker.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/GapWorker.java @@ -106,6 +106,10 @@ final class GapWorker implements Runnable { @Override public void addPosition(int layoutPosition, int pixelDistance) { + if (layoutPosition < 0) { + throw new IllegalArgumentException("Layout positions must be non-negative"); + } + if (pixelDistance < 0) { throw new IllegalArgumentException("Pixel distance must be non-negative"); } @@ -145,6 +149,7 @@ final class GapWorker implements Runnable { if (mPrefetchArray != null) { Arrays.fill(mPrefetchArray, -1); } + mCount = 0; } } @@ -210,8 +215,10 @@ final class GapWorker implements Runnable { int totalTaskCount = 0; for (int i = 0; i < viewCount; i++) { RecyclerView view = mRecyclerViews.get(i); - view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false); - totalTaskCount += view.mPrefetchRegistry.mCount; + if (view.getWindowVisibility() == View.VISIBLE) { + view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false); + totalTaskCount += view.mPrefetchRegistry.mCount; + } } // Populate task list from prefetch data... @@ -219,6 +226,11 @@ final class GapWorker implements Runnable { int totalTaskIndex = 0; for (int i = 0; i < viewCount; i++) { RecyclerView view = mRecyclerViews.get(i); + if (view.getWindowVisibility() != View.VISIBLE) { + // Invisible view, don't bother prefetching + continue; + } + LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry; final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx) + Math.abs(prefetchRegistry.mPrefetchDy); @@ -354,18 +366,23 @@ final class GapWorker implements Runnable { return; } - // Query last vsync so we can predict next one. Note that drawing time not yet + // Query most recent vsync so we can predict next one. Note that drawing time not yet // valid in animation/input callbacks, so query it here to be safe. - long lastFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos( - mRecyclerViews.get(0).getDrawingTime()); - if (lastFrameVsyncNs == 0) { - // abort - couldn't get last vsync for estimating next + final int size = mRecyclerViews.size(); + long latestFrameVsyncMs = 0; + for (int i = 0; i < size; i++) { + RecyclerView view = mRecyclerViews.get(i); + if (view.getWindowVisibility() == View.VISIBLE) { + latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs); + } + } + + if (latestFrameVsyncMs == 0) { + // abort - either no views visible, or couldn't get last vsync for estimating next return; } - // TODO: consider rebasing deadline if frame was already dropped due to long UI work. - // Next frame will still wait for VSYNC, so we can still use the gap if it exists. - long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs; + long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs; prefetch(nextFrameNs); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/GridLayoutManager.java b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/GridLayoutManager.java index 415a5d803..8639be76f 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/GridLayoutManager.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/GridLayoutManager.java @@ -58,7 +58,6 @@ public class GridLayoutManager extends LinearLayoutManager { // re-used variable to acquire decor insets from RecyclerView final Rect mDecorInsets = new Rect(); - /** * Creates a vertical GridLayoutManager * @@ -497,7 +496,7 @@ public class GridLayoutManager extends LinearLayoutManager { int count = 0; while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { final int pos = layoutState.mCurrentPosition; - layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset); + layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset)); final int spanSize = mSpanSizeLookup.getSpanSize(pos); remainingSpan -= spanSize; layoutState.mCurrentPosition += layoutState.mItemDirection; @@ -679,7 +678,7 @@ public class GridLayoutManager extends LinearLayoutManager { if (params.isItemRemoved() || params.isItemChanged()) { result.mIgnoreConsumed = true; } - result.mFocusable |= view.isFocusable(); + result.mFocusable |= view.hasFocusable(); } Arrays.fill(mSet, null); } @@ -1003,47 +1002,98 @@ public class GridLayoutManager extends LinearLayoutManager { limit = getChildCount(); } final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL(); - View weakCandidate = null; // somewhat matches but not strong - int weakCandidateSpanIndex = -1; - int weakCandidateOverlap = 0; // how many spans overlap + // The focusable candidate to be picked if no perfect focusable candidate is found. + // The best focusable candidate is the one with the highest amount of span overlap with + // the currently focused view. + View focusableWeakCandidate = null; // somewhat matches but not strong + int focusableWeakCandidateSpanIndex = -1; + int focusableWeakCandidateOverlap = 0; // how many spans overlap + + // The unfocusable candidate to become visible on the screen next, if no perfect or + // weak focusable candidates are found to receive focus next. + // We are only interested in partially visible unfocusable views. These are views that are + // not fully visible, that is either partially overlapping, or out-of-bounds and right below + // or above RV's padded bounded area. The best unfocusable candidate is the one with the + // highest amount of span overlap with the currently focused view. + View unfocusableWeakCandidate = null; // somewhat matches but not strong + int unfocusableWeakCandidateSpanIndex = -1; + int unfocusableWeakCandidateOverlap = 0; // how many spans overlap + + // The span group index of the start child. This indicates the span group index of the + // next focusable item to receive focus, if a focusable item within the same span group + // exists. Any focusable item beyond this group index are not relevant since they + // were already stored in the layout before onFocusSearchFailed call and were not picked + // by the focusSearch algorithm. + int focusableSpanGroupIndex = getSpanGroupIndex(recycler, state, start); for (int i = start; i != limit; i += inc) { + int spanGroupIndex = getSpanGroupIndex(recycler, state, i); View candidate = getChildAt(i); if (candidate == prevFocusedChild) { break; } - if (!candidate.isFocusable()) { + + if (candidate.hasFocusable() && spanGroupIndex != focusableSpanGroupIndex) { + // We are past the allowable span group index for the next focusable item. + // The search only continues if no focusable weak candidates have been found up + // until this point, in order to find the best unfocusable candidate to become + // visible on the screen next. + if (focusableWeakCandidate != null) { + break; + } continue; } + final LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams(); final int candidateStart = candidateLp.mSpanIndex; final int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize; - if (candidateStart == prevSpanStart && candidateEnd == prevSpanEnd) { + if (candidate.hasFocusable() && candidateStart == prevSpanStart + && candidateEnd == prevSpanEnd) { return candidate; // perfect match } boolean assignAsWeek = false; - if (weakCandidate == null) { + if ((candidate.hasFocusable() && focusableWeakCandidate == null) + || (!candidate.hasFocusable() && unfocusableWeakCandidate == null)) { assignAsWeek = true; } else { int maxStart = Math.max(candidateStart, prevSpanStart); int minEnd = Math.min(candidateEnd, prevSpanEnd); int overlap = minEnd - maxStart; - if (overlap > weakCandidateOverlap) { - assignAsWeek = true; - } else if (overlap == weakCandidateOverlap && - preferLastSpan == (candidateStart > weakCandidateSpanIndex)) { - assignAsWeek = true; + if (candidate.hasFocusable()) { + if (overlap > focusableWeakCandidateOverlap) { + assignAsWeek = true; + } else if (overlap == focusableWeakCandidateOverlap + && preferLastSpan == (candidateStart + > focusableWeakCandidateSpanIndex)) { + assignAsWeek = true; + } + } else if (focusableWeakCandidate == null + && isViewPartiallyVisible(candidate, false, true)) { + if (overlap > unfocusableWeakCandidateOverlap) { + assignAsWeek = true; + } else if (overlap == unfocusableWeakCandidateOverlap + && preferLastSpan == (candidateStart + > unfocusableWeakCandidateSpanIndex)) { + assignAsWeek = true; + } } } if (assignAsWeek) { - weakCandidate = candidate; - weakCandidateSpanIndex = candidateLp.mSpanIndex; - weakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) - - Math.max(candidateStart, prevSpanStart); + if (candidate.hasFocusable()) { + focusableWeakCandidate = candidate; + focusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; + focusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) + - Math.max(candidateStart, prevSpanStart); + } else { + unfocusableWeakCandidate = candidate; + unfocusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; + unfocusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) + - Math.max(candidateStart, prevSpanStart); + } } } - return weakCandidate; + return (focusableWeakCandidate != null) ? focusableWeakCandidate : unfocusableWeakCandidate; } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/LinearLayoutManager.java b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/LinearLayoutManager.java index 3f98bdc34..edf0a3ee2 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/LinearLayoutManager.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/LinearLayoutManager.java @@ -18,12 +18,14 @@ package org.telegram.messenger.support.widget; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static org.telegram.messenger.support.widget.RecyclerView.NO_POSITION; +import static org.telegram.messenger.support.widget.RecyclerView.VERBOSE_TRACING; import android.content.Context; import android.graphics.PointF; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.RestrictTo; +import android.support.v4.os.TraceCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat; @@ -38,7 +40,7 @@ import android.view.accessibility.AccessibilityEvent; import java.util.List; /** - * A {@link org.telegram.messenger.support.widget.RecyclerView.LayoutManager} implementation which provides + * A {@link android.support.v7.widget.RecyclerView.LayoutManager} implementation which provides * similar functionality to {@link android.widget.ListView}. */ public class LinearLayoutManager extends RecyclerView.LayoutManager implements @@ -146,7 +148,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements /** * Number of items to prefetch when first coming on screen with new data. */ - private int mInitialItemPrefetchCount = 2; + private int mInitialPrefetchItemCount = 2; /** * Creates a vertical LinearLayoutManager @@ -357,8 +359,8 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements * laid out at the end of the UI, second item is laid out before it etc. * * For horizontal layouts, it depends on the layout direction. - * When set to true, If {@link org.telegram.messenger.support.widget.RecyclerView} is LTR, than it will - * layout from RTL, if {@link org.telegram.messenger.support.widget.RecyclerView}} is RTL, it will layout + * When set to true, If {@link android.support.v7.widget.RecyclerView} is LTR, than it will + * layout from RTL, if {@link android.support.v7.widget.RecyclerView}} is RTL, it will layout * from LTR. * * If you are looking for the exact same behavior of @@ -396,14 +398,22 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements } /** - *

    Returns the amount of extra space that should be laid out by LayoutManager. - * By default, {@link android.support.v7.widget.LinearLayoutManager} lays out 1 extra page of - * items while smooth scrolling and 0 otherwise. You can override this method to implement your - * custom layout pre-cache logic.

    - *

    Laying out invisible elements will eventually come with performance cost. On the other - * hand, in places like smooth scrolling to an unknown location, this extra content helps - * LayoutManager to calculate a much smoother scrolling; which improves user experience.

    - *

    You can also use this if you are trying to pre-layout your upcoming views.

    + *

    Returns the amount of extra space that should be laid out by LayoutManager.

    + * + *

    By default, {@link android.support.v7.widget.LinearLayoutManager} lays out 1 extra page + * of items while smooth scrolling and 0 otherwise. You can override this method to implement + * your custom layout pre-cache logic.

    + * + *

    Note:Laying out invisible elements generally comes with significant + * performance cost. It's typically only desirable in places like smooth scrolling to an unknown + * location, where 1) the extra content helps LinearLayoutManager know in advance when its + * target is approaching, so it can decelerate early and smoothly and 2) while motion is + * continuous.

    + * + *

    Extending the extra layout space is especially expensive if done while the user may change + * scrolling direction. Changing direction will cause the extra layout space to swap to the + * opposite side of the viewport, incurring many rebinds/recycles, unless the cache is large + * enough to handle it.

    * * @return The extra space that should be laid out (in pixels). */ @@ -649,7 +659,8 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements * If necessary, layouts new items for predictive animations */ private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler, - RecyclerView.State state, int startOffset, int endOffset) { + RecyclerView.State state, int startOffset, + int endOffset) { // If there are scrap children that we did not layout, we need to find where they did go // and layout them accordingly so that animations can work as expected. // This case may happen if new views are added or an existing view expands and pushes @@ -829,7 +840,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements } anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd ? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper - .getTotalSpaceChange()) + .getTotalSpaceChange()) : mOrientationHelper.getDecoratedStart(child); } else { // item is not visible. if (getChildCount() > 0) { @@ -1174,7 +1185,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements LayoutPrefetchRegistry layoutPrefetchRegistry) { final int pos = layoutState.mCurrentPosition; if (pos >= 0 && pos < state.getItemCount()) { - layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset); + layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset)); } } @@ -1201,7 +1212,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements ? LayoutState.ITEM_DIRECTION_HEAD : LayoutState.ITEM_DIRECTION_TAIL; int targetPos = anchorPos; - for (int i = 0; i < mInitialItemPrefetchCount; i++) { + for (int i = 0; i < mInitialPrefetchItemCount; i++) { if (targetPos >= 0 && targetPos < adapterItemCount) { layoutPrefetchRegistry.addPosition(targetPos, 0); } else { @@ -1237,11 +1248,11 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements * @param itemCount Number of items to prefetch * * @see #isItemPrefetchEnabled() - * @see #getInitialItemPrefetchCount() + * @see #getInitialPrefetchItemCount() * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) */ public void setInitialPrefetchItemCount(int itemCount) { - mInitialItemPrefetchCount = itemCount; + mInitialPrefetchItemCount = itemCount; } /** @@ -1256,8 +1267,17 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements * * @return number of items to prefetch. */ + public int getInitialPrefetchItemCount() { + return mInitialPrefetchItemCount; + } + + + /** + * @deprecated Use {@link #getInitialPrefetchItemCount()} instead. + */ + @Deprecated public int getInitialItemPrefetchCount() { - return mInitialItemPrefetchCount; + return getInitialPrefetchItemCount(); } @Override @@ -1337,7 +1357,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements *

    * Checks both layout position and visible position to guarantee that the view is not visible. * - * @param recycler Recycler instance of {@link org.telegram.messenger.support.widget.RecyclerView} + * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView} * @param dt This can be used to add additional padding to the visible area. This is used * to detect children that will go out of bounds after scrolling, without * actually moving them. @@ -1382,7 +1402,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements *

    * Checks both layout position and visible position to guarantee that the view is not visible. * - * @param recycler Recycler instance of {@link org.telegram.messenger.support.widget.RecyclerView} + * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView} * @param dt This can be used to add additional padding to the visible area. This is used * to detect children that will go out of bounds after scrolling, without * actually moving them. @@ -1427,8 +1447,8 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements * @param layoutState Current layout state. Right now, this object does not change but * we may consider moving it out of this view so passing around as a * parameter for now, rather than accessing {@link #mLayoutState} - * @see #recycleViewsFromStart(org.telegram.messenger.support.widget.RecyclerView.Recycler, int) - * @see #recycleViewsFromEnd(org.telegram.messenger.support.widget.RecyclerView.Recycler, int) + * @see #recycleViewsFromStart(android.support.v7.widget.RecyclerView.Recycler, int) + * @see #recycleViewsFromEnd(android.support.v7.widget.RecyclerView.Recycler, int) * @see android.support.v7.widget.LinearLayoutManager.LayoutState#mLayoutDirection */ private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { @@ -1468,7 +1488,13 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements LayoutChunkResult layoutChunkResult = mLayoutChunkResult; while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunkResult.resetInternal(); + if (VERBOSE_TRACING) { + TraceCompat.beginSection("LLM LayoutChunk"); + } layoutChunk(recycler, state, layoutState, layoutChunkResult); + if (VERBOSE_TRACING) { + TraceCompat.endSection(); + } if (layoutChunkResult.mFinished) { break; } @@ -1573,7 +1599,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements if (params.isItemRemoved() || params.isItemChanged()) { result.mIgnoreConsumed = true; } - result.mFocusable = view.isFocusable(); + result.mFocusable = view.hasFocusable(); } @Override @@ -1760,6 +1786,32 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch; } + // returns the out-of-bound child view closest to RV's end bounds. An out-of-bound child is + // defined as a child that's either partially or fully invisible (outside RV's padding area). + private View findPartiallyOrCompletelyInvisibleChildClosestToEnd(RecyclerView.Recycler recycler, + RecyclerView.State state) { + return mShouldReverseLayout ? findFirstPartiallyOrCompletelyInvisibleChild(recycler, state) + : findLastPartiallyOrCompletelyInvisibleChild(recycler, state); + } + + // returns the out-of-bound child view closest to RV's starting bounds. An out-of-bound child is + // defined as a child that's either partially or fully invisible (outside RV's padding area). + private View findPartiallyOrCompletelyInvisibleChildClosestToStart( + RecyclerView.Recycler recycler, RecyclerView.State state) { + return mShouldReverseLayout ? findLastPartiallyOrCompletelyInvisibleChild(recycler, state) : + findFirstPartiallyOrCompletelyInvisibleChild(recycler, state); + } + + private View findFirstPartiallyOrCompletelyInvisibleChild(RecyclerView.Recycler recycler, + RecyclerView.State state) { + return findOnePartiallyOrCompletelyInvisibleChild(0, getChildCount()); + } + + private View findLastPartiallyOrCompletelyInvisibleChild(RecyclerView.Recycler recycler, + RecyclerView.State state) { + return findOnePartiallyOrCompletelyInvisibleChild(getChildCount() - 1, -1); + } + /** * Returns the adapter position of the first visible view. This position does not include * adapter changes that were dispatched after the last layout pass. @@ -1840,30 +1892,58 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements return child == null ? NO_POSITION : getPosition(child); } + // Returns the first child that is visible in the provided index range, i.e. either partially or + // fully visible depending on the arguments provided. Completely invisible children are not + // acceptable by this method, but could be returned + // using #findOnePartiallyOrCompletelyInvisibleChild View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible, boolean acceptPartiallyVisible) { ensureLayoutState(); - final int start = mOrientationHelper.getStartAfterPadding(); - final int end = mOrientationHelper.getEndAfterPadding(); - final int next = toIndex > fromIndex ? 1 : -1; - View partiallyVisible = null; - for (int i = fromIndex; i != toIndex; i+=next) { - final View child = getChildAt(i); - final int childStart = mOrientationHelper.getDecoratedStart(child); - final int childEnd = mOrientationHelper.getDecoratedEnd(child); - if (childStart < end && childEnd > start) { - if (completelyVisible) { - if (childStart >= start && childEnd <= end) { - return child; - } else if (acceptPartiallyVisible && partiallyVisible == null) { - partiallyVisible = child; - } - } else { - return child; - } - } + @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = 0; + @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = 0; + if (completelyVisible) { + preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_GT_PVS | ViewBoundsCheck.FLAG_CVS_EQ_PVS + | ViewBoundsCheck.FLAG_CVE_LT_PVE | ViewBoundsCheck.FLAG_CVE_EQ_PVE); + } else { + preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE + | ViewBoundsCheck.FLAG_CVE_GT_PVS); } - return partiallyVisible; + if (acceptPartiallyVisible) { + acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE + | ViewBoundsCheck.FLAG_CVE_GT_PVS); + } + return (mOrientation == HORIZONTAL) ? mHorizontalBoundCheck + .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, + acceptableBoundsFlag) : mVerticalBoundCheck + .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, + acceptableBoundsFlag); + } + + View findOnePartiallyOrCompletelyInvisibleChild(int fromIndex, int toIndex) { + ensureLayoutState(); + final int next = toIndex > fromIndex ? 1 : (toIndex < fromIndex ? -1 : 0); + if (next == 0) { + return getChildAt(fromIndex); + } + @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = 0; + @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = 0; + if (mOrientationHelper.getDecoratedStart(getChildAt(fromIndex)) + < mOrientationHelper.getStartAfterPadding()) { + preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVS | ViewBoundsCheck.FLAG_CVE_LT_PVE + | ViewBoundsCheck.FLAG_CVE_GT_PVS); + acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVS + | ViewBoundsCheck.FLAG_CVE_LT_PVE); + } else { + preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVE_GT_PVE | ViewBoundsCheck.FLAG_CVS_GT_PVS + | ViewBoundsCheck.FLAG_CVS_LT_PVE); + acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVE_GT_PVE + | ViewBoundsCheck.FLAG_CVS_GT_PVS); + } + return (mOrientation == HORIZONTAL) ? mHorizontalBoundCheck + .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, + acceptableBoundsFlag) : mVerticalBoundCheck + .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, + acceptableBoundsFlag); } @Override @@ -1879,35 +1959,38 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements return null; } ensureLayoutState(); - final View referenceChild; - if (layoutDir == LayoutState.LAYOUT_START) { - referenceChild = findReferenceChildClosestToStart(recycler, state); - } else { - referenceChild = findReferenceChildClosestToEnd(recycler, state); - } - if (referenceChild == null) { - if (DEBUG) { - Log.d(TAG, - "Cannot find a child with a valid position to be used for focus search."); - } - return null; - } ensureLayoutState(); final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace()); updateLayoutState(layoutDir, maxScroll, false, state); mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN; mLayoutState.mRecycle = false; fill(recycler, mLayoutState, state, true); + + // nextCandidate is the first child view in the layout direction that's partially + // within RV's bounds, i.e. part of it is visible or it's completely invisible but still + // touching RV's bounds. This will be the unfocusable candidate view to become visible onto + // the screen if no focusable views are found in the given layout direction. + final View nextCandidate; + if (layoutDir == LayoutState.LAYOUT_START) { + nextCandidate = findPartiallyOrCompletelyInvisibleChildClosestToStart(recycler, state); + } else { + nextCandidate = findPartiallyOrCompletelyInvisibleChildClosestToEnd(recycler, state); + } + // nextFocus is meaningful only if it refers to a focusable child, in which case it + // indicates the next view to gain focus. final View nextFocus; if (layoutDir == LayoutState.LAYOUT_START) { nextFocus = getChildClosestToStart(); } else { nextFocus = getChildClosestToEnd(); } - if (nextFocus == referenceChild || !nextFocus.isFocusable()) { - return null; + if (nextFocus.hasFocusable()) { + if (nextCandidate == null) { + return null; + } + return nextFocus; } - return nextFocus; + return nextCandidate; } /** @@ -1995,9 +2078,9 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements if (mShouldReverseLayout) { if (dropDirection == LayoutState.ITEM_DIRECTION_TAIL) { scrollToPositionWithOffset(targetPos, - mOrientationHelper.getEndAfterPadding() - - (mOrientationHelper.getDecoratedStart(target) + - mOrientationHelper.getDecoratedMeasurement(view))); + mOrientationHelper.getEndAfterPadding() + - (mOrientationHelper.getDecoratedStart(target) + + mOrientationHelper.getDecoratedMeasurement(view))); } else { scrollToPositionWithOffset(targetPos, mOrientationHelper.getEndAfterPadding() - diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/LinearSmoothScrollerMiddle.java b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/LinearSmoothScrollerMiddle.java new file mode 100644 index 000000000..8b8322296 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/LinearSmoothScrollerMiddle.java @@ -0,0 +1,151 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.messenger.support.widget; + +import android.content.Context; +import android.graphics.PointF; +import android.support.annotation.Nullable; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; + +public class LinearSmoothScrollerMiddle extends RecyclerView.SmoothScroller { + + private static final float MILLISECONDS_PER_INCH = 25f; + + private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000; + + private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f; + + protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator(); + + protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator(1.5f); + + protected PointF mTargetVector; + + private final float MILLISECONDS_PER_PX; + + protected int mInterimTargetDx = 0, mInterimTargetDy = 0; + + public LinearSmoothScrollerMiddle(Context context) { + MILLISECONDS_PER_PX = MILLISECONDS_PER_INCH / context.getResources().getDisplayMetrics().densityDpi; + } + + @Override + protected void onStart() { + + } + + @Override + protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { + final int dy = calculateDyToMakeVisible(targetView); + final int time = calculateTimeForDeceleration(dy); + if (time > 0) { + action.update(0, -dy, Math.max(400, time), mDecelerateInterpolator); + } + } + + @Override + protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) { + if (getChildCount() == 0) { + stop(); + return; + } + mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx); + mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy); + + if (mInterimTargetDx == 0 && mInterimTargetDy == 0) { + updateActionForInterimTarget(action); + } + } + + @Override + protected void onStop() { + mInterimTargetDx = mInterimTargetDy = 0; + mTargetVector = null; + } + + protected int calculateTimeForDeceleration(int dx) { + return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356); + } + + protected int calculateTimeForScrolling(int dx) { + return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX); + } + + protected void updateActionForInterimTarget(Action action) { + // find an interim target position + PointF scrollVector = computeScrollVectorForPosition(getTargetPosition()); + if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) { + final int target = getTargetPosition(); + action.jumpTo(target); + stop(); + return; + } + normalize(scrollVector); + mTargetVector = scrollVector; + + mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x); + mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y); + final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX); + // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the + // interim target. Since we track the distance travelled in onSeekTargetStep callback, it + // won't actually scroll more than what we need. + action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO) + , (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO) + , (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator); + } + + private int clampApplyScroll(int tmpDt, int dt) { + final int before = tmpDt; + tmpDt -= dt; + if (before * tmpDt <= 0) { + return 0; + } + return tmpDt; + } + + public int calculateDyToMakeVisible(View view) { + final RecyclerView.LayoutManager layoutManager = getLayoutManager(); + if (layoutManager == null || !layoutManager.canScrollVertically()) { + return 0; + } + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); + int top = layoutManager.getDecoratedTop(view) - params.topMargin; + int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin; + int start = layoutManager.getPaddingTop(); + int end = layoutManager.getHeight() - layoutManager.getPaddingBottom(); + + if (top > start && bottom < end) { + return 0; + } + int boxSize = end - start; + int viewSize = bottom - top; + start = (boxSize - viewSize) / 2; + end = start + viewSize; + final int dtStart = start - top; + if (dtStart > 0) { + return dtStart; + } + final int dtEnd = end - bottom; + if (dtEnd < 0) { + return dtEnd; + } + return 0; + } + + @Nullable + public PointF computeScrollVectorForPosition(int targetPosition) { + RecyclerView.LayoutManager layoutManager = getLayoutManager(); + if (layoutManager instanceof ScrollVectorProvider) { + return ((ScrollVectorProvider) layoutManager).computeScrollVectorForPosition(targetPosition); + } + return null; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java index f42b8674d..0a1e742ab 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java @@ -54,7 +54,6 @@ import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.support.v4.widget.EdgeEffectCompat; import android.support.v4.widget.ScrollerCompat; - import org.telegram.messenger.FileLog; import org.telegram.messenger.support.widget.RecyclerView.ItemAnimator.ItemHolderInfo; import android.util.AttributeSet; @@ -157,6 +156,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro static final boolean DEBUG = false; + static final boolean VERBOSE_TRACING = false; + private static final int[] NESTED_SCROLLING_ATTRS = {16843830 /* android.R.attr.nestedScrollingEnabled */}; @@ -480,12 +481,17 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro private final int[] mNestedOffsets = new int[2]; private int topGlowOffset = 0; + private int bottomGlowOffset = 0; private int glowColor = 0; public void setTopGlowOffset(int offset) { topGlowOffset = offset; } + public void setBottomGlowOffset(int offset) { + bottomGlowOffset = offset; + } + public void setGlowColor(int color) { glowColor = color; } @@ -521,38 +527,39 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro */ private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback = new ViewInfoStore.ProcessCallback() { - @Override - public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, - @Nullable ItemHolderInfo postInfo) { - mRecycler.unscrapView(viewHolder); - animateDisappearance(viewHolder, info, postInfo); - } - @Override - public void processAppeared(ViewHolder viewHolder, - ItemHolderInfo preInfo, ItemHolderInfo info) { - animateAppearance(viewHolder, preInfo, info); - } - - @Override - public void processPersistent(ViewHolder viewHolder, - @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) { - viewHolder.setIsRecyclable(false); - if (mDataSetHasChangedAfterLayout) { - // since it was rebound, use change instead as we'll be mapping them from - // stable ids. If stable ids were false, we would not be running any - // animations - if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) { - postAnimationRunner(); + @Override + public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, + @Nullable ItemHolderInfo postInfo) { + mRecycler.unscrapView(viewHolder); + animateDisappearance(viewHolder, info, postInfo); } - } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) { - postAnimationRunner(); - } - } - @Override - public void unused(ViewHolder viewHolder) { - mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler); - } - }; + @Override + public void processAppeared(ViewHolder viewHolder, + ItemHolderInfo preInfo, ItemHolderInfo info) { + animateAppearance(viewHolder, preInfo, info); + } + + @Override + public void processPersistent(ViewHolder viewHolder, + @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) { + viewHolder.setIsRecyclable(false); + if (mDataSetHasChangedAfterLayout) { + // since it was rebound, use change instead as we'll be mapping them from + // stable ids. If stable ids were false, we would not be running any + // animations + if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, + postInfo)) { + postAnimationRunner(); + } + } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) { + postAnimationRunner(); + } + } + @Override + public void unused(ViewHolder viewHolder) { + mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler); + } + }; public RecyclerView(Context context) { this(context, null); @@ -696,7 +703,13 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro @Override public void addView(View child, int index) { + if (VERBOSE_TRACING) { + TraceCompat.beginSection("RV addView"); + } RecyclerView.this.addView(child, index); + if (VERBOSE_TRACING) { + TraceCompat.endSection(); + } dispatchChildAttached(child); } @@ -711,7 +724,13 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (child != null) { dispatchChildDetached(child); } + if (VERBOSE_TRACING) { + TraceCompat.beginSection("RV removeViewAt"); + } RecyclerView.this.removeViewAt(index); + if (VERBOSE_TRACING) { + TraceCompat.endSection(); + } } @Override @@ -1319,7 +1338,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * * @param extension ViewCacheExtension to be used or null if you want to clear the existing one. * - * @see {@link ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)} + * @see ViewCacheExtension#getViewForPositionAndType(Recycler, int, int) */ public void setViewCacheExtension(ViewCacheExtension extension) { mRecycler.setViewCacheExtension(extension); @@ -1507,9 +1526,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * Convenience method to scroll to a certain position. * * RecyclerView does not implement scrolling logic, rather forwards the call to - * {@link org.telegram.messenger.support.widget.RecyclerView.LayoutManager#scrollToPosition(int)} + * {@link android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)} * @param position Scroll to this adapter position - * @see org.telegram.messenger.support.widget.RecyclerView.LayoutManager#scrollToPosition(int) + * @see android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int) */ public void scrollToPosition(int position) { if (mLayoutFrozen) { @@ -1722,7 +1741,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * LayoutManager.

    * * @return The horizontal offset of the scrollbar's thumb - * @see org.telegram.messenger.support.widget.RecyclerView.LayoutManager#computeHorizontalScrollOffset + * @see android.support.v7.widget.RecyclerView.LayoutManager#computeHorizontalScrollOffset * (RecyclerView.State) */ @Override @@ -1795,7 +1814,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * LayoutManager.

    * * @return The vertical offset of the scrollbar's thumb - * @see org.telegram.messenger.support.widget.RecyclerView.LayoutManager#computeVerticalScrollOffset + * @see android.support.v7.widget.RecyclerView.LayoutManager#computeVerticalScrollOffset * (RecyclerView.State) */ @Override @@ -2330,6 +2349,19 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro resumeRequestLayout(false); } } + if (result != null && !result.hasFocusable()) { + if (getFocusedChild() == null) { + // Scrolling to this unfocusable view is not meaningful since there is no currently + // focused view which RV needs to keep visible. + return super.focusSearch(focused, direction); + } + // If the next view returned by onFocusSearchFailed in layout manager has no focusable + // views, we still scroll to that view in order to make it visible on the screen. + // If it's focusable, framework already calls RV's requestChildFocus which handles + // bringing this newly focused item onto the screen. + requestChildOnScreen(result, null); + return focused; + } return isPreferredNextFocus(focused, result, direction) ? result : super.focusSearch(focused, direction); } @@ -2400,31 +2432,48 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro @Override public void requestChildFocus(View child, View focused) { if (!mLayout.onRequestChildFocus(this, mState, child, focused) && focused != null) { - mTempRect.set(0, 0, focused.getWidth(), focused.getHeight()); - - // get item decor offsets w/o refreshing. If they are invalid, there will be another - // layout pass to fix them, then it is LayoutManager's responsibility to keep focused - // View in viewport. - final ViewGroup.LayoutParams focusedLayoutParams = focused.getLayoutParams(); - if (focusedLayoutParams instanceof LayoutParams) { - // if focused child has item decors, use them. Otherwise, ignore. - final LayoutParams lp = (LayoutParams) focusedLayoutParams; - if (!lp.mInsetsDirty) { - final Rect insets = lp.mDecorInsets; - mTempRect.left -= insets.left; - mTempRect.right += insets.right; - mTempRect.top -= insets.top; - mTempRect.bottom += insets.bottom; - } - } - - offsetDescendantRectToMyCoords(focused, mTempRect); - offsetRectIntoDescendantCoords(child, mTempRect); - requestChildRectangleOnScreen(child, mTempRect, !mFirstLayoutComplete); + requestChildOnScreen(child, focused); } super.requestChildFocus(child, focused); } + /** + * Requests that the given child of the RecyclerView be positioned onto the screen. This method + * can be called for both unfocusable and focusable child views. For unfocusable child views, + * the {@param focused} parameter passed is null, whereas for a focusable child, this parameter + * indicates the actual descendant view within this child view that holds the focus. + * @param child The child view of this RecyclerView that wants to come onto the screen. + * @param focused The descendant view that actually has the focus if child is focusable, null + * otherwise. + */ + private void requestChildOnScreen(@NonNull View child, @Nullable View focused) { + View rectView = (focused != null) ? focused : child; + mTempRect.set(0, 0, rectView.getWidth(), rectView.getHeight()); + + // get item decor offsets w/o refreshing. If they are invalid, there will be another + // layout pass to fix them, then it is LayoutManager's responsibility to keep focused + // View in viewport. + final ViewGroup.LayoutParams focusedLayoutParams = rectView.getLayoutParams(); + if (focusedLayoutParams instanceof LayoutParams) { + // if focused child has item decors, use them. Otherwise, ignore. + final LayoutParams lp = (LayoutParams) focusedLayoutParams; + if (!lp.mInsetsDirty) { + final Rect insets = lp.mDecorInsets; + mTempRect.left -= insets.left; + mTempRect.right += insets.right; + mTempRect.top -= insets.top; + mTempRect.bottom += insets.bottom; + } + } + + if (focused != null) { + offsetDescendantRectToMyCoords(focused, mTempRect); + offsetRectIntoDescendantCoords(child, mTempRect); + } + mLayout.requestChildRectangleOnScreen(this, child, mTempRect, !mFirstLayoutComplete, + (focused == null)); + } + @Override public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { return mLayout.requestChildRectangleOnScreen(this, child, rect, immediate); @@ -2545,10 +2594,11 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro throw new IllegalStateException(message); } if (mDispatchScrollCounter > 0) { - Log.w(TAG, "Cannot call this method in a scroll callback. Scroll callbacks might be run" - + " during a measure & layout pass where you cannot change the RecyclerView" - + " data. Any method call that might change the structure of the RecyclerView" - + " or the adapter contents should be postponed to the next frame.", + Log.w(TAG, "Cannot call this method in a scroll callback. Scroll callbacks might" + + "be run during a measure & layout pass where you cannot change the" + + "RecyclerView data. Any method call that might change the structure" + + "of the RecyclerView or the adapter contents should be postponed to" + + "the next frame.", new IllegalStateException("")); } } @@ -3012,7 +3062,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro // custom onMeasure if (mAdapterUpdateDuringMeasure) { eatRequestLayout(); + onEnterLayoutOrScroll(); processAdapterUpdatesAndSetAnimationFlags(); + onExitLayoutOrScroll(); if (mState.mRunPredictiveAnimations) { mState.mInPreLayout = true; @@ -3223,10 +3275,10 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro mState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator != null && (mDataSetHasChangedAfterLayout - || animationTypeSupported - || mLayout.mRequestedSimpleAnimations) + || animationTypeSupported + || mLayout.mRequestedSimpleAnimations) && (!mDataSetHasChangedAfterLayout - || mAdapter.hasStableIds()); + || mAdapter.hasStableIds()); mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations && animationTypeSupported && !mDataSetHasChangedAfterLayout @@ -3871,7 +3923,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (mClipToPadding) { c.translate(-getWidth() + getPaddingRight(), -getHeight() + getPaddingBottom()); } else { - c.translate(-getWidth(), -getHeight()); + c.translate(-getWidth(), -getHeight() + bottomGlowOffset); } needsInvalidate |= mBottomGlow != null && mBottomGlow.draw(c); c.restoreToCount(restore); @@ -5725,9 +5777,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (forceRecycle || holder.isRecyclable()) { if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID - | ViewHolder.FLAG_REMOVED - | ViewHolder.FLAG_UPDATE - | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { + | ViewHolder.FLAG_REMOVED + | ViewHolder.FLAG_UPDATE + | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { // Retire oldest cached view int cachedViewSize = mCachedViews.size(); if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { @@ -6133,7 +6185,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro continue; } - final int pos = holder.getLayoutPosition(); + final int pos = holder.mPosition; if (pos >= positionStart && pos < positionEnd) { holder.addFlags(ViewHolder.FLAG_UPDATE); recycleCachedViewAt(i); @@ -6519,7 +6571,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * *

    The adapter may publish a variety of events describing specific changes. * Not all adapters may support all change types and some may fall back to a generic - * {@link org.telegram.messenger.support.widget.RecyclerView.AdapterDataObserver#onChanged() + * {@link android.support.v7.widget.RecyclerView.AdapterDataObserver#onChanged() * "something changed"} event if more specific data is not available.

    * *

    Components registering observers with an adapter are responsible for @@ -6827,6 +6879,109 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro ChildHelper mChildHelper; RecyclerView mRecyclerView; + /** + * The callback used for retrieving information about a RecyclerView and its children in the + * horizontal direction. + */ + private final ViewBoundsCheck.Callback mHorizontalBoundCheckCallback = + new ViewBoundsCheck.Callback() { + @Override + public int getChildCount() { + return LayoutManager.this.getChildCount(); + } + + @Override + public View getParent() { + return mRecyclerView; + } + + @Override + public View getChildAt(int index) { + return LayoutManager.this.getChildAt(index); + } + + @Override + public int getParentStart() { + return LayoutManager.this.getPaddingLeft(); + } + + @Override + public int getParentEnd() { + return LayoutManager.this.getWidth() - LayoutManager.this.getPaddingRight(); + } + + @Override + public int getChildStart(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedLeft(view) - params.leftMargin; + } + + @Override + public int getChildEnd(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedRight(view) + params.rightMargin; + } + }; + + /** + * The callback used for retrieving information about a RecyclerView and its children in the + * vertical direction. + */ + private final ViewBoundsCheck.Callback mVerticalBoundCheckCallback = + new ViewBoundsCheck.Callback() { + @Override + public int getChildCount() { + return LayoutManager.this.getChildCount(); + } + + @Override + public View getParent() { + return mRecyclerView; + } + + @Override + public View getChildAt(int index) { + return LayoutManager.this.getChildAt(index); + } + + @Override + public int getParentStart() { + return LayoutManager.this.getPaddingTop(); + } + + @Override + public int getParentEnd() { + return LayoutManager.this.getHeight() + - LayoutManager.this.getPaddingBottom(); + } + + @Override + public int getChildStart(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedTop(view) - params.topMargin; + } + + @Override + public int getChildEnd(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedBottom(view) + params.bottomMargin; + } + }; + + /** + * Utility objects used to check the boundaries of children against their parent + * RecyclerView. + * @see #isViewPartiallyVisible(View, boolean, boolean), + * {@link LinearLayoutManager#findOneVisibleChild(int, int, boolean, boolean)}, + * and {@link LinearLayoutManager#findOnePartiallyOrCompletelyInvisibleChild(int, int)}. + */ + ViewBoundsCheck mHorizontalBoundCheck = new ViewBoundsCheck(mHorizontalBoundCheckCallback); + ViewBoundsCheck mVerticalBoundCheck = new ViewBoundsCheck(mVerticalBoundCheckCallback); + @Nullable SmoothScroller mSmoothScroller; @@ -8891,7 +9046,11 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * *

    This is the LayoutManager's opportunity to populate views in the given direction * to fulfill the request if it can. The LayoutManager should attach and return - * the view to be focused. The default implementation returns null.

    + * the view to be focused, if a focusable view in the given direction is found. + * Otherwise, if all the existing (or the newly populated views) are unfocusable, it returns + * the next unfocusable view to become visible on the screen. This unfocusable view is + * typically the first view that's either partially or fully out of RV's padded bounded + * area in the given direction. The default implementation returns null.

    * * @param focused The currently focused view * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, @@ -8900,7 +9059,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * or 0 for not applicable * @param recycler The recycler to use for obtaining views for currently offscreen items * @param state Transient state of RecyclerView - * @return The chosen view to be focused + * @return The chosen view to be focused if a focusable view is found, otherwise an + * unfocusable view to become visible onto the screen, else null. */ @Nullable public View onFocusSearchFailed(View focused, int direction, Recycler recycler, @@ -8929,22 +9089,20 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } /** - * Called when a child of the RecyclerView wants a particular rectangle to be positioned - * onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View, - * android.graphics.Rect, boolean)} for more details. - * - *

    The base implementation will attempt to perform a standard programmatic scroll - * to bring the given rect into view, within the padded area of the RecyclerView.

    - * + * Returns the scroll amount that brings the given rect in child's coordinate system within + * the padded area of RecyclerView. + * @param parent The parent RecyclerView. * @param child The direct child making the request. - * @param rect The rectangle in the child's coordinates the child - * wishes to be on the screen. + * @param rect The rectangle in the child's coordinates the child + * wishes to be on the screen. * @param immediate True to forbid animated or delayed scrolling, * false otherwise - * @return Whether the group scrolled to handle the operation + * @return The array containing the scroll amount in x and y directions that brings the + * given rect into RV's padded area. */ - public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect, - boolean immediate) { + private int[] getChildRectangleOnScreenScrollAmount(RecyclerView parent, View child, + Rect rect, boolean immediate) { + int[] out = new int[2]; final int parentLeft = getPaddingLeft(); final int parentTop = getPaddingTop(); final int parentRight = getWidth() - getPaddingRight(); @@ -8975,18 +9133,124 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro // we should scroll to make bottom visible, make sure top does not go out of bounds. final int dy = offScreenTop != 0 ? offScreenTop : Math.min(childTop - parentTop, offScreenBottom); + out[0] = dx; + out[1] = dy; + return out; + } + /** + * Called when a child of the RecyclerView wants a particular rectangle to be positioned + * onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View, + * android.graphics.Rect, boolean)} for more details. + * + *

    The base implementation will attempt to perform a standard programmatic scroll + * to bring the given rect into view, within the padded area of the RecyclerView.

    + * + * @param child The direct child making the request. + * @param rect The rectangle in the child's coordinates the child + * wishes to be on the screen. + * @param immediate True to forbid animated or delayed scrolling, + * false otherwise + * @return Whether the group scrolled to handle the operation + */ + public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect, + boolean immediate) { + return requestChildRectangleOnScreen(parent, child, rect, immediate, false); + } - if (dx != 0 || dy != 0) { - if (immediate) { - parent.scrollBy(dx, dy); - } else { - parent.smoothScrollBy(dx, dy); + /** + * Requests that the given child of the RecyclerView be positioned onto the screen. This + * method can be called for both unfocusable and focusable child views. For unfocusable + * child views, focusedChildVisible is typically true in which case, layout manager + * makes the child view visible only if the currently focused child stays in-bounds of RV. + * @param parent The parent RecyclerView. + * @param child The direct child making the request. + * @param rect The rectangle in the child's coordinates the child + * wishes to be on the screen. + * @param immediate True to forbid animated or delayed scrolling, + * false otherwise + * @param focusedChildVisible Whether the currently focused view must stay visible. + * @return Whether the group scrolled to handle the operation + */ + public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect, + boolean immediate, + boolean focusedChildVisible) { + int[] scrollAmount = getChildRectangleOnScreenScrollAmount(parent, child, rect, + immediate); + int dx = scrollAmount[0]; + int dy = scrollAmount[1]; + if (!focusedChildVisible || isFocusedChildVisibleAfterScrolling(parent, dx, dy)) { + if (dx != 0 || dy != 0) { + if (immediate) { + parent.scrollBy(dx, dy); + } else { + parent.smoothScrollBy(dx, dy); + } + return true; } - return true; } return false; } + /** + * Returns whether the given child view is partially or fully visible within the padded + * bounded area of RecyclerView, depending on the input parameters. + * A view is partially visible if it has non-zero overlap with RV's padded bounded area. + * If acceptEndPointInclusion flag is set to true, it's also considered partially + * visible if it's located outside RV's bounds and it's hitting either RV's start or end + * bounds. + * + * @param child The child view to be examined. + * @param completelyVisible If true, the method returns true iff the child is completely + * visible. If false, the method returns true iff the child is only + * partially visible (that is it will return false if the child is + * either completely visible or out of RV's bounds). + * @param acceptEndPointInclusion If the view's endpoint intersection with RV's start of end + * bounds is enough to consider it partially visible, + * false otherwise. + * @return True if the given child is partially or fully visible, false otherwise. + */ + public boolean isViewPartiallyVisible(@NonNull View child, boolean completelyVisible, + boolean acceptEndPointInclusion) { + int boundsFlag = (ViewBoundsCheck.FLAG_CVS_GT_PVS | ViewBoundsCheck.FLAG_CVS_EQ_PVS + | ViewBoundsCheck.FLAG_CVE_LT_PVE | ViewBoundsCheck.FLAG_CVE_EQ_PVE); + boolean isViewFullyVisible = mHorizontalBoundCheck.isViewWithinBoundFlags(child, + boundsFlag) + && mVerticalBoundCheck.isViewWithinBoundFlags(child, boundsFlag); + if (completelyVisible) { + return isViewFullyVisible; + } else { + return !isViewFullyVisible; + } + } + + /** + * Returns whether the currently focused child stays within RV's bounds with the given + * amount of scrolling. + * @param parent The parent RecyclerView. + * @param dx The scrolling in x-axis direction to be performed. + * @param dy The scrolling in y-axis direction to be performed. + * @return {@code false} if the focused child is not at least partially visible after + * scrolling or no focused child exists, {@code true} otherwise. + */ + private boolean isFocusedChildVisibleAfterScrolling(RecyclerView parent, int dx, int dy) { + final View focusedChild = parent.getFocusedChild(); + if (focusedChild == null) { + return false; + } + final int parentLeft = getPaddingLeft(); + final int parentTop = getPaddingTop(); + final int parentRight = getWidth() - getPaddingRight(); + final int parentBottom = getHeight() - getPaddingBottom(); + final Rect bounds = mRecyclerView.mTempRect; + getDecoratedBoundsWithMargins(focusedChild, bounds); + + if (bounds.left - dx >= parentRight || bounds.right - dx <= parentLeft + || bounds.top - dy >= parentBottom || bounds.bottom - dy <= parentTop) { + return false; + } + return true; + } + /** * @deprecated Use {@link #onRequestChildFocus(RecyclerView, State, View, View)} */ diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/StaggeredGridLayoutManager.java b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/StaggeredGridLayoutManager.java index 9ddb6d2e6..80427100c 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/StaggeredGridLayoutManager.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/StaggeredGridLayoutManager.java @@ -1656,7 +1656,7 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine); } recycle(recycler, mLayoutState); - if (mLayoutState.mStopInFocusable && view.isFocusable()) { + if (mLayoutState.mStopInFocusable && view.hasFocusable()) { if (lp.mFullSpan) { mRemainingSpans.clear(); } else { @@ -2268,6 +2268,7 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple return view; } } + // either could not find from the desired span or prev view is full span. // traverse all spans if (preferLastSpan(layoutDir)) { @@ -2285,6 +2286,44 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple } } } + + // Could not find any focusable views from any of the existing spans. Now start the search + // to find the best unfocusable candidate to become visible on the screen next. The search + // is done in the same fashion: first, check the views in the desired span and if no + // candidate is found, traverse the views in all the remaining spans. + boolean shouldSearchFromStart = !mReverseLayout == (layoutDir == LayoutState.LAYOUT_START); + View unfocusableCandidate = null; + if (!prevFocusFullSpan) { + unfocusableCandidate = findViewByPosition(shouldSearchFromStart + ? prevFocusSpan.findFirstPartiallyVisibleItemPosition() : + prevFocusSpan.findLastPartiallyVisibleItemPosition()); + if (unfocusableCandidate != null && unfocusableCandidate != directChild) { + return unfocusableCandidate; + } + } + + if (preferLastSpan(layoutDir)) { + for (int i = mSpanCount - 1; i >= 0; i--) { + if (i == prevFocusSpan.mIndex) { + continue; + } + unfocusableCandidate = findViewByPosition(shouldSearchFromStart + ? mSpans[i].findFirstPartiallyVisibleItemPosition() : + mSpans[i].findLastPartiallyVisibleItemPosition()); + if (unfocusableCandidate != null && unfocusableCandidate != directChild) { + return unfocusableCandidate; + } + } + } else { + for (int i = 0; i < mSpanCount; i++) { + unfocusableCandidate = findViewByPosition(shouldSearchFromStart + ? mSpans[i].findFirstPartiallyVisibleItemPosition() : + mSpans[i].findLastPartiallyVisibleItemPosition()); + if (unfocusableCandidate != null && unfocusableCandidate != directChild) { + return unfocusableCandidate; + } + } + } return null; } @@ -2606,6 +2645,12 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple : findOneVisibleChild(0, mViews.size(), false); } + public int findFirstPartiallyVisibleItemPosition() { + return mReverseLayout + ? findOnePartiallyVisibleChild(mViews.size() - 1, -1, true) + : findOnePartiallyVisibleChild(0, mViews.size(), true); + } + public int findFirstCompletelyVisibleItemPosition() { return mReverseLayout ? findOneVisibleChild(mViews.size() - 1, -1, true) @@ -2618,13 +2663,45 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple : findOneVisibleChild(mViews.size() - 1, -1, false); } + public int findLastPartiallyVisibleItemPosition() { + return mReverseLayout + ? findOnePartiallyVisibleChild(0, mViews.size(), true) + : findOnePartiallyVisibleChild(mViews.size() - 1, -1, true); + } + public int findLastCompletelyVisibleItemPosition() { return mReverseLayout ? findOneVisibleChild(0, mViews.size(), true) : findOneVisibleChild(mViews.size() - 1, -1, true); } - int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) { + /** + * Returns the first view within this span that is partially or fully visible. Partially + * visible refers to a view that overlaps but is not fully contained within RV's padded + * bounded area. This view returned can be defined to have an area of overlap strictly + * greater than zero if acceptEndPointInclusion is false. If true, the view's endpoint + * inclusion is enough to consider it partially visible. The latter case can then refer to + * an out-of-bounds view positioned right at the top (or bottom) boundaries of RV's padded + * area. This is used e.g. inside + * {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)} for + * calculating the next unfocusable child to become visible on the screen. + * @param fromIndex The child position index to start the search from. + * @param toIndex The child position index to end the search at. + * @param completelyVisible True if we have to only consider completely visible views, + * false otherwise. + * @param acceptCompletelyVisible True if we can consider both partially or fully visible + * views, false, if only a partially visible child should be + * returned. + * @param acceptEndPointInclusion If the view's endpoint intersection with RV's padded + * bounded area is enough to consider it partially visible, + * false otherwise + * @return The adapter position of the first view that's either partially or fully visible. + * {@link RecyclerView#NO_POSITION} if no such view is found. + */ + int findOnePartiallyOrCompletelyVisibleChild(int fromIndex, int toIndex, + boolean completelyVisible, + boolean acceptCompletelyVisible, + boolean acceptEndPointInclusion) { final int start = mPrimaryOrientation.getStartAfterPadding(); final int end = mPrimaryOrientation.getEndAfterPadding(); final int next = toIndex > fromIndex ? 1 : -1; @@ -2632,12 +2709,22 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple final View child = mViews.get(i); final int childStart = mPrimaryOrientation.getDecoratedStart(child); final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); - if (childStart < end && childEnd > start) { - if (completelyVisible) { + boolean childStartInclusion = acceptEndPointInclusion ? (childStart <= end) + : (childStart < end); + boolean childEndInclusion = acceptEndPointInclusion ? (childEnd >= start) + : (childEnd > start); + if (childStartInclusion && childEndInclusion) { + if (completelyVisible && acceptCompletelyVisible) { + // the child has to be completely visible to be returned. if (childStart >= start && childEnd <= end) { return getPosition(child); } - } else { + } else if (acceptCompletelyVisible) { + // can return either a partially or completely visible child. + return getPosition(child); + } else if (childStart < start || childEnd > end) { + // should return a partially visible child if exists and a completely + // visible child is not acceptable in this case. return getPosition(child); } } @@ -2645,6 +2732,17 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple return NO_POSITION; } + int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) { + return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, completelyVisible, + true, false); + } + + int findOnePartiallyVisibleChild(int fromIndex, int toIndex, + boolean acceptEndPointInclusion) { + return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, false, false, + acceptEndPointInclusion); + } + /** * Depending on the layout direction, returns the View that is after the given position. */ @@ -2654,8 +2752,11 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple final int limit = mViews.size(); for (int i = 0; i < limit; i++) { final View view = mViews.get(i); - if (view.isFocusable() && - (getPosition(view) > referenceChildPosition == mReverseLayout) ) { + if ((mReverseLayout && getPosition(view) <= referenceChildPosition) + || (!mReverseLayout && getPosition(view) >= referenceChildPosition)) { + break; + } + if (view.hasFocusable()) { candidate = view; } else { break; @@ -2664,8 +2765,11 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple } else { for (int i = mViews.size() - 1; i >= 0; i--) { final View view = mViews.get(i); - if (view.isFocusable() && - (getPosition(view) > referenceChildPosition == !mReverseLayout)) { + if ((mReverseLayout && getPosition(view) >= referenceChildPosition) + || (!mReverseLayout && getPosition(view) <= referenceChildPosition)) { + break; + } + if (view.hasFocusable()) { candidate = view; } else { break; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/ViewBoundsCheck.java b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/ViewBoundsCheck.java new file mode 100644 index 000000000..822656c05 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/ViewBoundsCheck.java @@ -0,0 +1,274 @@ +/* + * 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 org.telegram.messenger.support.widget; + +import android.support.annotation.IntDef; +import android.view.View; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A utility class used to check the boundaries of a given view within its parent view based on + * a set of boundary flags. + */ +class ViewBoundsCheck { + + static final int GT = 1 << 0; + static final int EQ = 1 << 1; + static final int LT = 1 << 2; + + + static final int CVS_PVS_POS = 0; + /** + * The child view's start should be strictly greater than parent view's start. + */ + static final int FLAG_CVS_GT_PVS = GT << CVS_PVS_POS; + + /** + * The child view's start can be equal to its parent view's start. This flag follows with GT + * or LT indicating greater (less) than or equal relation. + */ + static final int FLAG_CVS_EQ_PVS = EQ << CVS_PVS_POS; + + /** + * The child view's start should be strictly less than parent view's start. + */ + static final int FLAG_CVS_LT_PVS = LT << CVS_PVS_POS; + + + static final int CVS_PVE_POS = 4; + /** + * The child view's start should be strictly greater than parent view's end. + */ + static final int FLAG_CVS_GT_PVE = GT << CVS_PVE_POS; + + /** + * The child view's start can be equal to its parent view's end. This flag follows with GT + * or LT indicating greater (less) than or equal relation. + */ + static final int FLAG_CVS_EQ_PVE = EQ << CVS_PVE_POS; + + /** + * The child view's start should be strictly less than parent view's end. + */ + static final int FLAG_CVS_LT_PVE = LT << CVS_PVE_POS; + + + static final int CVE_PVS_POS = 8; + /** + * The child view's end should be strictly greater than parent view's start. + */ + static final int FLAG_CVE_GT_PVS = GT << CVE_PVS_POS; + + /** + * The child view's end can be equal to its parent view's start. This flag follows with GT + * or LT indicating greater (less) than or equal relation. + */ + static final int FLAG_CVE_EQ_PVS = EQ << CVE_PVS_POS; + + /** + * The child view's end should be strictly less than parent view's start. + */ + static final int FLAG_CVE_LT_PVS = LT << CVE_PVS_POS; + + + static final int CVE_PVE_POS = 12; + /** + * The child view's end should be strictly greater than parent view's end. + */ + static final int FLAG_CVE_GT_PVE = GT << CVE_PVE_POS; + + /** + * The child view's end can be equal to its parent view's end. This flag follows with GT + * or LT indicating greater (less) than or equal relation. + */ + static final int FLAG_CVE_EQ_PVE = EQ << CVE_PVE_POS; + + /** + * The child view's end should be strictly less than parent view's end. + */ + static final int FLAG_CVE_LT_PVE = LT << CVE_PVE_POS; + + static final int MASK = GT | EQ | LT; + + final Callback mCallback; + BoundFlags mBoundFlags; + /** + * The set of flags that can be passed for checking the view boundary conditions. + * CVS in the flag name indicates the child view, and PV indicates the parent view.\ + * The following S, E indicate a view's start and end points, respectively. + * GT and LT indicate a strictly greater and less than relationship. + * Greater than or equal (or less than or equal) can be specified by setting both GT and EQ (or + * LT and EQ) flags. + * For instance, setting both {@link #FLAG_CVS_GT_PVS} and {@link #FLAG_CVS_EQ_PVS} indicate the + * child view's start should be greater than or equal to its parent start. + */ + @IntDef(flag = true, value = { + FLAG_CVS_GT_PVS, FLAG_CVS_EQ_PVS, FLAG_CVS_LT_PVS, + FLAG_CVS_GT_PVE, FLAG_CVS_EQ_PVE, FLAG_CVS_LT_PVE, + FLAG_CVE_GT_PVS, FLAG_CVE_EQ_PVS, FLAG_CVE_LT_PVS, + FLAG_CVE_GT_PVE, FLAG_CVE_EQ_PVE, FLAG_CVE_LT_PVE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ViewBounds {} + + ViewBoundsCheck(Callback callback) { + mCallback = callback; + mBoundFlags = new BoundFlags(); + } + + static class BoundFlags { + int mBoundFlags = 0; + int mRvStart, mRvEnd, mChildStart, mChildEnd; + + void setBounds(int rvStart, int rvEnd, int childStart, int childEnd) { + mRvStart = rvStart; + mRvEnd = rvEnd; + mChildStart = childStart; + mChildEnd = childEnd; + } + + void setFlags(@ViewBounds int flags, int mask) { + mBoundFlags = (mBoundFlags & ~mask) | (flags & mask); + } + + void addFlags(@ViewBounds int flags) { + mBoundFlags |= flags; + } + + void resetFlags() { + mBoundFlags = 0; + } + + int compare(int x, int y) { + if (x > y) { + return GT; + } + if (x == y) { + return EQ; + } + return LT; + } + + boolean boundsMatch() { + if ((mBoundFlags & (MASK << CVS_PVS_POS)) != 0) { + if ((mBoundFlags & (compare(mChildStart, mRvStart) << CVS_PVS_POS)) == 0) { + return false; + } + } + + if ((mBoundFlags & (MASK << CVS_PVE_POS)) != 0) { + if ((mBoundFlags & (compare(mChildStart, mRvEnd) << CVS_PVE_POS)) == 0) { + return false; + } + } + + if ((mBoundFlags & (MASK << CVE_PVS_POS)) != 0) { + if ((mBoundFlags & (compare(mChildEnd, mRvStart) << CVE_PVS_POS)) == 0) { + return false; + } + } + + if ((mBoundFlags & (MASK << CVE_PVE_POS)) != 0) { + if ((mBoundFlags & (compare(mChildEnd, mRvEnd) << CVE_PVE_POS)) == 0) { + return false; + } + } + return true; + } + }; + + /** + * Returns the first view starting from fromIndex to toIndex in views whose bounds lie within + * its parent bounds based on the provided preferredBoundFlags. If no match is found based on + * the preferred flags, and a nonzero acceptableBoundFlags is specified, the last view whose + * bounds lie within its parent view based on the acceptableBoundFlags is returned. If no such + * view is found based on either of these two flags, null is returned. + * @param fromIndex The view position index to start the search from. + * @param toIndex The view position index to end the search at. + * @param preferredBoundFlags The flags indicating the preferred match. Once a match is found + * based on this flag, that view is returned instantly. + * @param acceptableBoundFlags The flags indicating the acceptable match if no preferred match + * is found. If so, and if acceptableBoundFlags is non-zero, the + * last matching acceptable view is returned. Otherwise, null is + * returned. + * @return The first view that satisfies acceptableBoundFlags or the last view satisfying + * acceptableBoundFlags boundary conditions. + */ + View findOneViewWithinBoundFlags(int fromIndex, int toIndex, + @ViewBounds int preferredBoundFlags, + @ViewBounds int acceptableBoundFlags) { + final int start = mCallback.getParentStart(); + final int end = mCallback.getParentEnd(); + final int next = toIndex > fromIndex ? 1 : -1; + View acceptableMatch = null; + for (int i = fromIndex; i != toIndex; i += next) { + final View child = mCallback.getChildAt(i); + final int childStart = mCallback.getChildStart(child); + final int childEnd = mCallback.getChildEnd(child); + mBoundFlags.setBounds(start, end, childStart, childEnd); + if (preferredBoundFlags != 0) { + mBoundFlags.resetFlags(); + mBoundFlags.addFlags(preferredBoundFlags); + if (mBoundFlags.boundsMatch()) { + // found a perfect match + return child; + } + } + if (acceptableBoundFlags != 0) { + mBoundFlags.resetFlags(); + mBoundFlags.addFlags(acceptableBoundFlags); + if (mBoundFlags.boundsMatch()) { + acceptableMatch = child; + } + } + } + return acceptableMatch; + } + + /** + * Returns whether the specified view lies within the boundary condition of its parent view. + * @param child The child view to be checked. + * @param boundsFlags The flag against which the child view and parent view are matched. + * @return True if the view meets the boundsFlag, false otherwise. + */ + boolean isViewWithinBoundFlags(View child, @ViewBounds int boundsFlags) { + mBoundFlags.setBounds(mCallback.getParentStart(), mCallback.getParentEnd(), + mCallback.getChildStart(child), mCallback.getChildEnd(child)); + if (boundsFlags != 0) { + mBoundFlags.resetFlags(); + mBoundFlags.addFlags(boundsFlags); + return mBoundFlags.boundsMatch(); + } + return false; + } + + /** + * Callback provided by the user of this class in order to retrieve information about child and + * parent boundaries. + */ + interface Callback { + int getChildCount(); + View getParent(); + View getChildAt(int index); + int getParentStart(); + int getParentEnd(); + int getChildStart(View view); + int getChildEnd(View view); + } +} 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 e8789a14f..5c12d894f 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/video/MP4Builder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/video/MP4Builder.java @@ -132,11 +132,11 @@ public class MP4Builder { return flush; } - public int addTrack(MediaFormat mediaFormat, boolean isAudio) throws Exception { + public int addTrack(MediaFormat mediaFormat, boolean isAudio) { return currentMp4Movie.addTrack(mediaFormat, isAudio); } - public void finishMovie(boolean error) throws Exception { + public void finishMovie() throws Exception { if (mdat.getContentSize() != 0) { flushCurrentMdat(); } 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 3b8624b76..e7e47442b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/video/Mp4Movie.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/video/Mp4Movie.java @@ -66,7 +66,7 @@ public class Mp4Movie { return cacheFile; } - public void addSample(int trackIndex, long offset, MediaCodec.BufferInfo bufferInfo) throws Exception { + public void addSample(int trackIndex, long offset, MediaCodec.BufferInfo bufferInfo) { if (trackIndex < 0 || trackIndex >= tracks.size()) { return; } @@ -74,7 +74,7 @@ public class Mp4Movie { track.addSample(offset, bufferInfo); } - public int addTrack(MediaFormat mediaFormat, boolean isAudio) throws Exception { + public int addTrack(MediaFormat mediaFormat, boolean isAudio) { tracks.add(new Track(tracks.size(), mediaFormat, isAudio)); return tracks.size() - 1; } 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 7ec7079fd..3354e1628 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/video/OutputSurface.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/video/OutputSurface.java @@ -155,10 +155,6 @@ public class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { return mSurface; } - public void changeFragmentShader(String fragmentShader) { - mTextureRender.changeFragmentShader(fragmentShader); - } - public void awaitNewImage() { final int TIMEOUT_MS = 2500; synchronized (mFrameSyncObject) { 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 6379ef99a..accdeefbd 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/video/TextureRenderer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/video/TextureRenderer.java @@ -33,12 +33,6 @@ public class TextureRenderer { private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES; private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0; private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3; - private static final float[] mTriangleVerticesData = { - -1.0f, -1.0f, 0, 0.f, 0.f, - 1.0f, -1.0f, 0, 1.f, 0.f, - -1.0f, 1.0f, 0, 0.f, 1.f, - 1.0f, 1.0f, 0, 1.f, 1.f, - }; private FloatBuffer mTriangleVertices; private static final String VERTEX_SHADER = @@ -54,7 +48,7 @@ public class TextureRenderer { private static final String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + - "precision mediump float;\n" + + "precision highp float;\n" + "varying vec2 vTextureCoord;\n" + "uniform samplerExternalOES sTexture;\n" + "void main() {\n" + @@ -63,16 +57,23 @@ public class TextureRenderer { private float[] mMVPMatrix = new float[16]; private float[] mSTMatrix = new float[16]; - private int mProgram; private int mTextureID = -12345; + private int mProgram; private int muMVPMatrixHandle; private int muSTMatrixHandle; private int maPositionHandle; private int maTextureHandle; - private int rotationAngle = 0; + + private int rotationAngle; public TextureRenderer(int rotation) { rotationAngle = rotation; + float[] mTriangleVerticesData = { + -1.0f, -1.0f, 0, 0.f, 0.f, + 1.0f, -1.0f, 0, 1.f, 0.f, + -1.0f, 1.0f, 0, 0.f, 1.f, + 1.0f, 1.0f, 0, 1.f, 1.f, + }; mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer(); mTriangleVertices.put(mTriangleVerticesData).position(0); Matrix.setIdentityM(mSTMatrix, 0); @@ -105,6 +106,7 @@ public class TextureRenderer { checkGlError("glVertexAttribPointer maTextureHandle"); GLES20.glEnableVertexAttribArray(maTextureHandle); checkGlError("glEnableVertexAttribArray maTextureHandle"); + GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0); GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); @@ -142,8 +144,8 @@ public class TextureRenderer { mTextureID = textures[0]; GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); checkGlError("glBindTexture mTextureID"); - GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); - GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + 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"); @@ -154,14 +156,6 @@ public class TextureRenderer { } } - public void changeFragmentShader(String fragmentShader) { - GLES20.glDeleteProgram(mProgram); - mProgram = createProgram(VERTEX_SHADER, fragmentShader); - if (mProgram == 0) { - throw new RuntimeException("failed creating program"); - } - } - private int loadShader(int shaderType, String source) { int shader = GLES20.glCreateShader(shaderType); checkGlError("glCreateShader type=" + shaderType); 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 f45e08587..47d85025a 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/video/Track.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/video/Track.java @@ -84,12 +84,10 @@ public class Track { samplingFrequencyIndexMap.put(8000, 0xb); } - public Track(int id, MediaFormat format, boolean audio) throws Exception { + public Track(int id, MediaFormat format, boolean audio) { trackId = id; isAudio = audio; if (!isAudio) { - //sampleDurations.add((long) 3015); - //duration = 3015; width = format.getInteger(MediaFormat.KEY_WIDTH); height = format.getInteger(MediaFormat.KEY_HEIGHT); timeScale = 90000; @@ -210,8 +208,6 @@ public class Track { sampleDescriptionBox.addBox(visualSampleEntry); } } else { - //sampleDurations.add((long) 1024); - //duration = 1024; volume = 1; timeScale = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); handler = "soun"; @@ -235,8 +231,12 @@ public class Track { decoderConfigDescriptor.setObjectTypeIndication(0x40); decoderConfigDescriptor.setStreamType(5); decoderConfigDescriptor.setBufferSizeDB(1536); - decoderConfigDescriptor.setMaxBitRate(96000); - decoderConfigDescriptor.setAvgBitRate(96000); + if (format.containsKey("max-bitrate")) { + decoderConfigDescriptor.setMaxBitRate(format.getInteger("max-bitrate")); + } else { + decoderConfigDescriptor.setMaxBitRate(96000); + } + decoderConfigDescriptor.setAvgBitRate(timeScale); AudioSpecificConfig audioSpecificConfig = new AudioSpecificConfig(); audioSpecificConfig.setAudioObjectType(2); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/voip/AudioRecordJNI.java b/TMessagesProj/src/main/java/org/telegram/messenger/voip/AudioRecordJNI.java index e8102fd8b..dc7b66894 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/voip/AudioRecordJNI.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/voip/AudioRecordJNI.java @@ -32,27 +32,46 @@ public class AudioRecordJNI { private AutomaticGainControl agc; private NoiseSuppressor ns; private AcousticEchoCanceler aec; + private boolean needResampling=false; public AudioRecordJNI(long nativeInst) { this.nativeInst = nativeInst; } - private int getBufferSize(int min) { - return Math.max(AudioRecord.getMinBufferSize(48000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT), min); + private int getBufferSize(int min, int sampleRate) { + return Math.max(AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT), min); } public void init(int sampleRate, int bitsPerSample, int channels, int bufferSize) { if (audioRecord != null) { throw new IllegalStateException("already inited"); } - int size = getBufferSize(bufferSize); this.bufferSize = bufferSize; + boolean res=tryInit(MediaRecorder.AudioSource.VOICE_COMMUNICATION, 48000); + if(!res) + tryInit(MediaRecorder.AudioSource.MIC, 48000); + if(!res) + tryInit(MediaRecorder.AudioSource.VOICE_COMMUNICATION, 44100); + if(!res) + tryInit(MediaRecorder.AudioSource.MIC, 44100); + buffer = ByteBuffer.allocateDirect(bufferSize); + } + + private boolean tryInit(int source, int sampleRate){ + if(audioRecord!=null){ + try{ + audioRecord.release(); + }catch(Exception x){} + } + FileLog.d("Trying to initialize AudioRecord with source="+source+" and sample rate="+sampleRate); + int size = getBufferSize(bufferSize, 48000); try{ - audioRecord=new AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION, sampleRate, channels==1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, size); + audioRecord=new AudioRecord(source, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, size); }catch(Exception x){ FileLog.e("AudioRecord init failed!", x); } - buffer = ByteBuffer.allocateDirect(bufferSize); + needResampling=sampleRate!=48000; + return audioRecord!=null && audioRecord.getState()==AudioRecord.STATE_INITIALIZED; } public void stop() { @@ -145,12 +164,18 @@ public class AudioRecordJNI { throw new IllegalStateException("thread already started"); } running = true; + final ByteBuffer tmpBuf=needResampling ? ByteBuffer.allocateDirect(882*2) : null; thread = new Thread(new Runnable() { @Override public void run() { while (running) { try { - audioRecord.read(buffer, 960*2); + if(!needResampling){ + audioRecord.read(buffer, 960*2); + }else{ + audioRecord.read(tmpBuf, 882*2); + Resampler.convert44to48(tmpBuf, buffer); + } if (!running) { audioRecord.stop(); break; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/voip/AudioTrackJNI.java b/TMessagesProj/src/main/java/org/telegram/messenger/voip/AudioTrackJNI.java index 7065ae568..516bfc522 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/voip/AudioTrackJNI.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/voip/AudioTrackJNI.java @@ -25,22 +25,32 @@ public class AudioTrackJNI{ private Thread thread; private int bufferSize; private long nativeInst; + private boolean needResampling; public AudioTrackJNI(long nativeInst) { this.nativeInst = nativeInst; } - private int getBufferSize(int min) { - return Math.max(AudioTrack.getMinBufferSize(48000, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT), min); + private int getBufferSize(int min, int sampleRate) { + return Math.max(AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT), min); } public void init(int sampleRate, int bitsPerSample, int channels, int bufferSize) { if (audioTrack != null) { throw new IllegalStateException("already inited"); } - int size = getBufferSize(bufferSize); + int size = getBufferSize(bufferSize, 48000); this.bufferSize = bufferSize; - audioTrack=new AudioTrack(AudioManager.STREAM_VOICE_CALL, sampleRate, channels==1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, size, AudioTrack.MODE_STREAM); + audioTrack=new AudioTrack(AudioManager.STREAM_VOICE_CALL, 48000, channels==1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, size, AudioTrack.MODE_STREAM); + if(audioTrack.getState()!=AudioTrack.STATE_INITIALIZED){ + try{ + audioTrack.release(); + }catch(Throwable x){} + size=getBufferSize(bufferSize*6, 44100); + FileLog.d("buffer size: "+size); + audioTrack=new AudioTrack(AudioManager.STREAM_VOICE_CALL, 44100, channels==1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, size, AudioTrack.MODE_STREAM); + needResampling=true; + } } public void stop() { @@ -86,10 +96,22 @@ public class AudioTrackJNI{ FileLog.e("error starting AudioTrack", x); return; } + ByteBuffer tmp48=needResampling ? ByteBuffer.allocateDirect(960*2) : null; + ByteBuffer tmp44=needResampling ? ByteBuffer.allocateDirect(882*2) : null; while (running) { try { - nativeCallback(buffer); - audioTrack.write(buffer, 0, 960*2); + if(needResampling){ + nativeCallback(buffer); + tmp48.rewind(); + tmp48.put(buffer); + Resampler.convert48to44(tmp48, tmp44); + tmp44.rewind(); + tmp44.get(buffer, 0, 882*2); + audioTrack.write(buffer, 0, 882*2); + }else{ + nativeCallback(buffer); + audioTrack.write(buffer, 0, 960*2); + } if (!running) { audioTrack.stop(); break; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/voip/Resampler.java b/TMessagesProj/src/main/java/org/telegram/messenger/voip/Resampler.java new file mode 100644 index 000000000..0bbdaeab2 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/voip/Resampler.java @@ -0,0 +1,12 @@ +package org.telegram.messenger.voip; + +import java.nio.ByteBuffer; + +/** + * Created by grishka on 01.04.17. + */ + +public class Resampler{ + public static native int convert44to48(ByteBuffer from, ByteBuffer to); + public static native int convert48to44(ByteBuffer from, ByteBuffer to); +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/voip/VoIPController.java b/TMessagesProj/src/main/java/org/telegram/messenger/voip/VoIPController.java index db701542d..5fb8033fc 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/voip/VoIPController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/voip/VoIPController.java @@ -8,6 +8,8 @@ package org.telegram.messenger.voip; +import android.app.Activity; +import android.content.SharedPreferences; import android.media.audiofx.AcousticEchoCanceler; import android.media.audiofx.NoiseSuppressor; import android.os.Build; @@ -16,9 +18,13 @@ import android.os.SystemClock; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.BuildConfig; import org.telegram.tgnet.TLRPC; +import org.telegram.ui.Components.voip.VoIPHelper; import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.Locale; public class VoIPController { @@ -40,6 +46,7 @@ public class VoIPController { public static final int STATE_WAIT_INIT_ACK = 2; public static final int STATE_ESTABLISHED = 3; public static final int STATE_FAILED = 4; + public static final int STATE_RECONNECTING = 5; public static final int DATA_SAVING_NEVER=0; public static final int DATA_SAVING_MOBILE=1; @@ -122,7 +129,8 @@ public class VoIPController { } private void handleStateChange(int state) { - callStartTime = SystemClock.elapsedRealtime(); + if(state==STATE_ESTABLISHED && callStartTime==0) + callStartTime = SystemClock.elapsedRealtime(); if (listener != null) { listener.onConnectionStateChanged(state); } @@ -142,7 +150,7 @@ public class VoIPController { nativeSetMicMute(nativeInst, mute); } - public void setConfig(double recvTimeout, double initTimeout, int dataSavingOption){ + public void setConfig(double recvTimeout, double initTimeout, int dataSavingOption, long callID){ ensureNativeInstance(); boolean sysAecAvailable=false, sysNsAvailable=false; if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN){ @@ -153,10 +161,12 @@ public class VoIPController { } } + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + boolean dump = preferences.getBoolean("dbg_dump_call_stats", false); nativeSetConfig(nativeInst, recvTimeout, initTimeout, dataSavingOption, Build.VERSION.SDK_INT logs=new ArrayList<>(); + logs.addAll(Arrays.asList(_logs)); + while(logs.size()>20){ + File oldest=logs.get(0); + for(File file : logs){ + if(file.getName().endsWith(".log") && file.lastModified() pendingUpdates=new ArrayList<>(); + private Runnable afterSoundRunnable=new Runnable(){ + @Override + public void run(){ + soundPool.release(); + if(isBtHeadsetConnected) + ((AudioManager)ApplicationLoader.applicationContext.getSystemService(AUDIO_SERVICE)).stopBluetoothSco(); + ((AudioManager)ApplicationLoader.applicationContext.getSystemService(AUDIO_SERVICE)).setSpeakerphoneOn(false); + } + }; public static final String ACTION_HEADSET_PLUG = "android.intent.action.HEADSET_PLUG"; @@ -284,8 +299,6 @@ public class VoIPService extends Service implements VoIPController.ConnectionSta try { controller = new VoIPController(); controller.setConnectionStateListener(this); - controller.setConfig(MessagesController.getInstance().callPacketTimeout / 1000.0, MessagesController.getInstance().callConnectTimeout / 1000.0, - preferences.getInt("VoipDataSaving", VoIPController.DATA_SAVING_NEVER)); cpuWakelock = ((PowerManager) getSystemService(POWER_SERVICE)).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "telegram-voip"); cpuWakelock.acquire(); @@ -383,9 +396,15 @@ public class VoIPService extends Service implements VoIPController.ConnectionSta } cpuWakelock.release(); AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE); - if(isBtHeadsetConnected && !playingSound) + if(isBtHeadsetConnected && !playingSound){ am.stopBluetoothSco(); - am.setMode(AudioManager.MODE_NORMAL); + am.setSpeakerphoneOn(false); + } + try{ + am.setMode(AudioManager.MODE_NORMAL); + }catch(SecurityException x){ + FileLog.e("Error setting audio more to normal", x); + } am.unregisterMediaButtonEventReceiver(new ComponentName(this, VoIPMediaButtonReceiver.class)); if (haveAudioFocus) am.abandonAudioFocus(this); @@ -490,7 +509,8 @@ public class VoIPService extends Service implements VoIPController.ConnectionSta TLRPC.TL_phone_requestCall reqCall = new TLRPC.TL_phone_requestCall(); reqCall.user_id = MessagesController.getInputUser(user); reqCall.protocol = new TLRPC.TL_phoneCallProtocol(); - reqCall.protocol.udp_p2p = reqCall.protocol.udp_reflector = true; + reqCall.protocol.udp_p2p = true; + reqCall.protocol.udp_reflector = true; reqCall.protocol.min_layer = CALL_MIN_LAYER; reqCall.protocol.max_layer = CALL_MAX_LAYER; VoIPService.this.g_a=g_a; @@ -758,6 +778,7 @@ public class VoIPService extends Service implements VoIPController.ConnectionSta public void declineIncomingCall(int reason, final Runnable onDone) { if(currentState==STATE_REQUESTING){ + dispatchStateChanged(STATE_HANGING_UP); endCallAfterRequest=true; return; } @@ -796,24 +817,28 @@ public class VoIPService extends Service implements VoIPController.ConnectionSta break; } final boolean wasNotConnected=ConnectionsManager.getInstance().getConnectionState()!=ConnectionsManager.ConnectionStateConnected; + final Runnable stopper; if(wasNotConnected){ if (onDone != null) onDone.run(); callEnded(); + stopper=null; + }else{ + stopper=new Runnable(){ + private boolean done=false; + + @Override + public void run(){ + if(done) + return; + done=true; + if(onDone!=null) + onDone.run(); + callEnded(); + } + }; + AndroidUtilities.runOnUIThread(stopper, (int) (VoIPServerConfig.getDouble("hangup_ui_timeout", 5)*1000)); } - final Runnable stopper=new Runnable(){ - private boolean done=false; - @Override - public void run(){ - if(done) - return; - done=true; - if(onDone!=null) - onDone.run(); - callEnded(); - } - }; - AndroidUtilities.runOnUIThread(stopper, (int)(VoIPServerConfig.getDouble("hangup_ui_timeout", 5)*1000)); ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { @@ -872,14 +897,7 @@ public class VoIPService extends Service implements VoIPController.ConnectionSta dispatchStateChanged(STATE_BUSY); playingSound = true; soundPool.play(spBusyId, 1, 1, 0, -1, 1); - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - soundPool.release(); - if(isBtHeadsetConnected) - ((AudioManager)ApplicationLoader.applicationContext.getSystemService(AUDIO_SERVICE)).stopBluetoothSco(); - } - }, 2500); + AndroidUtilities.runOnUIThread(afterSoundRunnable, 1500); stopSelf(); } else { callEnded(); @@ -988,6 +1006,12 @@ public class VoIPService extends Service implements VoIPController.ConnectionSta } private void processAcceptedCall() { + if(!isProximityNear){ + Vibrator vibrator=(Vibrator) getSystemService(VIBRATOR_SERVICE); + if(vibrator.hasVibrator()) + vibrator.vibrate(100); + } + dispatchStateChanged(STATE_EXCHANGING_KEYS); BigInteger p = new BigInteger(1, MessagesStorage.secretPBytes); BigInteger i_authKey = new BigInteger(1, call.g_b); @@ -1054,13 +1078,51 @@ public class VoIPService extends Service implements VoIPController.ConnectionSta } try { FileLog.d("InitCall: keyID=" + keyFingerprint); + SharedPreferences nprefs=getSharedPreferences("notifications", MODE_PRIVATE); + HashSet hashes=new HashSet<>(nprefs.getStringSet("calls_access_hashes", Collections.EMPTY_SET)); + hashes.add(call.id+" "+call.access_hash+" "+System.currentTimeMillis()); + while(hashes.size()>20){ + String oldest=null; + long oldestTime=Long.MAX_VALUE; + Iterator itr=hashes.iterator(); + while(itr.hasNext()){ + String item=itr.next(); + String[] s=item.split(" "); + if(s.length<2){ + itr.remove(); + }else{ + try{ + long t=Long.parseLong(s[2]); + if(t { + protected NativeByteBuffer doInBackground(Void... voids) { + try { + String domain = String.format(Locale.US, native_isTestBackend() != 0 ? "tap%1$s.stel.com" : "ap%1$s.stel.com", dnsConfigVersion == 0 ? "" : "" + dnsConfigVersion); + URL downloadUrl = new URL("https://google.com/resolve?name=" + domain + "&type=16"); + URLConnection 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("Host", "dns.google.com"); + httpConnection.setConnectTimeout(5000); + httpConnection.setReadTimeout(5000); + httpConnection.connect(); + InputStream httpConnectionStream = httpConnection.getInputStream(); + + ByteArrayOutputStream outbuf = new ByteArrayOutputStream(); + + byte[] data = new byte[1024 * 32]; + while (true) { + if (isCancelled()) { + break; + } + int read = httpConnectionStream.read(data); + if (read > 0) { + outbuf.write(data, 0, read); + } else if (read == -1) { + break; + } else { + break; + } + } + try { + if (httpConnectionStream != null) { + httpConnectionStream.close(); + } + } catch (Throwable e) { + FileLog.e(e); + } + JSONObject jsonObject = new JSONObject(new String(outbuf.toByteArray(), "UTF-8")); + JSONArray array = jsonObject.getJSONArray("Answer"); + int len = array.length(); + ArrayList arrayList = new ArrayList<>(len); + for (int a = 0; a < len; a++) { + arrayList.add(array.getJSONObject(a).getString("data")); + } + Collections.sort(arrayList, new Comparator() { + @Override + public int compare(String o1, String o2) { + int l1 = o1.length(); + int l2 = o2.length(); + if (l1 > l2) { + return -1; + } else if (l1 < l2) { + return 1; + } + return 0; + } + }); + StringBuilder builder = new StringBuilder(); + for (int a = 0; a < arrayList.size(); a++) { + builder.append(arrayList.get(a).replace("\"", "")); + } + byte[] bytes = Base64.decode(builder.toString(), Base64.DEFAULT); + NativeByteBuffer buffer = new NativeByteBuffer(bytes.length); + buffer.writeBytes(bytes); + return buffer; + } catch (Throwable e) { + FileLog.e(e); + } + return null; + } + + @Override + protected void onPostExecute(NativeByteBuffer result) { + if (result != null) { + native_applyDnsConfig(result.address); + } + currentTask = null; + } + } + + private static class DnsLoadTask extends AsyncTask { + + protected NativeByteBuffer doInBackground(Void... voids) { + try { + URL downloadUrl; + if (native_isTestBackend() != 0) { + downloadUrl = new URL("https://google.com/test/"); + } else { + downloadUrl = new URL("https://google.com"); + } + URLConnection 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("Host", String.format(Locale.US, "dns-telegram%1$s.appspot.com", dnsConfigVersion == 0 ? "" : "" + dnsConfigVersion)); + httpConnection.setConnectTimeout(5000); + httpConnection.setReadTimeout(5000); + httpConnection.connect(); + InputStream httpConnectionStream = httpConnection.getInputStream(); + + ByteArrayOutputStream outbuf = new ByteArrayOutputStream(); + + byte[] data = new byte[1024 * 32]; + while (true) { + if (isCancelled()) { + break; + } + int read = httpConnectionStream.read(data); + if (read > 0) { + outbuf.write(data, 0, read); + } else if (read == -1) { + break; + } else { + break; + } + } + try { + if (httpConnectionStream != null) { + httpConnectionStream.close(); + } + } catch (Throwable e) { + FileLog.e(e); + } + byte[] bytes = Base64.decode(outbuf.toByteArray(), Base64.DEFAULT); + NativeByteBuffer buffer = new NativeByteBuffer(bytes.length); + buffer.writeBytes(bytes); + return buffer; + } catch (Throwable e) { + FileLog.e(e); + } + return null; + } + + @Override + protected void onPostExecute(NativeByteBuffer result) { + if (result != null) { + currentTask = null; + native_applyDnsConfig(result.address); + } else { + DnsTxtLoadTask task = new DnsTxtLoadTask(); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null); + currentTask = task; + } + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java b/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java index 50ce2dc3b..09b76c28f 100644 --- a/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java +++ b/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java @@ -57,7 +57,7 @@ public class TLRPC { public static final int MESSAGE_FLAG_EDITED = 0x00008000; public static final int MESSAGE_FLAG_MEGAGROUP = 0x80000000; - public static final int LAYER = 65; + public static final int LAYER = 68; public static class DraftMessage extends TLObject { public int flags; @@ -598,6 +598,7 @@ public class TLRPC { public int duration; public int flags; public TL_maskCoords mask_coords; + public boolean round_message; public String file_name; public int w; public int h; @@ -625,9 +626,12 @@ public class TLRPC { case 0x15590068: result = new TL_documentAttributeFilename(); break; - case 0x5910cccb: + case 0xef02ce6: result = new TL_documentAttributeVideo(); break; + case 0x5910cccb: + result = new TL_documentAttributeVideo_layer65(); + break; case 0xded218e0: result = new TL_documentAttributeAudio_layer45(); break; @@ -737,6 +741,28 @@ public class TLRPC { } public static class TL_documentAttributeVideo extends DocumentAttribute { + public static int constructor = 0xef02ce6; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + round_message = (flags & 1) != 0; + duration = stream.readInt32(exception); + w = stream.readInt32(exception); + h = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = round_message ? (flags | 1) : (flags &~ 1); + stream.writeInt32(flags); + stream.writeInt32(duration); + stream.writeInt32(w); + stream.writeInt32(h); + } + } + + public static class TL_documentAttributeVideo_layer65 extends TL_documentAttributeVideo { public static int constructor = 0x5910cccb; @@ -1137,6 +1163,61 @@ public class TLRPC { } } + public static class TL_channelBannedRights extends TLObject { + public static int constructor = 0x58cf4249; + + public int flags; + public boolean view_messages; + public boolean send_messages; + public boolean send_media; + public boolean send_stickers; + public boolean send_gifs; + public boolean send_games; + public boolean send_inline; + public boolean embed_links; + public int until_date; + + public static TL_channelBannedRights TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_channelBannedRights.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_channelBannedRights", constructor)); + } else { + return null; + } + } + TL_channelBannedRights result = new TL_channelBannedRights(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + view_messages = (flags & 1) != 0; + send_messages = (flags & 2) != 0; + send_media = (flags & 4) != 0; + send_stickers = (flags & 8) != 0; + send_gifs = (flags & 16) != 0; + send_games = (flags & 32) != 0; + send_inline = (flags & 64) != 0; + embed_links = (flags & 128) != 0; + until_date = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = view_messages ? (flags | 1) : (flags &~ 1); + flags = send_messages ? (flags | 2) : (flags &~ 2); + flags = send_media ? (flags | 4) : (flags &~ 4); + flags = send_stickers ? (flags | 8) : (flags &~ 8); + flags = send_gifs ? (flags | 16) : (flags &~ 16); + flags = send_games ? (flags | 32) : (flags &~ 32); + flags = send_inline ? (flags | 64) : (flags &~ 64); + flags = embed_links ? (flags | 128) : (flags &~ 128); + stream.writeInt32(flags); + stream.writeInt32(until_date); + } + } + public static class TL_auth_authorization extends TLObject { public static int constructor = 0xcd050916; @@ -1728,6 +1809,62 @@ public class TLRPC { } } + public static class TL_langPackDifference extends TLObject { + public static int constructor = 0xf385c1f6; + + public String lang_code; + public int from_version; + public int version; + public ArrayList strings = new ArrayList<>(); + + public static TL_langPackDifference TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_langPackDifference.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_langPackDifference", constructor)); + } else { + return null; + } + } + TL_langPackDifference result = new TL_langPackDifference(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + lang_code = stream.readString(exception); + from_version = stream.readInt32(exception); + version = 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++) { + LangPackString object = LangPackString.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + strings.add(object); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(lang_code); + stream.writeInt32(from_version); + stream.writeInt32(version); + stream.writeInt32(0x1cb5c415); + int count = strings.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + strings.get(a).serializeToStream(stream); + } + } + } + public static class TL_messages_affectedMessages extends TLObject { public static int constructor = 0x84d19185; @@ -2503,6 +2640,77 @@ public class TLRPC { } } + public static class TL_channelAdminLogEvent extends TLObject { + public static int constructor = 0x3b5a3e40; + + public long id; + public int date; + public int user_id; + public ChannelAdminLogEventAction action; + + public static TL_channelAdminLogEvent TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_channelAdminLogEvent.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_channelAdminLogEvent", constructor)); + } else { + return null; + } + } + TL_channelAdminLogEvent result = new TL_channelAdminLogEvent(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt64(exception); + date = stream.readInt32(exception); + user_id = stream.readInt32(exception); + action = ChannelAdminLogEventAction.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt64(id); + stream.writeInt32(date); + stream.writeInt32(user_id); + action.serializeToStream(stream); + } + } + + public static class TL_langPackLanguage extends TLObject { + public static int constructor = 0x117698f1; + + public String name; + public String native_name; + public String lang_code; + + public static TL_langPackLanguage TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_langPackLanguage.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_langPackLanguage", constructor)); + } else { + return null; + } + } + TL_langPackLanguage result = new TL_langPackLanguage(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + name = stream.readString(exception); + native_name = stream.readString(exception); + lang_code = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(name); + stream.writeString(native_name); + stream.writeString(lang_code); + } + } + public static class SendMessageAction extends TLObject { public int progress; @@ -2542,6 +2750,12 @@ public class TLRPC { case 0x628cbc6f: result = new TL_sendMessageChooseContactAction(); break; + case 0x88f27fbc: + result = new TL_sendMessageRecordRoundAction(); + break; + case 0x243e1c66: + result = new TL_sendMessageUploadRoundAction(); + break; case 0x16bf744e: result = new TL_sendMessageTypingAction(); break; @@ -2688,6 +2902,29 @@ public class TLRPC { } } + public static class TL_sendMessageRecordRoundAction extends SendMessageAction { + public static int constructor = 0x88f27fbc; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + + public static class TL_sendMessageUploadRoundAction extends SendMessageAction { + public static int constructor = 0x243e1c66; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + progress = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(progress); + } + } + public static class TL_sendMessageTypingAction extends SendMessageAction { public static int constructor = 0x16bf744e; @@ -3038,6 +3275,97 @@ public class TLRPC { } } + public static class TL_channels_adminLogResults extends TLObject { + public static int constructor = 0xed8af74d; + + public ArrayList events = new ArrayList<>(); + public ArrayList chats = new ArrayList<>(); + public ArrayList users = new ArrayList<>(); + + public static TL_channels_adminLogResults TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_channels_adminLogResults.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_channels_adminLogResults", constructor)); + } else { + return null; + } + } + TL_channels_adminLogResults result = new TL_channels_adminLogResults(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean 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_channelAdminLogEvent object = TL_channelAdminLogEvent.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + events.add(object); + } + magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + Chat object = Chat.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + chats.add(object); + } + magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + User object = User.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + users.add(object); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(0x1cb5c415); + int count = events.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + events.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = chats.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + chats.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = users.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + users.get(a).serializeToStream(stream); + } + } + } + public static class TL_inputPhoneContact extends TLObject { public static int constructor = 0xf392b7f4; @@ -3465,6 +3793,118 @@ public class TLRPC { } } + public static class LangPackString extends TLObject { + public int flags; + public String key; + public String zero_value; + public String one_value; + public String two_value; + public String few_value; + public String many_value; + public String other_value; + public String value; + + public static LangPackString TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + LangPackString result = null; + switch(constructor) { + case 0x6c47ac9f: + result = new TL_langPackStringPluralized(); + break; + case 0xcad181f6: + result = new TL_langPackString(); + break; + case 0x2979eeb2: + result = new TL_langPackStringDeleted(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in LangPackString", constructor)); + } + if (result != null) { + result.readParams(stream, exception); + } + return result; + } + } + + public static class TL_langPackStringPluralized extends LangPackString { + public static int constructor = 0x6c47ac9f; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + key = stream.readString(exception); + if ((flags & 1) != 0) { + zero_value = stream.readString(exception); + } + if ((flags & 2) != 0) { + one_value = stream.readString(exception); + } + if ((flags & 4) != 0) { + two_value = stream.readString(exception); + } + if ((flags & 8) != 0) { + few_value = stream.readString(exception); + } + if ((flags & 16) != 0) { + many_value = stream.readString(exception); + } + other_value = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + stream.writeString(key); + if ((flags & 1) != 0) { + stream.writeString(zero_value); + } + if ((flags & 2) != 0) { + stream.writeString(one_value); + } + if ((flags & 4) != 0) { + stream.writeString(two_value); + } + if ((flags & 8) != 0) { + stream.writeString(few_value); + } + if ((flags & 16) != 0) { + stream.writeString(many_value); + } + stream.writeString(other_value); + } + } + + public static class TL_langPackString extends LangPackString { + public static int constructor = 0xcad181f6; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + key = stream.readString(exception); + value = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(key); + stream.writeString(value); + } + } + + public static class TL_langPackStringDeleted extends LangPackString { + public static int constructor = 0x2979eeb2; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + key = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(key); + } + } + public static class TL_auth_sentCode extends TLObject { public static int constructor = 0x5e002502; @@ -4854,37 +5294,42 @@ public class TLRPC { } public static class ChatFull extends TLObject { - public int flags; - public boolean can_view_participants; - public boolean can_set_username; public int id; - public String about; - public int participants_count; - public int admins_count; - public int kicked_count; - public int read_inbox_max_id; - public int read_outbox_max_id; - public int unread_count; + public ChatParticipants participants; public Photo chat_photo; public PeerNotifySettings notify_settings; public ExportedChatInvite exported_invite; public ArrayList bot_info = new ArrayList<>(); + public int flags; + public boolean can_view_participants; + public boolean can_set_username; + public String about; + public int participants_count; + public int admins_count; + public int banned_count; + public int read_inbox_max_id; + public int read_outbox_max_id; + public int unread_count; public int migrated_from_chat_id; public int migrated_from_max_id; public int pinned_msg_id; - public ChatParticipants participants; + public int kicked_count; + public int unread_important_count; public static ChatFull TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { ChatFull result = null; switch(constructor) { - case 0xc3d5512f: + case 0x2e02a614: + result = new TL_chatFull(); + break; + case 0x95cb5f57: result = new TL_channelFull(); break; case 0x97bee562: result = new TL_channelFull_layer52(); break; - case 0x2e02a614: - result = new TL_chatFull(); + case 0xc3d5512f: + result = new TL_channelFull_layer67(); break; case 0x9e341ddf: result = new TL_channelFull_layer48(); @@ -4903,7 +5348,238 @@ public class TLRPC { } } + public static class TL_chatFull extends ChatFull { + public static int constructor = 0x2e02a614; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + participants = ChatParticipants.TLdeserialize(stream, stream.readInt32(exception), exception); + chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); + notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); + exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); + int magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + int count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + BotInfo object = BotInfo.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + bot_info.add(object); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + participants.serializeToStream(stream); + chat_photo.serializeToStream(stream); + notify_settings.serializeToStream(stream); + exported_invite.serializeToStream(stream); + stream.writeInt32(0x1cb5c415); + int count = bot_info.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + bot_info.get(a).serializeToStream(stream); + } + } + } + public static class TL_channelFull extends ChatFull { + public static int constructor = 0x95cb5f57; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + can_view_participants = (flags & 8) != 0; + can_set_username = (flags & 64) != 0; + id = stream.readInt32(exception); + about = stream.readString(exception); + if ((flags & 1) != 0) { + participants_count = stream.readInt32(exception); + } + if ((flags & 2) != 0) { + admins_count = stream.readInt32(exception); + } + if ((flags & 4) != 0) { + kicked_count = stream.readInt32(exception); + } + if ((flags & 4) != 0) { + banned_count = stream.readInt32(exception); + } + read_inbox_max_id = stream.readInt32(exception); + read_outbox_max_id = stream.readInt32(exception); + unread_count = stream.readInt32(exception); + chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); + notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); + exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); + int magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + int count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + BotInfo object = BotInfo.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + bot_info.add(object); + } + if ((flags & 16) != 0) { + migrated_from_chat_id = stream.readInt32(exception); + } + if ((flags & 16) != 0) { + migrated_from_max_id = stream.readInt32(exception); + } + if ((flags & 32) != 0) { + pinned_msg_id = stream.readInt32(exception); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = can_view_participants ? (flags | 8) : (flags &~ 8); + flags = can_set_username ? (flags | 64) : (flags &~ 64); + stream.writeInt32(flags); + stream.writeInt32(id); + stream.writeString(about); + if ((flags & 1) != 0) { + stream.writeInt32(participants_count); + } + if ((flags & 2) != 0) { + stream.writeInt32(admins_count); + } + if ((flags & 4) != 0) { + stream.writeInt32(kicked_count); + } + if ((flags & 4) != 0) { + stream.writeInt32(banned_count); + } + stream.writeInt32(read_inbox_max_id); + stream.writeInt32(read_outbox_max_id); + stream.writeInt32(unread_count); + chat_photo.serializeToStream(stream); + notify_settings.serializeToStream(stream); + exported_invite.serializeToStream(stream); + stream.writeInt32(0x1cb5c415); + int count = bot_info.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + bot_info.get(a).serializeToStream(stream); + } + if ((flags & 16) != 0) { + stream.writeInt32(migrated_from_chat_id); + } + if ((flags & 16) != 0) { + stream.writeInt32(migrated_from_max_id); + } + if ((flags & 32) != 0) { + stream.writeInt32(pinned_msg_id); + } + } + } + + public static class TL_channelFull_layer52 extends TL_channelFull { + public static int constructor = 0x97bee562; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + can_view_participants = (flags & 8) != 0; + can_set_username = (flags & 64) != 0; + id = stream.readInt32(exception); + about = stream.readString(exception); + if ((flags & 1) != 0) { + participants_count = stream.readInt32(exception); + } + if ((flags & 2) != 0) { + admins_count = stream.readInt32(exception); + } + if ((flags & 4) != 0) { + kicked_count = stream.readInt32(exception); + } + read_inbox_max_id = stream.readInt32(exception); + unread_count = stream.readInt32(exception); + unread_important_count = stream.readInt32(exception); + chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); + notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); + exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); + int magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + int count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + BotInfo object = BotInfo.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + bot_info.add(object); + } + if ((flags & 16) != 0) { + migrated_from_chat_id = stream.readInt32(exception); + } + if ((flags & 16) != 0) { + migrated_from_max_id = stream.readInt32(exception); + } + if ((flags & 32) != 0) { + pinned_msg_id = stream.readInt32(exception); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = can_view_participants ? (flags | 8) : (flags &~ 8); + flags = can_set_username ? (flags | 64) : (flags &~ 64); + stream.writeInt32(flags); + stream.writeInt32(id); + stream.writeString(about); + if ((flags & 1) != 0) { + stream.writeInt32(participants_count); + } + if ((flags & 2) != 0) { + stream.writeInt32(admins_count); + } + if ((flags & 4) != 0) { + stream.writeInt32(kicked_count); + } + stream.writeInt32(read_inbox_max_id); + stream.writeInt32(unread_count); + stream.writeInt32(unread_important_count); + chat_photo.serializeToStream(stream); + notify_settings.serializeToStream(stream); + exported_invite.serializeToStream(stream); + stream.writeInt32(0x1cb5c415); + int count = bot_info.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + bot_info.get(a).serializeToStream(stream); + } + if ((flags & 16) != 0) { + stream.writeInt32(migrated_from_chat_id); + } + if ((flags & 16) != 0) { + stream.writeInt32(migrated_from_max_id); + } + if ((flags & 32) != 0) { + stream.writeInt32(pinned_msg_id); + } + } + } + + public static class TL_channelFull_layer67 extends TL_channelFull { public static int constructor = 0xc3d5512f; @@ -4994,140 +5670,6 @@ public class TLRPC { } } - public static class TL_channelFull_layer52 extends TL_channelFull { - public static int constructor = 0x97bee562; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - flags = stream.readInt32(exception); - can_view_participants = (flags & 8) != 0; - can_set_username = (flags & 64) != 0; - id = stream.readInt32(exception); - about = stream.readString(exception); - if ((flags & 1) != 0) { - participants_count = stream.readInt32(exception); - } - if ((flags & 2) != 0) { - admins_count = stream.readInt32(exception); - } - if ((flags & 4) != 0) { - kicked_count = stream.readInt32(exception); - } - read_inbox_max_id = stream.readInt32(exception); - unread_count = stream.readInt32(exception); - stream.readInt32(exception); - chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); - notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); - exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); - int magic = stream.readInt32(exception); - if (magic != 0x1cb5c415) { - if (exception) { - throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); - } - return; - } - int count = stream.readInt32(exception); - for (int a = 0; a < count; a++) { - BotInfo object = BotInfo.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - bot_info.add(object); - } - if ((flags & 16) != 0) { - migrated_from_chat_id = stream.readInt32(exception); - } - if ((flags & 16) != 0) { - migrated_from_max_id = stream.readInt32(exception); - } - if ((flags & 32) != 0) { - pinned_msg_id = stream.readInt32(exception); - } - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - flags = can_view_participants ? (flags | 8) : (flags &~ 8); - flags = can_set_username ? (flags | 64) : (flags &~ 64); - stream.writeInt32(flags); - stream.writeInt32(id); - stream.writeString(about); - if ((flags & 1) != 0) { - stream.writeInt32(participants_count); - } - if ((flags & 2) != 0) { - stream.writeInt32(admins_count); - } - if ((flags & 4) != 0) { - stream.writeInt32(kicked_count); - } - stream.writeInt32(read_inbox_max_id); - stream.writeInt32(unread_count); - stream.writeInt32(0); - chat_photo.serializeToStream(stream); - notify_settings.serializeToStream(stream); - exported_invite.serializeToStream(stream); - stream.writeInt32(0x1cb5c415); - int count = bot_info.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - bot_info.get(a).serializeToStream(stream); - } - if ((flags & 16) != 0) { - stream.writeInt32(migrated_from_chat_id); - } - if ((flags & 16) != 0) { - stream.writeInt32(migrated_from_max_id); - } - if ((flags & 32) != 0) { - stream.writeInt32(pinned_msg_id); - } - } - } - - public static class TL_chatFull extends ChatFull { - public static int constructor = 0x2e02a614; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - participants = ChatParticipants.TLdeserialize(stream, stream.readInt32(exception), exception); - chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); - notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); - exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); - int magic = stream.readInt32(exception); - if (magic != 0x1cb5c415) { - if (exception) { - throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); - } - return; - } - int count = stream.readInt32(exception); - for (int a = 0; a < count; a++) { - BotInfo object = BotInfo.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - bot_info.add(object); - } - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - participants.serializeToStream(stream); - chat_photo.serializeToStream(stream); - notify_settings.serializeToStream(stream); - exported_invite.serializeToStream(stream); - stream.writeInt32(0x1cb5c415); - int count = bot_info.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - bot_info.get(a).serializeToStream(stream); - } - } - } - public static class TL_channelFull_layer48 extends TL_channelFull { public static int constructor = 0x9e341ddf; @@ -5148,7 +5690,7 @@ public class TLRPC { } read_inbox_max_id = stream.readInt32(exception); unread_count = stream.readInt32(exception); - stream.readInt32(exception); + unread_important_count = stream.readInt32(exception); chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); @@ -5192,7 +5734,7 @@ public class TLRPC { } stream.writeInt32(read_inbox_max_id); stream.writeInt32(unread_count); - stream.writeInt32(0); + stream.writeInt32(unread_important_count); chat_photo.serializeToStream(stream); notify_settings.serializeToStream(stream); exported_invite.serializeToStream(stream); @@ -5231,7 +5773,7 @@ public class TLRPC { } read_inbox_max_id = stream.readInt32(exception); unread_count = stream.readInt32(exception); - stream.readInt32(exception); + unread_important_count = stream.readInt32(exception); chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); @@ -5254,7 +5796,7 @@ public class TLRPC { } stream.writeInt32(read_inbox_max_id); stream.writeInt32(unread_count); - stream.writeInt32(0); + stream.writeInt32(unread_important_count); chat_photo.serializeToStream(stream); notify_settings.serializeToStream(stream); exported_invite.serializeToStream(stream); @@ -5326,16 +5868,22 @@ public class TLRPC { public static class Page extends TLObject { public ArrayList blocks = new ArrayList<>(); public ArrayList photos = new ArrayList<>(); - public ArrayList videos = new ArrayList<>(); + public ArrayList documents = new ArrayList<>(); public static Page TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { Page result = null; switch(constructor) { + case 0x556ec7aa: + result = new TL_pageFull(); + break; case 0x8dee6c44: - result = new TL_pagePart(); + result = new TL_pagePart_layer67(); break; case 0xd7a19d69: - result = new TL_pageFull(); + result = new TL_pageFull_layer67(); + break; + case 0x8e3f9ebe: + result = new TL_pagePart(); break; } if (result == null && exception) { @@ -5348,7 +5896,82 @@ public class TLRPC { } } - public static class TL_pagePart extends Page { + public static class TL_pageFull extends Page { + public static int constructor = 0x556ec7aa; + + + public void readParams(AbstractSerializedData stream, boolean 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++) { + PageBlock object = PageBlock.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + blocks.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++) { + Photo object = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + photos.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++) { + Document object = Document.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + documents.add(object); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(0x1cb5c415); + int count = blocks.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + blocks.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = photos.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + photos.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = documents.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + documents.get(a).serializeToStream(stream); + } + } + } + + public static class TL_pagePart_layer67 extends TL_pagePart { public static int constructor = 0x8dee6c44; @@ -5396,7 +6019,7 @@ public class TLRPC { if (object == null) { return; } - videos.add(object); + documents.add(object); } } @@ -5415,15 +6038,15 @@ public class TLRPC { photos.get(a).serializeToStream(stream); } stream.writeInt32(0x1cb5c415); - count = videos.size(); + count = documents.size(); stream.writeInt32(count); for (int a = 0; a < count; a++) { - videos.get(a).serializeToStream(stream); + documents.get(a).serializeToStream(stream); } } } - public static class TL_pageFull extends Page { + public static class TL_pageFull_layer67 extends TL_pageFull { public static int constructor = 0xd7a19d69; @@ -5471,7 +6094,7 @@ public class TLRPC { if (object == null) { return; } - videos.add(object); + documents.add(object); } } @@ -5490,10 +6113,85 @@ public class TLRPC { photos.get(a).serializeToStream(stream); } stream.writeInt32(0x1cb5c415); - count = videos.size(); + count = documents.size(); stream.writeInt32(count); for (int a = 0; a < count; a++) { - videos.get(a).serializeToStream(stream); + documents.get(a).serializeToStream(stream); + } + } + } + + public static class TL_pagePart extends Page { + public static int constructor = 0x8e3f9ebe; + + + public void readParams(AbstractSerializedData stream, boolean 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++) { + PageBlock object = PageBlock.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + blocks.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++) { + Photo object = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + photos.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++) { + Document object = Document.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + documents.add(object); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(0x1cb5c415); + int count = blocks.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + blocks.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = photos.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + photos.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = documents.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + documents.get(a).serializeToStream(stream); } } } @@ -7148,11 +7846,10 @@ public class TLRPC { public String last_name; public String username; public long access_hash; - public String phone; + public String phone; public UserProfilePhoto photo; public UserStatus status; - public boolean inactive; - public int flags; + public int flags; public boolean self; public boolean contact; public boolean mutual_contact; @@ -7161,13 +7858,15 @@ public class TLRPC { public boolean bot_chat_history; public boolean bot_nochats; public boolean verified; - public boolean explicit_content; - public int bot_info_version; public boolean restricted; public boolean min; public boolean bot_inline_geo; + public int bot_info_version; public String restriction_reason; public String bot_inline_placeholder; + public String lang_code; + public boolean inactive; + public boolean explicit_content; public static User TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { User result = null; @@ -7178,17 +7877,20 @@ public class TLRPC { case 0xf2fb8319: result = new TL_userContact_old(); break; + case 0x2e13f4c3: + result = new TL_user(); + break; case 0x720535ec: result = new TL_userSelf_old(); - break; - case 0x1c60e608: - result = new TL_userSelf_old3(); + break; + case 0x1c60e608: + result = new TL_userSelf_old3(); break; case 0xd6016d7a: result = new TL_userDeleted_old2(); break; case 0x200250ba: - result = new TL_userEmpty(); + result = new TL_userEmpty(); break; case 0x22e8ceb0: result = new TL_userRequest_old(); @@ -7199,42 +7901,42 @@ public class TLRPC { case 0x75cf7a8: result = new TL_userForeign_old2(); break; - case 0xd9ccc4ef: + case 0xd9ccc4ef: result = new TL_userRequest_old2(); - break; + break; case 0xb29ad7cc: result = new TL_userDeleted_old(); break; - case 0x7007b451: - result = new TL_userSelf_old2(); + case 0xd10d979a: + result = new TL_user_layer65(); break; case 0x22e49072: result = new TL_user_old(); break; - case 0xd10d979a: - result = new TL_user(); + case 0x7007b451: + result = new TL_userSelf_old2(); break; } if (result == null && exception) { throw new RuntimeException(String.format("can't parse magic %x in User", constructor)); - } - if (result != null) { + } + if (result != null) { result.readParams(stream, exception); } return result; - } - } + } + } public static class TL_userContact_old2 extends User { public static int constructor = 0xcab35e18; - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); first_name = stream.readString(exception); last_name = stream.readString(exception); username = stream.readString(exception); - access_hash = stream.readInt64(exception); + access_hash = stream.readInt64(exception); phone = stream.readString(exception); photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); @@ -7242,10 +7944,10 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeInt32(id); + stream.writeInt32(id); stream.writeString(first_name); stream.writeString(last_name); - stream.writeString(username); + stream.writeString(username); stream.writeInt64(access_hash); stream.writeString(phone); photo.serializeToStream(stream); @@ -7260,171 +7962,9 @@ public class TLRPC { public void readParams(AbstractSerializedData stream, boolean exception) { id = stream.readInt32(exception); first_name = stream.readString(exception); - last_name = stream.readString(exception); - access_hash = stream.readInt64(exception); - phone = stream.readString(exception); - photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(first_name); - stream.writeString(last_name); - stream.writeInt64(access_hash); - stream.writeString(phone); - photo.serializeToStream(stream); - status.serializeToStream(stream); - } - } - - public static class TL_userSelf_old extends TL_userSelf_old3 { - public static int constructor = 0x720535ec; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - first_name = stream.readString(exception); - last_name = stream.readString(exception); - phone = stream.readString(exception); - photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); - inactive = stream.readBool(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(first_name); - stream.writeString(last_name); - stream.writeString(phone); - photo.serializeToStream(stream); - status.serializeToStream(stream); - stream.writeBool(inactive); - } - } - - public static class TL_userSelf_old3 extends User { - public static int constructor = 0x1c60e608; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - first_name = stream.readString(exception); - last_name = stream.readString(exception); - username = stream.readString(exception); - phone = stream.readString(exception); - photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(first_name); - stream.writeString(last_name); - stream.writeString(username); - stream.writeString(phone); - photo.serializeToStream(stream); - status.serializeToStream(stream); - } - } - - public static class TL_userDeleted_old2 extends User { - public static int constructor = 0xd6016d7a; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - first_name = stream.readString(exception); - last_name = stream.readString(exception); - username = stream.readString(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(first_name); - stream.writeString(last_name); - stream.writeString(username); - } - } - - public static class TL_userEmpty extends User { - public static int constructor = 0x200250ba; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - } - } - - public static class TL_userRequest_old extends TL_userRequest_old2 { - public static int constructor = 0x22e8ceb0; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - first_name = stream.readString(exception); last_name = stream.readString(exception); access_hash = stream.readInt64(exception); phone = stream.readString(exception); - photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(first_name); - stream.writeString(last_name); - stream.writeInt64(access_hash); - stream.writeString(phone); - photo.serializeToStream(stream); - status.serializeToStream(stream); - } - } - - public static class TL_userForeign_old extends TL_userForeign_old2 { - public static int constructor = 0x5214c89d; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - first_name = stream.readString(exception); - last_name = stream.readString(exception); - access_hash = stream.readInt64(exception); - photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(first_name); - stream.writeString(last_name); - stream.writeInt64(access_hash); - photo.serializeToStream(stream); - status.serializeToStream(stream); - } - } - - public static class TL_userForeign_old2 extends User { - public static int constructor = 0x75cf7a8; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - first_name = stream.readString(exception); - last_name = stream.readString(exception); - username = stream.readString(exception); - access_hash = stream.readInt64(exception); photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); } @@ -7432,91 +7972,17 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); stream.writeInt32(id); - stream.writeString(first_name); - stream.writeString(last_name); - stream.writeString(username); + stream.writeString(first_name); + stream.writeString(last_name); stream.writeInt64(access_hash); + stream.writeString(phone); photo.serializeToStream(stream); status.serializeToStream(stream); } } - public static class TL_userRequest_old2 extends User { - public static int constructor = 0xd9ccc4ef; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - first_name = stream.readString(exception); - last_name = stream.readString(exception); - username = stream.readString(exception); - access_hash = stream.readInt64(exception); - phone = stream.readString(exception); - photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(first_name); - stream.writeString(last_name); - stream.writeString(username); - stream.writeInt64(access_hash); - stream.writeString(phone); - photo.serializeToStream(stream); - status.serializeToStream(stream); - } - } - - public static class TL_userDeleted_old extends TL_userDeleted_old2 { - public static int constructor = 0xb29ad7cc; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - first_name = stream.readString(exception); - last_name = stream.readString(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(first_name); - stream.writeString(last_name); - } - } - - public static class TL_userSelf_old2 extends TL_userSelf_old3 { - public static int constructor = 0x7007b451; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - first_name = stream.readString(exception); - last_name = stream.readString(exception); - username = stream.readString(exception); - phone = stream.readString(exception); - photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); - inactive = stream.readBool(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(first_name); - stream.writeString(last_name); - stream.writeString(username); - stream.writeString(phone); - photo.serializeToStream(stream); - status.serializeToStream(stream); - stream.writeBool(inactive); - } - } - - public static class TL_user_old extends TL_user { - public static int constructor = 0x22e49072; + public static class TL_user extends User { + public static int constructor = 0x2e13f4c3; public void readParams(AbstractSerializedData stream, boolean exception) { @@ -7529,7 +7995,9 @@ public class TLRPC { bot_chat_history = (flags & 32768) != 0; bot_nochats = (flags & 65536) != 0; verified = (flags & 131072) != 0; - explicit_content = (flags & 262144) != 0; + restricted = (flags & 262144) != 0; + min = (flags & 1048576) != 0; + bot_inline_geo = (flags & 2097152) != 0; id = stream.readInt32(exception); if ((flags & 1) != 0) { access_hash = stream.readInt64(exception); @@ -7555,6 +8023,15 @@ public class TLRPC { if ((flags & 16384) != 0) { bot_info_version = stream.readInt32(exception); } + if ((flags & 262144) != 0) { + restriction_reason = stream.readString(exception); + } + if ((flags & 524288) != 0) { + bot_inline_placeholder = stream.readString(exception); + } + if ((flags & 4194304) != 0) { + lang_code = stream.readString(exception); + } } public void serializeToStream(AbstractSerializedData stream) { @@ -7567,7 +8044,9 @@ public class TLRPC { flags = bot_chat_history ? (flags | 32768) : (flags &~ 32768); flags = bot_nochats ? (flags | 65536) : (flags &~ 65536); flags = verified ? (flags | 131072) : (flags &~ 131072); - flags = explicit_content ? (flags | 262144) : (flags &~ 262144); + flags = restricted ? (flags | 262144) : (flags &~ 262144); + flags = min ? (flags | 1048576) : (flags &~ 1048576); + flags = bot_inline_geo ? (flags | 2097152) : (flags &~ 2097152); stream.writeInt32(flags); stream.writeInt32(id); if ((flags & 1) != 0) { @@ -7594,10 +8073,227 @@ public class TLRPC { if ((flags & 16384) != 0) { stream.writeInt32(bot_info_version); } + if ((flags & 262144) != 0) { + stream.writeString(restriction_reason); + } + if ((flags & 524288) != 0) { + stream.writeString(bot_inline_placeholder); + } + if ((flags & 4194304) != 0) { + stream.writeString(lang_code); + } } } - public static class TL_user extends User { + public static class TL_userSelf_old extends TL_userSelf_old3 { + public static int constructor = 0x720535ec; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + first_name = stream.readString(exception); + last_name = stream.readString(exception); + phone = stream.readString(exception); + photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); + inactive = stream.readBool(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(first_name); + stream.writeString(last_name); + stream.writeString(phone); + photo.serializeToStream(stream); + status.serializeToStream(stream); + stream.writeBool(inactive); + } + } + + public static class TL_userSelf_old3 extends User { + public static int constructor = 0x1c60e608; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + first_name = stream.readString(exception); + last_name = stream.readString(exception); + username = stream.readString(exception); + phone = stream.readString(exception); + photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(first_name); + stream.writeString(last_name); + stream.writeString(username); + stream.writeString(phone); + photo.serializeToStream(stream); + status.serializeToStream(stream); + } + } + + public static class TL_userDeleted_old2 extends User { + public static int constructor = 0xd6016d7a; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + first_name = stream.readString(exception); + last_name = stream.readString(exception); + username = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(first_name); + stream.writeString(last_name); + stream.writeString(username); + } + } + + public static class TL_userEmpty extends User { + public static int constructor = 0x200250ba; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + } + } + + public static class TL_userRequest_old extends TL_userRequest_old2 { + public static int constructor = 0x22e8ceb0; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + first_name = stream.readString(exception); + last_name = stream.readString(exception); + access_hash = stream.readInt64(exception); + phone = stream.readString(exception); + photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(first_name); + stream.writeString(last_name); + stream.writeInt64(access_hash); + stream.writeString(phone); + photo.serializeToStream(stream); + status.serializeToStream(stream); + } + } + + public static class TL_userForeign_old extends TL_userForeign_old2 { + public static int constructor = 0x5214c89d; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + first_name = stream.readString(exception); + last_name = stream.readString(exception); + access_hash = stream.readInt64(exception); + photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(first_name); + stream.writeString(last_name); + stream.writeInt64(access_hash); + photo.serializeToStream(stream); + status.serializeToStream(stream); + } + } + + public static class TL_userForeign_old2 extends User { + public static int constructor = 0x75cf7a8; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + first_name = stream.readString(exception); + last_name = stream.readString(exception); + username = stream.readString(exception); + access_hash = stream.readInt64(exception); + photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(first_name); + stream.writeString(last_name); + stream.writeString(username); + stream.writeInt64(access_hash); + photo.serializeToStream(stream); + status.serializeToStream(stream); + } + } + + public static class TL_userRequest_old2 extends User { + public static int constructor = 0xd9ccc4ef; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + first_name = stream.readString(exception); + last_name = stream.readString(exception); + username = stream.readString(exception); + access_hash = stream.readInt64(exception); + phone = stream.readString(exception); + photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(first_name); + stream.writeString(last_name); + stream.writeString(username); + stream.writeInt64(access_hash); + stream.writeString(phone); + photo.serializeToStream(stream); + status.serializeToStream(stream); + } + } + + public static class TL_userDeleted_old extends TL_userDeleted_old2 { + public static int constructor = 0xb29ad7cc; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + first_name = stream.readString(exception); + last_name = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(first_name); + stream.writeString(last_name); + } + } + + public static class TL_user_layer65 extends TL_user { public static int constructor = 0xd10d979a; @@ -7695,57 +8391,115 @@ public class TLRPC { } } - public static class ChannelParticipantRole extends TLObject { - - public static ChannelParticipantRole TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { - ChannelParticipantRole result = null; - switch(constructor) { - case 0x820bfe8c: - result = new TL_channelRoleEditor(); - break; - case 0xb285a0c6: - result = new TL_channelRoleEmpty(); - break; - case 0x9618d975: - result = new TL_channelRoleModerator(); - break; - } - if (result == null && exception) { - throw new RuntimeException(String.format("can't parse magic %x in ChannelParticipantRole", constructor)); - } - if (result != null) { - result.readParams(stream, exception); - } - return result; - } - } - - public static class TL_channelRoleEditor extends ChannelParticipantRole { - public static int constructor = 0x820bfe8c; + public static class TL_user_old extends TL_user { + public static int constructor = 0x22e49072; - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - } - } + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + self = (flags & 1024) != 0; + contact = (flags & 2048) != 0; + mutual_contact = (flags & 4096) != 0; + deleted = (flags & 8192) != 0; + bot = (flags & 16384) != 0; + bot_chat_history = (flags & 32768) != 0; + bot_nochats = (flags & 65536) != 0; + verified = (flags & 131072) != 0; + explicit_content = (flags & 262144) != 0; + id = stream.readInt32(exception); + if ((flags & 1) != 0) { + access_hash = stream.readInt64(exception); + } + if ((flags & 2) != 0) { + first_name = stream.readString(exception); + } + if ((flags & 4) != 0) { + last_name = stream.readString(exception); + } + if ((flags & 8) != 0) { + username = stream.readString(exception); + } + if ((flags & 16) != 0) { + phone = stream.readString(exception); + } + if ((flags & 32) != 0) { + photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + } + if ((flags & 64) != 0) { + status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); + } + if ((flags & 16384) != 0) { + bot_info_version = stream.readInt32(exception); + } + } - public static class TL_channelRoleEmpty extends ChannelParticipantRole { - public static int constructor = 0xb285a0c6; + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = self ? (flags | 1024) : (flags &~ 1024); + flags = contact ? (flags | 2048) : (flags &~ 2048); + flags = mutual_contact ? (flags | 4096) : (flags &~ 4096); + flags = deleted ? (flags | 8192) : (flags &~ 8192); + flags = bot ? (flags | 16384) : (flags &~ 16384); + flags = bot_chat_history ? (flags | 32768) : (flags &~ 32768); + flags = bot_nochats ? (flags | 65536) : (flags &~ 65536); + flags = verified ? (flags | 131072) : (flags &~ 131072); + flags = explicit_content ? (flags | 262144) : (flags &~ 262144); + stream.writeInt32(flags); + stream.writeInt32(id); + if ((flags & 1) != 0) { + stream.writeInt64(access_hash); + } + if ((flags & 2) != 0) { + stream.writeString(first_name); + } + if ((flags & 4) != 0) { + stream.writeString(last_name); + } + if ((flags & 8) != 0) { + stream.writeString(username); + } + if ((flags & 16) != 0) { + stream.writeString(phone); + } + if ((flags & 32) != 0) { + photo.serializeToStream(stream); + } + if ((flags & 64) != 0) { + status.serializeToStream(stream); + } + if ((flags & 16384) != 0) { + stream.writeInt32(bot_info_version); + } + } + } + + public static class TL_userSelf_old2 extends TL_userSelf_old3 { + public static int constructor = 0x7007b451; - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - } - } + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + first_name = stream.readString(exception); + last_name = stream.readString(exception); + username = stream.readString(exception); + phone = stream.readString(exception); + photo = UserProfilePhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + status = UserStatus.TLdeserialize(stream, stream.readInt32(exception), exception); + inactive = stream.readBool(exception); + } - public static class TL_channelRoleModerator extends ChannelParticipantRole { - public static int constructor = 0x9618d975; - - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(first_name); + stream.writeString(last_name); + stream.writeString(username); + stream.writeString(phone); + photo.serializeToStream(stream); + status.serializeToStream(stream); + stream.writeBool(inactive); + } + } public static class TL_messages_highScores extends TLObject { public static int constructor = 0x9a3bfd99; @@ -7867,6 +8621,7 @@ public class TLRPC { } public static class ChannelParticipantsFilter extends TLObject { + public String q; public static ChannelParticipantsFilter TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { ChannelParticipantsFilter result = null; @@ -7877,12 +8632,18 @@ public class TLRPC { case 0xde3f3c79: result = new TL_channelParticipantsRecent(); break; - case 0x3c37bb7a: + case 0xa3b54985: result = new TL_channelParticipantsKicked(); break; + case 0x656ac4b: + result = new TL_channelParticipantsSearch(); + break; case 0xb0d1865b: result = new TL_channelParticipantsBots(); break; + case 0x1427a5e1: + result = new TL_channelParticipantsBanned(); + break; } if (result == null && exception) { throw new RuntimeException(String.format("can't parse magic %x in ChannelParticipantsFilter", constructor)); @@ -7913,11 +8674,30 @@ public class TLRPC { } public static class TL_channelParticipantsKicked extends ChannelParticipantsFilter { - public static int constructor = 0x3c37bb7a; + public static int constructor = 0xa3b54985; + public void readParams(AbstractSerializedData stream, boolean exception) { + q = stream.readString(exception); + } + public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); + stream.writeString(q); + } + } + + public static class TL_channelParticipantsSearch extends ChannelParticipantsFilter { + public static int constructor = 0x656ac4b; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + q = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(q); } } @@ -7930,6 +8710,20 @@ public class TLRPC { } } + public static class TL_channelParticipantsBanned extends ChannelParticipantsFilter { + public static int constructor = 0x1427a5e1; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + q = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(q); + } + } + public static class GeoChatMessage extends TLObject { public int chat_id; public int id; @@ -9381,6 +10175,9 @@ public class TLRPC { case 0x161d9628: result = new TL_topPeerCategoryChannels(); break; + case 0x1e76a78c: + result = new TL_topPeerCategoryPhoneCalls(); + break; case 0xab661b5b: result = new TL_topPeerCategoryBotsPM(); break; @@ -9431,6 +10228,15 @@ public class TLRPC { } } + public static class TL_topPeerCategoryPhoneCalls extends TopPeerCategory { + public static int constructor = 0x1e76a78c; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + public static class TL_topPeerCategoryBotsPM extends TopPeerCategory { public static int constructor = 0xab661b5b; @@ -10074,10 +10880,13 @@ public class TLRPC { public long author_photo_id; public int date; public ArrayList blocks = new ArrayList<>(); + public Chat channel; public PageBlock cover; + public long audio_id; public boolean first; //custom public boolean bottom; //custom public int level; //custom + public int mid; //custom public static PageBlock TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { PageBlock result = null; @@ -10139,6 +10948,9 @@ public class TLRPC { case 0x70abc3fd: result = new TL_pageBlockTitle(); break; + case 0xef1751b5: + result = new TL_pageBlockChannel(); + break; case 0x39f23300: result = new TL_pageBlockCover(); break; @@ -10151,6 +10963,9 @@ public class TLRPC { case 0x4f4456d3: result = new TL_pageBlockPullquote(); break; + case 0x31b81a7f: + result = new TL_pageBlockAudio(); + break; } if (result == null && exception) { throw new RuntimeException(String.format("can't parse magic %x in PageBlock", constructor)); @@ -10547,6 +11362,20 @@ public class TLRPC { } } + public static class TL_pageBlockChannel extends PageBlock { + public static int constructor = 0xef1751b5; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + channel = Chat.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + channel.serializeToStream(stream); + } + } + public static class TL_pageBlockCover extends PageBlock { public static int constructor = 0x39f23300; @@ -10627,6 +11456,22 @@ public class TLRPC { } } + public static class TL_pageBlockAudio extends PageBlock { + public static int constructor = 0x31b81a7f; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + audio_id = stream.readInt64(exception); + caption = RichText.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt64(audio_id); + caption.serializeToStream(stream); + } + } + public static class InputPrivacyRule extends TLObject { public ArrayList users = new ArrayList<>(); @@ -11632,6 +12477,76 @@ public class TLRPC { } } + public static class TL_channelAdminLogEventsFilter extends TLObject { + public static int constructor = 0xea107ae4; + + public int flags; + public boolean join; + public boolean leave; + public boolean invite; + public boolean ban; + public boolean unban; + public boolean kick; + public boolean unkick; + public boolean promote; + public boolean demote; + public boolean info; + public boolean settings; + public boolean pinned; + public boolean edit; + public boolean delete; + + public static TL_channelAdminLogEventsFilter TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_channelAdminLogEventsFilter.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_channelAdminLogEventsFilter", constructor)); + } else { + return null; + } + } + TL_channelAdminLogEventsFilter result = new TL_channelAdminLogEventsFilter(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + join = (flags & 1) != 0; + leave = (flags & 2) != 0; + invite = (flags & 4) != 0; + ban = (flags & 8) != 0; + unban = (flags & 16) != 0; + kick = (flags & 32) != 0; + unkick = (flags & 64) != 0; + promote = (flags & 128) != 0; + demote = (flags & 256) != 0; + info = (flags & 512) != 0; + settings = (flags & 1024) != 0; + pinned = (flags & 2048) != 0; + edit = (flags & 4096) != 0; + delete = (flags & 8192) != 0; + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = join ? (flags | 1) : (flags &~ 1); + flags = leave ? (flags | 2) : (flags &~ 2); + flags = invite ? (flags | 4) : (flags &~ 4); + flags = ban ? (flags | 8) : (flags &~ 8); + flags = unban ? (flags | 16) : (flags &~ 16); + flags = kick ? (flags | 32) : (flags &~ 32); + flags = unkick ? (flags | 64) : (flags &~ 64); + flags = promote ? (flags | 128) : (flags &~ 128); + flags = demote ? (flags | 256) : (flags &~ 256); + flags = info ? (flags | 512) : (flags &~ 512); + flags = settings ? (flags | 1024) : (flags &~ 1024); + flags = pinned ? (flags | 2048) : (flags &~ 2048); + flags = edit ? (flags | 4096) : (flags &~ 4096); + flags = delete ? (flags | 8192) : (flags &~ 8192); + stream.writeInt32(flags); + } + } + public static class UserStatus extends TLObject { public int expires; @@ -11945,6 +12860,7 @@ public class TLRPC { public String type; public MessageMedia media; public ArrayList entities = new ArrayList<>(); + public TL_langPackDifference difference; public boolean is_admin; public String offset; public PrivacyKey key; @@ -12061,6 +12977,9 @@ public class TLRPC { case 0x98592475: result = new TL_updateChannelPinnedMessage(); break; + case 0x56022f4d: + result = new TL_updateLangPack(); + break; case 0xb6901959: result = new TL_updateChatParticipantAdmin(); break; @@ -12124,6 +13043,9 @@ public class TLRPC { case 0x688a30aa: result = new TL_updateNewStickerSet(); break; + case 0x10c2404b: + result = new TL_updateLangPackTooLong(); + break; case 0x38fe25b7: result = new TL_updateEncryptedMessagesRead(); break; @@ -12777,6 +13699,20 @@ public class TLRPC { } } + public static class TL_updateLangPack extends Update { + public static int constructor = 0x56022f4d; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + difference = TL_langPackDifference.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + difference.serializeToStream(stream); + } + } + public static class TL_updateChatParticipantAdmin extends Update { public static int constructor = 0xb6901959; @@ -13203,6 +14139,15 @@ public class TLRPC { } } + public static class TL_updateLangPackTooLong extends Update { + public static int constructor = 0x10c2404b; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + public static class TL_updateEncryptedMessagesRead extends Update { public static int constructor = 0x38fe25b7; @@ -14706,7 +15651,7 @@ public class TLRPC { } public static class TL_config extends TLObject { - public static int constructor = 0xcb601684; + public static int constructor = 0x7feec888; public int flags; public boolean phonecalls_enabled; @@ -14738,6 +15683,8 @@ public class TLRPC { public int call_connect_timeout_ms; public int call_packet_timeout_ms; public String me_url_prefix; + public String suggested_lang_code; + public int lang_pack_version; public ArrayList disabled_features = new ArrayList<>(); public static TL_config TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { @@ -14800,6 +15747,12 @@ public class TLRPC { call_connect_timeout_ms = stream.readInt32(exception); call_packet_timeout_ms = stream.readInt32(exception); me_url_prefix = stream.readString(exception); + if ((flags & 4) != 0) { + suggested_lang_code = stream.readString(exception); + } + if ((flags & 4) != 0) { + lang_pack_version = stream.readInt32(exception); + } magic = stream.readInt32(exception); if (magic != 0x1cb5c415) { if (exception) { @@ -14856,6 +15809,12 @@ public class TLRPC { stream.writeInt32(call_connect_timeout_ms); stream.writeInt32(call_packet_timeout_ms); stream.writeString(me_url_prefix); + if ((flags & 4) != 0) { + stream.writeString(suggested_lang_code); + } + if ((flags & 4) != 0) { + stream.writeInt32(lang_pack_version); + } stream.writeInt32(0x1cb5c415); count = disabled_features.size(); stream.writeInt32(count); @@ -15907,145 +16866,207 @@ public class TLRPC { } } - public static class ChannelParticipant extends TLObject { - public int user_id; - public int date; - public int inviter_id; - public int kicked_by; + public static class ChannelParticipant extends TLObject { + public int user_id; + public int kicked_by; + public int date; + public TL_channelBannedRights banned_rights; + public int inviter_id; + public int flags; + public boolean can_edit; + public boolean left; + public int promoted_by; + public TL_channelAdminRights admin_rights; - public static ChannelParticipant TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { - ChannelParticipant result = null; - switch(constructor) { - case 0x15ebac1d: - result = new TL_channelParticipant(); - break; - case 0xa3289a6d: - result = new TL_channelParticipantSelf(); - break; - case 0x98192d61: - result = new TL_channelParticipantEditor(); - break; - case 0x8cc5e69a: - result = new TL_channelParticipantKicked(); - break; - case 0x91057fef: - result = new TL_channelParticipantModerator(); - break; - case 0xe3e2e1f9: - result = new TL_channelParticipantCreator(); - break; - } - if (result == null && exception) { - throw new RuntimeException(String.format("can't parse magic %x in ChannelParticipant", constructor)); - } - if (result != null) { - result.readParams(stream, exception); - } - return result; - } - } + public static ChannelParticipant TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + ChannelParticipant result = null; + switch(constructor) { + case 0x222c1886: + result = new TL_channelParticipantBanned(); + break; + case 0xe3e2e1f9: + result = new TL_channelParticipantCreator(); + break; + case 0x15ebac1d: + result = new TL_channelParticipant(); + break; + case 0x8cc5e69a: + result = new TL_channelParticipantKicked_layer67(); + break; + case 0xa3289a6d: + result = new TL_channelParticipantSelf(); + break; + case 0x91057fef: + result = new TL_channelParticipantModerator_layer67(); + break; + case 0x98192d61: + result = new TL_channelParticipantEditor_layer67(); + break; + case 0xa82fa898: + result = new TL_channelParticipantAdmin(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in ChannelParticipant", constructor)); + } + if (result != null) { + result.readParams(stream, exception); + } + return result; + } + } - public static class TL_channelParticipant extends ChannelParticipant { - public static int constructor = 0x15ebac1d; + public static class TL_channelParticipantBanned extends ChannelParticipant { + public static int constructor = 0x222c1886; - public void readParams(AbstractSerializedData stream, boolean exception) { - user_id = stream.readInt32(exception); - date = stream.readInt32(exception); - } + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + left = (flags & 1) != 0; + user_id = stream.readInt32(exception); + kicked_by = stream.readInt32(exception); + date = stream.readInt32(exception); + banned_rights = TL_channelBannedRights.TLdeserialize(stream, stream.readInt32(exception), exception); + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(user_id); - stream.writeInt32(date); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = left ? (flags | 1) : (flags &~ 1); + stream.writeInt32(flags); + stream.writeInt32(user_id); + stream.writeInt32(kicked_by); + stream.writeInt32(date); + banned_rights.serializeToStream(stream); + } + } - public static class TL_channelParticipantSelf extends ChannelParticipant { - public static int constructor = 0xa3289a6d; + public static class TL_channelParticipantCreator extends ChannelParticipant { + public static int constructor = 0xe3e2e1f9; - public void readParams(AbstractSerializedData stream, boolean exception) { - user_id = stream.readInt32(exception); - inviter_id = stream.readInt32(exception); - date = stream.readInt32(exception); - } + public void readParams(AbstractSerializedData stream, boolean exception) { + user_id = stream.readInt32(exception); + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(user_id); - stream.writeInt32(inviter_id); - stream.writeInt32(date); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(user_id); + } + } - public static class TL_channelParticipantEditor extends ChannelParticipant { - public static int constructor = 0x98192d61; + public static class TL_channelParticipant extends ChannelParticipant { + public static int constructor = 0x15ebac1d; - public void readParams(AbstractSerializedData stream, boolean exception) { - user_id = stream.readInt32(exception); - inviter_id = stream.readInt32(exception); - date = stream.readInt32(exception); - } + public void readParams(AbstractSerializedData stream, boolean exception) { + user_id = stream.readInt32(exception); + date = stream.readInt32(exception); + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(user_id); - stream.writeInt32(inviter_id); - stream.writeInt32(date); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(user_id); + stream.writeInt32(date); + } + } - public static class TL_channelParticipantKicked extends ChannelParticipant { - public static int constructor = 0x8cc5e69a; + public static class TL_channelParticipantKicked_layer67 extends ChannelParticipant { + public static int constructor = 0x8cc5e69a; - public void readParams(AbstractSerializedData stream, boolean exception) { - user_id = stream.readInt32(exception); - kicked_by = stream.readInt32(exception); - date = stream.readInt32(exception); - } + public void readParams(AbstractSerializedData stream, boolean exception) { + user_id = stream.readInt32(exception); + kicked_by = stream.readInt32(exception); + date = stream.readInt32(exception); + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(user_id); - stream.writeInt32(kicked_by); - stream.writeInt32(date); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(user_id); + stream.writeInt32(kicked_by); + stream.writeInt32(date); + } + } - public static class TL_channelParticipantModerator extends ChannelParticipant { - public static int constructor = 0x91057fef; + public static class TL_channelParticipantSelf extends ChannelParticipant { + public static int constructor = 0xa3289a6d; - public void readParams(AbstractSerializedData stream, boolean exception) { - user_id = stream.readInt32(exception); - inviter_id = stream.readInt32(exception); - date = stream.readInt32(exception); - } + public void readParams(AbstractSerializedData stream, boolean exception) { + user_id = stream.readInt32(exception); + inviter_id = stream.readInt32(exception); + date = stream.readInt32(exception); + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(user_id); - stream.writeInt32(inviter_id); - stream.writeInt32(date); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(user_id); + stream.writeInt32(inviter_id); + stream.writeInt32(date); + } + } - public static class TL_channelParticipantCreator extends ChannelParticipant { - public static int constructor = 0xe3e2e1f9; + public static class TL_channelParticipantModerator_layer67 extends TL_channelParticipantAdmin { + public static int constructor = 0x91057fef; - public void readParams(AbstractSerializedData stream, boolean exception) { - user_id = stream.readInt32(exception); - } + public void readParams(AbstractSerializedData stream, boolean exception) { + user_id = stream.readInt32(exception); + inviter_id = stream.readInt32(exception); + date = stream.readInt32(exception); + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(user_id); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(user_id); + stream.writeInt32(inviter_id); + stream.writeInt32(date); + } + } + + public static class TL_channelParticipantEditor_layer67 extends TL_channelParticipantAdmin { + public static int constructor = 0x98192d61; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + user_id = stream.readInt32(exception); + inviter_id = stream.readInt32(exception); + date = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(user_id); + stream.writeInt32(inviter_id); + stream.writeInt32(date); + } + } + + public static class TL_channelParticipantAdmin extends ChannelParticipant { + public static int constructor = 0xa82fa898; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + can_edit = (flags & 1) != 0; + user_id = stream.readInt32(exception); + inviter_id = stream.readInt32(exception); + promoted_by = stream.readInt32(exception); + date = stream.readInt32(exception); + admin_rights = TL_channelAdminRights.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = can_edit ? (flags | 1) : (flags &~ 1); + stream.writeInt32(flags); + stream.writeInt32(user_id); + stream.writeInt32(inviter_id); + stream.writeInt32(promoted_by); + stream.writeInt32(date); + admin_rights.serializeToStream(stream); + } + } public static class InputStickeredMedia extends TLObject { @@ -17081,6 +18102,278 @@ public class TLRPC { } } + public static class ChannelAdminLogEventAction extends TLObject { + public Message message; + public String prev_value; + public Message prev_message; + public Message new_message; + public ChannelParticipant prev_participant; + public ChannelParticipant new_participant; + public ChannelParticipant participant; + public ChatPhoto prev_photo; + public ChatPhoto new_photo; + + public static ChannelAdminLogEventAction TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + ChannelAdminLogEventAction result = null; + switch(constructor) { + case 0x1b7907ae: + result = new TL_channelAdminLogEventActionToggleInvites(); + break; + case 0xe9e82c18: + result = new TL_channelAdminLogEventActionUpdatePinned(); + break; + case 0x26ae0971: + result = new TL_channelAdminLogEventActionToggleSignatures(); + break; + case 0x55188a2e: + result = new TL_channelAdminLogEventActionChangeAbout(); + break; + case 0x709b2405: + result = new TL_channelAdminLogEventActionEditMessage(); + break; + case 0xd5676710: + result = new TL_channelAdminLogEventActionParticipantToggleAdmin(); + break; + case 0xe6dfb825: + result = new TL_channelAdminLogEventActionChangeTitle(); + break; + case 0x42e047bb: + result = new TL_channelAdminLogEventActionDeleteMessage(); + break; + case 0xe31c34d8: + result = new TL_channelAdminLogEventActionParticipantInvite(); + break; + case 0xf89777f2: + result = new TL_channelAdminLogEventActionParticipantLeave(); + break; + case 0x6a4afc38: + result = new TL_channelAdminLogEventActionChangeUsername(); + break; + case 0xb82f55c3: + result = new TL_channelAdminLogEventActionChangePhoto(); + break; + case 0xe6d83d7e: + result = new TL_channelAdminLogEventActionParticipantToggleBan(); + break; + case 0x183040d3: + result = new TL_channelAdminLogEventActionParticipantJoin(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in ChannelAdminLogEventAction", constructor)); + } + if (result != null) { + result.readParams(stream, exception); + } + return result; + } + } + + public static class TL_channelAdminLogEventActionToggleInvites extends ChannelAdminLogEventAction { + public static int constructor = 0x1b7907ae; + + public boolean new_value; + + public void readParams(AbstractSerializedData stream, boolean exception) { + new_value = stream.readBool(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeBool(new_value); + } + } + + public static class TL_channelAdminLogEventActionUpdatePinned extends ChannelAdminLogEventAction { + public static int constructor = 0xe9e82c18; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + message = Message.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + message.serializeToStream(stream); + } + } + + public static class TL_channelAdminLogEventActionToggleSignatures extends ChannelAdminLogEventAction { + public static int constructor = 0x26ae0971; + + public boolean new_value; + + public void readParams(AbstractSerializedData stream, boolean exception) { + new_value = stream.readBool(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeBool(new_value); + } + } + + public static class TL_channelAdminLogEventActionChangeAbout extends ChannelAdminLogEventAction { + public static int constructor = 0x55188a2e; + + public String new_value; + + public void readParams(AbstractSerializedData stream, boolean exception) { + prev_value = stream.readString(exception); + new_value = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(prev_value); + stream.writeString(new_value); + } + } + + public static class TL_channelAdminLogEventActionEditMessage extends ChannelAdminLogEventAction { + public static int constructor = 0x709b2405; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + prev_message = Message.TLdeserialize(stream, stream.readInt32(exception), exception); + new_message = Message.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + prev_message.serializeToStream(stream); + new_message.serializeToStream(stream); + } + } + + public static class TL_channelAdminLogEventActionParticipantToggleAdmin extends ChannelAdminLogEventAction { + public static int constructor = 0xd5676710; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + prev_participant = ChannelParticipant.TLdeserialize(stream, stream.readInt32(exception), exception); + new_participant = ChannelParticipant.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + prev_participant.serializeToStream(stream); + new_participant.serializeToStream(stream); + } + } + + public static class TL_channelAdminLogEventActionChangeTitle extends ChannelAdminLogEventAction { + public static int constructor = 0xe6dfb825; + + public String new_value; + + public void readParams(AbstractSerializedData stream, boolean exception) { + prev_value = stream.readString(exception); + new_value = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(prev_value); + stream.writeString(new_value); + } + } + + public static class TL_channelAdminLogEventActionDeleteMessage extends ChannelAdminLogEventAction { + public static int constructor = 0x42e047bb; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + message = Message.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + message.serializeToStream(stream); + } + } + + public static class TL_channelAdminLogEventActionParticipantInvite extends ChannelAdminLogEventAction { + public static int constructor = 0xe31c34d8; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + participant = ChannelParticipant.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + participant.serializeToStream(stream); + } + } + + public static class TL_channelAdminLogEventActionParticipantLeave extends ChannelAdminLogEventAction { + public static int constructor = 0xf89777f2; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + + public static class TL_channelAdminLogEventActionChangeUsername extends ChannelAdminLogEventAction { + public static int constructor = 0x6a4afc38; + + public String new_value; + + public void readParams(AbstractSerializedData stream, boolean exception) { + prev_value = stream.readString(exception); + new_value = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(prev_value); + stream.writeString(new_value); + } + } + + public static class TL_channelAdminLogEventActionChangePhoto extends ChannelAdminLogEventAction { + public static int constructor = 0xb82f55c3; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + prev_photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + new_photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + prev_photo.serializeToStream(stream); + new_photo.serializeToStream(stream); + } + } + + public static class TL_channelAdminLogEventActionParticipantToggleBan extends ChannelAdminLogEventAction { + public static int constructor = 0xe6d83d7e; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + prev_participant = ChannelParticipant.TLdeserialize(stream, stream.readInt32(exception), exception); + new_participant = ChannelParticipant.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + prev_participant.serializeToStream(stream); + new_participant.serializeToStream(stream); + } + } + + public static class TL_channelAdminLogEventActionParticipantJoin extends ChannelAdminLogEventAction { + public static int constructor = 0x183040d3; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + public static class TL_inputWebFileLocation extends TLObject { public static int constructor = 0xc239d686; @@ -17112,128 +18405,209 @@ public class TLRPC { } } - public static class Chat extends TLObject { - public int flags; - public boolean creator; - public boolean kicked; - public boolean admins_enabled; - public boolean admin; - public boolean deactivated; - public int id; - public String title; - public ChatPhoto photo; - public int participants_count; - public int date; - public int version; - public boolean editor; - public boolean moderator; - public boolean broadcast; - public boolean verified; - public boolean megagroup; - public boolean explicit_content; - public boolean left; - public long access_hash; - public String username; - public boolean restricted; - public boolean democracy; - public boolean signatures; - public String restriction_reason; - public boolean min; - public InputChannel migrated_to; - public String address; - public String venue; - public GeoPoint geo; - public boolean checked_in; + public static class TL_channelAdminRights extends TLObject { + public static int constructor = 0x5d7ceba5; - public static Chat TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { - Chat result = null; - switch(constructor) { - case 0x7312bc48: - result = new TL_chat_old2(); - break; - case 0x7328bdb: - result = new TL_chatForbidden(); - break; - case 0x8537784f: - result = new TL_channelForbidden(); - break; - case 0x678e9587: - result = new TL_channel_old(); - break; - case 0xfb0ccc41: - result = new TL_chatForbidden_old(); - break; - case 0xd91cdd54: - result = new TL_chat(); - break; - case 0x9ba2d800: - result = new TL_chatEmpty(); - break; - case 0x4b1b7506: - result = new TL_channel_layer48(); - break; - case 0x75eaea5a: - result = new TL_geoChat(); - break; - case 0x2d85832c: - result = new TL_channelForbidden_layer52(); - break; - case 0xa14dca52: - result = new TL_channel(); - break; - case 0x6e9c9bc7: - result = new TL_chat_old(); - break; - } - if (result == null && exception) { - throw new RuntimeException(String.format("can't parse magic %x in Chat", constructor)); - } - if (result != null) { - result.readParams(stream, exception); + public int flags; + public boolean change_info; + public boolean post_messages; + public boolean edit_messages; + public boolean delete_messages; + public boolean ban_users; + public boolean invite_users; + public boolean invite_link; + public boolean pin_messages; + public boolean add_admins; + + public static TL_channelAdminRights TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_channelAdminRights.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_channelAdminRights", constructor)); + } else { + return null; + } } + TL_channelAdminRights result = new TL_channelAdminRights(); + result.readParams(stream, exception); return result; } - } - - public static class TL_chat_old2 extends TL_chat { - public static int constructor = 0x7312bc48; public void readParams(AbstractSerializedData stream, boolean exception) { flags = stream.readInt32(exception); - creator = (flags & 1) != 0; - kicked = (flags & 2) != 0; - left = (flags & 4) != 0; - admins_enabled = (flags & 8) != 0; - admin = (flags & 16) != 0; - deactivated = (flags & 32) != 0; - id = stream.readInt32(exception); - title = stream.readString(exception); - photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - participants_count = stream.readInt32(exception); - date = stream.readInt32(exception); - version = stream.readInt32(exception); + change_info = (flags & 1) != 0; + post_messages = (flags & 2) != 0; + edit_messages = (flags & 4) != 0; + delete_messages = (flags & 8) != 0; + ban_users = (flags & 16) != 0; + invite_users = (flags & 32) != 0; + invite_link = (flags & 64) != 0; + pin_messages = (flags & 128) != 0; + add_admins = (flags & 512) != 0; } public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - flags = creator ? (flags | 1) : (flags &~ 1); - flags = kicked ? (flags | 2) : (flags &~ 2); - flags = left ? (flags | 4) : (flags &~ 4); - flags = admins_enabled ? (flags | 8) : (flags &~ 8); - flags = admin ? (flags | 16) : (flags &~ 16); - flags = deactivated ? (flags | 32) : (flags &~ 32); + flags = change_info ? (flags | 1) : (flags &~ 1); + flags = post_messages ? (flags | 2) : (flags &~ 2); + flags = edit_messages ? (flags | 4) : (flags &~ 4); + flags = delete_messages ? (flags | 8) : (flags &~ 8); + flags = ban_users ? (flags | 16) : (flags &~ 16); + flags = invite_users ? (flags | 32) : (flags &~ 32); + flags = invite_link ? (flags | 64) : (flags &~ 64); + flags = pin_messages ? (flags | 128) : (flags &~ 128); + flags = add_admins ? (flags | 512) : (flags &~ 512); stream.writeInt32(flags); - stream.writeInt32(id); - stream.writeString(title); - photo.serializeToStream(stream); - stream.writeInt32(participants_count); - stream.writeInt32(date); - stream.writeInt32(version); } } + public static class Chat extends TLObject { + public int id; + public String title; + public int date; + public int flags; + public boolean creator; + public boolean kicked; + public boolean admins_enabled; + public boolean admin; + public boolean deactivated; + public boolean left; + public ChatPhoto photo; + public int participants_count; + public int version; + public boolean broadcast; + public boolean megagroup; + public long access_hash; + public int until_date; + public boolean moderator; + public boolean verified; + public boolean restricted; + public boolean democracy; + public boolean signatures; + public String username; + public String restriction_reason; + public String address; + public String venue; + public GeoPoint geo; + public boolean checked_in; + public boolean min; + public boolean explicit_content; + public TL_channelAdminRights admin_rights; + public TL_channelBannedRights banned_rights; + public InputChannel migrated_to; + + public static Chat TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + Chat result = null; + switch(constructor) { + case 0xfb0ccc41: + result = new TL_chatForbidden_old(); + break; + case 0x7312bc48: + result = new TL_chat_old2(); + break; + case 0x289da732: + result = new TL_channelForbidden(); + break; + case 0x8537784f: + result = new TL_channelForbidden_layer67(); + break; + case 0x4b1b7506: + result = new TL_channel_layer48(); + break; + case 0x75eaea5a: + result = new TL_geoChat(); + break; + case 0x2d85832c: + result = new TL_channelForbidden_layer52(); + break; + case 0x7328bdb: + result = new TL_chatForbidden(); + break; + case 0xa14dca52: + result = new TL_channel_layer67(); + break; + case 0x678e9587: + result = new TL_channel_old(); + break; + case 0x6e9c9bc7: + result = new TL_chat_old(); + break; + case 0x9ba2d800: + result = new TL_chatEmpty(); + break; + case 0xcb44b1c: + result = new TL_channel(); + break; + case 0xd91cdd54: + result = new TL_chat(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in Chat", constructor)); + } + if (result != null) { + result.readParams(stream, exception); + } + return result; + } + } + + public static class TL_chatForbidden_old extends TL_chatForbidden { + public static int constructor = 0xfb0ccc41; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + title = stream.readString(exception); + date = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(title); + stream.writeInt32(date); + } + } + + public static class TL_chat_old2 extends TL_chat { + public static int constructor = 0x7312bc48; + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + creator = (flags & 1) != 0; + kicked = (flags & 2) != 0; + left = (flags & 4) != 0; + admins_enabled = (flags & 8) != 0; + admin = (flags & 16) != 0; + deactivated = (flags & 32) != 0; + id = stream.readInt32(exception); + title = stream.readString(exception); + photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + participants_count = stream.readInt32(exception); + date = stream.readInt32(exception); + version = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = creator ? (flags | 1) : (flags &~ 1); + flags = kicked ? (flags | 2) : (flags &~ 2); + flags = left ? (flags | 4) : (flags &~ 4); + flags = admins_enabled ? (flags | 8) : (flags &~ 8); + flags = admin ? (flags | 16) : (flags &~ 16); + flags = deactivated ? (flags | 32) : (flags &~ 32); + stream.writeInt32(flags); + stream.writeInt32(id); + stream.writeString(title); + photo.serializeToStream(stream); + stream.writeInt32(participants_count); + stream.writeInt32(date); + stream.writeInt32(version); + } + } + public static class TL_channelForbidden extends Chat { - public static int constructor = 0x8537784f; + public static int constructor = 0x289da732; public void readParams(AbstractSerializedData stream, boolean exception) { @@ -17243,6 +18617,9 @@ public class TLRPC { id = stream.readInt32(exception); access_hash = stream.readInt64(exception); title = stream.readString(exception); + if ((flags & 65536) != 0) { + until_date = stream.readInt32(exception); + } } public void serializeToStream(AbstractSerializedData stream) { @@ -17253,35 +18630,303 @@ public class TLRPC { stream.writeInt32(id); stream.writeInt64(access_hash); stream.writeString(title); + if ((flags & 65536) != 0) { + stream.writeInt32(until_date); + } } } - public static class TL_chatForbidden extends Chat { - public static int constructor = 0x7328bdb; + public static class TL_channelForbidden_layer67 extends TL_channelForbidden { + public static int constructor = 0x8537784f; - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - title = stream.readString(exception); - } + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + broadcast = (flags & 32) != 0; + megagroup = (flags & 256) != 0; + id = stream.readInt32(exception); + access_hash = stream.readInt64(exception); + title = stream.readString(exception); + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(title); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = broadcast ? (flags | 32) : (flags &~ 32); + flags = megagroup ? (flags | 256) : (flags &~ 256); + stream.writeInt32(flags); + stream.writeInt32(id); + stream.writeInt64(access_hash); + stream.writeString(title); + } + } + + public static class TL_channel_layer48 extends TL_channel { + public static int constructor = 0x4b1b7506; + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + creator = (flags & 1) != 0; + kicked = (flags & 2) != 0; + left = (flags & 4) != 0; + moderator = (flags & 16) != 0; + broadcast = (flags & 32) != 0; + verified = (flags & 128) != 0; + megagroup = (flags & 256) != 0; + restricted = (flags & 512) != 0; + democracy = (flags & 1024) != 0; + signatures = (flags & 2048) != 0; + id = stream.readInt32(exception); + access_hash = stream.readInt64(exception); + title = stream.readString(exception); + if ((flags & 64) != 0) { + username = stream.readString(exception); + } + photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + date = stream.readInt32(exception); + version = stream.readInt32(exception); + if ((flags & 512) != 0) { + restriction_reason = stream.readString(exception); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = creator ? (flags | 1) : (flags &~ 1); + flags = kicked ? (flags | 2) : (flags &~ 2); + flags = left ? (flags | 4) : (flags &~ 4); + flags = moderator ? (flags | 16) : (flags &~ 16); + flags = broadcast ? (flags | 32) : (flags &~ 32); + flags = verified ? (flags | 128) : (flags &~ 128); + flags = megagroup ? (flags | 256) : (flags &~ 256); + flags = restricted ? (flags | 512) : (flags &~ 512); + flags = democracy ? (flags | 1024) : (flags &~ 1024); + flags = signatures ? (flags | 2048) : (flags &~ 2048); + stream.writeInt32(flags); + stream.writeInt32(id); + stream.writeInt64(access_hash); + stream.writeString(title); + if ((flags & 64) != 0) { + stream.writeString(username); + } + photo.serializeToStream(stream); + stream.writeInt32(date); + stream.writeInt32(version); + if ((flags & 512) != 0) { + stream.writeString(restriction_reason); + } + } + } + + public static class TL_geoChat extends Chat { + public static int constructor = 0x75eaea5a; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + access_hash = stream.readInt64(exception); + title = stream.readString(exception); + address = stream.readString(exception); + venue = stream.readString(exception); + geo = GeoPoint.TLdeserialize(stream, stream.readInt32(exception), exception); + photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + participants_count = stream.readInt32(exception); + date = stream.readInt32(exception); + checked_in = stream.readBool(exception); + version = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeInt64(access_hash); + stream.writeString(title); + stream.writeString(address); + stream.writeString(venue); + geo.serializeToStream(stream); + photo.serializeToStream(stream); + stream.writeInt32(participants_count); + stream.writeInt32(date); + stream.writeBool(checked_in); + stream.writeInt32(version); + } + } + + public static class TL_channelForbidden_layer52 extends TL_channelForbidden { + public static int constructor = 0x2d85832c; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + access_hash = stream.readInt64(exception); + title = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeInt64(access_hash); + stream.writeString(title); + } + } + + public static class TL_chatForbidden extends Chat { + public static int constructor = 0x7328bdb; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + title = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(title); + } + } + + public static class TL_channel_layer67 extends TL_channel { + public static int constructor = 0xa14dca52; + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + creator = (flags & 1) != 0; + kicked = (flags & 2) != 0; + left = (flags & 4) != 0; + moderator = (flags & 16) != 0; + broadcast = (flags & 32) != 0; + verified = (flags & 128) != 0; + megagroup = (flags & 256) != 0; + restricted = (flags & 512) != 0; + democracy = (flags & 1024) != 0; + signatures = (flags & 2048) != 0; + min = (flags & 4096) != 0; + id = stream.readInt32(exception); + if ((flags & 8192) != 0) { + access_hash = stream.readInt64(exception); + } + title = stream.readString(exception); + if ((flags & 64) != 0) { + username = stream.readString(exception); + } + photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + date = stream.readInt32(exception); + version = stream.readInt32(exception); + if ((flags & 512) != 0) { + restriction_reason = stream.readString(exception); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = creator ? (flags | 1) : (flags &~ 1); + flags = kicked ? (flags | 2) : (flags &~ 2); + flags = left ? (flags | 4) : (flags &~ 4); + flags = moderator ? (flags | 16) : (flags &~ 16); + flags = broadcast ? (flags | 32) : (flags &~ 32); + flags = verified ? (flags | 128) : (flags &~ 128); + flags = megagroup ? (flags | 256) : (flags &~ 256); + flags = restricted ? (flags | 512) : (flags &~ 512); + flags = democracy ? (flags | 1024) : (flags &~ 1024); + flags = signatures ? (flags | 2048) : (flags &~ 2048); + flags = min ? (flags | 4096) : (flags &~ 4096); + stream.writeInt32(flags); + stream.writeInt32(id); + if ((flags & 8192) != 0) { + stream.writeInt64(access_hash); + } + stream.writeString(title); + if ((flags & 64) != 0) { + stream.writeString(username); + } + photo.serializeToStream(stream); + stream.writeInt32(date); + stream.writeInt32(version); + if ((flags & 512) != 0) { + stream.writeString(restriction_reason); + } + } + } + + public static class TL_channel_old extends TL_channel { + public static int constructor = 0x678e9587; + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + creator = (flags & 1) != 0; + kicked = (flags & 2) != 0; + left = (flags & 4) != 0; + moderator = (flags & 16) != 0; + broadcast = (flags & 32) != 0; + verified = (flags & 128) != 0; + megagroup = (flags & 256) != 0; + explicit_content = (flags & 512) != 0; + id = stream.readInt32(exception); + access_hash = stream.readInt64(exception); + title = stream.readString(exception); + if ((flags & 64) != 0) { + username = stream.readString(exception); + } + photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + date = stream.readInt32(exception); + version = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = creator ? (flags | 1) : (flags &~ 1); + flags = kicked ? (flags | 2) : (flags &~ 2); + flags = left ? (flags | 4) : (flags &~ 4); + flags = moderator ? (flags | 16) : (flags &~ 16); + flags = broadcast ? (flags | 32) : (flags &~ 32); + flags = verified ? (flags | 128) : (flags &~ 128); + flags = megagroup ? (flags | 256) : (flags &~ 256); + flags = explicit_content ? (flags | 512) : (flags &~ 512); + stream.writeInt32(flags); + stream.writeInt32(id); + stream.writeInt64(access_hash); + stream.writeString(title); + if ((flags & 64) != 0) { + stream.writeString(username); + } + photo.serializeToStream(stream); + stream.writeInt32(date); + stream.writeInt32(version); + } + } + + public static class TL_chat_old extends TL_chat { + public static int constructor = 0x6e9c9bc7; + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = stream.readInt32(exception); + title = stream.readString(exception); + photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + participants_count = stream.readInt32(exception); + date = stream.readInt32(exception); + left = stream.readBool(exception); + version = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(id); + stream.writeString(title); + photo.serializeToStream(stream); + stream.writeInt32(participants_count); + stream.writeInt32(date); + stream.writeBool(left); + stream.writeInt32(version); + } + } public static class TL_channel extends Chat { - public static int constructor = 0xa14dca52; + public static int constructor = 0xcb44b1c; public void readParams(AbstractSerializedData stream, boolean exception) { flags = stream.readInt32(exception); creator = (flags & 1) != 0; - kicked = (flags & 2) != 0; left = (flags & 4) != 0; - editor = (flags & 8) != 0; - moderator = (flags & 16) != 0; broadcast = (flags & 32) != 0; verified = (flags & 128) != 0; megagroup = (flags & 256) != 0; @@ -17303,6 +18948,12 @@ public class TLRPC { if ((flags & 512) != 0) { restriction_reason = stream.readString(exception); } + if ((flags & 16384) != 0) { + admin_rights = TL_channelAdminRights.TLdeserialize(stream, stream.readInt32(exception), exception); + } + if ((flags & 32768) != 0) { + banned_rights = TL_channelBannedRights.TLdeserialize(stream, stream.readInt32(exception), exception); + } } public void serializeToStream(AbstractSerializedData stream) { @@ -17310,8 +18961,6 @@ public class TLRPC { flags = creator ? (flags | 1) : (flags &~ 1); flags = kicked ? (flags | 2) : (flags &~ 2); flags = left ? (flags | 4) : (flags &~ 4); - flags = editor ? (flags | 8) : (flags &~ 8); - flags = moderator ? (flags | 16) : (flags &~ 16); flags = broadcast ? (flags | 32) : (flags &~ 32); flags = verified ? (flags | 128) : (flags &~ 128); flags = megagroup ? (flags | 256) : (flags &~ 256); @@ -17334,254 +18983,57 @@ public class TLRPC { if ((flags & 512) != 0) { stream.writeString(restriction_reason); } - } - } - - public static class TL_channel_old extends TL_channel { - public static int constructor = 0x678e9587; - - public void readParams(AbstractSerializedData stream, boolean exception) { - flags = stream.readInt32(exception); - creator = (flags & 1) != 0; - kicked = (flags & 2) != 0; - left = (flags & 4) != 0; - editor = (flags & 8) != 0; - moderator = (flags & 16) != 0; - broadcast = (flags & 32) != 0; - verified = (flags & 128) != 0; - megagroup = (flags & 256) != 0; - explicit_content = (flags & 512) != 0; - id = stream.readInt32(exception); - access_hash = stream.readInt64(exception); - title = stream.readString(exception); - if ((flags & 64) != 0) { - username = stream.readString(exception); + if ((flags & 16384) != 0) { + admin_rights.serializeToStream(stream); } - photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - date = stream.readInt32(exception); - version = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - flags = creator ? (flags | 1) : (flags &~ 1); - flags = kicked ? (flags | 2) : (flags &~ 2); - flags = left ? (flags | 4) : (flags &~ 4); - flags = editor ? (flags | 8) : (flags &~ 8); - flags = moderator ? (flags | 16) : (flags &~ 16); - flags = broadcast ? (flags | 32) : (flags &~ 32); - flags = verified ? (flags | 128) : (flags &~ 128); - flags = megagroup ? (flags | 256) : (flags &~ 256); - flags = explicit_content ? (flags | 512) : (flags &~ 512); - stream.writeInt32(flags); - stream.writeInt32(id); - stream.writeInt64(access_hash); - stream.writeString(title); - if ((flags & 64) != 0) { - stream.writeString(username); - } - photo.serializeToStream(stream); - stream.writeInt32(date); - stream.writeInt32(version); - } - } - - public static class TL_chatForbidden_old extends TL_chatForbidden { - public static int constructor = 0xfb0ccc41; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - title = stream.readString(exception); - date = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(title); - stream.writeInt32(date); - } - } - - public static class TL_chat extends Chat { - public static int constructor = 0xd91cdd54; - - public void readParams(AbstractSerializedData stream, boolean exception) { - flags = stream.readInt32(exception); - creator = (flags & 1) != 0; - kicked = (flags & 2) != 0; - left = (flags & 4) != 0; - admins_enabled = (flags & 8) != 0; - admin = (flags & 16) != 0; - deactivated = (flags & 32) != 0; - id = stream.readInt32(exception); - title = stream.readString(exception); - photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - participants_count = stream.readInt32(exception); - date = stream.readInt32(exception); - version = stream.readInt32(exception); - if ((flags & 64) != 0) { - migrated_to = InputChannel.TLdeserialize(stream, stream.readInt32(exception), exception); - } - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - flags = creator ? (flags | 1) : (flags &~ 1); - flags = kicked ? (flags | 2) : (flags &~ 2); - flags = left ? (flags | 4) : (flags &~ 4); - flags = admins_enabled ? (flags | 8) : (flags &~ 8); - flags = admin ? (flags | 16) : (flags &~ 16); - flags = deactivated ? (flags | 32) : (flags &~ 32); - stream.writeInt32(flags); - stream.writeInt32(id); - stream.writeString(title); - photo.serializeToStream(stream); - stream.writeInt32(participants_count); - stream.writeInt32(date); - stream.writeInt32(version); - if ((flags & 64) != 0) { - migrated_to.serializeToStream(stream); + if ((flags & 32768) != 0) { + banned_rights.serializeToStream(stream); } } } - public static class TL_geoChat extends Chat { - public static int constructor = 0x75eaea5a; + public static class TL_chat extends Chat { + public static int constructor = 0xd91cdd54; + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + creator = (flags & 1) != 0; + kicked = (flags & 2) != 0; + left = (flags & 4) != 0; + admins_enabled = (flags & 8) != 0; + admin = (flags & 16) != 0; + deactivated = (flags & 32) != 0; + id = stream.readInt32(exception); + title = stream.readString(exception); + photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + participants_count = stream.readInt32(exception); + date = stream.readInt32(exception); + version = stream.readInt32(exception); + if ((flags & 64) != 0) { + migrated_to = InputChannel.TLdeserialize(stream, stream.readInt32(exception), exception); + } + } - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - access_hash = stream.readInt64(exception); - title = stream.readString(exception); - address = stream.readString(exception); - venue = stream.readString(exception); - geo = GeoPoint.TLdeserialize(stream, stream.readInt32(exception), exception); - photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - participants_count = stream.readInt32(exception); - date = stream.readInt32(exception); - checked_in = stream.readBool(exception); - version = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeInt64(access_hash); - stream.writeString(title); - stream.writeString(address); - stream.writeString(venue); - geo.serializeToStream(stream); - photo.serializeToStream(stream); - stream.writeInt32(participants_count); - stream.writeInt32(date); - stream.writeBool(checked_in); - stream.writeInt32(version); - } - } - - public static class TL_channelForbidden_layer52 extends Chat { - public static int constructor = 0x2d85832c; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - access_hash = stream.readInt64(exception); - title = stream.readString(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeInt64(access_hash); - stream.writeString(title); - } - } - - public static class TL_chat_old extends TL_chat { - public static int constructor = 0x6e9c9bc7; - - public void readParams(AbstractSerializedData stream, boolean exception) { - id = stream.readInt32(exception); - title = stream.readString(exception); - photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - participants_count = stream.readInt32(exception); - date = stream.readInt32(exception); - left = stream.readBool(exception); - version = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(id); - stream.writeString(title); - photo.serializeToStream(stream); - stream.writeInt32(participants_count); - stream.writeInt32(date); - stream.writeBool(left); - stream.writeInt32(version); - } - } - - public static class TL_channel_layer48 extends TL_channel { - public static int constructor = 0x4b1b7506; - - public void readParams(AbstractSerializedData stream, boolean exception) { - flags = stream.readInt32(exception); - creator = (flags & 1) != 0; - kicked = (flags & 2) != 0; - left = (flags & 4) != 0; - editor = (flags & 8) != 0; - moderator = (flags & 16) != 0; - broadcast = (flags & 32) != 0; - verified = (flags & 128) != 0; - megagroup = (flags & 256) != 0; - restricted = (flags & 512) != 0; - democracy = (flags & 1024) != 0; - signatures = (flags & 2048) != 0; - id = stream.readInt32(exception); - access_hash = stream.readInt64(exception); - title = stream.readString(exception); - if ((flags & 64) != 0) { - username = stream.readString(exception); - } - photo = ChatPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - date = stream.readInt32(exception); - version = stream.readInt32(exception); - if ((flags & 512) != 0) { - restriction_reason = stream.readString(exception); - } - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - flags = creator ? (flags | 1) : (flags &~ 1); - flags = kicked ? (flags | 2) : (flags &~ 2); - flags = left ? (flags | 4) : (flags &~ 4); - flags = editor ? (flags | 8) : (flags &~ 8); - flags = moderator ? (flags | 16) : (flags &~ 16); - flags = broadcast ? (flags | 32) : (flags &~ 32); - flags = verified ? (flags | 128) : (flags &~ 128); - flags = megagroup ? (flags | 256) : (flags &~ 256); - flags = restricted ? (flags | 512) : (flags &~ 512); - flags = democracy ? (flags | 1024) : (flags &~ 1024); - flags = signatures ? (flags | 2048) : (flags &~ 2048); - stream.writeInt32(flags); - stream.writeInt32(id); - stream.writeInt64(access_hash); - stream.writeString(title); - if ((flags & 64) != 0) { - stream.writeString(username); - } - photo.serializeToStream(stream); - stream.writeInt32(date); - stream.writeInt32(version); - if ((flags & 512) != 0) { - stream.writeString(restriction_reason); - } - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = creator ? (flags | 1) : (flags &~ 1); + flags = kicked ? (flags | 2) : (flags &~ 2); + flags = left ? (flags | 4) : (flags &~ 4); + flags = admins_enabled ? (flags | 8) : (flags &~ 8); + flags = admin ? (flags | 16) : (flags &~ 16); + flags = deactivated ? (flags | 32) : (flags &~ 32); + stream.writeInt32(flags); + stream.writeInt32(id); + stream.writeString(title); + photo.serializeToStream(stream); + stream.writeInt32(participants_count); + stream.writeInt32(date); + stream.writeInt32(version); + if ((flags & 64) != 0) { + migrated_to.serializeToStream(stream); + } + } + } public static class StickerSet extends TLObject { public long id; @@ -17889,6 +19341,9 @@ public class TLRPC { case 0xd95e73bb: result = new TL_inputMessagesFilterPhotoVideoDocuments(); break; + case 0x7a7c17a4: + result = new TL_inputMessagesFilterRoundVoice(); + break; case 0x50f5c392: result = new TL_inputMessagesFilterVoice(); break; @@ -17901,6 +19356,9 @@ public class TLRPC { case 0x57e2f66c: result = new TL_inputMessagesFilterEmpty(); break; + case 0xb549da53: + result = new TL_inputMessagesFilterRoundVideo(); + break; } if (result == null && exception) { throw new RuntimeException(String.format("can't parse magic %x in MessagesFilter", constructor)); @@ -17984,6 +19442,15 @@ public class TLRPC { } } + public static class TL_inputMessagesFilterRoundVoice extends MessagesFilter { + public static int constructor = 0x7a7c17a4; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + public static class TL_inputMessagesFilterVoice extends MessagesFilter { public static int constructor = 0x50f5c392; @@ -18027,6 +19494,15 @@ public class TLRPC { } } + public static class TL_inputMessagesFilterRoundVideo extends MessagesFilter { + public static int constructor = 0xb549da53; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + public static class TL_geochats_located extends TLObject { public static int constructor = 0x48feb267; @@ -19804,6 +21280,8 @@ public class TLRPC { public boolean ipv6; public boolean media_only; public boolean tcpo_only; + public boolean cdn; + public boolean isStatic; public int id; public String ip_address; public int port; @@ -19826,6 +21304,8 @@ public class TLRPC { ipv6 = (flags & 1) != 0; media_only = (flags & 2) != 0; tcpo_only = (flags & 4) != 0; + cdn = (flags & 8) != 0; + isStatic = (flags & 16) != 0; id = stream.readInt32(exception); ip_address = stream.readString(exception); port = stream.readInt32(exception); @@ -19836,6 +21316,8 @@ public class TLRPC { flags = ipv6 ? (flags | 1) : (flags &~ 1); flags = media_only ? (flags | 2) : (flags &~ 2); flags = tcpo_only ? (flags | 4) : (flags &~ 4); + flags = cdn ? (flags | 8) : (flags &~ 8); + flags = isStatic ? (flags | 16) : (flags &~ 16); stream.writeInt32(flags); stream.writeInt32(id); stream.writeString(ip_address); @@ -20995,11 +22477,12 @@ public class TLRPC { } public static class TL_messages_search extends TLObject { - public static int constructor = 0xd4569248; + public static int constructor = 0xf288a275; public int flags; public InputPeer peer; public String q; + public InputUser from_id; public MessagesFilter filter; public int min_date; public int max_date; @@ -21016,6 +22499,9 @@ public class TLRPC { stream.writeInt32(flags); peer.serializeToStream(stream); stream.writeString(q); + if ((flags & 1) != 0) { + from_id.serializeToStream(stream); + } filter.serializeToStream(stream); stream.writeInt32(min_date); stream.writeInt32(max_date); @@ -22352,6 +23838,7 @@ public class TLRPC { public boolean correspondents; public boolean bots_pm; public boolean bots_inline; + public boolean phone_calls; public boolean groups; public boolean channels; public int offset; @@ -22367,6 +23854,7 @@ public class TLRPC { flags = correspondents ? (flags | 1) : (flags &~ 1); flags = bots_pm ? (flags | 2) : (flags &~ 2); flags = bots_inline ? (flags | 4) : (flags &~ 4); + flags = phone_calls ? (flags | 8) : (flags &~ 8); flags = groups ? (flags | 1024) : (flags &~ 1024); flags = channels ? (flags | 32768) : (flags &~ 32768); stream.writeInt32(flags); @@ -23854,11 +25342,11 @@ public class TLRPC { } public static class TL_channels_editAdmin extends TLObject { - public static int constructor = 0xeb7611d0; + public static int constructor = 0x20b88214; public InputChannel channel; public InputUser user_id; - public ChannelParticipantRole role; + public TL_channelAdminRights admin_rights; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return Updates.TLdeserialize(stream, constructor, exception); @@ -23868,7 +25356,7 @@ public class TLRPC { stream.writeInt32(constructor); channel.serializeToStream(stream); user_id.serializeToStream(stream); - role.serializeToStream(stream); + admin_rights.serializeToStream(stream); } } @@ -23992,25 +25480,6 @@ public class TLRPC { } } - public static class TL_channels_kickFromChannel extends TLObject { - public static int constructor = 0xa672de14; - - public InputChannel channel; - public InputUser user_id; - public boolean kicked; - - public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { - return Updates.TLdeserialize(stream, constructor, exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - channel.serializeToStream(stream); - user_id.serializeToStream(stream); - stream.writeBool(kicked); - } - } - public static class TL_channels_exportInvite extends TLObject { public static int constructor = 0xc7560885; @@ -24139,6 +25608,63 @@ public class TLRPC { } } + public static class TL_channels_editBanned extends TLObject { + public static int constructor = 0xbfd915cd; + + public InputChannel channel; + public InputUser user_id; + public TL_channelBannedRights banned_rights; + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return Updates.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + channel.serializeToStream(stream); + user_id.serializeToStream(stream); + banned_rights.serializeToStream(stream); + } + } + + public static class TL_channels_getAdminLog extends TLObject { + public static int constructor = 0x33ddf480; + + public int flags; + public InputChannel channel; + public String q; + public TL_channelAdminLogEventsFilter events_filter; + public ArrayList admins = new ArrayList<>(); + public long max_id; + public long min_id; + public int limit; + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return TL_channels_adminLogResults.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + channel.serializeToStream(stream); + stream.writeString(q); + if ((flags & 1) != 0) { + events_filter.serializeToStream(stream); + } + if ((flags & 2) != 0) { + stream.writeInt32(0x1cb5c415); + int count = admins.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + admins.get(a).serializeToStream(stream); + } + } + stream.writeInt64(max_id); + stream.writeInt64(min_id); + stream.writeInt32(limit); + } + } + public static class TL_phone_requestCall extends TLObject { public static int constructor = 0x5b95b3d4; @@ -24382,6 +25908,89 @@ public class TLRPC { } } + public static class TL_langpack_getLangPack extends TLObject { + public static int constructor = 0x9ab5c58e; + + public String lang_code; + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return TL_langPackDifference.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(lang_code); + } + } + + public static class TL_langpack_getStrings extends TLObject { + public static int constructor = 0x2e1ee318; + + public String lang_code; + public ArrayList keys = new ArrayList<>(); + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + Vector vector = new Vector(); + int size = stream.readInt32(exception); + for (int a = 0; a < size; a++) { + LangPackString object = LangPackString.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return vector; + } + vector.objects.add(object); + } + return vector; + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(lang_code); + stream.writeInt32(0x1cb5c415); + int count = keys.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + stream.writeString(keys.get(a)); + } + } + } + + public static class TL_langpack_getDifference extends TLObject { + public static int constructor = 0xb2e4d7d; + + public int from_version; + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return TL_langPackDifference.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(from_version); + } + } + + public static class TL_langpack_getLanguages extends TLObject { + public static int constructor = 0x800fd57d; + + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + Vector vector = new Vector(); + int size = stream.readInt32(exception); + for (int a = 0; a < size; a++) { + TL_langPackLanguage object = TL_langPackLanguage.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return vector; + } + vector.objects.add(object); + } + return vector; + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + //manually created //RichText start @@ -26140,6 +27749,42 @@ public class TLRPC { } } + public static class TL_upload_getCdnFile extends TLObject { + public static int constructor = 0x2000bcc3; + + public byte[] file_token; + public int offset; + public int limit; + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return upload_CdnFile.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeByteArray(file_token); + stream.writeInt32(offset); + stream.writeInt32(limit); + } + } + + public static class TL_upload_reuploadCdnFile extends TLObject { + public static int constructor = 0x2e7a2020; + + public byte[] file_token; + public byte[] request_token; + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return Bool.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeByteArray(file_token); + stream.writeByteArray(request_token); + } + } + public static class TL_upload_webFile extends TLObject { public static int constructor = 0x21e753bc; @@ -26191,31 +27836,107 @@ public class TLRPC { } } - public static class TL_upload_file extends TLObject { - public static int constructor = 0x96a18d5; + public static class upload_File extends TLObject { + public storage_FileType type; + public int mtime; + public NativeByteBuffer bytes; + public int dc_id; + public byte[] file_token; + public byte[] encryption_key; + public byte[] encryption_iv; - public storage_FileType type; - public int mtime; - public NativeByteBuffer bytes; + public static upload_File TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + upload_File result = null; + switch(constructor) { + case 0x96a18d5: + result = new TL_upload_file(); + break; + case 0x1508485a: + result = new TL_upload_fileCdnRedirect(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in upload_File", constructor)); + } + if (result != null) { + result.readParams(stream, exception); + } + return result; + } + } - public static TL_upload_file TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { - if (TL_upload_file.constructor != constructor) { - if (exception) { - throw new RuntimeException(String.format("can't parse magic %x in TL_upload_file", constructor)); - } else { - return null; - } - } - TL_upload_file result = new TL_upload_file(); - result.readParams(stream, exception); - return result; - } + public static class upload_CdnFile extends TLObject { + public NativeByteBuffer bytes; + public byte[] request_token; - public void readParams(AbstractSerializedData stream, boolean exception) { - type = storage_FileType.TLdeserialize(stream, stream.readInt32(exception), exception); - mtime = stream.readInt32(exception); - bytes = stream.readByteBuffer(exception); - } + public static upload_CdnFile TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + upload_CdnFile result = null; + switch(constructor) { + case 0xa99fca4f: + result = new TL_upload_cdnFile(); + break; + case 0xeea8e46e: + result = new TL_upload_cdnFileReuploadNeeded(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in upload_CdnFile", constructor)); + } + if (result != null) { + result.readParams(stream, exception); + } + return result; + } + } + + public static class TL_upload_cdnFile extends upload_CdnFile { + public static int constructor = 0xa99fca4f; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + bytes = stream.readByteBuffer(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeByteBuffer(bytes); + } + + @Override + public void freeResources() { + if (disableFree) { + return; + } + if (bytes != null) { + bytes.reuse(); + bytes = null; + } + } + } + + public static class TL_upload_cdnFileReuploadNeeded extends upload_CdnFile { + public static int constructor = 0xeea8e46e; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + request_token = stream.readByteArray(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeByteArray(request_token); + } + } + + public static class TL_upload_file extends upload_File { + public static int constructor = 0x96a18d5; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + type = storage_FileType.TLdeserialize(stream, stream.readInt32(exception), exception); + mtime = stream.readInt32(exception); + bytes = stream.readByteBuffer(exception); + } public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); @@ -26224,17 +27945,37 @@ public class TLRPC { stream.writeByteBuffer(bytes); } - @Override - public void freeResources() { - if (disableFree) { - return; - } - if (bytes != null) { - bytes.reuse(); - bytes = null; - } - } - } + @Override + public void freeResources() { + if (disableFree) { + return; + } + if (bytes != null) { + bytes.reuse(); + bytes = null; + } + } + } + + public static class TL_upload_fileCdnRedirect extends upload_File { + public static int constructor = 0x1508485a; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + dc_id = stream.readInt32(exception); + file_token = stream.readByteArray(exception); + encryption_key = stream.readByteArray(exception); + encryption_iv = stream.readByteArray(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(dc_id); + stream.writeByteArray(file_token); + stream.writeByteArray(encryption_key); + stream.writeByteArray(encryption_iv); + } + } public static class TL_phoneCallProtocol extends TLObject { public static int constructor = 0xa2bb35cb; diff --git a/TMessagesProj/src/main/java/org/telegram/tgnet/WriteToSocketDelegate.java b/TMessagesProj/src/main/java/org/telegram/tgnet/WriteToSocketDelegate.java new file mode 100644 index 000000000..8f6985086 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/tgnet/WriteToSocketDelegate.java @@ -0,0 +1,13 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.tgnet; + +public interface WriteToSocketDelegate { + void run(); +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java index ecd9c6310..883e88613 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java @@ -57,6 +57,8 @@ public class ActionBar extends FrameLayout { private boolean allowOverlayTitle; private CharSequence lastTitle; + private CharSequence lastSubtitle; + private Runnable titleActionRunnable; private boolean castShadows = true; protected boolean isSearchFieldVisible; @@ -70,6 +72,14 @@ public class ActionBar extends FrameLayout { public ActionBar(Context context) { super(context); + setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (titleActionRunnable != null) { + titleActionRunnable.run(); + } + } + }); } private void createBackButtonImage() { @@ -145,6 +155,7 @@ public class ActionBar extends FrameLayout { createSubtitleTextView(); } if (subtitleTextView != null) { + lastSubtitle = value; subtitleTextView.setVisibility(!TextUtils.isEmpty(value) && !isSearchFieldVisible ? VISIBLE : GONE); subtitleTextView.setText(value); } @@ -600,11 +611,11 @@ public class ActionBar extends FrameLayout { allowOverlayTitle = value; } - public void setTitleOverlayText(String text) { + public void setTitleOverlayText(String title, String subtitle, Runnable action) { if (!allowOverlayTitle || parentFragment.parentLayout == null) { return; } - CharSequence textToSet = text != null ? text : lastTitle; + CharSequence textToSet = title != null ? title : lastTitle; if (textToSet != null && titleTextView == null) { createTitleTextView(); } @@ -612,6 +623,15 @@ public class ActionBar extends FrameLayout { titleTextView.setVisibility(textToSet != null && !isSearchFieldVisible ? VISIBLE : INVISIBLE); titleTextView.setText(textToSet); } + textToSet = subtitle != null ? subtitle : lastSubtitle; + if (textToSet != null && subtitleTextView == null) { + createSubtitleTextView(); + } + if (subtitleTextView != null) { + subtitleTextView.setVisibility(!TextUtils.isEmpty(textToSet) && !isSearchFieldVisible ? VISIBLE : GONE); + subtitleTextView.setText(textToSet); + } + titleActionRunnable = action; } public boolean isSearchFieldVisible() { 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 a1014c752..0352d4b29 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java @@ -134,6 +134,9 @@ public class ActionBarLayout extends FrameLayout { private VelocityTracker velocityTracker; private boolean beginTrackingSent; private boolean transitionAnimationInProgress; + private boolean rebuildAfterAnimation; + private boolean rebuildLastAfterAnimation; + private boolean showLastAfterAnimation; private long transitionAnimationStartTime; private boolean inActionMode; private int startedTrackingPointerId; @@ -148,6 +151,8 @@ public class ActionBarLayout extends FrameLayout { private long lastFrameTime; private String titleOverlayText; + private String subtitleOverlayText; + private Runnable overlayAction; private ActionBarLayoutDelegate delegate = null; protected Activity parentActivity = null; @@ -333,6 +338,7 @@ public class ActionBarLayout extends FrameLayout { if (lastFragment.fragmentView != null) { ViewGroup parent = (ViewGroup) lastFragment.fragmentView.getParent(); if (parent != null) { + lastFragment.onRemoveFromParent(); parent.removeView(lastFragment.fragmentView); } } @@ -362,14 +368,10 @@ public class ActionBarLayout extends FrameLayout { View fragmentView = lastFragment.fragmentView; if (fragmentView == null) { fragmentView = lastFragment.createView(parentActivity); - } else { - ViewGroup parent = (ViewGroup) fragmentView.getParent(); - if (parent != null) { - parent.removeView(fragmentView); - } } ViewGroup parent = (ViewGroup) fragmentView.getParent(); if (parent != null) { + lastFragment.onRemoveFromParent(); parent.removeView(fragmentView); } if (lastFragment.actionBar != null && lastFragment.actionBar.getAddToContainer()) { @@ -381,7 +383,7 @@ public class ActionBarLayout extends FrameLayout { lastFragment.actionBar.setOccupyStatusBar(false); } containerViewBack.addView(lastFragment.actionBar); - lastFragment.actionBar.setTitleOverlayText(titleOverlayText); + lastFragment.actionBar.setTitleOverlayText(titleOverlayText, subtitleOverlayText, overlayAction); } containerViewBack.addView(fragmentView); ViewGroup.LayoutParams layoutParams = fragmentView.getLayoutParams(); @@ -568,6 +570,7 @@ public class ActionBarLayout extends FrameLayout { if (fragment.fragmentView != null) { ViewGroup parent = (ViewGroup) fragment.fragmentView.getParent(); if (parent != null) { + fragment.onRemoveFromParent(); parent.removeView(fragment.fragmentView); } } @@ -662,6 +665,7 @@ public class ActionBarLayout extends FrameLayout { } else { ViewGroup parent = (ViewGroup) fragmentView.getParent(); if (parent != null) { + fragment.onRemoveFromParent(); parent.removeView(fragmentView); } } @@ -674,7 +678,7 @@ public class ActionBarLayout extends FrameLayout { parent.removeView(fragment.actionBar); } containerViewBack.addView(fragment.actionBar); - fragment.actionBar.setTitleOverlayText(titleOverlayText); + fragment.actionBar.setTitleOverlayText(titleOverlayText, subtitleOverlayText, overlayAction); } containerViewBack.addView(fragmentView); @@ -832,6 +836,7 @@ public class ActionBarLayout extends FrameLayout { if (previousFragment.fragmentView != null) { ViewGroup parent = (ViewGroup) previousFragment.fragmentView.getParent(); if (parent != null) { + previousFragment.onRemoveFromParent(); parent.removeView(previousFragment.fragmentView); } } @@ -880,6 +885,7 @@ public class ActionBarLayout extends FrameLayout { } else { ViewGroup parent = (ViewGroup) fragmentView.getParent(); if (parent != null) { + previousFragment.onRemoveFromParent(); parent.removeView(fragmentView); } } @@ -892,7 +898,7 @@ public class ActionBarLayout extends FrameLayout { parent.removeView(previousFragment.actionBar); } containerView.addView(previousFragment.actionBar); - previousFragment.actionBar.setTitleOverlayText(titleOverlayText); + previousFragment.actionBar.setTitleOverlayText(titleOverlayText, subtitleOverlayText, overlayAction); } containerView.addView(fragmentView); ViewGroup.LayoutParams layoutParams = fragmentView.getLayoutParams(); @@ -1030,6 +1036,7 @@ public class ActionBarLayout extends FrameLayout { ViewGroup parent = (ViewGroup) previousFragment.fragmentView.getParent(); if (parent != null) { previousFragment.onPause(); + previousFragment.onRemoveFromParent(); parent.removeView(previousFragment.fragmentView); } } @@ -1042,6 +1049,7 @@ public class ActionBarLayout extends FrameLayout { } else { ViewGroup parent = (ViewGroup) fragmentView.getParent(); if (parent != null) { + previousFragment.onRemoveFromParent(); parent.removeView(fragmentView); } } @@ -1054,13 +1062,9 @@ public class ActionBarLayout extends FrameLayout { parent.removeView(previousFragment.actionBar); } containerView.addView(previousFragment.actionBar); - previousFragment.actionBar.setTitleOverlayText(titleOverlayText); + previousFragment.actionBar.setTitleOverlayText(titleOverlayText, subtitleOverlayText, overlayAction); } - containerView.addView(fragmentView); - ViewGroup.LayoutParams layoutParams = fragmentView.getLayoutParams(); - layoutParams.width = LayoutHelper.MATCH_PARENT; - layoutParams.height = LayoutHelper.MATCH_PARENT; - fragmentView.setLayoutParams(layoutParams); + containerView.addView(fragmentView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); previousFragment.onResume(); currentActionBar = previousFragment.actionBar; if (!previousFragment.hasOwnBackground && fragmentView.getBackground() == null) { @@ -1090,7 +1094,13 @@ public class ActionBarLayout extends FrameLayout { } } - public void rebuildAllFragmentViews(boolean last) { + public void rebuildAllFragmentViews(boolean last, boolean showLastAfter) { + if (transitionAnimationInProgress) { + rebuildAfterAnimation = true; + rebuildLastAfterAnimation = last; + showLastAfterAnimation = showLastAfter; + return; + } for (int a = 0; a < fragmentsStack.size() - (last ? 0 : 1); a++) { fragmentsStack.get(a).clearViews(); fragmentsStack.get(a).setParentLayout(this); @@ -1098,6 +1108,9 @@ public class ActionBarLayout extends FrameLayout { if (delegate != null) { delegate.onRebuildAllFragments(this); } + if (showLastAfter) { + showLastFragment(); + } } public boolean onKeyUp(int keyCode, KeyEvent event) { @@ -1136,6 +1149,14 @@ public class ActionBarLayout extends FrameLayout { onCloseAnimationEndRunnable.run(); onCloseAnimationEndRunnable = null; } + checkNeedRebuild(); + } + } + + private void checkNeedRebuild() { + if (rebuildAfterAnimation) { + rebuildAllFragmentViews(rebuildLastAfterAnimation, showLastAfterAnimation); + rebuildAfterAnimation = false; } } @@ -1154,6 +1175,7 @@ public class ActionBarLayout extends FrameLayout { onOpenAnimationEndRunnable.run(); onOpenAnimationEndRunnable = null; } + checkNeedRebuild(); } } @@ -1202,11 +1224,14 @@ public class ActionBarLayout extends FrameLayout { removeActionBarExtraHeight = value; } - public void setTitleOverlayText(String text) { - titleOverlayText = text; - for (BaseFragment fragment : fragmentsStack) { + public void setTitleOverlayText(String title, String subtitle, Runnable action) { + titleOverlayText = title; + subtitleOverlayText = subtitle; + overlayAction = action; + for (int a = 0; a < fragmentsStack.size(); a++) { + BaseFragment fragment = fragmentsStack.get(a); if (fragment.actionBar != null) { - fragment.actionBar.setTitleOverlayText(titleOverlayText); + fragment.actionBar.setTitleOverlayText(titleOverlayText, subtitleOverlayText, action); } } } 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 ff12b6f06..4bc3edf83 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java @@ -73,20 +73,20 @@ public class ActionBarMenuItem extends FrameLayout { private ImageView clearButton; protected ImageView iconView; private FrameLayout searchContainer; - private boolean isSearchField = false; + private boolean isSearchField; private ActionBarMenuItemSearchListener listener; private Rect rect; private int[] location; private View selectedMenuView; private Runnable showMenuRunnable; - private boolean showFromBottom; private int menuHeight = AndroidUtilities.dp(16); - private int subMenuOpenSide = 0; + private int subMenuOpenSide; private ActionBarMenuItemDelegate delegate; private boolean allowCloseAnimation = true; protected boolean overrideMenuClick; private boolean processedPopupClick; private boolean layoutInScreen; + private boolean animationEnabled = true; private static Method layoutInScreenMethod; public ActionBarMenuItem(Context context, ActionBarMenu menu, int backgroundColor, int iconColor) { @@ -185,13 +185,6 @@ public class ActionBarMenuItem extends FrameLayout { this.delegate = delegate; } - public void setShowFromBottom(boolean value) { - showFromBottom = value; - if (popupLayout != null) { - popupLayout.setShowedFromBotton(showFromBottom); - } - } - public void setIconColor(int color) { iconView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); if (clearButton != null) { @@ -207,34 +200,44 @@ public class ActionBarMenuItem extends FrameLayout { layoutInScreen = value; } - public TextView addSubItem(int id, String text) { - if (popupLayout == null) { - rect = new Rect(); - location = new int[2]; - popupLayout = new ActionBarPopupWindow.ActionBarPopupWindowLayout(getContext()); - popupLayout.setOnTouchListener(new OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - if (popupWindow != null && popupWindow.isShowing()) { - v.getHitRect(rect); - if (!rect.contains((int) event.getX(), (int) event.getY())) { - popupWindow.dismiss(); - } + private void createPopupLayout() { + if (popupLayout != null) { + return; + } + rect = new Rect(); + location = new int[2]; + popupLayout = new ActionBarPopupWindow.ActionBarPopupWindowLayout(getContext()); + popupLayout.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + if (popupWindow != null && popupWindow.isShowing()) { + v.getHitRect(rect); + if (!rect.contains((int) event.getX(), (int) event.getY())) { + popupWindow.dismiss(); } } - return false; } - }); - popupLayout.setDispatchKeyEventListener(new ActionBarPopupWindow.OnDispatchKeyEventListener() { - @Override - public void onDispatchKeyEvent(KeyEvent keyEvent) { - if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BACK && keyEvent.getRepeatCount() == 0 && popupWindow != null && popupWindow.isShowing()) { - popupWindow.dismiss(); - } + return false; + } + }); + popupLayout.setDispatchKeyEventListener(new ActionBarPopupWindow.OnDispatchKeyEventListener() { + @Override + public void onDispatchKeyEvent(KeyEvent keyEvent) { + if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BACK && keyEvent.getRepeatCount() == 0 && popupWindow != null && popupWindow.isShowing()) { + popupWindow.dismiss(); } - }); - } + } + }); + } + + public void addSubItem(View view, int width, int height) { + createPopupLayout(); + popupLayout.addView(view, new LinearLayout.LayoutParams(width, height)); + } + + public TextView addSubItem(int id, String text) { + createPopupLayout(); TextView textView = new TextView(getContext()); textView.setTextColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem)); textView.setBackgroundDrawable(Theme.getSelectorDrawable(false)); @@ -248,7 +251,6 @@ public class ActionBarMenuItem extends FrameLayout { textView.setMinWidth(AndroidUtilities.dp(196)); textView.setTag(id); textView.setText(text); - popupLayout.setShowedFromBotton(showFromBottom); popupLayout.addView(textView); LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) textView.getLayoutParams(); if (LocaleController.isRTL) { @@ -279,7 +281,7 @@ public class ActionBarMenuItem extends FrameLayout { return textView; } - protected void redrawPopup(int color) { + public void redrawPopup(int color) { if (popupLayout != null) { popupLayout.backgroundDrawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); popupLayout.invalidate(); @@ -317,11 +319,14 @@ public class ActionBarMenuItem extends FrameLayout { } if (popupWindow == null) { popupWindow = new ActionBarPopupWindow(popupLayout, LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT); - if (Build.VERSION.SDK_INT >= 19) { + if (animationEnabled && Build.VERSION.SDK_INT >= 19) { popupWindow.setAnimationStyle(0); } else { popupWindow.setAnimationStyle(R.style.PopupAnimation); } + if (!animationEnabled) { + popupWindow.setAnimationEnabled(animationEnabled); + } popupWindow.setOutsideTouchable(true); popupWindow.setClippingEnabled(true); if (layoutInScreen) { @@ -532,6 +537,13 @@ public class ActionBarMenuItem extends FrameLayout { return this; } + public void setPopupAnimationEnabled(boolean value) { + if (popupWindow != null) { + popupWindow.setAnimationEnabled(value); + } + animationEnabled = value; + } + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); @@ -542,58 +554,53 @@ public class ActionBarMenuItem extends FrameLayout { private void updateOrShowPopup(boolean show, boolean update) { int offsetY; - if (showFromBottom) { - getLocationOnScreen(location); - int diff = location[1] - AndroidUtilities.statusBarHeight + getMeasuredHeight() - menuHeight; - offsetY = -menuHeight; - if (diff < 0) { - offsetY -= diff; - } + + if (parentMenu != null) { + offsetY = -parentMenu.parentActionBar.getMeasuredHeight() + parentMenu.getTop(); } else { - if (parentMenu != null && subMenuOpenSide == 0) { - offsetY = -parentMenu.parentActionBar.getMeasuredHeight() + parentMenu.getTop(); - } else { - offsetY = -getMeasuredHeight(); - } + float scaleY = getScaleY(); + offsetY = -(int) (getMeasuredHeight() * scaleY - getTranslationY() / scaleY); } if (show) { popupLayout.scrollToTop(); } - if (subMenuOpenSide == 0) { - if (showFromBottom) { + if (parentMenu != null) { + View parent = parentMenu.parentActionBar; + if (subMenuOpenSide == 0) { if (show) { - popupWindow.showAsDropDown(this, -popupLayout.getMeasuredWidth() + getMeasuredWidth(), offsetY); + popupWindow.showAsDropDown(parent, getLeft() + parentMenu.getLeft() + getMeasuredWidth() - popupLayout.getMeasuredWidth(), offsetY); } if (update) { - popupWindow.update(this, -popupLayout.getMeasuredWidth() + getMeasuredWidth(), offsetY, -1, -1); + popupWindow.update(parent, getLeft() + parentMenu.getLeft() + getMeasuredWidth() - popupLayout.getMeasuredWidth(), offsetY, -1, -1); } } else { - if (parentMenu != null) { - View parent = parentMenu.parentActionBar; - if (show) { - popupWindow.showAsDropDown(parent, getLeft() + parentMenu.getLeft() + getMeasuredWidth() - popupLayout.getMeasuredWidth(), offsetY); - } - if (update) { - popupWindow.update(parent, getLeft() + parentMenu.getLeft() + getMeasuredWidth() - popupLayout.getMeasuredWidth(), offsetY, -1, -1); - } - } else if (getParent() != null) { - View parent = (View) getParent(); - if (show) { - popupWindow.showAsDropDown(parent, parent.getMeasuredWidth() - popupLayout.getMeasuredWidth() - getLeft() - parent.getLeft(), offsetY); - } - if (update) { - popupWindow.update(parent, parent.getMeasuredWidth() - popupLayout.getMeasuredWidth() - getLeft() - parent.getLeft(), offsetY, -1, -1); - } + if (show) { + popupWindow.showAsDropDown(parent, getLeft() - AndroidUtilities.dp(8), offsetY); + } + if (update) { + popupWindow.update(parent, getLeft() - AndroidUtilities.dp(8), offsetY, -1, -1); } } } else { - if (show) { - popupWindow.showAsDropDown(this, -AndroidUtilities.dp(8), offsetY); - } - if (update) { - popupWindow.update(this, -AndroidUtilities.dp(8), offsetY, -1, -1); + if (subMenuOpenSide == 0) { + if (getParent() != null) { + View parent = (View) getParent(); + if (show) { + popupWindow.showAsDropDown(parent, getLeft() + getMeasuredWidth() - popupLayout.getMeasuredWidth(), offsetY); + } + if (update) { + popupWindow.update(parent, getLeft() + getMeasuredWidth() - popupLayout.getMeasuredWidth(), offsetY, -1, -1); + } + } + } else { + if (show) { + popupWindow.showAsDropDown(this, -AndroidUtilities.dp(8), offsetY); + } + if (update) { + popupWindow.update(this, -AndroidUtilities.dp(8), offsetY, -1, -1); + } } } } 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 0d6eb7e59..e28a4f47f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarPopupWindow.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarPopupWindow.java @@ -72,7 +72,6 @@ public class ActionBarPopupWindow extends PopupWindow { public static class ActionBarPopupWindowLayout extends FrameLayout { private OnDispatchKeyEventListener mOnDispatchKeyEventListener; - protected Drawable backgroundDrawable; private float backScaleX = 1; private float backScaleY = 1; private int backAlpha = 255; @@ -84,6 +83,8 @@ public class ActionBarPopupWindow extends PopupWindow { private ScrollView scrollView; protected LinearLayout linearLayout; + protected Drawable backgroundDrawable; + public ActionBarPopupWindowLayout(Context context) { super(context); @@ -172,6 +173,10 @@ public class ActionBarPopupWindow extends PopupWindow { invalidate(); } + public void setBackgroundDrawable(Drawable drawable) { + backgroundDrawable = drawable; + } + private void startChildAnimation(View child) { if (animationEnabled) { AnimatorSet animatorSet = new AnimatorSet(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/AlertDialog.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/AlertDialog.java index 2272a569e..3dd454f33 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/AlertDialog.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/AlertDialog.java @@ -48,6 +48,7 @@ public class AlertDialog extends Dialog implements Drawable.Callback { private View customView; private TextView titleTextView; + private TextView subtitleTextView; private TextView messageTextView; private FrameLayout progressViewContainer; private TextView progressViewTextView; @@ -66,10 +67,14 @@ public class AlertDialog extends Dialog implements Drawable.Callback { private CharSequence[] items; private int[] itemIcons; private CharSequence title; + private CharSequence subtitle; private CharSequence message; + private int topResId; + private int topBackgroundColor; private int progressViewStyle; private int currentProgress; + private ImageView topImageView; private CharSequence positiveButtonText; private OnClickListener positiveButtonListener; private CharSequence negativeButtonText; @@ -83,7 +88,7 @@ public class AlertDialog extends Dialog implements Drawable.Callback { private Drawable shadowDrawable; private Rect backgroundPaddings; - private class AlertDialogCell extends FrameLayout { + public static class AlertDialogCell extends FrameLayout { private TextView textView; private ImageView imageView; @@ -139,7 +144,7 @@ public class AlertDialog extends Dialog implements Drawable.Callback { super(context, R.style.TransparentDialog); backgroundPaddings = new Rect(); - shadowDrawable = context.getResources().getDrawable(R.drawable.popup_fixed).mutate(); + shadowDrawable = context.getResources().getDrawable(R.drawable.popup_fixed_alert).mutate(); shadowDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogBackground), PorterDuff.Mode.MULTIPLY)); shadowDrawable.getPadding(backgroundPaddings); @@ -182,6 +187,15 @@ public class AlertDialog extends Dialog implements Drawable.Callback { layoutParams = (LayoutParams) titleTextView.getLayoutParams(); availableHeight -= titleTextView.getMeasuredHeight() + layoutParams.bottomMargin + layoutParams.topMargin; } + if (subtitleTextView != null) { + subtitleTextView.measure(childWidthMeasureSpec, heightMeasureSpec); + layoutParams = (LayoutParams) subtitleTextView.getLayoutParams(); + availableHeight -= subtitleTextView.getMeasuredHeight() + layoutParams.bottomMargin + layoutParams.topMargin; + } + if (topImageView != null) { + topImageView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(132), MeasureSpec.EXACTLY)); + availableHeight -= topImageView.getMeasuredHeight() - AndroidUtilities.dp(8); + } if (progressViewStyle == 0) { layoutParams = (LayoutParams) contentScrollView.getLayoutParams(); @@ -291,6 +305,16 @@ public class AlertDialog extends Dialog implements Drawable.Callback { final boolean hasButtons = positiveButtonText != null || negativeButtonText != null || neutralButtonText != null; + if (topResId != 0) { + topImageView = new ImageView(getContext()); + topImageView.setImageResource(topResId); + topImageView.setScaleType(ImageView.ScaleType.CENTER); + topImageView.setBackgroundDrawable(getContext().getResources().getDrawable(R.drawable.popup_fixed_top)); + topImageView.getBackground().setColorFilter(new PorterDuffColorFilter(topBackgroundColor, PorterDuff.Mode.MULTIPLY)); + topImageView.setPadding(0, 0, 0, 0); + containerView.addView(topImageView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 132, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, -8, -8, 0, 0)); + } + if (title != null) { titleTextView = new TextView(getContext()); titleTextView.setText(title); @@ -298,7 +322,16 @@ public class AlertDialog extends Dialog implements Drawable.Callback { titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20); titleTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); titleTextView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP); - containerView.addView(titleTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, 24, 19, 24, items != null ? 14 : 10)); + containerView.addView(titleTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, 24, 19, 24, (subtitle != null ? 2 : (items != null ? 14 : 10)))); + } + + if (subtitle != null) { + subtitleTextView = new TextView(getContext()); + subtitleTextView.setText(subtitle); + subtitleTextView.setTextColor(Theme.getColor(Theme.key_dialogIcon)); + subtitleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + subtitleTextView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP); + containerView.addView(subtitleTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, 24, 0, 24, items != null ? 14 : 10)); } if (progressViewStyle == 0) { @@ -526,7 +559,7 @@ public class AlertDialog extends Dialog implements Drawable.Callback { } lastScreenWidth = AndroidUtilities.displaySize.x; - final int calculatedWidth = AndroidUtilities.displaySize.x - AndroidUtilities.dp(56); + final int calculatedWidth = AndroidUtilities.displaySize.x - AndroidUtilities.dp(48); int maxWidth; if (AndroidUtilities.isTablet()) { if (AndroidUtilities.isSmallTablet()) { @@ -722,6 +755,17 @@ public class AlertDialog extends Dialog implements Drawable.Callback { return this; } + public Builder setSubtitle(CharSequence subtitle) { + alertDialog.subtitle = subtitle; + return this; + } + + public Builder setTopImage(int resId, int backgroundColor) { + alertDialog.topResId = resId; + alertDialog.topBackgroundColor = backgroundColor; + return this; + } + public Builder setMessage(CharSequence message) { alertDialog.message = message; return this; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java index 5a34df175..9456393b9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java @@ -65,6 +65,7 @@ public class BaseFragment { ViewGroup parent = (ViewGroup) fragmentView.getParent(); if (parent != null) { try { + onRemoveFromParent(); parent.removeView(fragmentView); } catch (Exception e) { FileLog.e(e); @@ -86,6 +87,10 @@ public class BaseFragment { parentLayout = null; } + protected void onRemoveFromParent() { + + } + protected void setParentLayout(ActionBarLayout layout) { if (parentLayout != layout) { parentLayout = layout; @@ -93,6 +98,7 @@ public class BaseFragment { ViewGroup parent = (ViewGroup) fragmentView.getParent(); if (parent != null) { try { + onRemoveFromParent(); parent.removeView(fragmentView); } catch (Exception e) { FileLog.e(e); @@ -187,6 +193,13 @@ public class BaseFragment { } } + public BaseFragment getFragmentForAlert(int offset) { + if (parentLayout == null || parentLayout.fragmentsStack.size() <= 1 + offset) { + return this; + } + return parentLayout.fragmentsStack.get(parentLayout.fragmentsStack.size() - 2 - offset); + } + public void onConfigurationChanged(android.content.res.Configuration newConfig) { } 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 f2f0ecd68..3eab15d9f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java @@ -79,6 +79,17 @@ public class Theme { return null; } + public String getName() { + if ("Default".equals(name)) { + return LocaleController.getString("Default", R.string.Default); + } else if ("Blue".equals(name)) { + return LocaleController.getString("ThemeBlue", R.string.ThemeBlue); + } else if ("Dark".equals(name)) { + return LocaleController.getString("ThemeDark", R.string.ThemeDark); + } + return name; + } + public static ThemeInfo createWithJson(JSONObject object) { if (object == null) { return null; @@ -196,7 +207,9 @@ public class Theme { public static Paint chat_statusPaint; public static Paint chat_statusRecordPaint; public static Paint chat_actionBackgroundPaint; + public static Paint chat_timeBackgroundPaint; public static Paint chat_composeBackgroundPaint; + public static Paint chat_radialProgressPaint; public static TextPaint chat_msgTextPaint; public static TextPaint chat_actionTextPaint; public static TextPaint chat_msgBotButtonPaint; @@ -227,6 +240,7 @@ public class Theme { public static TextPaint chat_contextResult_descriptionTextPaint; public static Drawable chat_composeShadowDrawable; + public static Drawable chat_roundVideoShadow; public static Drawable chat_msgInDrawable; public static Drawable chat_msgInSelectedDrawable; public static Drawable chat_msgInShadowDrawable; @@ -265,9 +279,7 @@ public class Theme { public static Drawable chat_msgOutMenuSelectedDrawable; public static Drawable chat_msgMediaMenuDrawable; public static Drawable chat_msgInInstantDrawable; - public static Drawable chat_msgInInstantSelectedDrawable; public static Drawable chat_msgOutInstantDrawable; - public static Drawable chat_msgOutInstantSelectedDrawable; public static Drawable chat_msgErrorDrawable; public static Drawable chat_muteIconDrawable; public static Drawable chat_lockIconDrawable; @@ -285,8 +297,6 @@ public class Theme { public static Drawable chat_botLinkDrawalbe; public static Drawable chat_botInlineDrawable; public static Drawable chat_systemDrawable; - public static Drawable chat_timeBackgroundDrawable; - public static Drawable chat_timeStickerBackgroundDrawable; public static Drawable chat_msgInCallDrawable; public static Drawable chat_msgInCallSelectedDrawable; public static Drawable chat_msgOutCallDrawable; @@ -301,9 +311,11 @@ public class Theme { public static Drawable[] chat_cornerOuter = new Drawable[4]; public static Drawable[] chat_cornerInner = new Drawable[4]; public static Drawable[][] chat_fileStatesDrawable = new Drawable[10][2]; + public static Drawable[][] chat_ivStatesDrawable = new Drawable[4][2]; public static Drawable[][] chat_photoStatesDrawables = new Drawable[13][2]; public static final String key_dialogBackground = "dialogBackground"; + public static final String key_dialogBackgroundGray = "dialogBackgroundGray"; public static final String key_dialogTextBlack = "dialogTextBlack"; public static final String key_dialogTextLink = "dialogTextLink"; public static final String key_dialogLinkSelection = "dialogLinkSelection"; @@ -546,6 +558,7 @@ public class Theme { public static final String key_chat_inSentClockSelected = "chat_inSentClockSelected"; public static final String key_chat_mediaSentCheck = "chat_mediaSentCheck"; public static final String key_chat_mediaSentClock = "chat_mediaSentClock"; + public static final String key_chat_mediaTimeBackground = "chat_mediaTimeBackground"; public static final String key_chat_outViews = "chat_outViews"; public static final String key_chat_outViewsSelected = "chat_outViewsSelected"; public static final String key_chat_inViews = "chat_inViews"; @@ -659,6 +672,9 @@ public class Theme { public static final String key_chat_messagePanelHint = "chat_messagePanelHint"; public static final String key_chat_messagePanelIcons = "chat_messagePanelIcons"; public static final String key_chat_messagePanelSend = "chat_messagePanelSend"; + public static final String key_chat_messagePanelVoiceLock = "key_chat_messagePanelVoiceLock"; + public static final String key_chat_messagePanelVoiceLockBackground = "key_chat_messagePanelVoiceLockBackground"; + public static final String key_chat_messagePanelVoiceLockShadow = "key_chat_messagePanelVoiceLockShadow"; public static final String key_chat_topPanelBackground = "chat_topPanelBackground"; public static final String key_chat_topPanelClose = "chat_topPanelClose"; public static final String key_chat_topPanelLine = "chat_topPanelLine"; @@ -841,6 +857,7 @@ public class Theme { static { defaultColors.put(key_dialogBackground, 0xffffffff); + defaultColors.put(key_dialogBackgroundGray, 0xfff0f0f0); defaultColors.put(key_dialogTextBlack, 0xff212121); defaultColors.put(key_dialogTextLink, 0xff2678b6); defaultColors.put(key_dialogLinkSelection, 0x3362a9e3); @@ -959,14 +976,14 @@ public class Theme { defaultColors.put(key_avatar_backgroundActionBarGreen, 0xff56a14c); defaultColors.put(key_avatar_backgroundActionBarCyan, 0xff4492ac); defaultColors.put(key_avatar_backgroundActionBarBlue, 0xff598fba); - defaultColors.put(key_avatar_backgroundActionBarPink, 0xff4c84b6); + defaultColors.put(key_avatar_backgroundActionBarPink, 0xff598fba); defaultColors.put(key_avatar_subtitleInProfileRed, 0xfff9cbc5); defaultColors.put(key_avatar_subtitleInProfileOrange, 0xfffdddc8); defaultColors.put(key_avatar_subtitleInProfileViolet, 0xffcdc4ed); defaultColors.put(key_avatar_subtitleInProfileGreen, 0xffc0edba); defaultColors.put(key_avatar_subtitleInProfileCyan, 0xffb8e2f0); defaultColors.put(key_avatar_subtitleInProfileBlue, 0xffd7eafa); - defaultColors.put(key_avatar_subtitleInProfilePink, 0xffb3d7f7); + defaultColors.put(key_avatar_subtitleInProfilePink, 0xffd7eafa); defaultColors.put(key_avatar_nameInMessageRed, 0xffca5650); defaultColors.put(key_avatar_nameInMessageOrange, 0xffd87b29); defaultColors.put(key_avatar_nameInMessageViolet, 0xff4e92cc); @@ -980,7 +997,7 @@ public class Theme { defaultColors.put(key_avatar_actionBarSelectorGreen, 0xff48953d); defaultColors.put(key_avatar_actionBarSelectorCyan, 0xff39849d); defaultColors.put(key_avatar_actionBarSelectorBlue, 0xff4981ad); - defaultColors.put(key_avatar_actionBarSelectorPink, 0xffd44e7b); + defaultColors.put(key_avatar_actionBarSelectorPink, 0xff4981ad); defaultColors.put(key_avatar_actionBarIconRed, 0xffffffff); defaultColors.put(key_avatar_actionBarIconOrange, 0xffffffff); defaultColors.put(key_avatar_actionBarIconViolet, 0xffffffff); @@ -1054,6 +1071,7 @@ public class Theme { defaultColors.put(key_chat_serviceText, 0xffffffff); defaultColors.put(key_chat_serviceLink, 0xffffffff); defaultColors.put(key_chat_serviceIcon, 0xffffffff); + defaultColors.put(key_chat_mediaTimeBackground, 0x66000000); defaultColors.put(key_chat_outSentCheck, 0xff5db050); defaultColors.put(key_chat_outSentCheckSelected, 0xff5db050); defaultColors.put(key_chat_outSentClock, 0xff75bd5e); @@ -1094,8 +1112,8 @@ public class Theme { defaultColors.put(key_chat_inViaBotNameText, 0xff3a8ccf); defaultColors.put(key_chat_outViaBotNameText, 0xff55ab4f); defaultColors.put(key_chat_stickerViaBotNameText, 0xffffffff); - defaultColors.put(key_chat_inReplyLine, 0xff70b4e8); - defaultColors.put(key_chat_outReplyLine, 0xff88c97b); + defaultColors.put(key_chat_inReplyLine, 0xff599fd8); + defaultColors.put(key_chat_outReplyLine, 0xff6eb969); defaultColors.put(key_chat_stickerReplyLine, 0xffffffff); defaultColors.put(key_chat_inReplyNameText, 0xff3a8ccf); defaultColors.put(key_chat_outReplyNameText, 0xff55ab4f); @@ -1224,6 +1242,9 @@ public class Theme { defaultColors.put(key_chat_recordedVoiceProgressInner, 0xffffffff); defaultColors.put(key_chat_recordVoiceCancel, 0xff999999); defaultColors.put(key_chat_messagePanelSend, 0xff62b0eb); + defaultColors.put(key_chat_messagePanelVoiceLock, 0xffa4a4a4); + defaultColors.put(key_chat_messagePanelVoiceLockBackground, 0xffffffff); + defaultColors.put(key_chat_messagePanelVoiceLockShadow, 0xff000000); defaultColors.put(key_chat_recordTime, 0xff4d4c4b); defaultColors.put(key_chat_emojiPanelNewTrending, 0xff4da6ea); defaultColors.put(key_chat_gifSaveHintText, 0xffffffff); @@ -1366,7 +1387,7 @@ public class Theme { currentColors = new HashMap<>(); ThemeInfo themeInfo = new ThemeInfo(); - themeInfo.name = LocaleController.getString("Default", R.string.Default); + themeInfo.name = "Default"; themes.add(currentTheme = defaultTheme = themeInfo); themesDict.put("Default", defaultTheme); @@ -1569,10 +1590,21 @@ public class Theme { } public static Drawable createCircleDrawableWithIcon(int size, int iconRes) { + return createCircleDrawableWithIcon(size, iconRes, 0); + } + + public static Drawable createCircleDrawableWithIcon(int size, int iconRes, int stroke) { OvalShape ovalShape = new OvalShape(); ovalShape.resize(size, size); ShapeDrawable defaultDrawable = new ShapeDrawable(ovalShape); - defaultDrawable.getPaint().setColor(0xffffffff); + Paint paint = defaultDrawable.getPaint(); + paint.setColor(0xffffffff); + if (stroke == 1) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(AndroidUtilities.dp(2)); + } else if (stroke == 2) { + paint.setAlpha(0); + } Drawable drawable = ApplicationLoader.applicationContext.getResources().getDrawable(iconRes).mutate(); CombinedDrawable combinedDrawable = new CombinedDrawable(defaultDrawable, drawable); combinedDrawable.setCustomSize(size, size); @@ -1860,7 +1892,7 @@ public class Theme { } public static String getCurrentThemeName() { - String text = currentTheme.name; + String text = currentTheme.getName(); if (text.endsWith(".attheme")) { text = text.substring(0, text.lastIndexOf('.')); } @@ -2184,6 +2216,10 @@ public class Theme { chat_locationAddressPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); chat_urlPaint = new Paint(); chat_textSearchSelectionPaint = new Paint(); + chat_radialProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + chat_radialProgressPaint.setStrokeCap(Paint.Cap.ROUND); + chat_radialProgressPaint.setStyle(Paint.Style.STROKE); + chat_radialProgressPaint.setColor(0x9fffffff); chat_audioTimePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); chat_audioTitlePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); chat_audioTitlePaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); @@ -2217,6 +2253,7 @@ public class Theme { chat_actionTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); chat_actionTextPaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); chat_actionBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + chat_timeBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); chat_contextResult_titleTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); chat_contextResult_titleTextPaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); chat_contextResult_descriptionTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); @@ -2261,9 +2298,7 @@ public class Theme { chat_msgOutMenuSelectedDrawable = resources.getDrawable(R.drawable.msg_actions).mutate(); chat_msgMediaMenuDrawable = resources.getDrawable(R.drawable.video_actions); chat_msgInInstantDrawable = resources.getDrawable(R.drawable.msg_instant).mutate(); - chat_msgInInstantSelectedDrawable = resources.getDrawable(R.drawable.msg_instant).mutate(); chat_msgOutInstantDrawable = resources.getDrawable(R.drawable.msg_instant).mutate(); - chat_msgOutInstantSelectedDrawable = resources.getDrawable(R.drawable.msg_instant).mutate(); chat_msgErrorDrawable = resources.getDrawable(R.drawable.msg_warning); chat_muteIconDrawable = resources.getDrawable(R.drawable.list_mute).mutate(); chat_lockIconDrawable = resources.getDrawable(R.drawable.ic_lock_header); @@ -2290,8 +2325,6 @@ public class Theme { chat_botLinkDrawalbe = resources.getDrawable(R.drawable.bot_link); chat_botInlineDrawable = resources.getDrawable(R.drawable.bot_lines); - chat_timeBackgroundDrawable = resources.getDrawable(R.drawable.phototime2_b); - chat_timeStickerBackgroundDrawable = resources.getDrawable(R.drawable.phototime2); chat_systemDrawable = resources.getDrawable(R.drawable.system); chat_contextResult_shadowUnderSwitchDrawable = resources.getDrawable(R.drawable.header_shadow).mutate(); @@ -2318,6 +2351,15 @@ public class Theme { chat_shareDrawable = resources.getDrawable(R.drawable.share_round); chat_shareIconDrawable = resources.getDrawable(R.drawable.share_arrow); + chat_ivStatesDrawable[0][0] = createCircleDrawableWithIcon(AndroidUtilities.dp(40), R.drawable.msg_round_play_m, 1); + chat_ivStatesDrawable[0][1] = createCircleDrawableWithIcon(AndroidUtilities.dp(40), R.drawable.msg_round_play_m, 1); + chat_ivStatesDrawable[1][0] = createCircleDrawableWithIcon(AndroidUtilities.dp(40), R.drawable.msg_round_pause_m, 1); + chat_ivStatesDrawable[1][1] = createCircleDrawableWithIcon(AndroidUtilities.dp(40), R.drawable.msg_round_pause_m, 1); + chat_ivStatesDrawable[2][0] = createCircleDrawableWithIcon(AndroidUtilities.dp(40), R.drawable.msg_round_load_m, 1); + chat_ivStatesDrawable[2][1] = createCircleDrawableWithIcon(AndroidUtilities.dp(40), R.drawable.msg_round_load_m, 1); + chat_ivStatesDrawable[3][0] = createCircleDrawableWithIcon(AndroidUtilities.dp(40), R.drawable.msg_round_cancel_m, 2); + chat_ivStatesDrawable[3][1] = createCircleDrawableWithIcon(AndroidUtilities.dp(40), R.drawable.msg_round_cancel_m, 2); + chat_fileStatesDrawable[0][0] = createCircleDrawableWithIcon(AndroidUtilities.dp(44), R.drawable.msg_round_play_m); chat_fileStatesDrawable[0][1] = createCircleDrawableWithIcon(AndroidUtilities.dp(44), R.drawable.msg_round_play_m); chat_fileStatesDrawable[1][0] = createCircleDrawableWithIcon(AndroidUtilities.dp(44), R.drawable.msg_round_pause_m); @@ -2373,6 +2415,25 @@ public class Theme { chat_composeShadowDrawable = context.getResources().getDrawable(R.drawable.compose_panel_shadow); + if (Build.VERSION.SDK_INT >= 16) { + try { + int bitmapSize = AndroidUtilities.roundMessageSize + AndroidUtilities.dp(6); + Bitmap bitmap = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setShadowLayer(AndroidUtilities.dp(4), 0, 0, 0x5f000000); + canvas.drawCircle(bitmapSize / 2, bitmapSize / 2, AndroidUtilities.roundMessageSize / 2 - AndroidUtilities.dp(1), paint); + try { + canvas.setBitmap(null); + } catch (Exception ignore) { + + } + chat_roundVideoShadow = new BitmapDrawable(bitmap); + } catch (Throwable ignore) { + + } + } + applyChatTheme(fontsOnly); } @@ -2406,9 +2467,11 @@ public class Theme { chat_instantViewPaint.setTextSize(AndroidUtilities.dp(13)); chat_instantViewRectPaint.setStrokeWidth(AndroidUtilities.dp(1)); chat_statusRecordPaint.setStrokeWidth(AndroidUtilities.dp(2)); - chat_actionTextPaint.setTextSize(AndroidUtilities.dp(MessagesController.getInstance().fontSize - 2)); + chat_actionTextPaint.setTextSize(AndroidUtilities.dp(Math.max(16, MessagesController.getInstance().fontSize) - 2)); chat_contextResult_titleTextPaint.setTextSize(AndroidUtilities.dp(15)); chat_contextResult_descriptionTextPaint.setTextSize(AndroidUtilities.dp(13)); + chat_radialProgressPaint.setStrokeWidth(AndroidUtilities.dp(3)); + } } @@ -2432,6 +2495,7 @@ public class Theme { chat_actionTextPaint.linkColor = getColor(key_chat_serviceLink); chat_contextResult_titleTextPaint.setColor(getColor(key_windowBackgroundWhiteBlackText)); chat_composeBackgroundPaint.setColor(getColor(key_chat_messagePanelBackground)); + chat_timeBackgroundPaint.setColor(getColor(key_chat_mediaTimeBackground)); setDrawableColorByKey(chat_msgInDrawable, key_chat_inBubble); setDrawableColorByKey(chat_msgInSelectedDrawable, key_chat_inBubbleSelected); @@ -2474,9 +2538,7 @@ public class Theme { setDrawableColorByKey(chat_msgOutMenuSelectedDrawable, key_chat_outMenuSelected); setDrawableColorByKey(chat_msgMediaMenuDrawable, key_chat_mediaMenu); setDrawableColorByKey(chat_msgOutInstantDrawable, key_chat_outInstant); - setDrawableColorByKey(chat_msgOutInstantSelectedDrawable, key_chat_outInstantSelected); setDrawableColorByKey(chat_msgInInstantDrawable, key_chat_inInstant); - setDrawableColorByKey(chat_msgInInstantSelectedDrawable, key_chat_inInstantSelected); setDrawableColorByKey(chat_msgErrorDrawable, key_chat_sentErrorIcon); setDrawableColorByKey(chat_muteIconDrawable, key_chat_muteIcon); setDrawableColorByKey(chat_lockIconDrawable, key_chat_lockIcon); @@ -2559,12 +2621,11 @@ public class Theme { chat_actionBackgroundPaint.setColor(serviceColor); colorFilter = new PorterDuffColorFilter(serviceColor, PorterDuff.Mode.MULTIPLY); currentColor = serviceColor; - if (chat_timeStickerBackgroundDrawable != null) { + if (chat_cornerOuter[0] != null) { for (int a = 0; a < 4; a++) { chat_cornerOuter[a].setColorFilter(colorFilter); chat_cornerInner[a].setColorFilter(colorFilter); } - chat_timeStickerBackgroundDrawable.setColorFilter(colorFilter); } } if (currentSelectedColor != servicePressedColor) { 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 b406a5ecb..ad100855e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java @@ -412,8 +412,6 @@ public class DialogsSearchAdapter extends RecyclerListView.SelectionAdapter { }); } - - public void putRecentSearch(final long did, TLObject object) { RecentSearchObject recentSearchObject = recentSearchObjectsById.get(did); if (recentSearchObject == null) { @@ -836,7 +834,7 @@ public class DialogsSearchAdapter extends RecyclerListView.SelectionAdapter { searchResultNames.clear(); searchResultHashtags.clear(); if (needMessagesSearch != 2) { - searchAdapterHelper.queryServerSearch(null, true, true, true); + searchAdapterHelper.queryServerSearch(null, true, true, true, true, 0, false); } searchMessagesInternal(null); notifyDataSetChanged(); @@ -880,7 +878,7 @@ public class DialogsSearchAdapter extends RecyclerListView.SelectionAdapter { @Override public void run() { if (needMessagesSearch != 2) { - searchAdapterHelper.queryServerSearch(query, true, true, true); + searchAdapterHelper.queryServerSearch(query, true, true, true, true, 0, false); } searchMessagesInternal(query); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/MentionsAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/MentionsAdapter.java index 3a4d9ec65..1eb3ebb84 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/MentionsAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/MentionsAdapter.java @@ -17,11 +17,14 @@ import android.content.pm.PackageManager; import android.location.Location; import android.os.Build; import android.text.TextUtils; +import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.ChatObject; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.MessagesController; @@ -36,10 +39,11 @@ import org.telegram.tgnet.RequestDelegate; import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.AlertDialog; -import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Cells.BotSwitchCell; import org.telegram.ui.Cells.ContextLinkCell; import org.telegram.ui.Cells.MentionCell; +import org.telegram.ui.ChatActivity; import org.telegram.ui.Components.RecyclerListView; import java.util.ArrayList; @@ -79,6 +83,7 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { private boolean needBotContext = true; private boolean isDarkTheme; private int botsCount; + private boolean inlineMediaEnabled = true; private String searchingContextUsername; private String searchingContextQuery; @@ -91,7 +96,7 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { private Runnable contextQueryRunnable; private Location lastKnownLocation; - private BaseFragment parentFragment; + private ChatActivity parentFragment; private SendMessagesHelper.LocationProvider locationProvider = new SendMessagesHelper.LocationProvider(new SendMessagesHelper.LocationProvider.LocationProviderDelegate() { @Override @@ -152,6 +157,7 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { contextQueryReqid = 0; } foundContextBot = null; + inlineMediaEnabled = true; searchingContextUsername = null; searchingContextQuery = null; noUserName = false; @@ -161,7 +167,7 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { allowNewMentions = value; } - public void setParentFragment(BaseFragment fragment) { + public void setParentFragment(ChatActivity fragment) { parentFragment = fragment; } @@ -218,6 +224,17 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { locationProvider.stop(); if (user != null && user.bot && user.bot_inline_placeholder != null) { foundContextBot = user; + if (parentFragment != null) { + TLRPC.Chat chat = parentFragment.getCurrentChat(); + if (chat != null) { + inlineMediaEnabled = ChatObject.canSendStickers(chat); + if (!inlineMediaEnabled) { + notifyDataSetChanged(); + delegate.needChangePanelVisibility(true); + return; + } + } + } if (foundContextBot.bot_inline_geo) { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE); boolean allowGeo = preferences.getBoolean("inlinegeo_" + foundContextBot.id, false); @@ -259,6 +276,7 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { } } else { foundContextBot = null; + inlineMediaEnabled = true; } if (foundContextBot == null) { noUserName = true; @@ -279,6 +297,9 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { searchResultBotContextSwitch = null; notifyDataSetChanged(); if (foundContextBot != null) { + if (!inlineMediaEnabled && username != null && query != null) { + return; + } delegate.needChangePanelVisibility(false); } if (contextQueryRunnable != null) { @@ -295,6 +316,7 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { contextQueryReqid = 0; } foundContextBot = null; + inlineMediaEnabled = true; searchingContextUsername = null; searchingContextQuery = null; locationProvider.stop(); @@ -340,9 +362,9 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { searchForContextBotResults(true, foundContextBot, query, ""); } else { searchingContextUsername = username; - TLRPC.User user = MessagesController.getInstance().getUser(searchingContextUsername); - if (user != null) { - processFoundUser(user); + TLObject object = MessagesController.getInstance().getUserOrChat(searchingContextUsername); + if (object instanceof TLRPC.User) { + processFoundUser((TLRPC.User) object); } else { TLRPC.TL_contacts_resolveUsername req = new TLRPC.TL_contacts_resolveUsername(); req.username = searchingContextUsername; @@ -420,6 +442,12 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { ConnectionsManager.getInstance().cancelRequest(contextQueryReqid, true); contextQueryReqid = 0; } + if (!inlineMediaEnabled) { + if (delegate != null) { + delegate.onContextSearch(false); + } + return; + } if (query == null || user == null) { searchingContextQuery = null; return; @@ -518,7 +546,7 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { } public void searchUsernameOrHashtag(String text, int position, ArrayList messageObjects) { - if (text == null || text.length() == 0) { + if (TextUtils.isEmpty(text)) { searchForContextBot(null, null); delegate.needChangePanelVisibility(false); lastText = null; @@ -648,7 +676,15 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { } } } - if (info != null && info.participants != null) { + TLRPC.Chat chat; + if (parentFragment != null) { + chat = parentFragment.getCurrentChat(); + } else if (info != null) { + chat = MessagesController.getInstance().getChat(info.id); + } else { + chat = null; + } + if (chat != null && info != null && info.participants != null && (!ChatObject.isChannel(chat) || chat.megagroup)) { for (int a = 0; a < info.participants.participants.size(); a++) { TLRPC.ChatParticipant chatParticipant = info.participants.participants.get(a); TLRPC.User user = MessagesController.getInstance().getUser(chatParticipant.user_id); @@ -761,6 +797,9 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { @Override public int getItemCount() { + if (foundContextBot != null && !inlineMediaEnabled) { + return 1; + } if (searchResultBotContext != null) { return searchResultBotContext.size() + (searchResultBotContextSwitch != null ? 1 : 0); } else if (searchResultUsernames != null) { @@ -775,7 +814,9 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { @Override public int getItemViewType(int position) { - if (searchResultBotContext != null) { + if (foundContextBot != null && !inlineMediaEnabled) { + return 3; + } else if (searchResultBotContext != null) { if (position == 0 && searchResultBotContextSwitch != null) { return 2; } @@ -847,38 +888,64 @@ public class MentionsAdapter extends RecyclerListView.SelectionAdapter { return searchResultBotContext != null; } + public boolean isBannedInline() { + return foundContextBot != null && !inlineMediaEnabled; + } + public boolean isMediaLayout() { return contextMedia; } @Override public boolean isEnabled(RecyclerView.ViewHolder holder) { - return true; + return foundContextBot == null || inlineMediaEnabled; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view; - if (viewType == 1) { - view = new ContextLinkCell(mContext); - ((ContextLinkCell) view).setDelegate(new ContextLinkCell.ContextLinkCellDelegate() { - @Override - public void didPressedImage(ContextLinkCell cell) { - delegate.onContextClick(cell.getResult()); - } - }); - } else if (viewType == 2) { - view = new BotSwitchCell(mContext); - } else { - view = new MentionCell(mContext); - ((MentionCell) view).setIsDarkTheme(isDarkTheme); + switch (viewType) { + case 0: + view = new MentionCell(mContext); + ((MentionCell) view).setIsDarkTheme(isDarkTheme); + break; + case 1: + view = new ContextLinkCell(mContext); + ((ContextLinkCell) view).setDelegate(new ContextLinkCell.ContextLinkCellDelegate() { + @Override + public void didPressedImage(ContextLinkCell cell) { + delegate.onContextClick(cell.getResult()); + } + }); + break; + case 2: + view = new BotSwitchCell(mContext); + break; + case 3: + default: + TextView textView = new TextView(mContext); + textView.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(8), AndroidUtilities.dp(8), AndroidUtilities.dp(8)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + textView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText2)); + view = textView; + break; } return new RecyclerListView.Holder(view); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - if (searchResultBotContext != null) { + if (holder.getItemViewType() == 3) { + TextView textView = (TextView) holder.itemView; + TLRPC.Chat chat = parentFragment.getCurrentChat(); + if (chat != null) { + if (AndroidUtilities.isBannedForever(chat.banned_rights.until_date)) { + textView.setText(LocaleController.getString("AttachInlineRestrictedForever", R.string.AttachInlineRestrictedForever)); + } else { + textView.setText(LocaleController.formatString("AttachInlineRestricted", R.string.AttachInlineRestricted, LocaleController.formatDateForBan(chat.banned_rights.until_date))); + } + } + } else if (searchResultBotContext != null) { boolean hasTop = searchResultBotContextSwitch != null; if (holder.getItemViewType() == 2) { if (hasTop) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/SearchAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/SearchAdapter.java index 75a6d6e35..495b7a5a6 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/SearchAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/SearchAdapter.java @@ -51,14 +51,16 @@ public class SearchAdapter extends RecyclerListView.SelectionAdapter { private boolean onlyMutual; private boolean allowChats; private boolean allowBots; + private int channelId; - public SearchAdapter(Context context, HashMap arg1, boolean usernameSearch, boolean mutual, boolean chats, boolean bots) { + public SearchAdapter(Context context, HashMap arg1, boolean usernameSearch, boolean mutual, boolean chats, boolean bots, int searchChannelId) { mContext = context; ignoreUsers = arg1; onlyMutual = mutual; allowUsernameSearch = usernameSearch; allowChats = chats; allowBots = bots; + channelId = searchChannelId; searchAdapterHelper = new SearchAdapterHelper(); searchAdapterHelper.setDelegate(new SearchAdapterHelper.SearchAdapterHelperDelegate() { @Override @@ -93,7 +95,7 @@ public class SearchAdapter extends RecyclerListView.SelectionAdapter { searchResult.clear(); searchResultNames.clear(); if (allowUsernameSearch) { - searchAdapterHelper.queryServerSearch(null, allowChats, allowBots, true); + searchAdapterHelper.queryServerSearch(null, true, allowChats, allowBots, true, channelId, false); } notifyDataSetChanged(); } else { @@ -118,7 +120,7 @@ public class SearchAdapter extends RecyclerListView.SelectionAdapter { @Override public void run() { if (allowUsernameSearch) { - searchAdapterHelper.queryServerSearch(query, allowChats, allowBots, true); + searchAdapterHelper.queryServerSearch(query, true, allowChats, allowBots, true, channelId, false); } final ArrayList contactsCopy = new ArrayList<>(); contactsCopy.addAll(ContactsController.getInstance().contacts); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/SearchAdapterHelper.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/SearchAdapterHelper.java index b72fa683c..fe8fd75b4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/SearchAdapterHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/SearchAdapterHelper.java @@ -12,6 +12,7 @@ import org.telegram.SQLite.SQLiteCursor; import org.telegram.SQLite.SQLitePreparedStatement; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.FileLog; +import org.telegram.messenger.MessagesController; import org.telegram.messenger.MessagesStorage; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.RequestDelegate; @@ -43,57 +44,154 @@ public class SearchAdapterHelper { private int lastReqId; private String lastFoundUsername = null; private ArrayList globalSearch = new ArrayList<>(); + private ArrayList groupSearch = new ArrayList<>(); + private ArrayList groupSearch2 = new ArrayList<>(); + + private int channelReqId = 0; + private int channelLastReqId; + private String lastFoundChannel; + + private int channelReqId2 = 0; + private int channelLastReqId2; + private String lastFoundChannel2; private ArrayList hashtags; private HashMap hashtagsByText; private boolean hashtagsLoadedFromDb = false; - public void queryServerSearch(final String query, final boolean allowChats, final boolean allowBots, final boolean allowSelf) { + public void queryServerSearch(final String query, final boolean allowUsername, final boolean allowChats, final boolean allowBots, final boolean allowSelf, final int channelId, final boolean kicked) { if (reqId != 0) { ConnectionsManager.getInstance().cancelRequest(reqId, true); reqId = 0; } - if (query == null || query.length() < 5) { + if (channelReqId != 0) { + ConnectionsManager.getInstance().cancelRequest(channelReqId, true); + channelReqId = 0; + } + if (channelReqId2 != 0) { + ConnectionsManager.getInstance().cancelRequest(channelReqId2, true); + channelReqId2 = 0; + } + if (query == null) { + groupSearch.clear(); + groupSearch2.clear(); globalSearch.clear(); lastReqId = 0; + channelLastReqId = 0; + channelLastReqId2 = 0; delegate.onDataSetChanged(); return; } - TLRPC.TL_contacts_search req = new TLRPC.TL_contacts_search(); - req.q = query; - req.limit = 50; - final int currentReqId = ++lastReqId; - reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(final TLObject response, final TLRPC.TL_error error) { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - if (currentReqId == lastReqId) { - if (error == null) { - TLRPC.TL_contacts_found res = (TLRPC.TL_contacts_found) response; - globalSearch.clear(); - if (allowChats) { - for (int a = 0; a < res.chats.size(); a++) { - globalSearch.add(res.chats.get(a)); - } - } - for (int a = 0; a < res.users.size(); a++) { - TLRPC.User user = res.users.get(a); - if (!allowBots && user.bot || !allowSelf && user.self) { - continue; - } - globalSearch.add(res.users.get(a)); - } - lastFoundUsername = query; - delegate.onDataSetChanged(); - } - } - reqId = 0; - } - }); + if (query.length() > 0 && channelId != 0) { + TLRPC.TL_channels_getParticipants req = new TLRPC.TL_channels_getParticipants(); + if (kicked) { + req.filter = new TLRPC.TL_channelParticipantsBanned(); + } else { + req.filter = new TLRPC.TL_channelParticipantsSearch(); } - }, ConnectionsManager.RequestFlagFailOnServerErrors); + req.filter.q = query; + req.limit = 50; + req.offset = 0; + req.channel = MessagesController.getInputChannel(channelId); + final int currentReqId = ++channelLastReqId; + channelReqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (currentReqId == channelLastReqId) { + if (error == null) { + TLRPC.TL_channels_channelParticipants res = (TLRPC.TL_channels_channelParticipants) response; + lastFoundChannel = query.toLowerCase(); + MessagesController.getInstance().putUsers(res.users, false); + groupSearch = res.participants; + delegate.onDataSetChanged(); + } + } + channelReqId = 0; + } + }); + } + }, ConnectionsManager.RequestFlagFailOnServerErrors); + if (kicked) { + req = new TLRPC.TL_channels_getParticipants(); + req.filter = new TLRPC.TL_channelParticipantsKicked(); + req.filter.q = query; + req.limit = 50; + req.offset = 0; + req.channel = MessagesController.getInputChannel(channelId); + final int currentReqId2 = ++channelLastReqId2; + channelReqId2 = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (currentReqId2 == channelLastReqId2) { + if (error == null) { + TLRPC.TL_channels_channelParticipants res = (TLRPC.TL_channels_channelParticipants) response; + lastFoundChannel2 = query.toLowerCase(); + MessagesController.getInstance().putUsers(res.users, false); + groupSearch2 = res.participants; + delegate.onDataSetChanged(); + } + } + channelReqId2 = 0; + } + }); + } + }, ConnectionsManager.RequestFlagFailOnServerErrors); + } + } else { + groupSearch.clear(); + groupSearch2.clear(); + channelLastReqId = 0; + delegate.onDataSetChanged(); + } + if (allowUsername) { + if (query.length() >= 5) { + TLRPC.TL_contacts_search req = new TLRPC.TL_contacts_search(); + req.q = query; + req.limit = 50; + final int currentReqId = ++lastReqId; + reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (currentReqId == lastReqId) { + if (error == null) { + TLRPC.TL_contacts_found res = (TLRPC.TL_contacts_found) response; + globalSearch.clear(); + if (allowChats) { + for (int a = 0; a < res.chats.size(); a++) { + globalSearch.add(res.chats.get(a)); + } + } + for (int a = 0; a < res.users.size(); a++) { + TLRPC.User user = res.users.get(a); + if (!allowBots && user.bot || !allowSelf && user.self) { + continue; + } + globalSearch.add(res.users.get(a)); + } + lastFoundUsername = query.toLowerCase(); + delegate.onDataSetChanged(); + } + } + reqId = 0; + } + }); + } + }, ConnectionsManager.RequestFlagFailOnServerErrors); + } else { + globalSearch.clear(); + lastReqId = 0; + delegate.onDataSetChanged(); + } + } } public void unloadRecentHashtags() { @@ -221,6 +319,14 @@ public class SearchAdapterHelper { return globalSearch; } + public ArrayList getGroupSearch() { + return groupSearch; + } + + public ArrayList getGroupSearch2() { + return groupSearch2; + } + public ArrayList getHashtags() { return hashtags; } @@ -229,6 +335,14 @@ public class SearchAdapterHelper { return lastFoundUsername; } + public String getLastFoundChannel() { + return lastFoundChannel; + } + + public String getLastFoundChannel2() { + return lastFoundChannel2; + } + public void clearRecentHashtags() { hashtags = new ArrayList<>(); hashtagsByText = new HashMap<>(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ArticleViewer.java b/TMessagesProj/src/main/java/org/telegram/ui/ArticleViewer.java index 39b83ec53..458ed1015 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ArticleViewer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ArticleViewer.java @@ -27,6 +27,8 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.SurfaceTexture; @@ -51,6 +53,7 @@ import android.util.TypedValue; import android.view.GestureDetector; import android.view.Gravity; import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.TextureView; @@ -68,7 +71,10 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.PopupWindow; import android.widget.TextView; +import android.widget.Toast; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; @@ -84,6 +90,7 @@ import org.telegram.messenger.MessagesController; import org.telegram.messenger.MessagesStorage; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; +import org.telegram.messenger.UserConfig; import org.telegram.messenger.browser.Browser; import org.telegram.messenger.exoplayer2.C; import org.telegram.messenger.exoplayer2.ExoPlayer; @@ -98,10 +105,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.ActionBarPopupWindow; import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.BackDrawable; +import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.AlertsCreator; import org.telegram.ui.Components.AnimatedFileDrawable; import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.ClippingImageView; @@ -115,11 +125,13 @@ import org.telegram.ui.Components.SeekBar; import org.telegram.ui.Components.ShareAlert; import org.telegram.ui.Components.TextPaintSpan; import org.telegram.ui.Components.TextPaintUrlSpan; +import org.telegram.ui.Components.TypefaceSpan; import org.telegram.ui.Components.VideoPlayer; import org.telegram.ui.Components.WebPlayerView; import java.io.File; import java.util.ArrayList; +import java.util.Calendar; import java.util.HashMap; import java.util.Locale; @@ -127,6 +139,7 @@ import java.util.Locale; public class ArticleViewer implements NotificationCenter.NotificationCenterDelegate, GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener { private Activity parentActivity; + private BaseFragment parentFragment; private ArrayList createdWebViews = new ArrayList<>(); private View customView; @@ -135,6 +148,9 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg private AspectRatioFrameLayout fullscreenAspectRatioView; private WebChromeClient.CustomViewCallback customViewCallback; + private TLRPC.Chat loadedChannel; + private boolean loadingChannel; + private Object lastInsets; private boolean isVisible; @@ -158,6 +174,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg private FrameLayout headerView; private ImageView backButton; private ImageView shareButton; + private ActionBarMenuItem settingsButton; private FrameLayout shareContainer; private ContextProgressView progressView; private BackDrawable backDrawable; @@ -169,12 +186,18 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg private Paint scrimPaint; private AnimatorSet progressViewAnimation; + private ActionBarPopupWindow popupWindow; + private ActionBarPopupWindow.ActionBarPopupWindowLayout popupLayout; + private Rect popupRect; + private WebPlayerView currentPlayingVideo; private WebPlayerView fullscreenedVideo; private Drawable slideDotDrawable; private Drawable slideDotBigDrawable; + private TLRPC.TL_pageBlockChannel channelBlock; + private int openUrlReqId; private int previewsReqId; @@ -186,8 +209,10 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg private CheckForTap pendingCheckForTap = null; private TextPaintUrlSpan pressedLink; + private int pressedLayoutY; private StaticLayout pressedLinkOwnerLayout; private View pressedLinkOwnerView; + private boolean drawBlockSelection; private LinkPath urlPath = new LinkPath(); public ArrayList blocks = new ArrayList<>(); @@ -210,6 +235,240 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg return localInstance; } + private static Paint colorPaint; + private static Paint selectorPaint; + private final int fontSizeCount = 5; + private int selectedFontSize = 2; + private int selectedColor = 0; + private int selectedFont = 0; + private boolean nightModeEnabled; + private ColorCell[] colorCells = new ColorCell[3]; + private ImageView nightModeImageView; + private FrameLayout nightModeHintView; + private FontCell[] fontCells = new FontCell[2]; + + private class SizeChooseView extends View { + + private Paint paint; + + private int circleSize; + private int gapSize; + private int sideSide; + private int lineSize; + + private boolean moving; + private boolean startMoving; + private float startX; + + private int startMovingQuality; + + public SizeChooseView(Context context) { + super(context); + + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + if (event.getAction() == MotionEvent.ACTION_DOWN) { + getParent().requestDisallowInterceptTouchEvent(true); + for (int a = 0; a < fontSizeCount; a++) { + int cx = sideSide + (lineSize + gapSize * 2 + circleSize) * a + circleSize / 2; + if (x > cx - AndroidUtilities.dp(15) && x < cx + AndroidUtilities.dp(15)) { + startMoving = a == selectedFontSize; + startX = x; + startMovingQuality = selectedFontSize; + break; + } + } + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (startMoving) { + if (Math.abs(startX - x) >= AndroidUtilities.getPixelsInCM(0.5f, true)) { + moving = true; + startMoving = false; + } + } else if (moving) { + for (int a = 0; a < fontSizeCount; a++) { + int cx = sideSide + (lineSize + gapSize * 2 + circleSize) * a + circleSize / 2; + int diff = lineSize / 2 + circleSize / 2 + gapSize; + if (x > cx - diff && x < cx + diff) { + if (selectedFontSize != a) { + selectedFontSize = a; + updatePaintSize(); + invalidate(); + } + break; + } + } + } + } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { + if (!moving) { + for (int a = 0; a < 5; a++) { + int cx = sideSide + (lineSize + gapSize * 2 + circleSize) * a + circleSize / 2; + if (x > cx - AndroidUtilities.dp(15) && x < cx + AndroidUtilities.dp(15)) { + if (selectedFontSize != a) { + selectedFontSize = a; + updatePaintSize(); + invalidate(); + } + break; + } + } + } else { + if (selectedFontSize != startMovingQuality) { + updatePaintSize(); + } + } + startMoving = false; + moving = false; + } + return true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + circleSize = AndroidUtilities.dp(5); + gapSize = AndroidUtilities.dp(2); + sideSide = AndroidUtilities.dp(17); + lineSize = (getMeasuredWidth() - circleSize * fontSizeCount - gapSize * 8 - sideSide * 2) / (fontSizeCount - 1); + } + + @Override + protected void onDraw(Canvas canvas) { + int cy = getMeasuredHeight() / 2; + for (int a = 0; a < fontSizeCount; a++) { + int cx = sideSide + (lineSize + gapSize * 2 + circleSize) * a + circleSize / 2; + if (a <= selectedFontSize) { + paint.setColor(0xff1495e9); + } else { + paint.setColor(0xffcccccc); + } + canvas.drawCircle(cx, cy, a == selectedFontSize ? AndroidUtilities.dp(4) : circleSize / 2, paint); + if (a != 0) { + int x = cx - circleSize / 2 - gapSize - lineSize; + canvas.drawRect(x, cy - AndroidUtilities.dp(1), x + lineSize, cy + AndroidUtilities.dp(1), paint); + } + } + } + } + + public class ColorCell extends FrameLayout { + + private TextView textView; + private int currentColor; + private boolean selected; + + public ColorCell(Context context) { + super(context); + + if (colorPaint == null) { + colorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + selectorPaint.setColor(0xff1495e9); + selectorPaint.setStyle(Paint.Style.STROKE); + selectorPaint.setStrokeWidth(AndroidUtilities.dp(2)); + } + + setBackgroundDrawable(Theme.createSelectorDrawable(0x0f000000, 2)); + + setWillNotDraw(false); + + textView = new TextView(context); + textView.setTextColor(0xff212121); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setLines(1); + textView.setMaxLines(1); + textView.setSingleLine(true); + textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + textView.setPadding(0, 0, 0, AndroidUtilities.dp(1)); + addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, (LocaleController.isRTL ? 17 : 17 + 36), 0, (LocaleController.isRTL ? 17 + 36 : 17), 0)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(48), MeasureSpec.EXACTLY)); + } + + public void setTextAndColor(String text, int color) { + textView.setText(text); + currentColor = color; + invalidate(); + } + + public void select(boolean value) { + if (selected == value) { + return; + } + selected = value; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + colorPaint.setColor(currentColor); + canvas.drawCircle(!LocaleController.isRTL ? AndroidUtilities.dp(28) : getMeasuredWidth() - AndroidUtilities.dp(28 + 20), getMeasuredHeight() / 2, AndroidUtilities.dp(10), colorPaint); + if (selected) { + selectorPaint.setStrokeWidth(AndroidUtilities.dp(2)); + selectorPaint.setColor(0xff1495e9); + canvas.drawCircle(!LocaleController.isRTL ? AndroidUtilities.dp(28) : getMeasuredWidth() - AndroidUtilities.dp(28 + 20), getMeasuredHeight() / 2, AndroidUtilities.dp(10), selectorPaint); + } else if (currentColor == 0xffffffff) { + selectorPaint.setStrokeWidth(AndroidUtilities.dp(1)); + selectorPaint.setColor(0xffbababa); + canvas.drawCircle(!LocaleController.isRTL ? AndroidUtilities.dp(28) : getMeasuredWidth() - AndroidUtilities.dp(28 + 20), getMeasuredHeight() / 2, AndroidUtilities.dp(9), selectorPaint); + } + } + } + + public class FontCell extends FrameLayout { + + private TextView textView; + private TextView textView2; + + public FontCell(Context context) { + super(context); + + setBackgroundDrawable(Theme.createSelectorDrawable(0x0f000000, 2)); + + textView = new TextView(context); + textView.setTextColor(0xff212121); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setLines(1); + textView.setMaxLines(1); + textView.setSingleLine(true); + 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, (LocaleController.isRTL ? 17 : 17 + 36), 0, (LocaleController.isRTL ? 17 + 36 : 17), 0)); + + textView2 = new TextView(context); + textView2.setTextColor(0xff212121); + textView2.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView2.setLines(1); + textView2.setMaxLines(1); + textView2.setSingleLine(true); + textView2.setText("Aa"); + textView2.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + addView(textView2, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, 17, 0, 17, 0)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(48), MeasureSpec.EXACTLY)); + } + + public void select(boolean value) { + textView2.setTextColor(value ? 0xff1495e9 : 0xff212121); + } + + public void setTextAndTypeface(String text, Typeface typeface) { + textView.setText(text); + textView.setTypeface(typeface); + textView2.setTypeface(typeface); + invalidate(); + } + } + private class FrameLayoutDrawer extends FrameLayout { public FrameLayoutDrawer(Context context) { @@ -554,11 +813,106 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg pressedLink = null; pressedLinkOwnerLayout = null; pressedLinkOwnerView.invalidate(); + } else if (pressedLinkOwnerLayout != null && pressedLinkOwnerView != null) { + int y = pressedLinkOwnerView.getTop() - AndroidUtilities.dp(54) + pressedLayoutY; + int x; + if (y < 0) { + y *= -1; + } + pressedLinkOwnerView.invalidate(); + drawBlockSelection = true; + showPopup(pressedLinkOwnerView, Gravity.TOP, 0, y); + listView.setLayoutFrozen(true); + listView.setLayoutFrozen(false); } } } } + private void showPopup(View parent, int gravity, int x, int y) { + if (popupWindow != null && popupWindow.isShowing()) { + popupWindow.dismiss(); + return; + } + + if (popupLayout == null) { + popupRect = new android.graphics.Rect(); + popupLayout = new ActionBarPopupWindow.ActionBarPopupWindowLayout(parentActivity); + popupLayout.setBackgroundDrawable(parentActivity.getResources().getDrawable(R.drawable.menu_copy)); + popupLayout.setAnimationEnabled(false); + popupLayout.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + if (popupWindow != null && popupWindow.isShowing()) { + v.getHitRect(popupRect); + if (!popupRect.contains((int) event.getX(), (int) event.getY())) { + popupWindow.dismiss(); + } + } + } + return false; + } + }); + popupLayout.setDispatchKeyEventListener(new ActionBarPopupWindow.OnDispatchKeyEventListener() { + @Override + public void onDispatchKeyEvent(KeyEvent keyEvent) { + if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BACK && keyEvent.getRepeatCount() == 0 && popupWindow != null && popupWindow.isShowing()) { + popupWindow.dismiss(); + } + } + }); + popupLayout.setShowedFromBotton(false); + + TextView deleteView = new TextView(parentActivity); + deleteView.setTextColor(0xff000000); + deleteView.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + deleteView.setGravity(Gravity.CENTER_VERTICAL); + deleteView.setPadding(AndroidUtilities.dp(14), 0, AndroidUtilities.dp(14), 0); + deleteView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + deleteView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + deleteView.setText(LocaleController.getString("Copy", R.string.Copy).toUpperCase()); + deleteView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (pressedLinkOwnerLayout == null) { + return; + } + AndroidUtilities.addToClipboard(pressedLinkOwnerLayout.getText()); + Toast.makeText(parentActivity, LocaleController.getString("TextCopied", R.string.TextCopied), Toast.LENGTH_SHORT).show(); + if (popupWindow != null && popupWindow.isShowing()) { + popupWindow.dismiss(true); + } + } + }); + popupLayout.addView(deleteView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 38)); + + popupWindow = new ActionBarPopupWindow(popupLayout, LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT); + popupWindow.setAnimationEnabled(false); + popupWindow.setAnimationStyle(R.style.PopupAnimation); + popupWindow.setOutsideTouchable(true); + popupWindow.setClippingEnabled(true); + popupWindow.setInputMethodMode(ActionBarPopupWindow.INPUT_METHOD_NOT_NEEDED); + popupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED); + popupWindow.getContentView().setFocusableInTouchMode(true); + popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() { + @Override + public void onDismiss() { + if (pressedLinkOwnerView != null) { + pressedLinkOwnerLayout = null; + pressedLinkOwnerView.invalidate(); + pressedLinkOwnerView = null; + } + } + }); + } + + popupLayout.measure(View.MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(1000), View.MeasureSpec.AT_MOST), View.MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(1000), View.MeasureSpec.AT_MOST)); + popupWindow.setFocusable(true); + popupWindow.showAtLocation(parent, gravity, x, y); + popupWindow.startAnimation(); + } + private void setRichTextParents(TLRPC.RichText parentRichText, TLRPC.RichText richText) { if (richText == null) { return; @@ -590,15 +944,17 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if (currentPage == null || currentPage.cached_page == null) { return; } + channelBlock = null; blocks.clear(); photoBlocks.clear(); int numBlocks = 0; - for (int a = 0; a < currentPage.cached_page.blocks.size(); a++) { + int count = currentPage.cached_page.blocks.size(); + for (int a = 0; a < count; a++) { TLRPC.PageBlock block = currentPage.cached_page.blocks.get(a); if (block instanceof TLRPC.TL_pageBlockUnsupported) { continue; } else if (block instanceof TLRPC.TL_pageBlockAnchor) { - anchors.put(block.name, blocks.size()); + anchors.put(block.name.toLowerCase(), blocks.size()); continue; } setRichTextParents(null, block.text); @@ -622,9 +978,19 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg setRichTextParents(null, innerBlock.items.get(i).text); setRichTextParents(null, innerBlock.items.get(i).caption); } + } else if (block instanceof TLRPC.TL_pageBlockAudio) { + block.mid = ((int) currentPage.id) + a; } if (a == 0) { block.first = true; + if (block instanceof TLRPC.TL_pageBlockCover && block.cover.caption != null && !(block.cover.caption instanceof TLRPC.TL_textEmpty) && count > 1) { + TLRPC.PageBlock next = currentPage.cached_page.blocks.get(1); + if (next instanceof TLRPC.TL_pageBlockChannel) { + channelBlock = (TLRPC.TL_pageBlockChannel) next; + } + } + } else if (a == 1 && channelBlock != null) { + continue; } addAllMediaFromBlock(block); blocks.add(block); @@ -636,7 +1002,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if (innerBlock instanceof TLRPC.TL_pageBlockUnsupported) { continue; } else if (innerBlock instanceof TLRPC.TL_pageBlockAnchor) { - anchors.put(innerBlock.name, blocks.size()); + anchors.put(innerBlock.name.toLowerCase(), blocks.size()); continue; } innerBlock.level = 1; @@ -682,7 +1048,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg updateInterfaceForCurrentPage(false); if (anchor != null) { - Integer row = anchors.get(anchor); + Integer row = anchors.get(anchor.toLowerCase()); if (row != null) { layoutManager.scrollToPositionWithOffset(row, 0); } @@ -728,6 +1094,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg private static final int TEXT_FLAG_UNDERLINE = 16; private static final int TEXT_FLAG_STRIKE = 32; + private static TextPaint audioTimePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); private static TextPaint errorTextPaint; private static HashMap captionTextPaints = new HashMap<>(); private static HashMap titleTextPaints = new HashMap<>(); @@ -749,6 +1116,9 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg private static TextPaint embedPostAuthorPaint; private static TextPaint embedPostDatePaint; + private static TextPaint channelNamePaint; + + private static TextPaint listTextPointerPaint; private static Paint preformattedBackgroundPaint; private static Paint quoteLinePaint; @@ -846,86 +1216,122 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg return null; } + private int getTextColor() { + switch (getSelectedColor()) { + case 0: + case 1: + return 0xff212121; + case 2: + default: + return 0xff999999; + } + } + + private int getGrayTextColor() { + switch (getSelectedColor()) { + case 0: + return 0xff838c96; + case 1: + return 0xff4d4b45; + case 2: + default: + return 0xff666666; + } + } + private TextPaint getTextPaint(TLRPC.RichText parentRichText, TLRPC.RichText richText, TLRPC.PageBlock parentBlock) { int flags = getTextFlags(richText); HashMap currentMap = null; int textSize = AndroidUtilities.dp(14); int textColor = 0xffff0000; + int additionalSize; + if (selectedFontSize == 0) { + additionalSize = -AndroidUtilities.dp(4); + } else if (selectedFontSize == 1) { + additionalSize = -AndroidUtilities.dp(2); + } else if (selectedFontSize == 3) { + additionalSize = AndroidUtilities.dp(2); + } else if (selectedFontSize == 4) { + additionalSize = AndroidUtilities.dp(4); + } else { + additionalSize = 0; + } + if (parentBlock instanceof TLRPC.TL_pageBlockPhoto) { currentMap = captionTextPaints; textSize = AndroidUtilities.dp(14); - textColor = 0xff838c96; + textColor = getGrayTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockTitle) { currentMap = titleTextPaints; textSize = AndroidUtilities.dp(24); - textColor = 0xff000000; + textColor = getTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockAuthorDate) { currentMap = authorTextPaints; textSize = AndroidUtilities.dp(14); - textColor = 0xff838c96; + textColor = getGrayTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockFooter) { currentMap = footerTextPaints; textSize = AndroidUtilities.dp(14); - textColor = 0xff838c96; + textColor = getGrayTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockSubtitle) { currentMap = subtitleTextPaints; textSize = AndroidUtilities.dp(21); - textColor = 0xff000000; + textColor = getTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockHeader) { currentMap = headerTextPaints; textSize = AndroidUtilities.dp(21); - textColor = 0xff000000; + textColor = getTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockSubheader) { currentMap = subheaderTextPaints; textSize = AndroidUtilities.dp(18); - textColor = 0xff000000; + textColor = getTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockBlockquote || parentBlock instanceof TLRPC.TL_pageBlockPullquote) { if (parentBlock.text == parentRichText) { currentMap = quoteTextPaints; textSize = AndroidUtilities.dp(15); - textColor = 0xff000000; + textColor = getTextColor(); } else if (parentBlock.caption == parentRichText) { currentMap = subquoteTextPaints; textSize = AndroidUtilities.dp(14); - textColor = 0xff838c96; + textColor = getGrayTextColor(); } } else if (parentBlock instanceof TLRPC.TL_pageBlockPreformatted) { currentMap = preformattedTextPaints; textSize = AndroidUtilities.dp(14); - textColor = 0xff000000; + textColor = getTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockParagraph) { if (parentBlock.caption == parentRichText) { currentMap = embedPostCaptionTextPaints; textSize = AndroidUtilities.dp(14); - textColor = 0xff838c96; + textColor = getGrayTextColor(); } else { currentMap = paragraphTextPaints; textSize = AndroidUtilities.dp(16); - textColor = 0xff000000; + textColor = getTextColor(); } } else if (parentBlock instanceof TLRPC.TL_pageBlockList) { currentMap = listTextPaints; textSize = AndroidUtilities.dp(15); - textColor = 0xff000000; + textColor = getTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockEmbed) { currentMap = embedTextPaints; textSize = AndroidUtilities.dp(14); - textColor = 0xff838c96; + textColor = getGrayTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockSlideshow) { currentMap = slideshowTextPaints; textSize = AndroidUtilities.dp(14); - textColor = 0xff838c96; + textColor = getGrayTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockEmbedPost) { if (richText != null) { currentMap = embedPostTextPaints; textSize = AndroidUtilities.dp(14); - textColor = 0xff000000; + textColor = getTextColor(); } - } else if (parentBlock instanceof TLRPC.TL_pageBlockVideo) { + } else if (parentBlock instanceof TLRPC.TL_pageBlockVideo || parentBlock instanceof TLRPC.TL_pageBlockAudio) { currentMap = videoTextPaints; textSize = AndroidUtilities.dp(14); - textColor = 0xff000000; + textColor = getTextColor(); } if (currentMap == null) { if (errorTextPaint == null) { @@ -941,7 +1347,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if ((flags & TEXT_FLAG_MONO) != 0) { paint.setTypeface(AndroidUtilities.getTypeface("fonts/rmono.ttf")); } else { - if (parentBlock instanceof TLRPC.TL_pageBlockTitle || parentBlock instanceof TLRPC.TL_pageBlockHeader || parentBlock instanceof TLRPC.TL_pageBlockSubtitle || parentBlock instanceof TLRPC.TL_pageBlockSubheader) { + if (selectedFont == 1 || parentBlock instanceof TLRPC.TL_pageBlockTitle || parentBlock instanceof TLRPC.TL_pageBlockHeader || parentBlock instanceof TLRPC.TL_pageBlockSubtitle || parentBlock instanceof TLRPC.TL_pageBlockSubheader) { if ((flags & TEXT_FLAG_MEDIUM) != 0 && (flags & TEXT_FLAG_ITALIC) != 0) { paint.setTypeface(Typeface.create("serif", Typeface.BOLD_ITALIC)); } else if ((flags & TEXT_FLAG_MEDIUM) != 0) { @@ -968,12 +1374,13 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg paint.setFlags(paint.getFlags() | TextPaint.UNDERLINE_TEXT_FLAG); } if ((flags & TEXT_FLAG_URL) != 0) { - textColor = 0xff4d83b3; + paint.setFlags(paint.getFlags() | TextPaint.UNDERLINE_TEXT_FLAG); + textColor = getTextColor(); } paint.setColor(textColor); currentMap.put(flags, paint); } - paint.setTextSize(textSize); + paint.setTextSize(textSize + additionalSize); return paint; } @@ -982,15 +1389,28 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg return null; } + int color = getSelectedColor(); if (quoteLinePaint == null) { quoteLinePaint = new Paint(); - quoteLinePaint.setColor(0xff000000); + quoteLinePaint.setColor(getTextColor()); preformattedBackgroundPaint = new Paint(); - preformattedBackgroundPaint.setColor(0xfff5f8fc); + if (color == 0) { + preformattedBackgroundPaint.setColor(0xfff5f8fc); + } else if (color == 1) { + preformattedBackgroundPaint.setColor(0xffe5dec8); + } else if (color == 2) { + preformattedBackgroundPaint.setColor(0xff262626); + } urlPaint = new Paint(); - urlPaint.setColor(0x3362a9e3); + if (color == 0) { + urlPaint.setColor(0xffebebeb); + } else if (color == 1) { + urlPaint.setColor(0xffe5dec8); + } else if (color == 2) { + urlPaint.setColor(0xff262626); + } } CharSequence text; @@ -1007,22 +1427,59 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if (parentBlock.author == plainText) { if (embedPostAuthorPaint == null) { embedPostAuthorPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - embedPostAuthorPaint.setColor(0xff000000); + embedPostAuthorPaint.setColor(getTextColor()); } embedPostAuthorPaint.setTextSize(AndroidUtilities.dp(15)); paint = embedPostAuthorPaint; } else { if (embedPostDatePaint == null) { embedPostDatePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - embedPostDatePaint.setColor(0xff8f97a0); + if (color == 0) { + embedPostDatePaint.setColor(0xff8f97a0); + } else if (color == 1) { + embedPostDatePaint.setColor(0xff4d4b45); + } else if (color == 2) { + embedPostDatePaint.setColor(0xff666666); + } } embedPostDatePaint.setTextSize(AndroidUtilities.dp(14)); paint = embedPostDatePaint; } + } else if (parentBlock instanceof TLRPC.TL_pageBlockChannel) { + if (channelNamePaint == null) { + channelNamePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + if (channelBlock == null) { + channelNamePaint.setColor(getTextColor()); + } else { + channelNamePaint.setColor(0xffffffff); + } + channelNamePaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + } + channelNamePaint.setTextSize(AndroidUtilities.dp(15)); + paint = channelNamePaint; + } else if (parentBlock instanceof TLRPC.TL_pageBlockList && plainText != null) { + if (listTextPointerPaint == null) { + listTextPointerPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + listTextPointerPaint.setColor(getTextColor()); + } + int additionalSize; + if (selectedFontSize == 0) { + additionalSize = -AndroidUtilities.dp(4); + } else if (selectedFontSize == 1) { + additionalSize = -AndroidUtilities.dp(2); + } else if (selectedFontSize == 3) { + additionalSize = AndroidUtilities.dp(2); + } else if (selectedFontSize == 4) { + additionalSize = AndroidUtilities.dp(4); + } else { + additionalSize = 0; + } + listTextPointerPaint.setTextSize(AndroidUtilities.dp(15) + additionalSize); + paint = listTextPointerPaint; } else { paint = getTextPaint(richText, richText, parentBlock); } - if (parentBlock instanceof TLRPC.TL_pageBlockPullquote) { + if (parentBlock instanceof TLRPC.TL_pageBlockPullquote || richText != null && parentBlock != null && richText == parentBlock.caption) { return new StaticLayout(text, paint, width, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); } else { return new StaticLayout(text, paint, width, Layout.Alignment.ALIGN_NORMAL, 1.0f, AndroidUtilities.dp(4), false); @@ -1030,11 +1487,22 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } private void drawLayoutLink(Canvas canvas, StaticLayout layout) { - if (canvas == null || pressedLink == null || pressedLinkOwnerLayout != layout) { + if (canvas == null || pressedLinkOwnerLayout != layout) { return; } if (pressedLink != null) { canvas.drawPath(urlPath, urlPaint); + } else if (drawBlockSelection) { + float width; + float x; + if (layout.getLineCount() == 1) { + width = layout.getLineWidth(0); + x = layout.getLineLeft(0); + } else { + width = layout.getWidth(); + x = 0; + } + canvas.drawRect(-AndroidUtilities.dp(2) + x, 0, x + width + AndroidUtilities.dp(2), layout.getHeight(), urlPaint); } } @@ -1042,51 +1510,51 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if (parentView == null || layout == null) { return false; } - CharSequence text = layout.getText(); - if (!(text instanceof Spannable)) { - return false; - } int x = (int) event.getX(); int y = (int) event.getY(); boolean removeLink = false; if (event.getAction() == MotionEvent.ACTION_DOWN) { if (x >= layoutX && x <= layoutX + layout.getWidth() && y >= layoutY && y <= layoutY + layout.getHeight()) { - try { - int checkX = x - layoutX; - int checkY = y - layoutY; - final int line = layout.getLineForVertical(checkY); - final int off = layout.getOffsetForHorizontal(line, checkX); - final float left = layout.getLineLeft(line); - if (left <= checkX && left + layout.getLineWidth(line) >= checkX) { - Spannable buffer = (Spannable) layout.getText(); - TextPaintUrlSpan[] link = buffer.getSpans(off, off, TextPaintUrlSpan.class); - if (link != null && link.length > 0) { - pressedLink = link[0]; - int pressedStart = buffer.getSpanStart(pressedLink); - int pressedEnd = buffer.getSpanEnd(pressedLink); - for (int a = 1; a < link.length; a++) { - TextPaintUrlSpan span = link[a]; - int start = buffer.getSpanStart(span); - int end = buffer.getSpanEnd(span); - if (pressedStart > start || end > pressedEnd) { - pressedLink = span; - pressedStart = start; - pressedEnd = end; + pressedLinkOwnerLayout = layout; + pressedLinkOwnerView = parentView; + pressedLayoutY = layoutY; + CharSequence text = layout.getText(); + if (text instanceof Spannable) { + try { + int checkX = x - layoutX; + int checkY = y - layoutY; + final int line = layout.getLineForVertical(checkY); + final int off = layout.getOffsetForHorizontal(line, checkX); + final float left = layout.getLineLeft(line); + if (left <= checkX && left + layout.getLineWidth(line) >= checkX) { + Spannable buffer = (Spannable) layout.getText(); + TextPaintUrlSpan[] link = buffer.getSpans(off, off, TextPaintUrlSpan.class); + if (link != null && link.length > 0) { + pressedLink = link[0]; + int pressedStart = buffer.getSpanStart(pressedLink); + int pressedEnd = buffer.getSpanEnd(pressedLink); + for (int a = 1; a < link.length; a++) { + TextPaintUrlSpan span = link[a]; + int start = buffer.getSpanStart(span); + int end = buffer.getSpanEnd(span); + if (pressedStart > start || end > pressedEnd) { + pressedLink = span; + pressedStart = start; + pressedEnd = end; + } + } + try { + urlPath.setCurrentLayout(layout, pressedStart, 0); + layout.getSelectionPath(pressedStart, pressedEnd, urlPath); + parentView.invalidate(); + } catch (Exception e) { + FileLog.e(e); } } - pressedLinkOwnerLayout = layout; - pressedLinkOwnerView = parentView; - try { - urlPath.setCurrentLayout(layout, pressedStart, 0); - layout.getSelectionPath(pressedStart, pressedEnd, urlPath); - parentView.invalidate(); - } catch (Exception e) { - FileLog.e(e); - } } + } catch (Exception e) { + FileLog.e(e); } - } catch (Exception e) { - FileLog.e(e); } } } else if (event.getAction() == MotionEvent.ACTION_UP) { @@ -1144,19 +1612,19 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { removeLink = true; } - if (removeLink && pressedLink != null) { + if (removeLink) { pressedLink = null; pressedLinkOwnerLayout = null; pressedLinkOwnerView = null; parentView.invalidate(); } - if (pressedLink != null && event.getAction() == MotionEvent.ACTION_DOWN) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { startCheckLongPress(); } if (event.getAction() != MotionEvent.ACTION_DOWN && event.getAction() != MotionEvent.ACTION_MOVE) { cancelCheckLongPress(); } - return pressedLink != null; + return pressedLinkOwnerLayout != null; } private TLRPC.Photo getPhotoWithId(long id) { @@ -1182,8 +1650,8 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if (currentPage.document != null && currentPage.document.id == id) { return currentPage.document; } - for (int a = 0; a < currentPage.cached_page.videos.size(); a++) { - TLRPC.Document document = currentPage.cached_page.videos.get(a); + for (int a = 0; a < currentPage.cached_page.documents.size(); a++) { + TLRPC.Document document = currentPage.cached_page.documents.get(a); if (document.id == id) { return document; } @@ -1227,17 +1695,275 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if (captionTextView != null) { captionTextView.invalidate(); } + } else if (id == NotificationCenter.messagePlayingDidStarted) { + MessageObject messageObject = (MessageObject) args[0]; + + if (listView != null) { + int count = listView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = listView.getChildAt(a); + if (view instanceof BlockAudioCell) { + BlockAudioCell cell = (BlockAudioCell) view; + cell.updateButtonState(false); + } + } + } + } else if (id == NotificationCenter.messagePlayingDidReset || id == NotificationCenter.messagePlayingPlayStateChanged) { + if (listView != null) { + int count = listView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = listView.getChildAt(a); + if (view instanceof BlockAudioCell) { + BlockAudioCell cell = (BlockAudioCell) view; + MessageObject messageObject = cell.getMessageObject(); + if (messageObject != null) { + cell.updateButtonState(false); + } + } + } + } + } else if (id == NotificationCenter.messagePlayingProgressDidChanged) { + Integer mid = (Integer) args[0]; + if (listView != null) { + int count = listView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = listView.getChildAt(a); + if (view instanceof BlockAudioCell) { + BlockAudioCell cell = (BlockAudioCell) view; + MessageObject playing = cell.getMessageObject(); + if (playing != null && playing.getId() == mid) { + MessageObject player = MediaController.getInstance().getPlayingMessageObject(); + if (player != null) { + playing.audioProgress = player.audioProgress; + playing.audioProgressSec = player.audioProgressSec; + cell.updatePlayingMessageProgress(); + } + break; + } + } + } + } } } - public void setParentActivity(Activity activity) { + private void updatePaintSize() { + ApplicationLoader.applicationContext.getSharedPreferences("articles", Activity.MODE_PRIVATE).edit().putInt("font_size", selectedFontSize).commit(); + adapter.notifyDataSetChanged(); + } + + private void updatePaintFonts() { + ApplicationLoader.applicationContext.getSharedPreferences("articles", Activity.MODE_PRIVATE).edit().putInt("font_type", selectedFont).commit(); + Typeface typefaceNormal = selectedFont == 0 ? Typeface.DEFAULT : Typeface.SERIF; + Typeface typefaceItalic = selectedFont == 0 ? AndroidUtilities.getTypeface("fonts/ritalic.ttf") : Typeface.create("serif", Typeface.ITALIC); + Typeface typefaceBold = selectedFont == 0 ? AndroidUtilities.getTypeface("fonts/rmedium.ttf") : Typeface.create("serif", Typeface.BOLD); + Typeface typefaceBoldItalic = selectedFont == 0 ? AndroidUtilities.getTypeface("fonts/rmediumitalic.ttf") : Typeface.create("serif", Typeface.BOLD_ITALIC); + + for (HashMap.Entry entry : quoteTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : preformattedTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : paragraphTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : listTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : embedPostTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : videoTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : captionTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : authorTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : footerTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : subquoteTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : embedPostCaptionTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : embedTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + for (HashMap.Entry entry : slideshowTextPaints.entrySet()) { + updateFontEntry(entry, typefaceNormal, typefaceBoldItalic, typefaceBold, typefaceItalic); + } + } + + private void updateFontEntry(HashMap.Entry entry, Typeface typefaceNormal, Typeface typefaceBoldItalic, Typeface typefaceBold, Typeface typefaceItalic) { + Integer flags = entry.getKey(); + TextPaint paint = entry.getValue(); + if ((flags & TEXT_FLAG_MEDIUM) != 0 && (flags & TEXT_FLAG_ITALIC) != 0) { + paint.setTypeface(typefaceBoldItalic); + } else if ((flags & TEXT_FLAG_MEDIUM) != 0) { + paint.setTypeface(typefaceBold); + } else if ((flags & TEXT_FLAG_ITALIC) != 0) { + paint.setTypeface(typefaceItalic); + } else { + paint.setTypeface(typefaceNormal); + } + } + + private int getSelectedColor() { + int currentColor = selectedColor; + if (nightModeEnabled && currentColor != 2) { + int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); + if (hour >= 22 && hour <= 24 || hour >= 0 && hour <= 6) { + currentColor = 2; + } + } + return currentColor; + } + + private void updatePaintColors() { + ApplicationLoader.applicationContext.getSharedPreferences("articles", Activity.MODE_PRIVATE).edit().putInt("font_color", selectedColor).commit(); + int currentColor = getSelectedColor(); + if (currentColor == 0) { + backgroundPaint.setColor(0xffffffff); + listView.setGlowColor(0xfff5f6f7); + } else if (currentColor == 1) { + backgroundPaint.setColor(0xfff5efdc); + listView.setGlowColor(0xfff5efdc); + } else if (currentColor == 2) { + backgroundPaint.setColor(0xff141414); + listView.setGlowColor(0xff141414); + } + + for (int a = 0; a < Theme.chat_ivStatesDrawable.length; a++) { + Theme.setCombinedDrawableColor(Theme.chat_ivStatesDrawable[a][0], getTextColor(), false); + Theme.setCombinedDrawableColor(Theme.chat_ivStatesDrawable[a][0], getTextColor(), true); + Theme.setCombinedDrawableColor(Theme.chat_ivStatesDrawable[a][1], getTextColor(), false); + Theme.setCombinedDrawableColor(Theme.chat_ivStatesDrawable[a][1], getTextColor(), true); + } + + if (quoteLinePaint != null) { + quoteLinePaint.setColor(getTextColor()); + } + if (preformattedBackgroundPaint != null) { + if (currentColor == 0) { + preformattedBackgroundPaint.setColor(0xfff5f8fc); + } else if (currentColor == 1) { + preformattedBackgroundPaint.setColor(0xffe5dec8); + } else if (currentColor == 2) { + preformattedBackgroundPaint.setColor(0xff262626); + } + } + if (urlPaint != null) { + if (currentColor == 0) { + urlPaint.setColor(0xffebebeb); + } else if (currentColor == 1) { + urlPaint.setColor(0xffe5dec8); + } else if (currentColor == 2) { + urlPaint.setColor(0xff262626); + } + } + + if (embedPostAuthorPaint != null) { + embedPostAuthorPaint.setColor(getTextColor()); + } + if (channelNamePaint != null) { + if (channelBlock == null) { + channelNamePaint.setColor(getTextColor()); + } else { + channelNamePaint.setColor(0xffffffff); + } + } + if (embedPostDatePaint != null) { + if (currentColor == 0) { + embedPostDatePaint.setColor(0xff8f97a0); + } else if (currentColor == 1) { + embedPostDatePaint.setColor(0xff4d4b45); + } else if (currentColor == 2) { + embedPostDatePaint.setColor(0xff666666); + } + } + if (dividerPaint != null) { + if (currentColor == 0) { + dividerPaint.setColor(0xffcdd1d5); + } else if (currentColor == 1) { + dividerPaint.setColor(0xffc1baa5); + } else if (currentColor == 2) { + dividerPaint.setColor(0xff444444); + } + } + + for (HashMap.Entry entry : titleTextPaints.entrySet()) { + entry.getValue().setColor(getTextColor()); + } + for (HashMap.Entry entry : subtitleTextPaints.entrySet()) { + entry.getValue().setColor(getTextColor()); + } + for (HashMap.Entry entry : headerTextPaints.entrySet()) { + entry.getValue().setColor(getTextColor()); + } + for (HashMap.Entry entry : subheaderTextPaints.entrySet()) { + entry.getValue().setColor(getTextColor()); + } + for (HashMap.Entry entry : quoteTextPaints.entrySet()) { + entry.getValue().setColor(getTextColor()); + } + for (HashMap.Entry entry : preformattedTextPaints.entrySet()) { + entry.getValue().setColor(getTextColor()); + } + for (HashMap.Entry entry : paragraphTextPaints.entrySet()) { + entry.getValue().setColor(getTextColor()); + } + for (HashMap.Entry entry : listTextPaints.entrySet()) { + entry.getValue().setColor(getTextColor()); + } + for (HashMap.Entry entry : embedPostTextPaints.entrySet()) { + entry.getValue().setColor(getTextColor()); + } + for (HashMap.Entry entry : videoTextPaints.entrySet()) { + entry.getValue().setColor(getTextColor()); + } + for (HashMap.Entry entry : captionTextPaints.entrySet()) { + entry.getValue().setColor(getGrayTextColor()); + } + for (HashMap.Entry entry : authorTextPaints.entrySet()) { + entry.getValue().setColor(getGrayTextColor()); + } + for (HashMap.Entry entry : footerTextPaints.entrySet()) { + entry.getValue().setColor(getGrayTextColor()); + } + for (HashMap.Entry entry : subquoteTextPaints.entrySet()) { + entry.getValue().setColor(getGrayTextColor()); + } + for (HashMap.Entry entry : embedPostCaptionTextPaints.entrySet()) { + entry.getValue().setColor(getGrayTextColor()); + } + for (HashMap.Entry entry : embedTextPaints.entrySet()) { + entry.getValue().setColor(getGrayTextColor()); + } + for (HashMap.Entry entry : slideshowTextPaints.entrySet()) { + entry.getValue().setColor(getGrayTextColor()); + } + } + + public void setParentActivity(Activity activity, BaseFragment fragment) { + parentFragment = fragment; if (parentActivity == activity) { + updatePaintColors(); return; } parentActivity = activity; + SharedPreferences sharedPreferences = ApplicationLoader.applicationContext.getSharedPreferences("articles", Activity.MODE_PRIVATE); + selectedFontSize = sharedPreferences.getInt("font_size", 2); + selectedFont = sharedPreferences.getInt("font_type", 0); + selectedColor = sharedPreferences.getInt("font_color", 0); + nightModeEnabled = sharedPreferences.getBoolean("nightModeEnabled", false); + backgroundPaint = new Paint(); - backgroundPaint.setColor(0xffffffff); layerShadowDrawable = activity.getResources().getDrawable(R.drawable.layer_shadow); slideDotDrawable = activity.getResources().getDrawable(R.drawable.slide_dot_small); @@ -1310,13 +2036,32 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg windowView.addView(barBackground); } - listView = new RecyclerListView(activity); + listView = new RecyclerListView(activity) { + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + int count = getChildCount(); + for (int a = 0; a < count; a++) { + View child = getChildAt(a); + if (child.getTag() instanceof Integer) { + Integer tag = (Integer) child.getTag(); + if (tag == 90) { + int bottom = child.getBottom(); + if (bottom < getMeasuredHeight()) { + int height = getMeasuredHeight(); + child.layout(0, height - child.getMeasuredHeight(), child.getMeasuredWidth(), height); + break; + } + } + } + } + } + }; listView.setLayoutManager(layoutManager = new LinearLayoutManager(parentActivity, LinearLayoutManager.VERTICAL, false)); listView.setAdapter(adapter = new WebpageAdapter(parentActivity)); listView.setClipToPadding(false); listView.setPadding(0, AndroidUtilities.dp(56), 0, 0); listView.setTopGlowOffset(AndroidUtilities.dp(56)); - listView.setGlowColor(0xfff5f6f7); containerView.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView.setOnItemLongClickListener(new RecyclerListView.OnItemLongClickListener() { @Override @@ -1331,9 +2076,9 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if (previewsReqId != 0) { return; } - TLRPC.User user = MessagesController.getInstance().getUser("previews"); - if (user != null) { - openPreviewsChat(user, currentPage.id); + TLObject object = MessagesController.getInstance().getUserOrChat("previews"); + if (object instanceof TLRPC.TL_user) { + openPreviewsChat((TLRPC.User) object, currentPage.id); } else { final long pageId = currentPage.id; showProgressView(true); @@ -1363,6 +2108,12 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } }); } + } else if (position >= 0 && position < blocks.size()) { + TLRPC.PageBlock pageBlock = blocks.get(position); + if (pageBlock instanceof TLRPC.TL_pageBlockChannel) { + MessagesController.openByUserName(pageBlock.channel.username, parentFragment, 2); + close(false, true); + } } } }); @@ -1406,6 +2157,123 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } }); + LinearLayout settingsContainer = new LinearLayout(parentActivity); + settingsContainer.setPadding(0, AndroidUtilities.dp(4), 0, AndroidUtilities.dp(4)); + settingsContainer.setOrientation(LinearLayout.VERTICAL); + for (int a = 0; a < 3; a++) { + colorCells[a] = new ColorCell(parentActivity); + switch (a) { + case 0: + nightModeImageView = new ImageView(parentActivity); + nightModeImageView.setScaleType(ImageView.ScaleType.CENTER); + nightModeImageView.setImageResource(R.drawable.moon); + nightModeImageView.setColorFilter(new PorterDuffColorFilter(nightModeEnabled && selectedColor != 2 ? 0xff1495e9 : 0xffcccccc, PorterDuff.Mode.MULTIPLY)); + nightModeImageView.setBackgroundDrawable(Theme.createSelectorDrawable(0x0f000000)); + colorCells[a].addView(nightModeImageView, LayoutHelper.createFrame(48, 48, Gravity.TOP | Gravity.RIGHT)); + nightModeImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + nightModeEnabled = !nightModeEnabled; + ApplicationLoader.applicationContext.getSharedPreferences("articles", Activity.MODE_PRIVATE).edit().putBoolean("nightModeEnabled", nightModeEnabled).commit(); + updateNightModeButton(); + updatePaintColors(); + adapter.notifyDataSetChanged(); + if (nightModeEnabled) { + showNightModeHint(); + } + } + }); + colorCells[a].setTextAndColor(LocaleController.getString("ColorWhite", R.string.ColorWhite), 0xffffffff); + break; + case 1: + colorCells[a].setTextAndColor(LocaleController.getString("ColorSepia", R.string.ColorSepia), 0xffeae5c9); + break; + case 2: + colorCells[a].setTextAndColor(LocaleController.getString("ColorDark", R.string.ColorDark), 0xff232323); + break; + } + colorCells[a].select(a == selectedColor); + colorCells[a].setTag(a); + colorCells[a].setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int num = (Integer) v.getTag(); + selectedColor = num; + for (int a = 0; a < 3; a++) { + colorCells[a].select(a == num); + } + updateNightModeButton(); + updatePaintColors(); + adapter.notifyDataSetChanged(); + } + }); + settingsContainer.addView(colorCells[a], LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); + } + updateNightModeButton(); + View divider = new View(parentActivity); + divider.setBackgroundColor(0xffe0e0e0); + settingsContainer.addView(divider, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 1, 15, 4, 15, 4)); + divider.getLayoutParams().height = 1; + + for (int a = 0; a < 2; a++) { + fontCells[a] = new FontCell(parentActivity); + switch (a) { + case 0: + fontCells[a].setTextAndTypeface("Roboto", Typeface.DEFAULT); + break; + case 1: + fontCells[a].setTextAndTypeface("Serif", Typeface.SERIF); + break; + } + fontCells[a].select(a == selectedFont); + fontCells[a].setTag(a); + fontCells[a].setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int num = (Integer) v.getTag(); + selectedFont = num; + for (int a = 0; a < 2; a++) { + fontCells[a].select(a == num); + } + updatePaintFonts(); + adapter.notifyDataSetChanged(); + } + }); + settingsContainer.addView(fontCells[a], LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); + } + divider = new View(parentActivity); + divider.setBackgroundColor(0xffe0e0e0); + settingsContainer.addView(divider, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 1, 15, 4, 15, 4)); + divider.getLayoutParams().height = 1; + + TextView textView = new TextView(parentActivity); + textView.setTextColor(0xff212121); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setLines(1); + textView.setMaxLines(1); + textView.setSingleLine(true); + textView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP); + textView.setText(LocaleController.getString("FontSize", R.string.FontSize)); + settingsContainer.addView(textView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, 17, 12, 17, 0)); + + SizeChooseView sizeChooseView = new SizeChooseView(parentActivity); + settingsContainer.addView(sizeChooseView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 38, 0, 0, 0, 1)); + + settingsButton = new ActionBarMenuItem(parentActivity, null, Theme.ACTION_BAR_WHITE_SELECTOR_COLOR, 0xffffffff); + settingsButton.setPopupAnimationEnabled(false); + settingsButton.setLayoutInScreen(true); + textView = new TextView(parentActivity); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + textView.setText("Aa"); + textView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + textView.setTextColor(0xffb3b3b3); + textView.setGravity(Gravity.CENTER); + settingsButton.addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + settingsButton.addSubItem(settingsContainer, AndroidUtilities.dp(220), LayoutHelper.WRAP_CONTENT); + settingsButton.redrawPopup(0xffffffff); + headerView.addView(settingsButton, LayoutHelper.createFrame(48, 56, Gravity.TOP | Gravity.RIGHT, 0, 0, 56, 0)); + shareContainer = new FrameLayout(activity); headerView.addView(shareContainer, LayoutHelper.createFrame(48, 56, Gravity.TOP | Gravity.RIGHT)); shareContainer.setOnClickListener(new View.OnClickListener() { @@ -1658,6 +2526,77 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg rightImage.setCrossfadeAlpha((byte) 2); rightImage.setInvalidateAll(true); rightImage.setDelegate(imageReceiverDelegate); + + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidReset); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingPlayStateChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidStarted); + + int color = getSelectedColor(); + if (color == 0) { + backgroundPaint.setColor(0xffffffff); + listView.setGlowColor(0xfff5f6f7); + } else if (color == 1) { + backgroundPaint.setColor(0xfff5efdc); + listView.setGlowColor(0xfff5efdc); + } else if (color == 2) { + backgroundPaint.setColor(0xff141414); + listView.setGlowColor(0xff141414); + } + + for (int a = 0; a < Theme.chat_ivStatesDrawable.length; a++) { + Theme.setCombinedDrawableColor(Theme.chat_ivStatesDrawable[a][0], getTextColor(), false); + Theme.setCombinedDrawableColor(Theme.chat_ivStatesDrawable[a][0], getTextColor(), true); + Theme.setCombinedDrawableColor(Theme.chat_ivStatesDrawable[a][1], getTextColor(), false); + Theme.setCombinedDrawableColor(Theme.chat_ivStatesDrawable[a][1], getTextColor(), true); + } + } + + private void showNightModeHint() { + if (parentActivity == null || nightModeHintView != null || !nightModeEnabled) { + return; + } + nightModeHintView = new FrameLayout(parentActivity); + nightModeHintView.setBackgroundColor(0xff333333); + containerView.addView(nightModeHintView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.BOTTOM | Gravity.LEFT)); + + ImageView nightModeImageView = new ImageView(parentActivity); + nightModeImageView.setScaleType(ImageView.ScaleType.CENTER); + nightModeImageView.setImageResource(R.drawable.moon); + nightModeHintView.addView(nightModeImageView, LayoutHelper.createFrame(56, 56, Gravity.LEFT | Gravity.CENTER_VERTICAL)); + + TextView textView = new TextView(parentActivity); + textView.setText(LocaleController.getString("InstantViewNightMode", R.string.InstantViewNightMode)); + textView.setTextColor(0xffffffff); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + nightModeHintView.addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 56, 11, 10, 12)); + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(nightModeHintView, "translationY", AndroidUtilities.dp(100), 0)); + animatorSet.setInterpolator(new DecelerateInterpolator(1.5f)); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(nightModeHintView, "translationY", AndroidUtilities.dp(100))); + animatorSet.setInterpolator(new DecelerateInterpolator(1.5f)); + animatorSet.setDuration(250); + animatorSet.start(); + } + }, 3000); + } + }); + animatorSet.setDuration(250); + animatorSet.start(); + } + + private void updateNightModeButton() { + nightModeImageView.setEnabled(selectedColor != 2); + nightModeImageView.setAlpha(selectedColor == 2 ? 0.5f : 1.0f); + nightModeImageView.setColorFilter(new PorterDuffColorFilter(nightModeEnabled && selectedColor != 2 ? 0xff1495e9 : 0xffcccccc, PorterDuff.Mode.MULTIPLY)); } private void checkScroll(int dy) { @@ -1678,7 +2617,10 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg backButton.setTranslationY((maxHeight - currentHeaderHeight) / 2); shareContainer.setScaleX(scale); shareContainer.setScaleY(scale); + settingsButton.setScaleX(scale); + settingsButton.setScaleY(scale); shareContainer.setTranslationY((maxHeight - currentHeaderHeight) / 2); + settingsButton.setTranslationY((maxHeight - currentHeaderHeight) / 2); headerView.setTranslationY(currentHeaderHeight - maxHeight); listView.setTopGlowOffset(currentHeaderHeight); } @@ -1782,6 +2724,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg captionTextViewOld.setVisibility(View.GONE); shareContainer.setAlpha(0.0f); backButton.setAlpha(0.0f); + settingsButton.setAlpha(0.0f); layoutManager.scrollToPositionWithOffset(0, 0); checkScroll(-AndroidUtilities.dp(56)); @@ -1898,7 +2841,8 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( ObjectAnimator.ofFloat(backButton, "alpha", 0.0f), - ObjectAnimator.ofFloat(shareContainer, "alpha", 0.0f)); + ObjectAnimator.ofFloat(shareContainer, "alpha", 0.0f), + ObjectAnimator.ofFloat(settingsButton, "alpha", 0.0f)); animatorSet.setDuration(250); animatorSet.setInterpolator(new DecelerateInterpolator()); animatorSet.start(); @@ -1908,7 +2852,8 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( ObjectAnimator.ofFloat(backButton, "alpha", 1.0f), - ObjectAnimator.ofFloat(shareContainer, "alpha", 1.0f)); + ObjectAnimator.ofFloat(shareContainer, "alpha", 1.0f), + ObjectAnimator.ofFloat(settingsButton, "alpha", 1.0f)); animatorSet.setDuration(150); animatorSet.setStartDelay(delay); animatorSet.start(); @@ -2129,6 +3074,10 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if (parentActivity == null || !isVisible || checkAnimation()) { return; } + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidReset); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingPlayStateChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidStarted); if (fullscreenVideoContainer.getVisibility() == View.VISIBLE) { if (customView != null) { fullscreenVideoContainer.setVisibility(View.INVISIBLE); @@ -2165,6 +3114,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } } + parentFragment = null; try { if (visibleDialog != null) { visibleDialog.dismiss(); @@ -2242,6 +3192,95 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg }); } + private void loadChannel(final BlockChannelCell cell, TLRPC.Chat channel) { + if (loadingChannel || TextUtils.isEmpty(channel.username)) { + return; + } + loadingChannel = true; + TLRPC.TL_contacts_resolveUsername req = new TLRPC.TL_contacts_resolveUsername(); + req.username = channel.username; + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + loadingChannel = false; + if (parentFragment == null || blocks == null || blocks.isEmpty()) { + return; + } + if (error == null) { + TLRPC.TL_contacts_resolvedPeer res = (TLRPC.TL_contacts_resolvedPeer) response; + if (!res.chats.isEmpty()) { + MessagesController.getInstance().putUsers(res.users, false); + MessagesController.getInstance().putChats(res.chats, false); + MessagesStorage.getInstance().putUsersAndChats(res.users, res.chats, false, true); + loadedChannel = res.chats.get(0); + if (loadedChannel.left && !loadedChannel.kicked) { + cell.setState(0, false); + } else { + cell.setState(4, false); + } + } else { + cell.setState(4, false); + } + } else { + cell.setState(4, false); + } + } + }); + } + }); + } + + private void joinChannel(final BlockChannelCell cell, final TLRPC.Chat channel) { + final TLRPC.TL_channels_joinChannel req = new TLRPC.TL_channels_joinChannel(); + req.channel = MessagesController.getInputChannel(channel); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, final TLRPC.TL_error error) { + if (error != null) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + cell.setState(0, false); + AlertsCreator.processError(error, parentFragment, req, true); + } + }); + return; + } + boolean hasJoinMessage = false; + TLRPC.Updates updates = (TLRPC.Updates) response; + for (int a = 0; a < updates.updates.size(); a++) { + TLRPC.Update update = updates.updates.get(a); + if (update instanceof TLRPC.TL_updateNewChannelMessage) { + if (((TLRPC.TL_updateNewChannelMessage) update).message.action instanceof TLRPC.TL_messageActionChatAddUser) { + hasJoinMessage = true; + break; + } + } + } + MessagesController.getInstance().processUpdates(updates, false); + if (!hasJoinMessage) { + MessagesController.getInstance().generateJoinMessage(channel.id, true); + } + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + cell.setState(2, false); + } + }); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().loadFullChat(channel.id, 0, true); + } + }, 1000); + MessagesStorage.getInstance().updateDialogsWithDeletedMessages(new ArrayList(), null, true, channel.id); + } + }); + } + private boolean checkAnimation() { if (animationInProgress != 0) { if (Math.abs(transitionAnimationStartTime - System.currentTimeMillis()) >= 500) { @@ -2279,6 +3318,8 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } catch (Exception e) { FileLog.e(e); } + parentActivity = null; + parentFragment = null; Instance = null; } @@ -2394,10 +3435,18 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg view = new BlockEmbedPostCell(context); break; } - case 17:{ + case 17: { view = new BlockCollageCell(context); break; } + case 18: { + view = new BlockChannelCell(context, 0); + break; + } + case 19: { + view = new BlockAudioCell(context); + break; + } case 90: default: { FrameLayout frameLayout = new FrameLayout(context) { @@ -2406,10 +3455,9 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(44), MeasureSpec.EXACTLY)); } }; + frameLayout.setTag(90); TextView textView = new TextView(context); frameLayout.addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 34, Gravity.LEFT | Gravity.TOP, 0, 10, 0, 0)); - textView.setTextColor(0xff78828d); - textView.setBackgroundColor(0xffedeff0); textView.setText(LocaleController.getString("PreviewFeedback", R.string.PreviewFeedback)); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); textView.setGravity(Gravity.CENTER); @@ -2526,6 +3574,33 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg cell.setBlock((TLRPC.TL_pageBlockCollage) block); break; } + case 18: { + BlockChannelCell cell = (BlockChannelCell) holder.itemView; + cell.setBlock((TLRPC.TL_pageBlockChannel) block); + break; + } + case 19: { + BlockAudioCell cell = (BlockAudioCell) holder.itemView; + cell.setBlock((TLRPC.TL_pageBlockAudio) block, position == 0, position == blocks.size() - 1); + } + } + } else { + switch (holder.getItemViewType()) { + case 90: { + TextView textView = (TextView) ((ViewGroup) holder.itemView).getChildAt(0); + int color = getSelectedColor(); + if (color == 0) { + textView.setTextColor(0xff78828d); + textView.setBackgroundColor(0xffedeff0); + } else if (color == 1) { + textView.setTextColor(getGrayTextColor()); + textView.setBackgroundColor(0xffe5dec8); + } else if (color == 2) { + textView.setTextColor(getGrayTextColor()); + textView.setBackgroundColor(0xff262626); + } + break; + } } } } @@ -2567,6 +3642,10 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg return 16; } else if (block instanceof TLRPC.TL_pageBlockCollage) { return 17; + } else if (block instanceof TLRPC.TL_pageBlockChannel) { + return 18; + } else if (block instanceof TLRPC.TL_pageBlockAudio) { + return 19; } else if (block instanceof TLRPC.TL_pageBlockCover) { return getTypeForBlock(block.cover); } @@ -2587,11 +3666,12 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } } - private class BlockVideoCell extends View implements MediaController.FileDownloadProgressListener { + private class BlockVideoCell extends FrameLayout implements MediaController.FileDownloadProgressListener { private StaticLayout textLayout; private ImageReceiver imageView; private RadialProgress radialProgress; + private BlockChannelCell channelCell; private int lastCreatedWidth; private int currentType; private boolean isFirst; @@ -2616,11 +3696,15 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg public BlockVideoCell(Context context, int type) { super(context); + setWillNotDraw(false); imageView = new ImageReceiver(this); currentType = type; radialProgress = new RadialProgress(this); + radialProgress.setAlphaForPrevious(true); radialProgress.setProgressColor(Theme.ARTICLE_VIEWER_MEDIA_PROGRESS_COLOR); TAG = MediaController.getInstance().generateObserverTag(); + channelCell = new BlockChannelCell(context, 1); + addView(channelCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); } public void setBlock(TLRPC.TL_pageBlockVideo block, boolean first, boolean last) { @@ -2632,19 +3716,34 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg lastCreatedWidth = 0; isFirst = first; isLast = last; + channelCell.setVisibility(INVISIBLE); updateButtonState(false); requestLayout(); } public void setParentBlock(TLRPC.PageBlock block) { parentBlock = block; + if (channelBlock != null && parentBlock instanceof TLRPC.TL_pageBlockCover) { + channelCell.setBlock(channelBlock); + channelCell.setVisibility(VISIBLE); + } + } + + public View getChannelCell() { + return channelCell; } @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); - + if (channelCell.getVisibility() == VISIBLE && y > channelCell.getTranslationY() && y < channelCell.getTranslationY() + AndroidUtilities.dp(39)) { + if (channelBlock != null && event.getAction() == MotionEvent.ACTION_UP) { + MessagesController.openByUserName(channelBlock.channel.username, parentFragment, 2); + close(false, true); + } + return true; + } if (event.getAction() == MotionEvent.ACTION_DOWN && imageView.isInsideImage(x, y)) { if (buttonState != -1 && x >= buttonX && x <= buttonX + AndroidUtilities.dp(48) && y >= buttonY && y <= buttonY + AndroidUtilities.dp(48) || buttonState == 0) { buttonPressed = 1; @@ -2735,12 +3834,15 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if (!isFirst && currentType == 0 && currentBlock.level <= 0) { height += AndroidUtilities.dp(8); } - if (currentType != 2) { + boolean nextIsChannel = parentBlock instanceof TLRPC.TL_pageBlockCover && blocks != null && blocks.size() > 1 && blocks.get(1) instanceof TLRPC.TL_pageBlockChannel; + if (currentType != 2 && !nextIsChannel) { height += AndroidUtilities.dp(8); } } else { height = 1; } + channelCell.measure(widthMeasureSpec, heightMeasureSpec); + channelCell.setTranslationY(imageView.getImageHeight() - AndroidUtilities.dp(39)); setMeasuredDimension(width, height); } @@ -2767,7 +3869,6 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } private Drawable getDrawableForCurrentState() { - radialProgress.setAlphaForPrevious(true); if (buttonState >= 0 && buttonState < 4) { return Theme.chat_photoStatesDrawables[buttonState][buttonPressed]; } @@ -2881,6 +3982,351 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } } + private class BlockAudioCell extends View implements MediaController.FileDownloadProgressListener { + + private StaticLayout textLayout; + private RadialProgress radialProgress; + private SeekBar seekBar; + private int lastCreatedWidth; + private boolean isFirst; + private boolean isLast; + private int textX; + private int textY = AndroidUtilities.dp(54); + + private String lastTimeString; + + private StaticLayout titleLayout; + private StaticLayout durationLayout; + + private int seekBarX; + private int seekBarY; + + private int buttonX; + private int buttonY; + private int buttonState; + private int buttonPressed; + + private int TAG; + + private TLRPC.TL_pageBlockAudio currentBlock; + private TLRPC.Document currentDocument; + private MessageObject currentMessageObject; + + public BlockAudioCell(Context context) { + super(context); + + radialProgress = new RadialProgress(this); + radialProgress.setAlphaForPrevious(true); + radialProgress.setDiff(AndroidUtilities.dp(0)); + radialProgress.setStrikeWidth(AndroidUtilities.dp(2)); + TAG = MediaController.getInstance().generateObserverTag(); + + seekBar = new SeekBar(context); + + seekBar.setDelegate(new SeekBar.SeekBarDelegate() { + @Override + public void onSeekBarDrag(float progress) { + if (currentMessageObject == null) { + return; + } + currentMessageObject.audioProgress = progress; + MediaController.getInstance().seekToProgress(currentMessageObject, progress); + } + }); + } + + public void setBlock(TLRPC.TL_pageBlockAudio block, boolean first, boolean last) { + currentBlock = block; + currentDocument = getDocumentWithId(currentBlock.audio_id); + + TLRPC.TL_message message = new TLRPC.TL_message(); + message.out = true; + message.id = currentBlock.mid; + message.to_id = new TLRPC.TL_peerUser(); + message.to_id.user_id = message.from_id = UserConfig.getClientUserId(); + message.date = (int) (System.currentTimeMillis() / 1000); + message.message = "-1"; + message.media = new TLRPC.TL_messageMediaDocument(); + message.media.document = currentDocument; + message.flags |= TLRPC.MESSAGE_FLAG_HAS_MEDIA | TLRPC.MESSAGE_FLAG_HAS_FROM_ID; + currentMessageObject = new MessageObject(message, null, false); + + lastCreatedWidth = 0; + isFirst = first; + isLast = last; + + radialProgress.setProgressColor(getTextColor()); + seekBar.setColors(getTextColor() & 0x3fffffff, getTextColor(), getTextColor()); + + updateButtonState(false); + requestLayout(); + } + + public MessageObject getMessageObject() { + return currentMessageObject; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + + boolean result = seekBar.onTouch(event.getAction(), event.getX() - seekBarX, event.getY() - seekBarY); + if (result) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + getParent().requestDisallowInterceptTouchEvent(true); + } + invalidate(); + return true; + } + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (buttonState != -1 && x >= buttonX && x <= buttonX + AndroidUtilities.dp(48) && y >= buttonY && y <= buttonY + AndroidUtilities.dp(48) || buttonState == 0) { + buttonPressed = 1; + invalidate(); + } + } else if (event.getAction() == MotionEvent.ACTION_UP) { + if (buttonPressed == 1) { + buttonPressed = 0; + playSoundEffect(SoundEffectConstants.CLICK); + didPressedButton(false); + radialProgress.swapBackground(getDrawableForCurrentState()); + invalidate(); + } + } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { + buttonPressed = 0; + } + return buttonPressed != 0 || checkLayoutForLinks(event, this, textLayout, textX, textY) || super.onTouchEvent(event); + } + + @SuppressLint("DrawAllocation") + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = AndroidUtilities.dp(54); + + if (currentBlock != null) { + int textWidth; + if (currentBlock.level > 0) { + textX = AndroidUtilities.dp(14 * currentBlock.level) + AndroidUtilities.dp(18); + } else { + textX = AndroidUtilities.dp(18); + } + textWidth = width - textX - AndroidUtilities.dp(18); + int size = AndroidUtilities.dp(40); + buttonX = AndroidUtilities.dp(16); + buttonY = AndroidUtilities.dp(7); + currentBlock.caption = new TLRPC.TL_textPlain(); + radialProgress.setProgressRect(buttonX, buttonY, buttonX + size, buttonY + size); + if (lastCreatedWidth != width) { + textLayout = createLayoutForText(null, currentBlock.caption, textWidth, currentBlock); + if (textLayout != null) { + height += AndroidUtilities.dp(8) + textLayout.getHeight(); + } + //lastCreatedWidth = width; + } + if (!isFirst && currentBlock.level <= 0) { + height += AndroidUtilities.dp(8); + } + + String author = currentMessageObject.getMusicAuthor(false); + String title = currentMessageObject.getMusicTitle(false); + seekBarX = buttonX + AndroidUtilities.dp(50) + size; + int w = width - seekBarX - AndroidUtilities.dp(18); + if (!TextUtils.isEmpty(title) || !TextUtils.isEmpty(author)) { + SpannableStringBuilder stringBuilder; + if (!TextUtils.isEmpty(title) && !TextUtils.isEmpty(author)) { + stringBuilder = new SpannableStringBuilder(String.format("%s - %s", author, title)); + } else if (!TextUtils.isEmpty(title)) { + stringBuilder = new SpannableStringBuilder(title); + } else { + stringBuilder = new SpannableStringBuilder(author); + } + if (!TextUtils.isEmpty(author)) { + TypefaceSpan span = new TypefaceSpan(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + stringBuilder.setSpan(span, 0, author.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + CharSequence stringFinal = TextUtils.ellipsize(stringBuilder, Theme.chat_audioTitlePaint, w, TextUtils.TruncateAt.END); + titleLayout = new StaticLayout(stringFinal, audioTimePaint, w, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + seekBarY = buttonY + (size - AndroidUtilities.dp(30)) / 2 + AndroidUtilities.dp(11); + } else { + titleLayout = null; + seekBarY = buttonY + (size - AndroidUtilities.dp(30)) / 2; + } + seekBar.setSize(w, AndroidUtilities.dp(30)); + } else { + height = 1; + } + + setMeasuredDimension(width, height); + updatePlayingMessageProgress(); + } + + @Override + protected void onDraw(Canvas canvas) { + if (currentBlock == null) { + return; + } + radialProgress.draw(canvas); + canvas.save(); + canvas.translate(seekBarX, seekBarY); + seekBar.draw(canvas); + canvas.restore(); + if (durationLayout != null) { + canvas.save(); + canvas.translate(buttonX + AndroidUtilities.dp(54), seekBarY + AndroidUtilities.dp(6)); + durationLayout.draw(canvas); + canvas.restore(); + } + if (titleLayout != null) { + canvas.save(); + canvas.translate(buttonX + AndroidUtilities.dp(54), seekBarY - AndroidUtilities.dp(16)); + titleLayout.draw(canvas); + canvas.restore(); + } + if (textLayout != null) { + canvas.save(); + canvas.translate(textX, textY); + drawLayoutLink(canvas, textLayout); + textLayout.draw(canvas); + canvas.restore(); + } + if (currentBlock.level > 0) { + canvas.drawRect(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(20), getMeasuredHeight() - (currentBlock.bottom ? AndroidUtilities.dp(6) : 0), quoteLinePaint); + } + } + + private Drawable getDrawableForCurrentState() { + return Theme.chat_ivStatesDrawable[buttonState][buttonPressed != 0 ? 1 : 0]; + } + + public void updatePlayingMessageProgress() { + if (currentDocument == null || currentMessageObject == null) { + return; + } + + if (!seekBar.isDragging()) { + seekBar.setProgress(currentMessageObject.audioProgress); + } + + int duration = 0; + + if (MediaController.getInstance().isPlayingMessage(currentMessageObject)) { + duration = currentMessageObject.audioProgressSec; + } else { + for (int a = 0; a < currentDocument.attributes.size(); a++) { + TLRPC.DocumentAttribute attribute = currentDocument.attributes.get(a); + if (attribute instanceof TLRPC.TL_documentAttributeAudio) { + duration = attribute.duration; + break; + } + } + } + String timeString = String.format("%d:%02d", duration / 60, duration % 60); + if (lastTimeString == null || lastTimeString != null && !lastTimeString.equals(timeString)) { + lastTimeString = timeString; + audioTimePaint.setTextSize(AndroidUtilities.dp(16)); + int timeWidth = (int) Math.ceil(audioTimePaint.measureText(timeString)); + durationLayout = new StaticLayout(timeString, audioTimePaint, timeWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + } + audioTimePaint.setColor(getTextColor()); + invalidate(); + } + + public void updateButtonState(boolean animated) { + String fileName = FileLoader.getAttachFileName(currentDocument); + File path = FileLoader.getPathToAttach(currentDocument, true); + boolean fileExists = path.exists(); + if (TextUtils.isEmpty(fileName)) { + radialProgress.setBackground(null, false, false); + return; + } + if (fileExists) { + MediaController.getInstance().removeLoadingFileObserver(this); + boolean playing = MediaController.getInstance().isPlayingMessage(currentMessageObject); + if (!playing || playing && MediaController.getInstance().isMessagePaused()) { + buttonState = 0; + } else { + buttonState = 1; + } + radialProgress.setBackground(getDrawableForCurrentState(), false, animated); + } else { + MediaController.getInstance().addLoadingFileObserver(fileName, null, this); + if (!FileLoader.getInstance().isLoadingFile(fileName)) { + buttonState = 2; + radialProgress.setProgress(0, animated); + radialProgress.setBackground(getDrawableForCurrentState(), false, animated); + } else { + buttonState = 3; + Float progress = ImageLoader.getInstance().getFileProgress(fileName); + if (progress != null) { + radialProgress.setProgress(progress, animated); + } else { + radialProgress.setProgress(0, animated); + } + radialProgress.setBackground(getDrawableForCurrentState(), true, animated); + } + } + updatePlayingMessageProgress(); + } + + private void didPressedButton(boolean animated) { + if (buttonState == 0) { + if (MediaController.getInstance().playMessage(currentMessageObject)) { + buttonState = 1; + radialProgress.setBackground(getDrawableForCurrentState(), false, false); + invalidate(); + } + } else if (buttonState == 1) { + boolean result = MediaController.getInstance().pauseMessage(currentMessageObject); + if (result) { + buttonState = 0; + radialProgress.setBackground(getDrawableForCurrentState(), false, false); + invalidate(); + } + } else if (buttonState == 2) { + radialProgress.setProgress(0, false); + FileLoader.getInstance().loadFile(currentDocument, true, true); + buttonState = 3; + radialProgress.setBackground(getDrawableForCurrentState(), true, false); + invalidate(); + } else if (buttonState == 3) { + FileLoader.getInstance().cancelLoadFile(currentDocument); + buttonState = 2; + radialProgress.setBackground(getDrawableForCurrentState(), false, false); + invalidate(); + } + } + + @Override + public void onFailedDownload(String fileName) { + updateButtonState(true); + } + + @Override + public void onSuccessDownload(String fileName) { + radialProgress.setProgress(1, true); + updateButtonState(true); + } + + @Override + public void onProgressUpload(String fileName, float progress, boolean isEncrypted) { + + } + + @Override + public void onProgressDownload(String fileName, float progress) { + radialProgress.setProgress(progress, true); + if (buttonState != 3) { + updateButtonState(false); + } + } + + @Override + public int getObserverTag() { + return TAG; + } + } + private class BlockEmbedPostCell extends View { private ImageReceiver avatarImageView; @@ -3264,19 +4710,6 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg super.onPageFinished(view, url); //progressBar.setVisibility(INVISIBLE); } - - /*@TargetApi(Build.VERSION_CODES.LOLLIPOP) TODO check - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - Browser.openUrl(parentActivity, request.getUrl()); - return true; - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - Browser.openUrl(parentActivity, url); - return true; - }*/ }); addView(webView); } @@ -3455,7 +4888,6 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg super.requestLayout(); } }; - innerListView.setGlowColor(0xfff5f6f7); innerListView.addItemDecoration(new RecyclerView.ItemDecoration() { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { @@ -3525,6 +4957,14 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg currentBlock = block; lastCreatedWidth = 0; adapter.notifyDataSetChanged(); + int color = getSelectedColor(); + if (color == 0) { + innerListView.setGlowColor(0xfff5f6f7); + } else if (color == 1) { + innerListView.setGlowColor(0xfff5efdc); + } else if (color == 2) { + innerListView.setGlowColor(0xff141414); + } requestLayout(); } @@ -3709,7 +5149,14 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } } }); - AndroidUtilities.setViewPagerEdgeEffectColor(innerListView, 0xfff5f6f7); + int color = getSelectedColor(); + if (color == 0) { + AndroidUtilities.setViewPagerEdgeEffectColor(innerListView, 0xfff5f6f7); + } else if (color == 1) { + AndroidUtilities.setViewPagerEdgeEffectColor(innerListView, 0xfff5efdc); + } else if (color == 2) { + AndroidUtilities.setViewPagerEdgeEffectColor(innerListView, 0xff141414); + } addView(innerListView); dotsContainer = new View(context) { @@ -3956,7 +5403,14 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg super(context); if (dividerPaint == null) { dividerPaint = new Paint(); - dividerPaint.setColor(0xffcdd1d5); + int color = getSelectedColor(); + if (color == 0) { + dividerPaint.setColor(0xffcdd1d5); + } else if (color == 1) { + dividerPaint.setColor(0xffc1baa5); + } else if (color == 2) { + dividerPaint.setColor(0xff444444); + } } } @@ -4185,10 +5639,11 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } } - private class BlockPhotoCell extends View { + private class BlockPhotoCell extends FrameLayout { private StaticLayout textLayout; private ImageReceiver imageView; + private BlockChannelCell channelCell; private int lastCreatedWidth; private int currentType; private boolean isFirst; @@ -4203,7 +5658,10 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg public BlockPhotoCell(Context context, int type) { super(context); + setWillNotDraw(false); imageView = new ImageReceiver(this); + channelCell = new BlockChannelCell(context, 1); + addView(channelCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); currentType = type; //imageView.setAspectFit(currentType == 1); } @@ -4214,17 +5672,33 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg lastCreatedWidth = 0; isFirst = first; isLast = last; + channelCell.setVisibility(INVISIBLE); requestLayout(); } public void setParentBlock(TLRPC.PageBlock block) { parentBlock = block; + if (channelBlock != null && parentBlock instanceof TLRPC.TL_pageBlockCover) { + channelCell.setBlock(channelBlock); + channelCell.setVisibility(VISIBLE); + } + } + + public View getChannelCell() { + return channelCell; } @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); + if (channelCell.getVisibility() == VISIBLE && y > channelCell.getTranslationY() && y < channelCell.getTranslationY() + AndroidUtilities.dp(39)) { + if (channelBlock != null && event.getAction() == MotionEvent.ACTION_UP) { + MessagesController.openByUserName(channelBlock.channel.username, parentFragment, 2); + close(false, true); + } + return true; + } if (event.getAction() == MotionEvent.ACTION_DOWN && imageView.isInsideImage(x, y)) { photoPressed = true; } else if (event.getAction() == MotionEvent.ACTION_UP && photoPressed) { @@ -4302,12 +5776,15 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if (!isFirst && currentType == 0 && currentBlock.level <= 0) { height += AndroidUtilities.dp(8); } - if (currentType != 2) { + boolean nextIsChannel = parentBlock instanceof TLRPC.TL_pageBlockCover && blocks != null && blocks.size() > 1 && blocks.get(1) instanceof TLRPC.TL_pageBlockChannel; + if (currentType != 2 && !nextIsChannel) { height += AndroidUtilities.dp(8); } } else { height = 1; } + channelCell.measure(widthMeasureSpec, heightMeasureSpec); + channelCell.setTranslationY(imageView.getImageHeight() - AndroidUtilities.dp(39)); setMeasuredDimension(width, height); } @@ -4331,6 +5808,178 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } } + private class BlockChannelCell extends FrameLayout { + + private ContextProgressView progressView; + private TextView textView; + private ImageView imageView; + private int currentState; + + private StaticLayout textLayout; + private int buttonWidth; + private int lastCreatedWidth; + private int textX = AndroidUtilities.dp(18); + private int textY = AndroidUtilities.dp(11); + private int textX2; + private int textY2 = AndroidUtilities.dp(11.5f); + private Paint backgroundPaint; + private AnimatorSet currentAnimation; + private int currentType; + + private TLRPC.TL_pageBlockChannel currentBlock; + + public BlockChannelCell(Context context, int type) { + super(context); + setWillNotDraw(false); + backgroundPaint = new Paint(); + currentType = type; + + textView = new TextView(context); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + textView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + textView.setText(LocaleController.getString("ChannelJoin", R.string.ChannelJoin)); + textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); + addView(textView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 39, Gravity.RIGHT | Gravity.TOP)); + textView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (currentState != 0) { + return; + } + setState(1, true); + joinChannel(BlockChannelCell.this, loadedChannel); + } + }); + + imageView = new ImageView(context); + imageView.setImageResource(R.drawable.list_check); + imageView.setScaleType(ImageView.ScaleType.CENTER); + addView(imageView, LayoutHelper.createFrame(39, 39, Gravity.RIGHT | Gravity.TOP)); + + progressView = new ContextProgressView(context, 0); + addView(progressView, LayoutHelper.createFrame(39, 39, Gravity.RIGHT | Gravity.TOP)); + } + + public void setBlock(TLRPC.TL_pageBlockChannel block) { + currentBlock = block; + int color = getSelectedColor(); + if (currentType == 0) { + textView.setTextColor(0xff1d8dd8); + if (color == 0) { + backgroundPaint.setColor(0xfff7f7f7); + } else if (color == 1) { + backgroundPaint.setColor(0xffe5dec8); + } else if (color == 2) { + backgroundPaint.setColor(0xff262626); + } + imageView.setColorFilter(new PorterDuffColorFilter(0xff999999, PorterDuff.Mode.MULTIPLY)); + } else { + textView.setTextColor(0xffffffff); + backgroundPaint.setColor(0x7f000000); + imageView.setColorFilter(new PorterDuffColorFilter(0xffffffff, PorterDuff.Mode.MULTIPLY)); + } + lastCreatedWidth = 0; + TLRPC.Chat channel = MessagesController.getInstance().getChat(block.channel.id); + if (channel == null || channel.min) { + loadChannel(this, block.channel); + setState(1, false); + } else { + loadedChannel = channel; + if (channel.left && !channel.kicked) { + setState(0, false); + } else { + setState(4, false); + } + } + requestLayout(); + } + + public void setState(int state, boolean animated) { + if (currentAnimation != null) { + currentAnimation.cancel(); + } + currentState = state; + if (animated) { + currentAnimation = new AnimatorSet(); + currentAnimation.playTogether( + ObjectAnimator.ofFloat(textView, "alpha", state == 0 ? 1.0f : 0.0f), + ObjectAnimator.ofFloat(textView, "scaleX", state == 0 ? 1.0f : 0.1f), + ObjectAnimator.ofFloat(textView, "scaleY", state == 0 ? 1.0f : 0.1f), + + ObjectAnimator.ofFloat(progressView, "alpha", state == 1 ? 1.0f : 0.0f), + ObjectAnimator.ofFloat(progressView, "scaleX", state == 1 ? 1.0f : 0.1f), + ObjectAnimator.ofFloat(progressView, "scaleY", state == 1 ? 1.0f : 0.1f), + + ObjectAnimator.ofFloat(imageView, "alpha", state == 2 ? 1.0f : 0.0f), + ObjectAnimator.ofFloat(imageView, "scaleX", state == 2 ? 1.0f : 0.1f), + ObjectAnimator.ofFloat(imageView, "scaleY", state == 2 ? 1.0f : 0.1f) + ); + currentAnimation.setDuration(150); + currentAnimation.start(); + } else { + textView.setAlpha(state == 0 ? 1.0f : 0.0f); + textView.setScaleX(state == 0 ? 1.0f : 0.1f); + textView.setScaleY(state == 0 ? 1.0f : 0.1f); + + progressView.setAlpha(state == 1 ? 1.0f : 0.0f); + progressView.setScaleX(state == 1 ? 1.0f : 0.1f); + progressView.setScaleY(state == 1 ? 1.0f : 0.1f); + + imageView.setAlpha(state == 2 ? 1.0f : 0.0f); + imageView.setScaleX(state == 2 ? 1.0f : 0.1f); + imageView.setScaleY(state == 2 ? 1.0f : 0.1f); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (currentType != 0) { + return super.onTouchEvent(event); + } + return checkLayoutForLinks(event, this, textLayout, textX, textY) || super.onTouchEvent(event); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + setMeasuredDimension(width, AndroidUtilities.dp(39 + 9)); + + textView.measure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(39), MeasureSpec.EXACTLY)); + buttonWidth = textView.getMeasuredWidth(); + progressView.measure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(39), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(39), MeasureSpec.EXACTLY)); + imageView.measure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(39), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(39), MeasureSpec.EXACTLY)); + if (currentBlock != null) { + if (lastCreatedWidth != width) { + textLayout = createLayoutForText(currentBlock.channel.title, null, width - AndroidUtilities.dp(36 + 16) - buttonWidth, currentBlock); + //lastCreatedWidth = width; + textX2 = getMeasuredWidth() - textX - buttonWidth; + } + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + imageView.layout(textX2 + buttonWidth / 2 - AndroidUtilities.dp(19), 0, textX2 + buttonWidth / 2 + AndroidUtilities.dp(20), AndroidUtilities.dp(39)); + progressView.layout(textX2 + buttonWidth / 2 - AndroidUtilities.dp(19), 0, textX2 + buttonWidth / 2 + AndroidUtilities.dp(20), AndroidUtilities.dp(39)); + textView.layout(textX2, 0, textX2 + textView.getMeasuredWidth(), textView.getMeasuredHeight()); + } + + @Override + protected void onDraw(Canvas canvas) { + if (currentBlock == null) { + return; + } + canvas.drawRect(0, 0, getMeasuredWidth(), AndroidUtilities.dp(39), backgroundPaint); + if (textLayout != null) { + canvas.save(); + canvas.translate(textX, textY); + drawLayoutLink(canvas, textLayout); + textLayout.draw(canvas); + canvas.restore(); + } + } + } + private class BlockAuthorDateCell extends View { private StaticLayout textLayout; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java index 69a82bfb4..336de6919 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java @@ -156,19 +156,19 @@ public class AudioPlayerActivity extends BaseFragment implements NotificationCen @Override public boolean onFragmentCreate() { TAG = MediaController.getInstance().generateObserverTag(); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidReset); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioPlayStateChanged); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidStarted); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioProgressDidChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidReset); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingPlayStateChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidStarted); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingProgressDidChanged); return super.onFragmentCreate(); } @Override public void onFragmentDestroy() { - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidReset); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioPlayStateChanged); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidStarted); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioProgressDidChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidReset); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingPlayStateChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidStarted); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingProgressDidChanged); MediaController.getInstance().removeLoadingFileObserver(this); super.onFragmentDestroy(); } @@ -285,10 +285,10 @@ public class AudioPlayerActivity extends BaseFragment implements NotificationCen if (MediaController.getInstance().isDownloadingCurrentMessage()) { return; } - if (MediaController.getInstance().isAudioPaused()) { - MediaController.getInstance().playAudio(MediaController.getInstance().getPlayingMessageObject()); + if (MediaController.getInstance().isMessagePaused()) { + MediaController.getInstance().playMessage(MediaController.getInstance().getPlayingMessageObject()); } else { - MediaController.getInstance().pauseAudio(MediaController.getInstance().getPlayingMessageObject()); + MediaController.getInstance().pauseMessage(MediaController.getInstance().getPlayingMessageObject()); } } }); @@ -325,9 +325,9 @@ public class AudioPlayerActivity extends BaseFragment implements NotificationCen @Override public void didReceivedNotification(int id, Object... args) { - if (id == NotificationCenter.audioDidStarted || id == NotificationCenter.audioPlayStateChanged || id == NotificationCenter.audioDidReset) { - updateTitle(id == NotificationCenter.audioDidReset && (Boolean) args[1]); - } else if (id == NotificationCenter.audioProgressDidChanged) { + if (id == NotificationCenter.messagePlayingDidStarted || id == NotificationCenter.messagePlayingPlayStateChanged || id == NotificationCenter.messagePlayingDidReset) { + updateTitle(id == NotificationCenter.messagePlayingDidReset && (Boolean) args[1]); + } else if (id == NotificationCenter.messagePlayingProgressDidChanged) { MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); if (messageObject != null && messageObject.isMusic()) { updateProgress(messageObject); @@ -446,7 +446,7 @@ public class AudioPlayerActivity extends BaseFragment implements NotificationCen checkIfMusicDownloaded(messageObject); updateProgress(messageObject); - if (MediaController.getInstance().isAudioPaused()) { + 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))); } else { playButton.setImageDrawable(Theme.createSimpleSelectorDrawable(playButton.getContext(), R.drawable.pl_pause, Theme.getColor(Theme.key_player_button), Theme.getColor(Theme.key_player_buttonActive))); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java index 97fb9a490..3101368d3 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java @@ -69,7 +69,7 @@ public class AudioSelectActivity extends BaseFragment implements NotificationCen public boolean onFragmentCreate() { super.onFragmentCreate(); NotificationCenter.getInstance().addObserver(this, NotificationCenter.closeChats); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidReset); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidReset); loadAudio(); return true; } @@ -78,8 +78,8 @@ public class AudioSelectActivity extends BaseFragment implements NotificationCen public void onFragmentDestroy() { super.onFragmentDestroy(); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.closeChats); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidReset); - if (playingAudio != null && MediaController.getInstance().isPlayingAudio(playingAudio)) { + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidReset); + if (playingAudio != null && MediaController.getInstance().isPlayingMessage(playingAudio)) { MediaController.getInstance().cleanupPlayer(true, true); } } @@ -168,7 +168,7 @@ public class AudioSelectActivity extends BaseFragment implements NotificationCen public void didReceivedNotification(int id, Object... args) { if (id == NotificationCenter.closeChats) { removeSelfFromStack(); - } else if (id == NotificationCenter.audioDidReset) { + } else if (id == NotificationCenter.messagePlayingDidReset) { if (listViewAdapter != null) { listViewAdapter.notifyDataSetChanged(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java index 1c26f2b7e..f5576d7c5 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java @@ -182,7 +182,7 @@ public class BlockedUsersActivity extends BaseFragment implements NotificationCe } @Override - public void didSelectContact(final TLRPC.User user, String param) { + public void didSelectContact(final TLRPC.User user, String param, ContactsActivity activity) { if (user == null) { return; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java index 5122dcf1e..527f7b8b5 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java @@ -316,11 +316,19 @@ public class CacheControlActivity extends BaseFragment { } if (position == keepMediaRow) { BottomSheet.Builder builder = new BottomSheet.Builder(getParentActivity()); - builder.setItems(new CharSequence[]{LocaleController.formatPluralString("Weeks", 1), LocaleController.formatPluralString("Months", 1), LocaleController.getString("KeepMediaForever", R.string.KeepMediaForever)}, new DialogInterface.OnClickListener() { + builder.setItems(new CharSequence[]{LocaleController.formatPluralString("Days", 3), LocaleController.formatPluralString("Weeks", 1), LocaleController.formatPluralString("Months", 1), LocaleController.getString("KeepMediaForever", R.string.KeepMediaForever)}, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, final int which) { SharedPreferences.Editor editor = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).edit(); - editor.putInt("keep_media", which).commit(); + if (which == 0) { + editor.putInt("keep_media", 3).commit(); + } else if (which == 1) { + editor.putInt("keep_media", 0).commit(); + } else if (which == 2) { + editor.putInt("keep_media", 1).commit(); + } else if (which == 3) { + editor.putInt("keep_media", 2).commit(); + } if (listAdapter != null) { listAdapter.notifyDataSetChanged(); } @@ -587,6 +595,8 @@ public class CacheControlActivity extends BaseFragment { value = LocaleController.formatPluralString("Weeks", 1); } else if (keepMedia == 1) { value = LocaleController.formatPluralString("Months", 1); + } else if (keepMedia == 3) { + value = LocaleController.formatPluralString("Days", 3); } else { value = LocaleController.getString("KeepMediaForever", R.string.KeepMediaForever); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/CallLogActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/CallLogActivity.java index f5c49056b..0e8adc7cf 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/CallLogActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/CallLogActivity.java @@ -232,14 +232,23 @@ public class CallLogActivity extends BaseFragment implements NotificationCenter. return false; } final CallLogRow row = calls.get(position); + ArrayList items=new ArrayList(); + items.add(LocaleController.getString("Delete", R.string.Delete)); + if(VoIPHelper.canRateCall((TLRPC.TL_messageActionPhoneCall) row.calls.get(0).action)) + items.add(LocaleController.getString("CallMessageReportProblem", R.string.CallMessageReportProblem)); new AlertDialog.Builder(getParentActivity()) .setTitle(LocaleController.getString("Calls", R.string.Calls)) - .setItems(new String[]{ - LocaleController.getString("Delete", R.string.Delete) - }, new DialogInterface.OnClickListener() { + .setItems(items.toArray(new String[items.size()]), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - confirmAndDelete(row); + switch(which){ + case 0: + confirmAndDelete(row); + break; + case 1: + VoIPHelper.showRateAlert(getParentActivity(), (TLRPC.TL_messageActionPhoneCall) row.calls.get(0).action); + break; + } } }) .show(); @@ -330,7 +339,7 @@ public class CallLogActivity extends BaseFragment implements NotificationCenter. ContactsActivity contactsFragment = new ContactsActivity(args); contactsFragment.setDelegate(new ContactsActivity.ContactsActivityDelegate() { @Override - public void didSelectContact(TLRPC.User user, String param) { + public void didSelectContact(TLRPC.User user, String param, ContactsActivity activity) { VoIPHelper.startCall(user, getParentActivity(), null); } }); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/AudioCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/AudioCell.java index 5c2fd9e6f..2567a5f5f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/AudioCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/AudioCell.java @@ -60,8 +60,8 @@ public class AudioCell extends FrameLayout { @Override public void onClick(View v) { if (audioEntry != null) { - if (MediaController.getInstance().isPlayingAudio(audioEntry.messageObject) && !MediaController.getInstance().isAudioPaused()) { - MediaController.getInstance().pauseAudio(audioEntry.messageObject); + if (MediaController.getInstance().isPlayingMessage(audioEntry.messageObject) && !MediaController.getInstance().isMessagePaused()) { + MediaController.getInstance().pauseMessage(audioEntry.messageObject); setPlayDrawable(false); } else { ArrayList arrayList = new ArrayList<>(); @@ -169,7 +169,7 @@ public class AudioCell extends FrameLayout { genreTextView.setText(audioEntry.genre); authorTextView.setText(audioEntry.author); timeTextView.setText(String.format("%d:%02d", audioEntry.duration / 60, audioEntry.duration % 60)); - setPlayDrawable(MediaController.getInstance().isPlayingAudio(audioEntry.messageObject) && !MediaController.getInstance().isAudioPaused()); + setPlayDrawable(MediaController.getInstance().isPlayingMessage(audioEntry.messageObject) && !MediaController.getInstance().isMessagePaused()); needDivider = divider; setWillNotDraw(!divider); 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 0851136a3..3af1f219e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java @@ -319,7 +319,7 @@ public class ChatActionCell extends BaseCell { int count = textLayout.getLineCount(); for (int a = line + 1; a < count; a++) { int w = (int) Math.ceil(textLayout.getLineWidth(a)); - if (Math.abs(w - width) < AndroidUtilities.dp(12)) { + if (Math.abs(w - width) < AndroidUtilities.dp(10)) { width = Math.max(w, width); } else { break; @@ -327,7 +327,7 @@ public class ChatActionCell extends BaseCell { } for (int a = line - 1; a >= 0; a--) { int w = (int) Math.ceil(textLayout.getLineWidth(a)); - if (Math.abs(w - width) < AndroidUtilities.dp(12)) { + if (Math.abs(w - width) < AndroidUtilities.dp(10)) { width = Math.max(w, width); } else { break; @@ -336,6 +336,14 @@ public class ChatActionCell extends BaseCell { return width; } + private boolean isLineTop(int prevWidth, int currentWidth, int line, int count, int cornerRest) { + return line == 0 || !(line < 0 || line >= count) && findMaxWidthAroundLine(line - 1) + cornerRest * 3 < prevWidth; + } + + private boolean isLineBottom(int nextWidth, int currentWidth, int line, int count, int cornerRest) { + return line == count - 1 || !(line < 0 || line > count - 1) && findMaxWidthAroundLine(line + 1) + cornerRest * 3 < nextWidth; + } + @Override protected void onDraw(Canvas canvas) { if (currentMessageObject != null && currentMessageObject.type == 11) { @@ -344,15 +352,19 @@ public class ChatActionCell extends BaseCell { if (textLayout != null) { final int count = textLayout.getLineCount(); - final int corner = AndroidUtilities.dp(6); + final int corner = AndroidUtilities.dp(11); + final int cornerOffset = AndroidUtilities.dp(6); + final int cornerRest = corner - cornerOffset; + final int cornerIn = AndroidUtilities.dp(8); int y = AndroidUtilities.dp(7); int previousLineBottom = 0; int dx; + int dx2; int dy; for (int a = 0; a < count; a++) { int width = findMaxWidthAroundLine(a); - int x = (getMeasuredWidth() - width) / 2 - AndroidUtilities.dp(3); - width += AndroidUtilities.dp(6); + int x = (getMeasuredWidth() - width - cornerRest) / 2; + width += cornerRest; int lineBottom = textLayout.getLineBottom(a); int height = lineBottom - previousLineBottom; int additionalHeight = 0; @@ -369,55 +381,114 @@ public class ChatActionCell extends BaseCell { height += AndroidUtilities.dp(3); } - canvas.drawRect(x, y, x + width, y + height, Theme.chat_actionBackgroundPaint); + int yOld = y; + int hOld = height; + int drawInnerBottom = 0; + int drawInnerTop = 0; + int nextLineWidth = 0; + int prevLineWidth = 0; if (!drawBottomCorners && a + 1 < count) { - int nextLineWidth = findMaxWidthAroundLine(a + 1) + AndroidUtilities.dp(6); - if (nextLineWidth + corner * 2 < width) { - int nextX = (getMeasuredWidth() - nextLineWidth) / 2; + nextLineWidth = findMaxWidthAroundLine(a + 1) + cornerRest; + if (nextLineWidth + cornerRest * 2 < width) { + drawInnerBottom = 1; drawBottomCorners = true; + } else if (width + cornerRest * 2 < nextLineWidth) { + drawInnerBottom = 2; + } else { + drawInnerBottom = 3; + } + } + if (!drawTopCorners && a > 0) { + prevLineWidth = findMaxWidthAroundLine(a - 1) + cornerRest; + if (prevLineWidth + cornerRest * 2 < width) { + drawInnerTop = 1; + drawTopCorners = true; + } else if (width + cornerRest * 2 < prevLineWidth) { + drawInnerTop = 2; + } else { + drawInnerTop = 3; + } + } + + if (drawInnerBottom != 0) { + if (drawInnerBottom == 1) { + int nextX = (getMeasuredWidth() - nextLineWidth) / 2; additionalHeight = AndroidUtilities.dp(3); - canvas.drawRect(x, y + height, nextX, y + height + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); - canvas.drawRect(nextX + nextLineWidth, y + height, x + width, y + height + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); - } else if (width + corner * 2 < nextLineWidth) { + if (isLineBottom(nextLineWidth, width, a + 1, count, cornerRest)) { + canvas.drawRect(x + cornerOffset, y + height, nextX - cornerRest, y + height + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); + canvas.drawRect(nextX + nextLineWidth + cornerRest, y + height, x + width - cornerOffset, y + height + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); + } else { + canvas.drawRect(x + cornerOffset, y + height, nextX, y + height + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); + canvas.drawRect(nextX + nextLineWidth, y + height, x + width - cornerOffset, y + height + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); + } + } else if (drawInnerBottom == 2) { additionalHeight = AndroidUtilities.dp(3); - dy = y + height - AndroidUtilities.dp(9); + dy = y + height - AndroidUtilities.dp(11); - dx = x - corner * 2; - Theme.chat_cornerInner[2].setBounds(dx, dy, dx + corner, dy + corner); + dx = x - cornerIn; + if (drawInnerTop != 2 && drawInnerTop != 3) { + dx -= cornerRest; + } + if (drawTopCorners || drawBottomCorners) { + canvas.drawRect(dx + cornerIn, dy + AndroidUtilities.dp(3), dx + cornerIn + corner, dy + corner, Theme.chat_actionBackgroundPaint); + } + Theme.chat_cornerInner[2].setBounds(dx, dy, dx + cornerIn, dy + cornerIn); Theme.chat_cornerInner[2].draw(canvas); - dx = x + width + corner; - Theme.chat_cornerInner[3].setBounds(dx, dy, dx + corner, dy + corner); + dx = x + width; + if (drawInnerTop != 2 && drawInnerTop != 3) { + dx += cornerRest; + } + if (drawTopCorners || drawBottomCorners) { + canvas.drawRect(dx - corner, dy + AndroidUtilities.dp(3), dx, dy + corner, Theme.chat_actionBackgroundPaint); + } + Theme.chat_cornerInner[3].setBounds(dx, dy, dx + cornerIn, dy + cornerIn); Theme.chat_cornerInner[3].draw(canvas); } else { additionalHeight = AndroidUtilities.dp(6); } } - if (!drawTopCorners && a > 0) { - int prevLineWidth = findMaxWidthAroundLine(a - 1) + AndroidUtilities.dp(6); - if (prevLineWidth + corner * 2 < width) { + if (drawInnerTop != 0) { + if (drawInnerTop == 1) { int prevX = (getMeasuredWidth() - prevLineWidth) / 2; - drawTopCorners = true; + y -= AndroidUtilities.dp(3); height += AndroidUtilities.dp(3); - canvas.drawRect(x, y, prevX, y + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); - canvas.drawRect(prevX + prevLineWidth, y, x + width, y + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); - } else if (width + corner * 2 < prevLineWidth) { + if (isLineTop(prevLineWidth, width, a - 1, count, cornerRest)) { + canvas.drawRect(x + cornerOffset, y, prevX - cornerRest, y + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); + canvas.drawRect(prevX + prevLineWidth + cornerRest, y, x + width - cornerOffset, y + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); + } else { + canvas.drawRect(x + cornerOffset, y, prevX, y + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); + canvas.drawRect(prevX + prevLineWidth, y, x + width - cornerOffset, y + AndroidUtilities.dp(3), Theme.chat_actionBackgroundPaint); + } + } else if (drawInnerTop == 2) { y -= AndroidUtilities.dp(3); height += AndroidUtilities.dp(3); - dy = y + corner; + dy = y + AndroidUtilities.dp(6.2f); - dx = x - corner * 2; - Theme.chat_cornerInner[0].setBounds(dx, dy, dx + corner, dy + corner); + dx = x - cornerIn; + if (drawInnerBottom != 2 && drawInnerBottom != 3) { + dx -= cornerRest; + } + if (drawTopCorners || drawBottomCorners) { + canvas.drawRect(dx + cornerIn, y + AndroidUtilities.dp(3), dx + cornerIn + corner, y + AndroidUtilities.dp(11), Theme.chat_actionBackgroundPaint); + } + Theme.chat_cornerInner[0].setBounds(dx, dy, dx + cornerIn, dy + cornerIn); Theme.chat_cornerInner[0].draw(canvas); - dx = x + width + corner; - Theme.chat_cornerInner[1].setBounds(dx, dy, dx + corner, dy + corner); + dx = x + width; + if (drawInnerBottom != 2 && drawInnerBottom != 3) { + dx += cornerRest; + } + if (drawTopCorners || drawBottomCorners) { + canvas.drawRect(dx - corner, y + AndroidUtilities.dp(3), dx, y + AndroidUtilities.dp(11), Theme.chat_actionBackgroundPaint); + } + Theme.chat_cornerInner[1].setBounds(dx, dy, dx + cornerIn, dy + cornerIn); Theme.chat_cornerInner[1].draw(canvas); } else { y -= AndroidUtilities.dp(6); @@ -425,27 +496,37 @@ public class ChatActionCell extends BaseCell { } } - canvas.drawRect(x - corner, y + corner, x, y + height + additionalHeight - corner, Theme.chat_actionBackgroundPaint); - canvas.drawRect(x + width, y + corner, x + width + corner, y + height + additionalHeight - corner, Theme.chat_actionBackgroundPaint); + if (drawTopCorners || drawBottomCorners) { + canvas.drawRect(x + cornerOffset, yOld, x + width - cornerOffset, yOld + hOld, Theme.chat_actionBackgroundPaint); + } else { + canvas.drawRect(x, yOld, x + width, yOld + hOld, Theme.chat_actionBackgroundPaint); + } + + dx = x - cornerRest; + dx2 = x + width - cornerOffset; + if (drawTopCorners && !drawBottomCorners && drawInnerBottom != 2) { + canvas.drawRect(dx, y + corner, dx + corner, y + height + additionalHeight - AndroidUtilities.dp(6), Theme.chat_actionBackgroundPaint); + canvas.drawRect(dx2, y + corner, dx2 + corner, y + height + additionalHeight - AndroidUtilities.dp(6), Theme.chat_actionBackgroundPaint); + } else if (drawBottomCorners && !drawTopCorners && drawInnerTop != 2) { + canvas.drawRect(dx, y + corner - AndroidUtilities.dp(5), dx + corner, y + height + additionalHeight - corner, Theme.chat_actionBackgroundPaint); + canvas.drawRect(dx2, y + corner - AndroidUtilities.dp(5), dx2 + corner, y + height + additionalHeight - corner, Theme.chat_actionBackgroundPaint); + } else if (drawTopCorners || drawBottomCorners) { + canvas.drawRect(dx, y + corner, dx + corner, y + height + additionalHeight - corner, Theme.chat_actionBackgroundPaint); + canvas.drawRect(dx2, y + corner, dx2 + corner, y + height + additionalHeight - corner, Theme.chat_actionBackgroundPaint); + } if (drawTopCorners) { - dx = x - corner; Theme.chat_cornerOuter[0].setBounds(dx, y, dx + corner, y + corner); Theme.chat_cornerOuter[0].draw(canvas); - - dx = x + width; - Theme.chat_cornerOuter[1].setBounds(dx, y, dx + corner, y + corner); + Theme.chat_cornerOuter[1].setBounds(dx2, y, dx2 + corner, y + corner); Theme.chat_cornerOuter[1].draw(canvas); } if (drawBottomCorners) { dy = y + height + additionalHeight - corner; - dx = x + width; - Theme.chat_cornerOuter[2].setBounds(dx, dy, dx + corner, dy + corner); + Theme.chat_cornerOuter[2].setBounds(dx2, dy, dx2 + corner, dy + corner); Theme.chat_cornerOuter[2].draw(canvas); - - dx = x - corner; Theme.chat_cornerOuter[3].setBounds(dx, dy, dx + corner, dy + corner); Theme.chat_cornerOuter[3].draw(canvas); } 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 56a496317..047646121 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatMessageCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatMessageCell.java @@ -10,13 +10,16 @@ package org.telegram.ui.Cells; import android.annotation.SuppressLint; import android.content.Context; +import android.content.res.ColorStateList; import android.graphics.Canvas; +import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; import android.os.Build; import android.text.Layout; import android.text.Spannable; @@ -28,6 +31,7 @@ import android.text.TextUtils; import android.text.style.CharacterStyle; import android.text.style.ClickableSpan; import android.text.style.URLSpan; +import android.util.StateSet; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.View; @@ -35,6 +39,7 @@ import android.view.ViewStructure; import org.telegram.PhoneFormat.PhoneFormat; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ChatObject; import org.telegram.messenger.ContactsController; import org.telegram.messenger.Emoji; import org.telegram.messenger.ImageLoader; @@ -55,6 +60,7 @@ import org.telegram.tgnet.TLRPC; import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.LinkPath; import org.telegram.ui.Components.RadialProgress; +import org.telegram.ui.Components.RoundVideoPlayingDrawable; import org.telegram.ui.Components.SeekBar; import org.telegram.ui.Components.SeekBarWaveform; import org.telegram.ui.Components.StaticLayoutEx; @@ -85,8 +91,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate void didPressedShare(ChatMessageCell cell); void didPressedOther(ChatMessageCell cell); void didPressedBotButton(ChatMessageCell cell, TLRPC.KeyboardButton button); - void didPressedInstantButton(ChatMessageCell cell); - boolean needPlayAudio(MessageObject messageObject); + void didPressedInstantButton(ChatMessageCell cell, int type); + boolean needPlayMessage(MessageObject messageObject); boolean canPerformActions(); } @@ -97,6 +103,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private final static int DOCUMENT_ATTACH_TYPE_VIDEO = 4; private final static int DOCUMENT_ATTACH_TYPE_MUSIC = 5; private final static int DOCUMENT_ATTACH_TYPE_STICKER = 6; + private final static int DOCUMENT_ATTACH_TYPE_ROUND = 7; private class BotButton { private int x; @@ -142,6 +149,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private TLRPC.Document documentAttach; private boolean drawPhotoImage; private boolean hasLinkPreview; + private boolean hasOldCaptionPreview; private boolean hasGamePreview; private boolean hasInvoicePreview; private int linkPreviewHeight; @@ -158,9 +166,17 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private StaticLayout authorLayout; private StaticLayout instantViewLayout; private boolean drawInstantView; + private int drawInstantViewType; + private boolean drawJoinGroupView; + private boolean drawJoinChannelView; private int instantTextX; private int instantWidth; private boolean instantPressed; + private boolean instantButtonPressed; + private Drawable instantViewSelectorDrawable; + private int pressedState[] = new int[] {android.R.attr.state_enabled, android.R.attr.state_pressed}; + + private RoundVideoPlayingDrawable roundVideoPlayingDrawable; private StaticLayout docTitleLayout; private int docTitleOffsetX; @@ -244,6 +260,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private int viaWidth; private int viaNameWidth; private int availableTimeWidth; + private int widthBeforeNewTimeLine; private int backgroundWidth = 100; @@ -337,6 +354,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate seekBarWaveform = new SeekBarWaveform(context); seekBarWaveform.setDelegate(this); seekBarWaveform.setParentView(this); + roundVideoPlayingDrawable = new RoundVideoPlayingDrawable(this); } private void resetPressedLink(int type) { @@ -624,7 +642,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate int x = (int) event.getX(); int y = (int) event.getY(); - if (x >= textX && x <= textX + backgroundWidth && y >= textY + currentMessageObject.textHeight && y <= textY + currentMessageObject.textHeight + linkPreviewHeight + AndroidUtilities.dp(8)) { + if (x >= textX && x <= textX + backgroundWidth && y >= textY + currentMessageObject.textHeight && y <= textY + currentMessageObject.textHeight + linkPreviewHeight + AndroidUtilities.dp(8 + (drawInstantView ? 46 : 0))) { if (event.getAction() == MotionEvent.ACTION_DOWN) { if (descriptionLayout != null && y >= descriptionY) { try { @@ -670,6 +688,13 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate return true; } else if (drawInstantView) { instantPressed = true; + if (Build.VERSION.SDK_INT >= 21 && instantViewSelectorDrawable != null) { + if (instantViewSelectorDrawable.getBounds().contains(x, y)) { + instantViewSelectorDrawable.setState(pressedState); + instantViewSelectorDrawable.setHotspot(x, y); + instantButtonPressed = true; + } + } invalidate(); return true; } else if (documentAttachType != DOCUMENT_ATTACH_TYPE_DOCUMENT && drawPhotoImage && photoImage.isInsideImage(x, y)) { @@ -685,10 +710,13 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else if (event.getAction() == MotionEvent.ACTION_UP) { if (instantPressed) { if (delegate != null) { - delegate.didPressedInstantButton(this); + delegate.didPressedInstantButton(this, drawInstantViewType); } playSoundEffect(SoundEffectConstants.CLICK); - instantPressed = false; + if (Build.VERSION.SDK_INT >= 21 && instantViewSelectorDrawable != null) { + instantViewSelectorDrawable.setState(StateSet.NOTHING); + } + instantPressed = instantButtonPressed = false; invalidate(); } else if (pressedLinkType == 2 || buttonPressed != 0 || linkPreviewPressed) { if (buttonPressed != 0) { @@ -704,13 +732,19 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } resetPressedLink(2); } else { - if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF && drawImageButton) { + if (documentAttachType == DOCUMENT_ATTACH_TYPE_ROUND) { + if (!MediaController.getInstance().isPlayingMessage(currentMessageObject) || MediaController.getInstance().isMessagePaused()) { + delegate.needPlayMessage(currentMessageObject); + } else { + MediaController.getInstance().pauseMessage(currentMessageObject); + } + } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF && drawImageButton) { if (buttonState == -1) { if (MediaController.getInstance().canAutoplayGifs()) { delegate.didPressedImage(this); } else { buttonState = 2; - currentMessageObject.audioProgress = 1; + currentMessageObject.gifState = 1; photoImage.setAllowStartAnimation(false); photoImage.stopAnimation(); radialProgress.setBackground(getDrawableForCurrentState(), false, false); @@ -738,6 +772,10 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { resetPressedLink(2); } + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (instantButtonPressed && Build.VERSION.SDK_INT >= 21 && instantViewSelectorDrawable != null) { + instantViewSelectorDrawable.setHotspot(x, y); + } } } return false; @@ -818,7 +856,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } if (imagePressed) { - if (currentMessageObject.isSecretPhoto()) { + if (currentMessageObject.isSecretPhoto() && currentMessageObject.type != 5) { imagePressed = false; } else if (currentMessageObject.isSendError()) { imagePressed = false; @@ -826,6 +864,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else if (currentMessageObject.type == 8 && buttonState == -1 && MediaController.getInstance().canAutoplayGifs() && photoImage.getAnimation() == null) { imagePressed = false; result = false; + } else if (currentMessageObject.type == 5 && buttonState != -1) { + imagePressed = false; + result = false; } } } else { @@ -910,7 +951,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } private boolean checkBotButtonMotionEvent(MotionEvent event) { - if (botButtons.isEmpty()) { + if (botButtons.isEmpty() || currentMessageObject.eventId != 0) { return false; } @@ -985,7 +1026,10 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate linkPreviewPressed = false; otherPressed = false; imagePressed = false; - instantPressed = false; + instantPressed = instantButtonPressed = false; + if (Build.VERSION.SDK_INT >= 21 && instantViewSelectorDrawable != null) { + instantViewSelectorDrawable.setState(StateSet.NOTHING); + } result = false; resetPressedLink(-1); } @@ -1121,24 +1165,63 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate return result; } - public void updateAudioProgress() { - if (currentMessageObject == null || documentAttach == null) { + public void updatePlayingMessageProgress() { + if (currentMessageObject == null) { return; } - if (useSeekBarWaweform) { - if (!seekBarWaveform.isDragging()) { - seekBarWaveform.setProgress(currentMessageObject.audioProgress); + if (currentMessageObject.isRoundVideo()) { + int duration = 0; + TLRPC.Document document = currentMessageObject.getDocument(); + for (int a = 0; a < document.attributes.size(); a++) { + TLRPC.DocumentAttribute attribute = document.attributes.get(a); + if (attribute instanceof TLRPC.TL_documentAttributeVideo) { + duration = attribute.duration; + break; + } } - } else { - if (!seekBar.isDragging()) { - seekBar.setProgress(currentMessageObject.audioProgress); + if (MediaController.getInstance().isPlayingMessage(currentMessageObject)) { + duration = Math.max(0, duration - currentMessageObject.audioProgressSec); + } + String timeString = String.format("%02d:%02d", duration / 60, duration % 60); + if (lastTimeString == null || lastTimeString != null && !lastTimeString.equals(timeString)) { + lastTimeString = timeString; + timeWidthAudio = (int) Math.ceil(Theme.chat_timePaint.measureText(timeString)); + durationLayout = new StaticLayout(timeString, Theme.chat_timePaint, timeWidthAudio, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + invalidate(); + } + } else if (documentAttach != null) { + if (useSeekBarWaweform) { + if (!seekBarWaveform.isDragging()) { + seekBarWaveform.setProgress(currentMessageObject.audioProgress); + } + } else { + if (!seekBar.isDragging()) { + seekBar.setProgress(currentMessageObject.audioProgress); + } } - } - int duration = 0; - if (documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO) { - if (!MediaController.getInstance().isPlayingAudio(currentMessageObject)) { + int duration = 0; + if (documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO) { + if (!MediaController.getInstance().isPlayingMessage(currentMessageObject)) { + for (int a = 0; a < documentAttach.attributes.size(); a++) { + TLRPC.DocumentAttribute attribute = documentAttach.attributes.get(a); + if (attribute instanceof TLRPC.TL_documentAttributeAudio) { + duration = attribute.duration; + break; + } + } + } else { + duration = currentMessageObject.audioProgressSec; + } + String timeString = String.format("%02d:%02d", duration / 60, duration % 60); + if (lastTimeString == null || lastTimeString != null && !lastTimeString.equals(timeString)) { + lastTimeString = timeString; + timeWidthAudio = (int) Math.ceil(Theme.chat_audioTimePaint.measureText(timeString)); + durationLayout = new StaticLayout(timeString, Theme.chat_audioTimePaint, timeWidthAudio, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + } + } else { + int currentProgress = 0; for (int a = 0; a < documentAttach.attributes.size(); a++) { TLRPC.DocumentAttribute attribute = documentAttach.attributes.get(a); if (attribute instanceof TLRPC.TL_documentAttributeAudio) { @@ -1146,35 +1229,18 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate break; } } - } else { - duration = currentMessageObject.audioProgressSec; - } - String timeString = String.format("%02d:%02d", duration / 60, duration % 60); - if (lastTimeString == null || lastTimeString != null && !lastTimeString.equals(timeString)) { - lastTimeString = timeString; - timeWidthAudio = (int) Math.ceil(Theme.chat_audioTimePaint.measureText(timeString)); - durationLayout = new StaticLayout(timeString, Theme.chat_audioTimePaint, timeWidthAudio, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - } - } else { - int currentProgress = 0; - for (int a = 0; a < documentAttach.attributes.size(); a++) { - TLRPC.DocumentAttribute attribute = documentAttach.attributes.get(a); - if (attribute instanceof TLRPC.TL_documentAttributeAudio) { - duration = attribute.duration; - break; + if (MediaController.getInstance().isPlayingMessage(currentMessageObject)) { + currentProgress = currentMessageObject.audioProgressSec; + } + String timeString = String.format("%d:%02d / %d:%02d", currentProgress / 60, currentProgress % 60, duration / 60, duration % 60); + if (lastTimeString == null || lastTimeString != null && !lastTimeString.equals(timeString)) { + lastTimeString = timeString; + int timeWidth = (int) Math.ceil(Theme.chat_audioTimePaint.measureText(timeString)); + durationLayout = new StaticLayout(timeString, Theme.chat_audioTimePaint, timeWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); } } - if (MediaController.getInstance().isPlayingAudio(currentMessageObject)) { - currentProgress = currentMessageObject.audioProgressSec; - } - String timeString = String.format("%d:%02d / %d:%02d", currentProgress / 60, currentProgress % 60, duration / 60, duration % 60); - if (lastTimeString == null || lastTimeString != null && !lastTimeString.equals(timeString)) { - lastTimeString = timeString; - int timeWidth = (int) Math.ceil(Theme.chat_audioTimePaint.measureText(timeString)); - durationLayout = new StaticLayout(timeString, Theme.chat_audioTimePaint, timeWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - } + invalidate(); } - invalidate(); } public void downloadAudioIfNeed() { @@ -1274,15 +1340,19 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else if (currentMessageObject.type == 12) { TLRPC.User user = MessagesController.getInstance().getUser(currentMessageObject.messageOwner.media.user_id); delegate.didPressedUserAvatar(this, user); + } else if (currentMessageObject.type == 5) { + if (!MediaController.getInstance().isPlayingMessage(currentMessageObject) || MediaController.getInstance().isMessagePaused()) { + delegate.needPlayMessage(currentMessageObject); + } else { + MediaController.getInstance().pauseMessage(currentMessageObject); + } } else if (currentMessageObject.type == 8) { if (buttonState == -1) { if (MediaController.getInstance().canAutoplayGifs()) { - if (!currentMessageObject.isVideoVoice()) { //TODO - delegate.didPressedImage(this); - } + delegate.didPressedImage(this); } else { buttonState = 2; - currentMessageObject.audioProgress = 1; + currentMessageObject.gifState = 1; photoImage.setAllowStartAnimation(false); photoImage.stopAnimation(); radialProgress.setBackground(getDrawableForCurrentState(), false, false); @@ -1351,7 +1421,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate return true; } else if (currentMessageObject != null && photoNotSet) { File cacheFile = FileLoader.getPathToMessage(currentMessageObject.messageOwner); - if (cacheFile.exists()) { //TODO + if (cacheFile.exists()) { return true; } } @@ -1461,6 +1531,21 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { updateButtonState(false); } + if (currentMessageObject != null && currentMessageObject.isRoundVideo()) { + checkRoundVideoPlayback(true); + } + } + + public void checkRoundVideoPlayback(boolean allowStart) { + if (allowStart) { + allowStart = MediaController.getInstance().getPlayingMessageObject() == null; + } + photoImage.setAllowStartAnimation(allowStart); + if (allowStart) { + photoImage.startAnimation(); + } else { + photoImage.stopAnimation(); + } } @Override @@ -1484,7 +1569,10 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate invalidate(); } if (instantPressed) { - instantPressed = false; + instantPressed = instantButtonPressed = false; + if (Build.VERSION.SDK_INT >= 21 && instantViewSelectorDrawable != null) { + instantViewSelectorDrawable.setState(StateSet.NOTHING); + } invalidate(); } if (delegate != null) { @@ -1575,7 +1663,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate break; } } - availableTimeWidth = maxWidth - AndroidUtilities.dp(76 + 18) - (int) Math.ceil(Theme.chat_audioTimePaint.measureText("00:00")); + widthBeforeNewTimeLine = maxWidth - AndroidUtilities.dp(76 + 18) - (int) Math.ceil(Theme.chat_audioTimePaint.measureText("00:00")); + availableTimeWidth = maxWidth - AndroidUtilities.dp(18); measureTime(messageObject); int minSize = AndroidUtilities.dp(40 + 14 + 20 + 90 + 10) + timeWidth; if (!hasLinkPreview) { @@ -1609,7 +1698,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } int durationWidth = (int) Math.ceil(Theme.chat_audioTimePaint.measureText(String.format("%d:%02d / %d:%02d", duration / 60, duration % 60, duration / 60, duration % 60))); - availableTimeWidth = backgroundWidth - AndroidUtilities.dp(76 + 18) - durationWidth; + widthBeforeNewTimeLine = backgroundWidth - AndroidUtilities.dp(18 + 76) - durationWidth; + availableTimeWidth = backgroundWidth - AndroidUtilities.dp(18); return durationWidth; } else if (MessageObject.isVideoDocument(documentAttach)) { documentAttachType = DOCUMENT_ATTACH_TYPE_VIDEO; @@ -1682,7 +1772,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } private void calcBackgroundWidth(int maxWidth, int timeMore, int maxChildWidth) { - if (hasLinkPreview || hasGamePreview || hasInvoicePreview || maxWidth - currentMessageObject.lastLineWidth < timeMore || currentMessageObject.hasRtl) { + if (hasLinkPreview || hasOldCaptionPreview || hasGamePreview || hasInvoicePreview || maxWidth - currentMessageObject.lastLineWidth < timeMore || currentMessageObject.hasRtl) { totalHeight += AndroidUtilities.dp(14); backgroundWidth = Math.max(maxChildWidth, currentMessageObject.lastLineWidth) + AndroidUtilities.dp(31); backgroundWidth = Math.max(backgroundWidth, timeWidth + AndroidUtilities.dp(31)); @@ -1746,6 +1836,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == instantViewSelectorDrawable; + } + public void setMessageObject(MessageObject messageObject, boolean bottomNear, boolean topNear) { if (messageObject.checkLayout()) { currentMessageObject = null; @@ -1756,6 +1851,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (messageChanged || dataChanged || isPhotoDataChanged(messageObject) || pinnedBottom != bottomNear || pinnedTop != topNear) { pinnedBottom = bottomNear; pinnedTop = topNear; + lastTimeString = null; + widthBeforeNewTimeLine = -1; currentMessageObject = messageObject; lastSendState = messageObject.messageOwner.send_state; lastDeleteDate = messageObject.messageOwner.destroyTime; @@ -1781,9 +1878,14 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate messageObject.forceUpdate = false; drawPhotoImage = false; hasLinkPreview = false; + hasOldCaptionPreview = false; hasGamePreview = false; hasInvoicePreview = false; - instantPressed = false; + instantPressed = instantButtonPressed = false; + if (Build.VERSION.SDK_INT >= 21 && instantViewSelectorDrawable != null) { + instantViewSelectorDrawable.setVisible(false, false); + instantViewSelectorDrawable.setState(StateSet.NOTHING); + } linkPreviewPressed = false; buttonPressed = 0; pressedBotButton = -1; @@ -1811,11 +1913,14 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate drawName = false; useSeekBarWaweform = false; drawInstantView = false; + drawInstantViewType = 0; drawForwardedName = false; mediaBackground = false; + int captionNewLine = 0; availableTimeWidth = 0; photoImage.setNeedsQualityThumb(false); photoImage.setShouldGenerateQualityThumb(false); + photoImage.setAllowDecodeSingleFrame(false); photoImage.setParentMessageObject(null); photoImage.setRoundRadius(AndroidUtilities.dp(3)); @@ -1830,7 +1935,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate int maxWidth; if (AndroidUtilities.isTablet()) { - if (isChat && !messageObject.isOutOwner() && messageObject.isFromUser()) { + if (isChat && !messageObject.isOutOwner() && messageObject.needDrawAvatar()) { maxWidth = AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(122); drawName = true; } else { @@ -1838,7 +1943,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate maxWidth = AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(80); } } else { - if (isChat && !messageObject.isOutOwner() && messageObject.isFromUser()) { + if (isChat && !messageObject.isOutOwner() && messageObject.needDrawAvatar()) { maxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) - AndroidUtilities.dp(122); drawName = true; } else { @@ -1857,6 +1962,57 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate hasInvoicePreview = messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaInvoice; hasLinkPreview = messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage && messageObject.messageOwner.media.webpage instanceof TLRPC.TL_webPage; drawInstantView = Build.VERSION.SDK_INT >= 16 && hasLinkPreview && messageObject.messageOwner.media.webpage.cached_page != null; + String webpageType = hasLinkPreview ? messageObject.messageOwner.media.webpage.type : null; + if (!drawInstantView) { + if ("telegram_channel".equals(webpageType)) { + drawInstantView = true; + drawInstantViewType = 1; + } else if ("telegram_group".equals(webpageType)) { + drawInstantView = true; + drawInstantViewType = 2; + } + } + if (Build.VERSION.SDK_INT >= 21 && drawInstantView) { + if (instantViewSelectorDrawable == null) { + final Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + maskPaint.setColor(0xffffffff); + Drawable maskDrawable = new Drawable() { + + RectF rect = new RectF(); + + @Override + public void draw(Canvas canvas) { + android.graphics.Rect bounds = getBounds(); + rect.set(bounds.left, bounds.top, bounds.right, bounds.bottom); + canvas.drawRoundRect(rect, AndroidUtilities.dp(6), AndroidUtilities.dp(6), maskPaint); + } + + @Override + public void setAlpha(int alpha) { + + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + + } + + @Override + public int getOpacity() { + return 255; + } + }; + ColorStateList colorStateList = new ColorStateList( + new int[][]{StateSet.WILD_CARD}, + new int[]{Theme.getColor(currentMessageObject.isOutOwner() ? Theme.key_chat_outPreviewInstantText : Theme.key_chat_inPreviewInstantText) & 0x5fffffff} + ); + instantViewSelectorDrawable = new RippleDrawable(colorStateList, null, maskDrawable); + instantViewSelectorDrawable.setCallback(this); + } else { + Theme.setSelectorDrawableColor(instantViewSelectorDrawable, Theme.getColor(currentMessageObject.isOutOwner() ? Theme.key_chat_outPreviewInstantText : Theme.key_chat_inPreviewInstantText) & 0x5fffffff, true); + } + instantViewSelectorDrawable.setVisible(true, false); + } backgroundWidth = maxWidth; if (hasLinkPreview || hasGamePreview || hasInvoicePreview || maxWidth - messageObject.lastLineWidth < timeMore) { backgroundWidth = Math.max(backgroundWidth, messageObject.lastLineWidth) + AndroidUtilities.dp(31); @@ -1888,13 +2044,13 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (hasLinkPreview || hasGamePreview || hasInvoicePreview) { int linkPreviewMaxWidth; if (AndroidUtilities.isTablet()) { - if (messageObject.isFromUser() && (currentMessageObject.messageOwner.to_id.channel_id != 0 || currentMessageObject.messageOwner.to_id.chat_id != 0) && !currentMessageObject.isOut()) { + if (messageObject.needDrawAvatar() && (currentMessageObject.messageOwner.to_id.channel_id != 0 || currentMessageObject.messageOwner.to_id.chat_id != 0) && !currentMessageObject.isOut()) { linkPreviewMaxWidth = AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(122); } else { linkPreviewMaxWidth = AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(80); } } else { - if (messageObject.isFromUser() && (currentMessageObject.messageOwner.to_id.channel_id != 0 || currentMessageObject.messageOwner.to_id.chat_id != 0) && !currentMessageObject.isOutOwner()) { + if (messageObject.needDrawAvatar() && (currentMessageObject.messageOwner.to_id.channel_id != 0 || currentMessageObject.messageOwner.to_id.chat_id != 0) && !currentMessageObject.isOutOwner()) { linkPreviewMaxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) - AndroidUtilities.dp(122); } else { linkPreviewMaxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) - AndroidUtilities.dp(80); @@ -1927,8 +2083,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (site_name != null && photo != null && site_name.toLowerCase().equals("instagram")) { linkPreviewMaxWidth = Math.max(AndroidUtilities.displaySize.y / 3, currentMessageObject.textWidth); } - smallImage = !drawInstantView && type != null && (type.equals("app") || type.equals("profile") || type.equals("article")); - isSmallImage = !drawInstantView && description != null && type != null && (type.equals("app") || type.equals("profile") || type.equals("article")) && currentMessageObject.photoThumbs != null; + smallImage = !drawInstantView && document == null && type != null && (type.equals("app") || type.equals("profile") || type.equals("article")); + isSmallImage = !drawInstantView && document == null && description != null && type != null && (type.equals("app") || type.equals("profile") || type.equals("article")) && currentMessageObject.photoThumbs != null; } else if (hasInvoicePreview) { site_name = messageObject.messageOwner.media.title; title = null; @@ -2137,9 +2293,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (document != null) { if (MessageObject.isGifDocument(document)){ if (!MediaController.getInstance().canAutoplayGifs()) { - messageObject.audioProgress = 1; + messageObject.gifState = 1; } - photoImage.setAllowStartAnimation(messageObject.audioProgress != 1); + photoImage.setAllowStartAnimation(messageObject.gifState != 1); currentPhotoObject = document.thumb; if (currentPhotoObject != null && (currentPhotoObject.w == 0 || currentPhotoObject.h == 0)) { for (int a = 0; a < document.attributes.size(); a++) { @@ -2188,6 +2344,10 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } documentAttach = document; documentAttachType = DOCUMENT_ATTACH_TYPE_STICKER; + } else if (MessageObject.isRoundVideoDocument(document)) { + currentPhotoObject = document.thumb; + documentAttach = document; + documentAttachType = DOCUMENT_ATTACH_TYPE_ROUND; } else { calcBackgroundWidth(maxWidth, timeMore, maxChildWidth); if (!MessageObject.isStickerDocument(document)) { @@ -2260,6 +2420,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { maxPhotoWidth = (int) (AndroidUtilities.displaySize.x * 0.5f); } + } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_ROUND) { + maxPhotoWidth = AndroidUtilities.roundMessageSize; + photoImage.setAllowDecodeSingleFrame(true); } maxChildWidth = Math.max(maxChildWidth, maxPhotoWidth - (hasInvoicePreview ? AndroidUtilities.dp(12) : 0) + additinalWidth); @@ -2274,7 +2437,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate int width; int height; - if (smallImage) { + if (smallImage || documentAttachType == DOCUMENT_ATTACH_TYPE_ROUND) { width = height = maxPhotoWidth; } else { if (hasGamePreview || hasInvoicePreview) { @@ -2319,15 +2482,21 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate photoImage.setImage(documentAttach, null, currentPhotoFilter, null, currentPhotoObject != null ? currentPhotoObject.location : null, "b1", documentAttach.size, "webp", true); } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO) { photoImage.setImage(null, null, currentPhotoObject.location, currentPhotoFilter, 0, null, false); - } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { - boolean photoExist = messageObject.mediaExists; + } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF || documentAttachType == DOCUMENT_ATTACH_TYPE_ROUND) { String fileName = FileLoader.getAttachFileName(document); - if (hasGamePreview || photoExist || MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_GIF) || FileLoader.getInstance().isLoadingFile(fileName)) { + boolean autoDownload = false; + if (MessageObject.isNewGifDocument(document)) { + autoDownload = MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_GIF); + } else if (MessageObject.isRoundVideoDocument(document)) { + photoImage.setRoundRadius(AndroidUtilities.roundMessageSize / 2); + autoDownload = MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_VIDEOMESSAGE); + } + if (!messageObject.isSending() && (messageObject.mediaExists || FileLoader.getInstance().isLoadingFile(fileName) || autoDownload)) { photoNotSet = false; - photoImage.setImage(document, null, currentPhotoObject.location, currentPhotoFilter, document.size, null, false); + photoImage.setImage(document, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilterThumb, document.size, null, false); } else { photoNotSet = true; - photoImage.setImage(null, null, currentPhotoObject.location, currentPhotoFilter, 0, null, false); + photoImage.setImage(null, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilterThumb, 0, null, false); } } else { boolean photoExist = messageObject.mediaExists; @@ -2396,16 +2565,23 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate calcBackgroundWidth(maxWidth, timeMore, maxChildWidth); } if (drawInstantView) { + String str; instantWidth = AndroidUtilities.dp(12 + 9 + 12); - String str = LocaleController.getString("InstantView", R.string.InstantView); + if (drawInstantViewType == 1) { + str = LocaleController.getString("OpenChannel", R.string.OpenChannel); + } else if (drawInstantViewType == 2) { + str = LocaleController.getString("OpenGroup", R.string.OpenGroup); + } else { + str = LocaleController.getString("InstantView", R.string.InstantView); + } int mWidth = backgroundWidth - AndroidUtilities.dp(10 + 24 + 10 + 31); instantViewLayout = new StaticLayout(TextUtils.ellipsize(str, Theme.chat_instantViewPaint, mWidth, TextUtils.TruncateAt.END), Theme.chat_instantViewPaint, mWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + instantWidth = backgroundWidth - AndroidUtilities.dp(34); + totalHeight += AndroidUtilities.dp(46); if (instantViewLayout != null && instantViewLayout.getLineCount() > 0) { - instantTextX = (int) -instantViewLayout.getLineLeft(0); - instantWidth += instantViewLayout.getLineWidth(0) + instantTextX; + instantTextX = (int) (instantWidth - Math.ceil(instantViewLayout.getLineWidth(0))) / 2 + (drawInstantViewType == 0 ? AndroidUtilities.dp(8) : 0); + instantTextX += (int) -instantViewLayout.getLineLeft(0); } - linkPreviewHeight += AndroidUtilities.dp(40); - totalHeight += AndroidUtilities.dp(40); } } else { photoImage.setImageBitmap((Drawable) null); @@ -2416,9 +2592,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate drawForwardedName = false; drawPhotoImage = false; if (AndroidUtilities.isTablet()) { - backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } else { - backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } availableTimeWidth = backgroundWidth - AndroidUtilities.dp(31); @@ -2465,9 +2641,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate drawPhotoImage = true; photoImage.setRoundRadius(AndroidUtilities.dp(22)); if (AndroidUtilities.isTablet()) { - backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } else { - backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } availableTimeWidth = backgroundWidth - AndroidUtilities.dp(31); @@ -2523,9 +2699,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else if (messageObject.type == 2) { drawForwardedName = true; if (AndroidUtilities.isTablet()) { - backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } else { - backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } createDocumentLayout(backgroundWidth, messageObject); @@ -2537,9 +2713,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } else if (messageObject.type == 14) { if (AndroidUtilities.isTablet()) { - backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } else { - backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } createDocumentLayout(backgroundWidth, messageObject); @@ -2560,18 +2736,23 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate int photoHeight = 0; int additionHeight = 0; - if (messageObject.audioProgress != 2 && !MediaController.getInstance().canAutoplayGifs() && messageObject.type == 8) { - messageObject.audioProgress = 1; + if (messageObject.gifState != 2 && !MediaController.getInstance().canAutoplayGifs() && (messageObject.type == 8 || messageObject.type == 5)) { + messageObject.gifState = 1; } - photoImage.setAllowStartAnimation(messageObject.audioProgress == 0); + if (messageObject.isRoundVideo()) { + photoImage.setAllowDecodeSingleFrame(true); + photoImage.setAllowStartAnimation(MediaController.getInstance().getPlayingMessageObject() == null); + } else { + photoImage.setAllowStartAnimation(messageObject.gifState == 0); + } photoImage.setForcePreview(messageObject.isSecretPhoto()); if (messageObject.type == 9) { if (AndroidUtilities.isTablet()) { - backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } else { - backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } if (checkNeedDrawShareButton(messageObject)) { backgroundWidth -= AndroidUtilities.dp(20); @@ -2605,9 +2786,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (messageObject.messageOwner.media.title != null && messageObject.messageOwner.media.title.length() > 0) { if (AndroidUtilities.isTablet()) { - backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } else { - backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); } if (checkNeedDrawShareButton(messageObject)) { backgroundWidth -= AndroidUtilities.dp(20); @@ -2683,13 +2864,17 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } else { int maxPhotoWidth; - if (AndroidUtilities.isTablet()) { - maxPhotoWidth = photoWidth = (int) (AndroidUtilities.getMinTabletSide() * 0.7f); + if (messageObject.type == 5) { + maxPhotoWidth = photoWidth = AndroidUtilities.roundMessageSize; } else { - maxPhotoWidth = photoWidth = (int) (Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) * 0.7f); + if (AndroidUtilities.isTablet()) { + maxPhotoWidth = photoWidth = (int) (AndroidUtilities.getMinTabletSide() * 0.7f); + } else { + maxPhotoWidth = photoWidth = (int) (Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) * 0.7f); + } } photoHeight = photoWidth + AndroidUtilities.dp(100); - if (checkNeedDrawShareButton(messageObject)) { + if (messageObject.type != 5 && checkNeedDrawShareButton(messageObject)) { maxPhotoWidth -= AndroidUtilities.dp(20); photoWidth -= AndroidUtilities.dp(20); } @@ -2709,6 +2894,10 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate photoImage.setNeedsQualityThumb(true); photoImage.setShouldGenerateQualityThumb(true); photoImage.setParentMessageObject(messageObject); + } else if (messageObject.type == 5) { //round video + photoImage.setNeedsQualityThumb(true); + photoImage.setShouldGenerateQualityThumb(true); + photoImage.setParentMessageObject(messageObject); } else if (messageObject.type == 8) { //gif String str = AndroidUtilities.formatFileSize(messageObject.messageOwner.media.document.size); infoWidth = (int) Math.ceil(Theme.chat_infoPaint.measureText(str)); @@ -2755,6 +2944,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } } + if (messageObject.type == 5) { + w = h = AndroidUtilities.roundMessageSize; + } if ((w == 0 || h == 0) && messageObject.type == 8) { for (int a = 0; a < messageObject.messageOwner.media.document.attributes.size(); a++) { @@ -2791,24 +2983,26 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } availableTimeWidth = maxPhotoWidth - AndroidUtilities.dp(14); + if (messageObject.type == 5) { + availableTimeWidth -= Math.ceil(Theme.chat_audioTimePaint.measureText("00:00")) + AndroidUtilities.dp(26); + } measureTime(messageObject); int timeWidthTotal = timeWidth + AndroidUtilities.dp(14 + (messageObject.isOutOwner() ? 20 : 0)); if (w < timeWidthTotal) { w = timeWidthTotal; } - if (messageObject.isSecretPhoto()) { + if (messageObject.isRoundVideo()) { + w = h = Math.min(w, h); + drawBackground = false; + photoImage.setRoundRadius(w / 2); + } else if (messageObject.isSecretPhoto()) { if (AndroidUtilities.isTablet()) { w = h = (int) (AndroidUtilities.getMinTabletSide() * 0.5f); } else { w = h = (int) (Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) * 0.5f); } } - if (messageObject.isVideoVoice()) { - w = h = Math.min(w, h); - drawBackground = false; - photoImage.setRoundRadius(w / 2); - } photoWidth = w; photoHeight = h; @@ -2825,6 +3019,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate float lastLineWidth = captionLayout.getLineWidth(captionLayout.getLineCount() - 1) + captionLayout.getLineLeft(captionLayout.getLineCount() - 1); if (photoWidth - AndroidUtilities.dp(8) - lastLineWidth < timeWidthTotal) { additionHeight += AndroidUtilities.dp(14); + captionNewLine = 1; } } } catch (Exception e) { @@ -2832,17 +3027,18 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } - currentPhotoFilter = String.format(Locale.US, "%d_%d", (int) (w / AndroidUtilities.density), (int) (h / AndroidUtilities.density)); - if (messageObject.photoThumbs != null && messageObject.photoThumbs.size() > 1 || messageObject.type == 3 || messageObject.type == 8) { + currentPhotoFilter = currentPhotoFilterThumb = String.format(Locale.US, "%d_%d", (int) (w / AndroidUtilities.density), (int) (h / AndroidUtilities.density)); + if (messageObject.photoThumbs != null && messageObject.photoThumbs.size() > 1 || messageObject.type == 3 || messageObject.type == 8 || messageObject.type == 5) { if (messageObject.isSecretPhoto()) { currentPhotoFilter += "_b2"; + currentPhotoFilterThumb += "_b2"; } else { - currentPhotoFilter += "_b"; + currentPhotoFilterThumb += "_b"; } } boolean noSize = false; - if (messageObject.type == 3 || messageObject.type == 8) { + if (messageObject.type == 3 || messageObject.type == 8 || messageObject.type == 5) { noSize = true; } if (currentPhotoObject != null && !noSize && currentPhotoObject.size == 0) { @@ -2862,11 +3058,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate photoExist = false; } if (photoExist || MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_PHOTO) || FileLoader.getInstance().isLoadingFile(fileName)) { - photoImage.setImage(currentPhotoObject.location, currentPhotoFilter, currentPhotoObjectThumb != null ? currentPhotoObjectThumb.location : null, currentPhotoFilter, noSize ? 0 : currentPhotoObject.size, null, false); + photoImage.setImage(currentPhotoObject.location, currentPhotoFilter, currentPhotoObjectThumb != null ? currentPhotoObjectThumb.location : null, currentPhotoFilterThumb, noSize ? 0 : currentPhotoObject.size, null, false); } else { photoNotSet = true; if (currentPhotoObjectThumb != null) { - photoImage.setImage(null, null, currentPhotoObjectThumb.location, currentPhotoFilter, 0, null, false); + photoImage.setImage(null, null, currentPhotoObjectThumb.location, currentPhotoFilterThumb, 0, null, false); } else { photoImage.setImageBitmap((Drawable) null); } @@ -2875,7 +3071,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate photoImage.setImageBitmap((BitmapDrawable) null); } } - } else if (messageObject.type == 8) { + } else if (messageObject.type == 8 || messageObject.type == 5) { String fileName = FileLoader.getAttachFileName(messageObject.messageOwner.media.document); int localFile = 0; if (messageObject.attachPathExists) { @@ -2884,24 +3080,32 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else if (messageObject.mediaExists) { localFile = 2; } - if (!messageObject.isSending() && (localFile != 0 || MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_GIF) && MessageObject.isNewGifDocument(messageObject.messageOwner.media.document) || FileLoader.getInstance().isLoadingFile(fileName))) { + boolean autoDownload = false; + if (MessageObject.isNewGifDocument(messageObject.messageOwner.media.document)) { + autoDownload = MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_GIF); + } else if (messageObject.type == 5) { + autoDownload = MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_VIDEOMESSAGE); + } + if (!messageObject.isSending() && (localFile != 0 || FileLoader.getInstance().isLoadingFile(fileName) || autoDownload)) { if (localFile == 1) { - photoImage.setImage(null, messageObject.isSendError() ? null : messageObject.messageOwner.attachPath, null, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilter, 0, null, false); + photoImage.setImage(null, messageObject.isSendError() ? null : messageObject.messageOwner.attachPath, null, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilterThumb, 0, null, false); } else { - photoImage.setImage(messageObject.messageOwner.media.document, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilter, messageObject.messageOwner.media.document.size, null, false); + photoImage.setImage(messageObject.messageOwner.media.document, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilterThumb, messageObject.messageOwner.media.document.size, null, false); } } else { photoNotSet = true; - photoImage.setImage(null, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilter, 0, null, false); + photoImage.setImage(null, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilterThumb, 0, null, false); } } else { - photoImage.setImage(null, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilter, 0, null, false); + photoImage.setImage(null, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilterThumb, 0, null, false); } } setMessageObjectInternal(messageObject); if (drawForwardedName) { - namesOffset += AndroidUtilities.dp(5); + if (messageObject.type != 5) { + namesOffset += AndroidUtilities.dp(5); + } } else if (drawNameLayout && messageObject.messageOwner.reply_to_msg_id == 0) { namesOffset += AndroidUtilities.dp(7); } @@ -2913,6 +3117,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate photoImage.setImageCoords(0, AndroidUtilities.dp(7) + namesOffset, photoWidth, photoHeight); invalidate(); } + if (captionLayout == null && messageObject.caption != null && messageObject.type != 13) { try { int width = backgroundWidth - AndroidUtilities.dp(31); @@ -2925,11 +3130,64 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (width - AndroidUtilities.dp(8) - lastLineWidth < timeWidthTotal) { totalHeight += AndroidUtilities.dp(14); captionHeight += AndroidUtilities.dp(14); + captionNewLine = 2; } } } catch (Exception e) { FileLog.e(e); } + } else if (widthBeforeNewTimeLine != -1 && availableTimeWidth - widthBeforeNewTimeLine < timeWidth) { + totalHeight += AndroidUtilities.dp(14); + } + + if (currentMessageObject.eventId != 0 && !currentMessageObject.isMediaEmpty() && currentMessageObject.messageOwner.media.webpage != null) { + int linkPreviewMaxWidth = backgroundWidth - AndroidUtilities.dp(41); + hasOldCaptionPreview = true; + linkPreviewHeight = 0; + TLRPC.WebPage webPage = currentMessageObject.messageOwner.media.webpage; + try { + int width = (int) Math.ceil(Theme.chat_replyNamePaint.measureText(webPage.site_name)); + siteNameLayout = new StaticLayout(webPage.site_name, Theme.chat_replyNamePaint, Math.min(width, linkPreviewMaxWidth), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + int height = siteNameLayout.getLineBottom(siteNameLayout.getLineCount() - 1); + linkPreviewHeight += height; + totalHeight += height; + } catch (Exception e) { + FileLog.e(e); + } + + try { + descriptionX = 0; + if (linkPreviewHeight != 0) { + totalHeight += AndroidUtilities.dp(2); + } + + descriptionLayout = StaticLayoutEx.createStaticLayout(webPage.description, Theme.chat_replyTextPaint, linkPreviewMaxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, AndroidUtilities.dp(1), false, TextUtils.TruncateAt.END, linkPreviewMaxWidth, 6); + + int height = descriptionLayout.getLineBottom(descriptionLayout.getLineCount() - 1); + linkPreviewHeight += height; + totalHeight += height; + + for (int a = 0; a < descriptionLayout.getLineCount(); a++) { + int lineLeft = (int) Math.ceil(descriptionLayout.getLineLeft(a)); + if (lineLeft != 0) { + if (descriptionX == 0) { + descriptionX = -lineLeft; + } else { + descriptionX = Math.max(descriptionX, -lineLeft); + } + } + } + } catch (Exception e) { + FileLog.e(e); + } + + totalHeight += AndroidUtilities.dp(17); + if (captionNewLine != 0) { + totalHeight -= AndroidUtilities.dp(14); + if (captionNewLine == 2) { + captionHeight -= AndroidUtilities.dp(14); + } + } } botButtons.clear(); @@ -2945,7 +3203,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate widthForButtons = backgroundWidth; boolean fullWidth = false; if (messageObject.wantedBotKeyboardWidth > widthForButtons) { - int maxButtonWidth = -AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 62 : 10); + int maxButtonWidth = -AndroidUtilities.dp(isChat && messageObject.needDrawAvatar() && !messageObject.isOutOwner() ? 62 : 10); if (AndroidUtilities.isTablet()) { maxButtonWidth += AndroidUtilities.getMinTabletSide(); } else { @@ -3061,16 +3319,16 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (timeTextWidth < 0) { timeTextWidth = AndroidUtilities.dp(10); } - timeLayout = new StaticLayout(currentTimeString, Theme.chat_timePaint, timeTextWidth + AndroidUtilities.dp(6), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + timeLayout = new StaticLayout(currentTimeString, Theme.chat_timePaint, timeTextWidth + AndroidUtilities.dp(100), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); if (!mediaBackground) { if (!currentMessageObject.isOutOwner()) { - timeX = backgroundWidth - AndroidUtilities.dp(9) - timeWidth + (isChat && currentMessageObject.isFromUser() ? AndroidUtilities.dp(48) : 0); + timeX = backgroundWidth - AndroidUtilities.dp(9) - timeWidth + (isChat && currentMessageObject.needDrawAvatar() ? AndroidUtilities.dp(48) : 0); } else { timeX = layoutWidth - timeWidth - AndroidUtilities.dp(38.5f); } } else { if (!currentMessageObject.isOutOwner()) { - timeX = backgroundWidth - AndroidUtilities.dp(4) - timeWidth + (isChat && currentMessageObject.isFromUser() ? AndroidUtilities.dp(48) : 0); + timeX = backgroundWidth - AndroidUtilities.dp(4) - timeWidth + (isChat && currentMessageObject.needDrawAvatar() ? AndroidUtilities.dp(48) : 0); } else { timeX = layoutWidth - timeWidth - AndroidUtilities.dp(42.0f); } @@ -3092,13 +3350,16 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (currentMessageObject.type == 0) { textY = AndroidUtilities.dp(10) + namesOffset; } + if (currentMessageObject.isRoundVideo()) { + updatePlayingMessageProgress(); + } if (documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO) { if (currentMessageObject.isOutOwner()) { seekBarX = layoutWidth - backgroundWidth + AndroidUtilities.dp(57); buttonX = layoutWidth - backgroundWidth + AndroidUtilities.dp(14); timeAudioX = layoutWidth - backgroundWidth + AndroidUtilities.dp(67); } else { - if (isChat && currentMessageObject.isFromUser()) { + if (isChat && currentMessageObject.needDrawAvatar()) { seekBarX = AndroidUtilities.dp(114); buttonX = AndroidUtilities.dp(71); timeAudioX = AndroidUtilities.dp(124); @@ -3119,14 +3380,14 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate buttonY = AndroidUtilities.dp(13) + namesOffset + mediaOffsetY; radialProgress.setProgressRect(buttonX, buttonY, buttonX + AndroidUtilities.dp(44), buttonY + AndroidUtilities.dp(44)); - updateAudioProgress(); + updatePlayingMessageProgress(); } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { if (currentMessageObject.isOutOwner()) { seekBarX = layoutWidth - backgroundWidth + AndroidUtilities.dp(56); buttonX = layoutWidth - backgroundWidth + AndroidUtilities.dp(14); timeAudioX = layoutWidth - backgroundWidth + AndroidUtilities.dp(67); } else { - if (isChat && currentMessageObject.isFromUser()) { + if (isChat && currentMessageObject.needDrawAvatar()) { seekBarX = AndroidUtilities.dp(113); buttonX = AndroidUtilities.dp(71); timeAudioX = AndroidUtilities.dp(124); @@ -3146,12 +3407,12 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate buttonY = AndroidUtilities.dp(13) + namesOffset + mediaOffsetY; radialProgress.setProgressRect(buttonX, buttonY, buttonX + AndroidUtilities.dp(44), buttonY + AndroidUtilities.dp(44)); - updateAudioProgress(); + updatePlayingMessageProgress(); } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_DOCUMENT && !drawPhotoImage) { if (currentMessageObject.isOutOwner()) { buttonX = layoutWidth - backgroundWidth + AndroidUtilities.dp(14); } else { - if (isChat && currentMessageObject.isFromUser()) { + if (isChat && currentMessageObject.needDrawAvatar()) { buttonX = AndroidUtilities.dp(71); } else { buttonX = AndroidUtilities.dp(23); @@ -3169,7 +3430,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (currentMessageObject.isOutOwner()) { x = layoutWidth - backgroundWidth + AndroidUtilities.dp(14); } else { - if (isChat && currentMessageObject.isFromUser()) { + if (isChat && currentMessageObject.needDrawAvatar()) { x = AndroidUtilities.dp(72); } else { x = AndroidUtilities.dp(23); @@ -3178,17 +3439,33 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate photoImage.setImageCoords(x, AndroidUtilities.dp(13) + namesOffset, AndroidUtilities.dp(44), AndroidUtilities.dp(44)); } else { int x; - if (currentMessageObject.isOutOwner()) { - if (mediaBackground) { - x = layoutWidth - backgroundWidth - AndroidUtilities.dp(3); + if (currentMessageObject.type == 0 && (hasLinkPreview || hasGamePreview || hasInvoicePreview)) { + int linkX; + if (hasGamePreview) { + linkX = textX - AndroidUtilities.dp(10); + } else if (hasInvoicePreview) { + linkX = textX + AndroidUtilities.dp(1); } else { - x = layoutWidth - backgroundWidth + AndroidUtilities.dp(6); + linkX = textX + AndroidUtilities.dp(1); + } + if (isSmallImage) { + x = linkX + backgroundWidth - AndroidUtilities.dp(81); + } else { + x = linkX + (hasInvoicePreview ? -AndroidUtilities.dp(6.3f) : AndroidUtilities.dp(10)); } } else { - if (isChat && currentMessageObject.isFromUser()) { - x = AndroidUtilities.dp(63); + if (currentMessageObject.isOutOwner()) { + if (mediaBackground) { + x = layoutWidth - backgroundWidth - AndroidUtilities.dp(3); + } else { + x = layoutWidth - backgroundWidth + AndroidUtilities.dp(6); + } } else { - x = AndroidUtilities.dp(15); + if (isChat && currentMessageObject.needDrawAvatar()) { + x = AndroidUtilities.dp(63); + } else { + x = AndroidUtilities.dp(15); + } } } photoImage.setImageCoords(x, photoImage.getImageY(), photoImage.getImageWidth(), photoImage.getImageHeight()); @@ -3385,7 +3662,12 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate radialProgress.setProgressRect(buttonX, buttonY, buttonX + size, buttonY + size); } } - imageDrawn = photoImage.draw(canvas); + if (currentMessageObject.isRoundVideo() && MediaController.getInstance().isPlayingMessage(currentMessageObject) && MediaController.getInstance().isRoundVideoDrawingReady()) { + imageDrawn = true; + drawTime = true; + } else { + imageDrawn = photoImage.draw(canvas); + } } if (videoInfoLayout != null && (!drawPhotoImage || photoImage.getVisible())) { int x; @@ -3394,8 +3676,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (drawPhotoImage) { x = photoImage.getImageX() + AndroidUtilities.dp(8.5f); y = photoImage.getImageY() + AndroidUtilities.dp(6); - Theme.chat_timeBackgroundDrawable.setBounds(x - AndroidUtilities.dp(4), y - AndroidUtilities.dp(1.5f), x + durationWidth + AndroidUtilities.dp(4), y + AndroidUtilities.dp(16.5f)); - Theme.chat_timeBackgroundDrawable.draw(canvas); + rect.set(x - AndroidUtilities.dp(4), y - AndroidUtilities.dp(1.5f), x + durationWidth + AndroidUtilities.dp(4), y + AndroidUtilities.dp(16.5f)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(4), AndroidUtilities.dp(4), Theme.chat_timeBackgroundPaint); } else { x = linkX; y = linkPreviewY; @@ -3403,8 +3685,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { x = photoImage.getImageX() + photoImage.getImageWidth() - AndroidUtilities.dp(8) - durationWidth; y = photoImage.getImageY() + photoImage.getImageHeight() - AndroidUtilities.dp(19); - Theme.chat_timeBackgroundDrawable.setBounds(x - AndroidUtilities.dp(4), y - AndroidUtilities.dp(1.5f), x + durationWidth + AndroidUtilities.dp(4), y + AndroidUtilities.dp(14.5f)); - Theme.chat_timeBackgroundDrawable.draw(canvas); + rect.set(x - AndroidUtilities.dp(4), y - AndroidUtilities.dp(1.5f), x + durationWidth + AndroidUtilities.dp(4), y + AndroidUtilities.dp(14.5f)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(4), AndroidUtilities.dp(4), Theme.chat_timeBackgroundPaint); } canvas.save(); @@ -3426,26 +3708,31 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (drawInstantView) { Drawable instantDrawable; - int instantX = linkX + AndroidUtilities.dp(10); int instantY = linkPreviewY + AndroidUtilities.dp(4); Paint backPaint = Theme.chat_instantViewRectPaint; if (currentMessageObject.isOutOwner()) { - instantDrawable = instantPressed ? Theme.chat_msgOutInstantSelectedDrawable : Theme.chat_msgOutInstantDrawable; - Theme.chat_instantViewPaint.setColor(Theme.getColor(instantPressed ? Theme.key_chat_outPreviewInstantSelectedText : Theme.key_chat_outPreviewInstantText)); - backPaint.setColor(Theme.getColor(instantPressed ? Theme.key_chat_outPreviewInstantSelectedText : Theme.key_chat_outPreviewInstantText)); + instantDrawable = Theme.chat_msgOutInstantDrawable; + Theme.chat_instantViewPaint.setColor(Theme.getColor(Theme.key_chat_outPreviewInstantText)); + backPaint.setColor(Theme.getColor(Theme.key_chat_outPreviewInstantText)); } else { - instantDrawable = instantPressed ? Theme.chat_msgInInstantSelectedDrawable : Theme.chat_msgInInstantDrawable; - Theme.chat_instantViewPaint.setColor(Theme.getColor(instantPressed ? Theme.key_chat_inPreviewInstantSelectedText : Theme.key_chat_inPreviewInstantText)); - backPaint.setColor(Theme.getColor(instantPressed ? Theme.key_chat_inPreviewInstantSelectedText : Theme.key_chat_inPreviewInstantText)); + instantDrawable = Theme.chat_msgInInstantDrawable; + Theme.chat_instantViewPaint.setColor(Theme.getColor(Theme.key_chat_inPreviewInstantText)); + backPaint.setColor(Theme.getColor(Theme.key_chat_inPreviewInstantText)); } - rect.set(instantX, instantY, instantX + instantWidth, instantY + AndroidUtilities.dp(30)); - canvas.drawRoundRect(rect, AndroidUtilities.dp(3), AndroidUtilities.dp(3), backPaint); - setDrawableBounds(instantDrawable, instantX + AndroidUtilities.dp(9), instantY + AndroidUtilities.dp(9), AndroidUtilities.dp(9), AndroidUtilities.dp(13)); - instantDrawable.draw(canvas); + if (Build.VERSION.SDK_INT >= 21) { + instantViewSelectorDrawable.setBounds(linkX, instantY, linkX + instantWidth, instantY + AndroidUtilities.dp(36)); + instantViewSelectorDrawable.draw(canvas); + } + rect.set(linkX, instantY, linkX + instantWidth, instantY + AndroidUtilities.dp(36)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(6), AndroidUtilities.dp(6), backPaint); + if (drawInstantViewType == 0) { + setDrawableBounds(instantDrawable, instantTextX + linkX - AndroidUtilities.dp(15), instantY + AndroidUtilities.dp(11.5f), AndroidUtilities.dp(9), AndroidUtilities.dp(13)); + instantDrawable.draw(canvas); + } if (instantViewLayout != null) { canvas.save(); - canvas.translate(instantX + instantTextX + AndroidUtilities.dp(24), instantY + AndroidUtilities.dp(8)); + canvas.translate(linkX + instantTextX, instantY + AndroidUtilities.dp(10.5f)); instantViewLayout.draw(canvas); canvas.restore(); } @@ -3453,11 +3740,23 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } drawTime = true; } else if (drawPhotoImage) { - imageDrawn = photoImage.draw(canvas); - drawTime = photoImage.getVisible(); + if (currentMessageObject.isRoundVideo() && MediaController.getInstance().isPlayingMessage(currentMessageObject) && MediaController.getInstance().isRoundVideoDrawingReady()) { + imageDrawn = true; + drawTime = true; + } else { + if (currentMessageObject.type == 5 && Theme.chat_roundVideoShadow != null) { + int x = photoImage.getImageX() - AndroidUtilities.dp(3); + int y = photoImage.getImageY() - AndroidUtilities.dp(2); + Theme.chat_roundVideoShadow.setAlpha((int) (photoImage.getCurrentAlpha() * 255)); + Theme.chat_roundVideoShadow.setBounds(x, y, x + AndroidUtilities.roundMessageSize + AndroidUtilities.dp(6), y + AndroidUtilities.roundMessageSize + AndroidUtilities.dp(6)); + Theme.chat_roundVideoShadow.draw(canvas); + } + imageDrawn = photoImage.draw(canvas); + drawTime = photoImage.getVisible(); + } } - if (buttonState == -1 && currentMessageObject.isSecretPhoto()) { + if (buttonState == -1 && currentMessageObject.isSecretPhoto() && !MediaController.getInstance().isPlayingMessage(currentMessageObject)) { int drawable = 4; if (currentMessageObject.messageOwner.destroyTime != 0) { if (currentMessageObject.isOutOwner()) { @@ -3481,11 +3780,51 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } - if ((documentAttachType == DOCUMENT_ATTACH_TYPE_GIF || currentMessageObject.type == 8) && !currentMessageObject.isVideoVoice()) { //TODO + if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF || currentMessageObject.type == 8) { if (photoImage.getVisible() && !hasGamePreview) { setDrawableBounds(Theme.chat_msgMediaMenuDrawable, otherX = photoImage.getImageX() + photoImage.getImageWidth() - AndroidUtilities.dp(14), otherY = photoImage.getImageY() + AndroidUtilities.dp(8.1f)); Theme.chat_msgMediaMenuDrawable.draw(canvas); } + } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_ROUND || currentMessageObject.type == 5) { + if (durationLayout != null) { + int x1; + int y1; + + boolean playing = MediaController.getInstance().isPlayingMessage(currentMessageObject); + if (playing) { + rect.set(photoImage.getImageX() + AndroidUtilities.dpf2(1.5f), photoImage.getImageY() + AndroidUtilities.dpf2(1.5f), photoImage.getImageX2() - AndroidUtilities.dpf2(1.5f), photoImage.getImageY2() - AndroidUtilities.dpf2(1.5f)); + canvas.drawArc(rect, -90, 360 * currentMessageObject.audioProgress, false, Theme.chat_radialProgressPaint); + } + if (documentAttachType == DOCUMENT_ATTACH_TYPE_ROUND) { + x1 = backgroundDrawableLeft + AndroidUtilities.dp(currentMessageObject.isOutOwner() ? 12 : 18); + y1 = layoutHeight - AndroidUtilities.dp(6.3f - (pinnedBottom ? 2 : 0)) - timeLayout.getHeight(); + } else { + x1 = backgroundDrawableLeft + AndroidUtilities.dp(8); + y1 = layoutHeight - AndroidUtilities.dp(28); + rect.set(x1, y1, x1 + timeWidthAudio + AndroidUtilities.dp(8 + 12 + 2), y1 + AndroidUtilities.dp(17)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(4), AndroidUtilities.dp(4), Theme.chat_actionBackgroundPaint); + + if (!playing && currentMessageObject.messageOwner.to_id.channel_id == 0 && currentMessageObject.isContentUnread()) { + Theme.chat_docBackPaint.setColor(Theme.getColor(Theme.key_chat_mediaTimeText)); + canvas.drawCircle(x1 + timeWidthAudio + AndroidUtilities.dp(12), y1 + AndroidUtilities.dp(8.3f), AndroidUtilities.dp(3), Theme.chat_docBackPaint); + } else { + if (playing && !MediaController.getInstance().isMessagePaused()) { + roundVideoPlayingDrawable.start(); + } else { + roundVideoPlayingDrawable.stop(); + } + setDrawableBounds(roundVideoPlayingDrawable, x1 + timeWidthAudio + AndroidUtilities.dp(6), y1 + AndroidUtilities.dp(2.3f)); + roundVideoPlayingDrawable.draw(canvas); + } + x1 += AndroidUtilities.dp(4); + y1 += AndroidUtilities.dp(1.7f); + } + + canvas.save(); + canvas.translate(x1, y1); + durationLayout.draw(canvas); + canvas.restore(); + } } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { if (currentMessageObject.isOutOwner()) { Theme.chat_audioTitlePaint.setColor(Theme.getColor(Theme.key_chat_outAudioTitleText)); @@ -3506,7 +3845,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate canvas.restore(); canvas.save(); - if (MediaController.getInstance().isPlayingAudio(currentMessageObject)) { + if (MediaController.getInstance().isPlayingMessage(currentMessageObject)) { canvas.translate(seekBarX, seekBarY); seekBar.draw(canvas); } else { @@ -3568,8 +3907,10 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (infoLayout != null && (buttonState == 1 || buttonState == 0 || buttonState == 3 || currentMessageObject.isSecretPhoto())) { Theme.chat_infoPaint.setColor(Theme.getColor(Theme.key_chat_mediaInfoText)); - setDrawableBounds(Theme.chat_timeBackgroundDrawable, photoImage.getImageX() + AndroidUtilities.dp(4), photoImage.getImageY() + AndroidUtilities.dp(4), infoWidth + AndroidUtilities.dp(8), AndroidUtilities.dp(16.5f)); - Theme.chat_timeBackgroundDrawable.draw(canvas); + int x1 = photoImage.getImageX() + AndroidUtilities.dp(4); + int y1 = photoImage.getImageY() + AndroidUtilities.dp(4); + rect.set(x1, y1, x1 + infoWidth + AndroidUtilities.dp(8), y1 + AndroidUtilities.dp(16.5f)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(4), AndroidUtilities.dp(4), Theme.chat_timeBackgroundPaint); canvas.save(); canvas.translate(photoImage.getImageX() + AndroidUtilities.dp(8), photoImage.getImageY() + AndroidUtilities.dp(5.5f)); @@ -3613,7 +3954,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (currentMessageObject.isOutOwner()) { x = layoutWidth - backgroundWidth + AndroidUtilities.dp(16); } else { - if (isChat && currentMessageObject.isFromUser()) { + if (isChat && currentMessageObject.needDrawAvatar()) { x = AndroidUtilities.dp(74); } else { x = AndroidUtilities.dp(25); @@ -3682,6 +4023,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate canvas.save(); if (currentMessageObject.type == 1 || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO || currentMessageObject.type == 8) { canvas.translate(captionX = photoImage.getImageX() + AndroidUtilities.dp(5), captionY = photoImage.getImageY() + photoImage.getImageHeight() + AndroidUtilities.dp(6)); + } else if (hasOldCaptionPreview) { + canvas.translate(captionX = backgroundDrawableLeft + AndroidUtilities.dp(currentMessageObject.isOutOwner() ? 11 : 17), captionY = totalHeight - captionHeight - AndroidUtilities.dp(pinnedTop ? 9 : 10) - linkPreviewHeight - AndroidUtilities.dp(17)); } else { canvas.translate(captionX = backgroundDrawableLeft + AndroidUtilities.dp(currentMessageObject.isOutOwner() ? 11 : 17), captionY = totalHeight - captionHeight - AndroidUtilities.dp(pinnedTop ? 9 : 10)); } @@ -3698,6 +4041,47 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate canvas.restore(); } + if (hasOldCaptionPreview) { + int linkX; + if (currentMessageObject.type == 1 || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO || currentMessageObject.type == 8) { + linkX = photoImage.getImageX() + AndroidUtilities.dp(5); + } else { + linkX = backgroundDrawableLeft + AndroidUtilities.dp(currentMessageObject.isOutOwner() ? 11 : 17); + } + int startY = totalHeight - AndroidUtilities.dp(pinnedTop ? 9 : 10) - linkPreviewHeight - AndroidUtilities.dp(8); + int linkPreviewY = startY; + + Theme.chat_replyLinePaint.setColor(Theme.getColor(currentMessageObject.isOutOwner() ? Theme.key_chat_outPreviewLine : Theme.key_chat_inPreviewLine)); + canvas.drawRect(linkX, linkPreviewY - AndroidUtilities.dp(3), linkX + AndroidUtilities.dp(2), linkPreviewY + linkPreviewHeight, Theme.chat_replyLinePaint); + + if (siteNameLayout != null) { + Theme.chat_replyNamePaint.setColor(Theme.getColor(currentMessageObject.isOutOwner() ? Theme.key_chat_outSiteNameText : Theme.key_chat_inSiteNameText)); + canvas.save(); + canvas.translate(linkX + (hasInvoicePreview ? 0 : AndroidUtilities.dp(10)), linkPreviewY - AndroidUtilities.dp(3)); + siteNameLayout.draw(canvas); + canvas.restore(); + linkPreviewY += siteNameLayout.getLineBottom(siteNameLayout.getLineCount() - 1); + } + + if (currentMessageObject.isOutOwner()) { + Theme.chat_replyTextPaint.setColor(Theme.getColor(Theme.key_chat_messageTextOut)); + } else { + Theme.chat_replyTextPaint.setColor(Theme.getColor(Theme.key_chat_messageTextIn)); + } + + if (descriptionLayout != null) { + if (linkPreviewY != startY) { + linkPreviewY += AndroidUtilities.dp(2); + } + descriptionY = linkPreviewY - AndroidUtilities.dp(3); + canvas.save(); + canvas.translate(linkX + AndroidUtilities.dp(10) + descriptionX, descriptionY); + descriptionLayout.draw(canvas); + canvas.restore(); + } + drawTime = true; + } + if (documentAttachType == DOCUMENT_ATTACH_TYPE_DOCUMENT) { Drawable menuDrawable; if (currentMessageObject.isOutOwner()) { @@ -3895,16 +4279,16 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } private int getMaxNameWidth() { - if (documentAttachType == DOCUMENT_ATTACH_TYPE_STICKER) { + if (documentAttachType == DOCUMENT_ATTACH_TYPE_STICKER || currentMessageObject.type == 5) { int maxWidth; if (AndroidUtilities.isTablet()) { - if (isChat && !currentMessageObject.isOutOwner() && currentMessageObject.isFromUser()) { + if (isChat && !currentMessageObject.isOutOwner() && currentMessageObject.needDrawAvatar()) { maxWidth = AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(42); } else { maxWidth = AndroidUtilities.getMinTabletSide(); } } else { - if (isChat && !currentMessageObject.isOutOwner() && currentMessageObject.isFromUser()) { + if (isChat && !currentMessageObject.isOutOwner() && currentMessageObject.needDrawAvatar()) { maxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) - AndroidUtilities.dp(42); } else { maxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y); @@ -3924,7 +4308,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } fileName = FileLoader.getAttachFileName(currentPhotoObject); fileExists = currentMessageObject.mediaExists; - } else if (currentMessageObject.type == 8 || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO || currentMessageObject.type == 9 || documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO || documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { + } else if (currentMessageObject.type == 8 || currentMessageObject.type == 5 || documentAttachType == DOCUMENT_ATTACH_TYPE_ROUND || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO || currentMessageObject.type == 9 || documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO || documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { if (currentMessageObject.useCustomPhoto) { buttonState = 1; radialProgress.setBackground(getDrawableForCurrentState(), false, animated); @@ -3967,8 +4351,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { if (fileExists) { MediaController.getInstance().removeLoadingFileObserver(this); - boolean playing = MediaController.getInstance().isPlayingAudio(currentMessageObject); - if (!playing || playing && MediaController.getInstance().isAudioPaused()) { + boolean playing = MediaController.getInstance().isPlayingMessage(currentMessageObject); + if (!playing || playing && MediaController.getInstance().isMessagePaused()) { buttonState = 0; } else { buttonState = 1; @@ -3992,7 +4376,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } } - updateAudioProgress(); + updatePlayingMessageProgress(); } else if (currentMessageObject.type == 0 && documentAttachType != DOCUMENT_ATTACH_TYPE_DOCUMENT && documentAttachType != DOCUMENT_ATTACH_TYPE_VIDEO) { if (currentPhotoObject == null || !drawImageButton) { return; @@ -4062,9 +4446,15 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate float setProgress = 0; boolean progressVisible = false; if (!FileLoader.getInstance().isLoadingFile(fileName)) { - if (!cancelLoading && - (currentMessageObject.type == 1 && MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_PHOTO) || - currentMessageObject.type == 8 && MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_GIF) && MessageObject.isNewGifDocument(currentMessageObject.messageOwner.media.document)) ) { + boolean autoDownload = false; + if (currentMessageObject.type == 1) { + autoDownload = MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_PHOTO); + } else if (currentMessageObject.type == 8 && MessageObject.isNewGifDocument(currentMessageObject.messageOwner.media.document)) { + autoDownload = MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_GIF); + } else if (currentMessageObject.type == 5) { + autoDownload = MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_VIDEOMESSAGE); + } + if (!cancelLoading && autoDownload) { progressVisible = true; buttonState = 1; } else { @@ -4101,7 +4491,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private void didPressedButton(boolean animated) { if (buttonState == 0) { if (documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO || documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { - if (delegate.needPlayAudio(currentMessageObject)) { + if (delegate.needPlayMessage(currentMessageObject)) { buttonState = 1; radialProgress.setBackground(getDrawableForCurrentState(), false, false); invalidate(); @@ -4110,18 +4500,26 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate cancelLoading = false; radialProgress.setProgress(0, false); if (currentMessageObject.type == 1) { - photoImage.setImage(currentPhotoObject.location, currentPhotoFilter, currentPhotoObjectThumb != null ? currentPhotoObjectThumb.location : null, currentPhotoFilter, currentPhotoObject.size, null, false); + photoImage.setImage(currentPhotoObject.location, currentPhotoFilter, currentPhotoObjectThumb != null ? currentPhotoObjectThumb.location : null, currentPhotoFilterThumb, currentPhotoObject.size, null, false); } else if (currentMessageObject.type == 8) { - currentMessageObject.audioProgress = 2; - photoImage.setImage(currentMessageObject.messageOwner.media.document, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilter, currentMessageObject.messageOwner.media.document.size, null, false); + currentMessageObject.gifState = 2; + photoImage.setImage(currentMessageObject.messageOwner.media.document, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilterThumb, currentMessageObject.messageOwner.media.document.size, null, false); + } else if (currentMessageObject.isRoundVideo()) { + if (currentMessageObject.isSecretMedia()) { + FileLoader.getInstance().loadFile(currentMessageObject.getDocument(), true, true); + } else { + currentMessageObject.gifState = 2; + TLRPC.Document document = currentMessageObject.getDocument(); + photoImage.setImage(document, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilterThumb, document.size, null, false); + } } else if (currentMessageObject.type == 9) { FileLoader.getInstance().loadFile(currentMessageObject.messageOwner.media.document, false, false); } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO) { FileLoader.getInstance().loadFile(documentAttach, true, false); } else if (currentMessageObject.type == 0 && documentAttachType != DOCUMENT_ATTACH_TYPE_NONE) { if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { - photoImage.setImage(currentMessageObject.messageOwner.media.webpage.document, null, currentPhotoObject.location, currentPhotoFilter, currentMessageObject.messageOwner.media.webpage.document.size, null, false); - currentMessageObject.audioProgress = 2; + photoImage.setImage(currentMessageObject.messageOwner.media.webpage.document, null, currentPhotoObject.location, currentPhotoFilterThumb, currentMessageObject.messageOwner.media.webpage.document.size, null, false); + currentMessageObject.gifState = 2; } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_DOCUMENT) { FileLoader.getInstance().loadFile(currentMessageObject.messageOwner.media.webpage.document, false, false); } @@ -4134,7 +4532,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } else if (buttonState == 1) { if (documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO || documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { - boolean result = MediaController.getInstance().pauseAudio(currentMessageObject); + boolean result = MediaController.getInstance().pauseMessage(currentMessageObject); if (result) { buttonState = 0; radialProgress.setBackground(getDrawableForCurrentState(), false, false); @@ -4147,7 +4545,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate cancelLoading = true; if (documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO || documentAttachType == DOCUMENT_ATTACH_TYPE_DOCUMENT) { FileLoader.getInstance().cancelLoadFile(documentAttach); - } else if (currentMessageObject.type == 0 || currentMessageObject.type == 1 || currentMessageObject.type == 8) { + } else if (currentMessageObject.type == 0 || currentMessageObject.type == 1 || currentMessageObject.type == 8 || currentMessageObject.type == 5) { photoImage.cancelLoadImage(); } else if (currentMessageObject.type == 9) { FileLoader.getInstance().cancelLoadFile(currentMessageObject.messageOwner.media.document); @@ -4167,7 +4565,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { photoImage.setAllowStartAnimation(true); photoImage.startAnimation(); - currentMessageObject.audioProgress = 0; + currentMessageObject.gifState = 0; buttonState = -1; radialProgress.setBackground(getDrawableForCurrentState(), false, animated); } @@ -4202,7 +4600,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { radialProgress.setProgress(1, true); if (currentMessageObject.type == 0) { - if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF && currentMessageObject.audioProgress != 1) { + if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF && currentMessageObject.gifState != 1) { buttonState = 2; didPressedButton(true); } else if (!photoNotSet) { @@ -4211,8 +4609,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate setMessageObject(currentMessageObject, pinnedBottom, pinnedTop); } } else { - if (!photoNotSet || currentMessageObject.type == 8 && currentMessageObject.audioProgress != 1) { - if (currentMessageObject.type == 8 && currentMessageObject.audioProgress != 1) { + if (!photoNotSet || (currentMessageObject.type == 8 || currentMessageObject.type == 5) && currentMessageObject.gifState != 1) { + if ((currentMessageObject.type == 8 || currentMessageObject.type == 5) && currentMessageObject.gifState != 1) { photoNotSet = false; buttonState = 2; didPressedButton(true); @@ -4323,7 +4721,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } private boolean checkNeedDrawShareButton(MessageObject messageObject) { - if (messageObject.type == 13) { + if (messageObject.eventId != 0) { + return false; + } else if (messageObject.type == 13) { return false; } else if (messageObject.messageOwner.fwd_from != null && messageObject.messageOwner.fwd_from.channel_id != 0 && !messageObject.isOut()) { return true; @@ -4371,7 +4771,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate currentChat = MessagesController.getInstance().getChat(currentMessageObject.messageOwner.to_id.channel_id); } - if (isChat && !messageObject.isOutOwner() && messageObject.isFromUser()) { + if (isChat && !messageObject.isOutOwner() && messageObject.needDrawAvatar()) { isAvatarVisible = true; if (currentUser != null) { if (currentUser.photo != null) { @@ -4442,7 +4842,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate viaNameWidth += AndroidUtilities.dp(4); } int color; - if (currentMessageObject.type == 13) { + if (currentMessageObject.type == 13 || currentMessageObject.type == 5) { color = Theme.getColor(Theme.key_chat_stickerViaBotNameText); } else { color = Theme.getColor(currentMessageObject.isOutOwner() ? Theme.key_chat_outViaBotNameText : Theme.key_chat_inViaBotNameText); @@ -4531,7 +4931,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate forwardedNameWidth = Math.max((int) Math.ceil(forwardedNameLayout[0].getLineWidth(0)), (int) Math.ceil(forwardedNameLayout[1].getLineWidth(0))); forwardNameOffsetX[0] = forwardedNameLayout[0].getLineLeft(0); forwardNameOffsetX[1] = forwardedNameLayout[1].getLineLeft(0); - namesOffset += AndroidUtilities.dp(36); + if (messageObject.type != 5) { + namesOffset += AndroidUtilities.dp(36); + } } catch (Exception e) { FileLog.e(e); } @@ -4539,17 +4941,15 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } if (messageObject.isReply()) { - namesOffset += AndroidUtilities.dp(42); - if (messageObject.type != 0) { - if (messageObject.type == 13) { - namesOffset -= AndroidUtilities.dp(42); - } else { + if (messageObject.type != 13 && messageObject.type != 5) { + namesOffset += AndroidUtilities.dp(42); + if (messageObject.type != 0) { namesOffset += AndroidUtilities.dp(5); } } int maxWidth = getMaxNameWidth(); - if (messageObject.type != 13) { + if (messageObject.type != 13 && messageObject.type != 5) { maxWidth -= AndroidUtilities.dp(10); } @@ -4564,6 +4964,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate replyImageReceiver.setImageBitmap((Drawable) null); needReplyImage = false; } else { + if (messageObject.replyMessageObject.isRoundVideo()) { + replyImageReceiver.setRoundRadius(AndroidUtilities.dp(22)); + } else { + replyImageReceiver.setRoundRadius(0); + } currentReplyPhoto = photoSize.location; replyImageReceiver.setImage(photoSize.location, "50_50", null, null, true); needReplyImage = true; @@ -4591,10 +4996,10 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } } - - if (name != null) { - stringFinalName = TextUtils.ellipsize(name.replace('\n', ' '), Theme.chat_replyNamePaint, maxWidth, TextUtils.TruncateAt.END); + if (name == null) { + name = LocaleController.getString("Loading", R.string.Loading); } + stringFinalName = TextUtils.ellipsize(name.replace('\n', ' '), Theme.chat_replyNamePaint, maxWidth, TextUtils.TruncateAt.END); if (messageObject.replyMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGame) { stringFinalText = Emoji.replaceEmoji(messageObject.replyMessageObject.messageOwner.media.game.title, Theme.chat_replyTextPaint.getFontMetricsInt(), AndroidUtilities.dp(14), false); stringFinalText = TextUtils.ellipsize(stringFinalText, Theme.chat_replyTextPaint, maxWidth, TextUtils.TruncateAt.END); @@ -4611,23 +5016,24 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate stringFinalText = TextUtils.ellipsize(stringFinalText, Theme.chat_replyTextPaint, maxWidth, TextUtils.TruncateAt.END); } } - if (stringFinalName == null) { - stringFinalName = LocaleController.getString("Loading", R.string.Loading); - } try { - replyNameLayout = new StaticLayout(stringFinalName, Theme.chat_replyNamePaint, maxWidth + AndroidUtilities.dp(6), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - if (replyNameLayout.getLineCount() > 0) { - replyNameWidth = (int)Math.ceil(replyNameLayout.getLineWidth(0)) + AndroidUtilities.dp(12 + (needReplyImage ? 44 : 0)); - replyNameOffset = replyNameLayout.getLineLeft(0); + replyNameWidth = AndroidUtilities.dp(4 + (needReplyImage ? 44 : 0)); + if (stringFinalName != null) { + replyNameLayout = new StaticLayout(stringFinalName, Theme.chat_replyNamePaint, maxWidth + AndroidUtilities.dp(6), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + if (replyNameLayout.getLineCount() > 0) { + replyNameWidth += (int) Math.ceil(replyNameLayout.getLineWidth(0)) + AndroidUtilities.dp(8); + replyNameOffset = replyNameLayout.getLineLeft(0); + } } } catch (Exception e) { FileLog.e(e); } try { + replyTextWidth = AndroidUtilities.dp(4 + (needReplyImage ? 44 : 0)); if (stringFinalText != null) { replyTextLayout = new StaticLayout(stringFinalText, Theme.chat_replyTextPaint, maxWidth + AndroidUtilities.dp(6), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); if (replyTextLayout.getLineCount() > 0) { - replyTextWidth = (int) Math.ceil(replyTextLayout.getLineWidth(0)) + AndroidUtilities.dp(12 + (needReplyImage ? 44 : 0)); + replyTextWidth += (int) Math.ceil(replyTextLayout.getLineWidth(0)) + AndroidUtilities.dp(8); replyTextOffset = replyTextLayout.getLineLeft(0); } } @@ -4686,18 +5092,21 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } } - - if (mediaBackground) { - if (currentMessageObject.type == 13) { - Theme.chat_timePaint.setColor(Theme.getColor(Theme.key_chat_serviceText)); - } else { - Theme.chat_timePaint.setColor(Theme.getColor(Theme.key_chat_mediaTimeText)); - } + if (currentMessageObject.type == 5) { + Theme.chat_timePaint.setColor(Theme.getColor(Theme.key_chat_mediaTimeText)); } else { - if (currentMessageObject.isOutOwner()) { - Theme.chat_timePaint.setColor(Theme.getColor(isDrawSelectedBackground() ? Theme.key_chat_outTimeSelectedText : Theme.key_chat_outTimeText)); + if (mediaBackground) { + if (currentMessageObject.type == 13 || currentMessageObject.type == 5) { + Theme.chat_timePaint.setColor(Theme.getColor(Theme.key_chat_serviceText)); + } else { + Theme.chat_timePaint.setColor(Theme.getColor(Theme.key_chat_mediaTimeText)); + } } else { - Theme.chat_timePaint.setColor(Theme.getColor(isDrawSelectedBackground() ? Theme.key_chat_inTimeSelectedText : Theme.key_chat_inTimeText)); + if (currentMessageObject.isOutOwner()) { + Theme.chat_timePaint.setColor(Theme.getColor(isDrawSelectedBackground() ? Theme.key_chat_outTimeSelectedText : Theme.key_chat_outTimeText)); + } else { + Theme.chat_timePaint.setColor(Theme.getColor(isDrawSelectedBackground() ? Theme.key_chat_inTimeSelectedText : Theme.key_chat_inTimeText)); + } } } @@ -4754,7 +5163,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate currentBackgroundShadowDrawable = Theme.chat_msgInMediaShadowDrawable; } } - backgroundDrawableLeft = AndroidUtilities.dp((isChat && currentMessageObject.isFromUser() ? 48 : 0) + (!mediaBackground ? 3 : 9)); + backgroundDrawableLeft = AndroidUtilities.dp((isChat && isAvatarVisible ? 48 : 0) + (!mediaBackground ? 3 : 9)); int backgroundRight = backgroundWidth - (mediaBackground ? 0 : AndroidUtilities.dp(3)); int backgroundLeft = backgroundDrawableLeft; if (!mediaBackground && pinnedBottom) { @@ -4795,7 +5204,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (drawNameLayout && nameLayout != null) { canvas.save(); - if (currentMessageObject.type == 13) { + if (currentMessageObject.type == 13 || currentMessageObject.type == 5) { Theme.chat_namePaint.setColor(Theme.getColor(Theme.key_chat_stickerNameText)); int backWidth; if (currentMessageObject.isOutOwner()) { @@ -4816,7 +5225,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (currentUser != null) { Theme.chat_namePaint.setColor(AvatarDrawable.getNameColorForId(currentUser.id)); } else if (currentChat != null) { - Theme.chat_namePaint.setColor(AvatarDrawable.getNameColorForId(currentChat.id)); + if (ChatObject.isChannel(currentChat) && !currentChat.megagroup) { + Theme.chat_namePaint.setColor(AvatarDrawable.getNameColorForId(5)); + } else { + Theme.chat_namePaint.setColor(AvatarDrawable.getNameColorForId(currentChat.id)); + } } else { Theme.chat_namePaint.setColor(AvatarDrawable.getNameColorForId(0)); } @@ -4828,16 +5241,31 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } if (drawForwardedName && forwardedNameLayout[0] != null && forwardedNameLayout[1] != null) { - forwardNameY = AndroidUtilities.dp(10 + (drawNameLayout ? 19 : 0)); - if (currentMessageObject.isOutOwner()) { - Theme.chat_forwardNamePaint.setColor(Theme.getColor(Theme.key_chat_outForwardedNameText)); - forwardNameX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(11); + if (currentMessageObject.type == 5) { + Theme.chat_forwardNamePaint.setColor(Theme.getColor(Theme.key_chat_stickerReplyNameText)); + if (currentMessageObject.isOutOwner()) { + forwardNameX = AndroidUtilities.dp(23); + } else { + forwardNameX = currentBackgroundDrawable.getBounds().right + AndroidUtilities.dp(17); + } + forwardNameY = AndroidUtilities.dp(12); + + int backWidth = forwardedNameWidth + AndroidUtilities.dp(14); + Theme.chat_systemDrawable.setColorFilter(Theme.colorFilter); + Theme.chat_systemDrawable.setBounds(forwardNameX - AndroidUtilities.dp(7), forwardNameY - AndroidUtilities.dp(6), forwardNameX - AndroidUtilities.dp(7) + backWidth, forwardNameY + AndroidUtilities.dp(38)); + Theme.chat_systemDrawable.draw(canvas); } else { - Theme.chat_forwardNamePaint.setColor(Theme.getColor(Theme.key_chat_inForwardedNameText)); - if (mediaBackground) { + forwardNameY = AndroidUtilities.dp(10 + (drawNameLayout ? 19 : 0)); + if (currentMessageObject.isOutOwner()) { + Theme.chat_forwardNamePaint.setColor(Theme.getColor(Theme.key_chat_outForwardedNameText)); forwardNameX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(11); } else { - forwardNameX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(!mediaBackground && pinnedBottom ? 11 : 17); + Theme.chat_forwardNamePaint.setColor(Theme.getColor(Theme.key_chat_inForwardedNameText)); + if (mediaBackground) { + forwardNameX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(11); + } else { + forwardNameX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(!mediaBackground && pinnedBottom ? 11 : 17); + } } } for (int a = 0; a < 2; a++) { @@ -4849,7 +5277,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } if (currentMessageObject.isReply()) { - if (currentMessageObject.type == 13) { + if (currentMessageObject.type == 13 || currentMessageObject.type == 5) { Theme.chat_replyLinePaint.setColor(Theme.getColor(Theme.key_chat_stickerReplyLine)); Theme.chat_replyNamePaint.setColor(Theme.getColor(Theme.key_chat_stickerReplyNameText)); Theme.chat_replyTextPaint.setColor(Theme.getColor(Theme.key_chat_stickerReplyMessageText)); @@ -4858,11 +5286,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { replyStartX = currentBackgroundDrawable.getBounds().right + AndroidUtilities.dp(17); } - replyStartY = layoutHeight - AndroidUtilities.dp(58); + replyStartY = AndroidUtilities.dp(12); if (nameLayout != null) { replyStartY -= AndroidUtilities.dp(25 + 6); } - int backWidth = Math.max(replyNameWidth, replyTextWidth) + AndroidUtilities.dp(14 + (needReplyImage ? 44 : 0)); + int backWidth = Math.max(replyNameWidth, replyTextWidth) + AndroidUtilities.dp(14); Theme.chat_systemDrawable.setColorFilter(Theme.colorFilter); Theme.chat_systemDrawable.setBounds(replyStartX - AndroidUtilities.dp(7), replyStartY - AndroidUtilities.dp(6), replyStartX - AndroidUtilities.dp(7) + backWidth, replyStartY + AndroidUtilities.dp(41)); Theme.chat_systemDrawable.draw(canvas); @@ -4917,28 +5345,30 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate canvas.translate(0, AndroidUtilities.dp(2)); } if (mediaBackground) { - Drawable drawable; - if (currentMessageObject.type == 13) { - drawable = Theme.chat_timeStickerBackgroundDrawable; + Paint paint; + if (currentMessageObject.type == 13 || currentMessageObject.type == 5) { + paint = Theme.chat_actionBackgroundPaint; } else { - drawable = Theme.chat_timeBackgroundDrawable; + paint = Theme.chat_timeBackgroundPaint; } - setDrawableBounds(drawable, timeX - AndroidUtilities.dp(4), layoutHeight - AndroidUtilities.dp(27), timeWidth + AndroidUtilities.dp(8 + (currentMessageObject.isOutOwner() ? 20 : 0)), AndroidUtilities.dp(17)); - drawable.draw(canvas); + int x1 = timeX - AndroidUtilities.dp(4); + int y1 = layoutHeight - AndroidUtilities.dp(28); + rect.set(x1, y1, x1 + timeWidth + AndroidUtilities.dp(8 + (currentMessageObject.isOutOwner() ? 20 : 0)), y1 + AndroidUtilities.dp(17)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(4), AndroidUtilities.dp(4), paint); - int additionalX = 0; + int additionalX = (int) (-timeLayout.getLineLeft(0)); if ((currentMessageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { - additionalX = (int) (timeWidth - timeLayout.getLineWidth(0)); + additionalX += (int) (timeWidth - timeLayout.getLineWidth(0)); if (currentMessageObject.isSending()) { if (!currentMessageObject.isOutOwner()) { - setDrawableBounds(Theme.chat_msgMediaClockDrawable, timeX + AndroidUtilities.dp(11), layoutHeight - AndroidUtilities.dp(13.0f) - Theme.chat_msgMediaClockDrawable.getIntrinsicHeight()); + setDrawableBounds(Theme.chat_msgMediaClockDrawable, timeX + AndroidUtilities.dp(11), layoutHeight - AndroidUtilities.dp(14.0f) - Theme.chat_msgMediaClockDrawable.getIntrinsicHeight()); Theme.chat_msgMediaClockDrawable.draw(canvas); } } else if (currentMessageObject.isSendError()) { if (!currentMessageObject.isOutOwner()) { int x = timeX + AndroidUtilities.dp(11); - int y = layoutHeight - AndroidUtilities.dp(26.5f); + int y = layoutHeight - AndroidUtilities.dp(27.5f); rect.set(x, y, x + AndroidUtilities.dp(14), y + AndroidUtilities.dp(14)); canvas.drawRoundRect(rect, AndroidUtilities.dp(1), AndroidUtilities.dp(1), Theme.chat_msgErrorPaint); setDrawableBounds(Theme.chat_msgErrorDrawable, x + AndroidUtilities.dp(6), y + AndroidUtilities.dp(2)); @@ -4946,17 +5376,17 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } else { Drawable viewsDrawable; - if (currentMessageObject.type == 13) { + if (currentMessageObject.type == 13 || currentMessageObject.type == 5) { viewsDrawable = Theme.chat_msgStickerViewsDrawable; } else { viewsDrawable = Theme.chat_msgMediaViewsDrawable; } - setDrawableBounds(viewsDrawable, timeX, layoutHeight - AndroidUtilities.dp(9.5f) - timeLayout.getHeight()); + setDrawableBounds(viewsDrawable, timeX, layoutHeight - AndroidUtilities.dp(10.5f) - timeLayout.getHeight()); viewsDrawable.draw(canvas); if (viewsLayout != null) { canvas.save(); - canvas.translate(timeX + viewsDrawable.getIntrinsicWidth() + AndroidUtilities.dp(3), layoutHeight - AndroidUtilities.dp(11.3f) - timeLayout.getHeight()); + canvas.translate(timeX + viewsDrawable.getIntrinsicWidth() + AndroidUtilities.dp(3), layoutHeight - AndroidUtilities.dp(12.3f) - timeLayout.getHeight()); viewsLayout.draw(canvas); canvas.restore(); } @@ -4964,13 +5394,13 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } canvas.save(); - canvas.translate(timeX + additionalX, layoutHeight - AndroidUtilities.dp(11.3f) - timeLayout.getHeight()); + canvas.translate(timeX + additionalX, layoutHeight - AndroidUtilities.dp(12.3f) - timeLayout.getHeight()); timeLayout.draw(canvas); canvas.restore(); } else { - int additionalX = 0; + int additionalX = (int) (-timeLayout.getLineLeft(0)); if ((currentMessageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { - additionalX = (int) (timeWidth - timeLayout.getLineWidth(0)); + additionalX += (int) (timeWidth - timeLayout.getLineWidth(0)); if (currentMessageObject.isSending()) { if (!currentMessageObject.isOutOwner()) { @@ -5047,11 +5477,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate setDrawableBounds(Theme.chat_msgOutClockDrawable, layoutWidth - AndroidUtilities.dp(18.5f) - Theme.chat_msgOutClockDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(8.5f) - Theme.chat_msgOutClockDrawable.getIntrinsicHeight()); Theme.chat_msgOutClockDrawable.draw(canvas); } else { - if (currentMessageObject.type == 13) { - setDrawableBounds(Theme.chat_msgStickerClockDrawable, layoutWidth - AndroidUtilities.dp(22.0f) - Theme.chat_msgStickerClockDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.chat_msgStickerClockDrawable.getIntrinsicHeight()); + if (currentMessageObject.type == 13 || currentMessageObject.type == 5) { + setDrawableBounds(Theme.chat_msgStickerClockDrawable, layoutWidth - AndroidUtilities.dp(22.0f) - Theme.chat_msgStickerClockDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(13.5f) - Theme.chat_msgStickerClockDrawable.getIntrinsicHeight()); Theme.chat_msgStickerClockDrawable.draw(canvas); } else { - setDrawableBounds(Theme.chat_msgMediaClockDrawable, layoutWidth - AndroidUtilities.dp(22.0f) - Theme.chat_msgMediaClockDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.chat_msgMediaClockDrawable.getIntrinsicHeight()); + setDrawableBounds(Theme.chat_msgMediaClockDrawable, layoutWidth - AndroidUtilities.dp(22.0f) - Theme.chat_msgMediaClockDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(13.5f) - Theme.chat_msgMediaClockDrawable.getIntrinsicHeight()); Theme.chat_msgMediaClockDrawable.draw(canvas); } } @@ -5063,7 +5493,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate setDrawableBounds(Theme.chat_msgBroadcastDrawable, layoutWidth - AndroidUtilities.dp(20.5f) - Theme.chat_msgBroadcastDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(8.0f) - Theme.chat_msgBroadcastDrawable.getIntrinsicHeight()); Theme.chat_msgBroadcastDrawable.draw(canvas); } else { - setDrawableBounds(Theme.chat_msgBroadcastMediaDrawable, layoutWidth - AndroidUtilities.dp(24.0f) - Theme.chat_msgBroadcastMediaDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(13.0f) - Theme.chat_msgBroadcastMediaDrawable.getIntrinsicHeight()); + setDrawableBounds(Theme.chat_msgBroadcastMediaDrawable, layoutWidth - AndroidUtilities.dp(24.0f) - Theme.chat_msgBroadcastMediaDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(14.0f) - Theme.chat_msgBroadcastMediaDrawable.getIntrinsicHeight()); Theme.chat_msgBroadcastMediaDrawable.draw(canvas); } } @@ -5078,18 +5508,18 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } drawable.draw(canvas); } else { - if (currentMessageObject.type == 13) { + if (currentMessageObject.type == 13 || currentMessageObject.type == 5) { if (drawCheck1) { - setDrawableBounds(Theme.chat_msgStickerCheckDrawable, layoutWidth - AndroidUtilities.dp(26.3f) - Theme.chat_msgStickerCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.chat_msgStickerCheckDrawable.getIntrinsicHeight()); + setDrawableBounds(Theme.chat_msgStickerCheckDrawable, layoutWidth - AndroidUtilities.dp(26.3f) - Theme.chat_msgStickerCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(13.5f) - Theme.chat_msgStickerCheckDrawable.getIntrinsicHeight()); } else { - setDrawableBounds(Theme.chat_msgStickerCheckDrawable, layoutWidth - AndroidUtilities.dp(21.5f) - Theme.chat_msgStickerCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.chat_msgStickerCheckDrawable.getIntrinsicHeight()); + setDrawableBounds(Theme.chat_msgStickerCheckDrawable, layoutWidth - AndroidUtilities.dp(21.5f) - Theme.chat_msgStickerCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(13.5f) - Theme.chat_msgStickerCheckDrawable.getIntrinsicHeight()); } Theme.chat_msgStickerCheckDrawable.draw(canvas); } else { if (drawCheck1) { - setDrawableBounds(Theme.chat_msgMediaCheckDrawable, layoutWidth - AndroidUtilities.dp(26.3f) - Theme.chat_msgMediaCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.chat_msgMediaCheckDrawable.getIntrinsicHeight()); + setDrawableBounds(Theme.chat_msgMediaCheckDrawable, layoutWidth - AndroidUtilities.dp(26.3f) - Theme.chat_msgMediaCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(13.5f) - Theme.chat_msgMediaCheckDrawable.getIntrinsicHeight()); } else { - setDrawableBounds(Theme.chat_msgMediaCheckDrawable, layoutWidth - AndroidUtilities.dp(21.5f) - Theme.chat_msgMediaCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.chat_msgMediaCheckDrawable.getIntrinsicHeight()); + setDrawableBounds(Theme.chat_msgMediaCheckDrawable, layoutWidth - AndroidUtilities.dp(21.5f) - Theme.chat_msgMediaCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(13.5f) - Theme.chat_msgMediaCheckDrawable.getIntrinsicHeight()); } Theme.chat_msgMediaCheckDrawable.draw(canvas); } @@ -5101,11 +5531,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate setDrawableBounds(drawable, layoutWidth - AndroidUtilities.dp(18) - drawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(8.0f) - drawable.getIntrinsicHeight()); drawable.draw(canvas); } else { - if (currentMessageObject.type == 13) { - setDrawableBounds(Theme.chat_msgStickerHalfCheckDrawable, layoutWidth - AndroidUtilities.dp(21.5f) - Theme.chat_msgStickerHalfCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.chat_msgStickerHalfCheckDrawable.getIntrinsicHeight()); + if (currentMessageObject.type == 13 || currentMessageObject.type == 5) { + setDrawableBounds(Theme.chat_msgStickerHalfCheckDrawable, layoutWidth - AndroidUtilities.dp(21.5f) - Theme.chat_msgStickerHalfCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(13.5f) - Theme.chat_msgStickerHalfCheckDrawable.getIntrinsicHeight()); Theme.chat_msgStickerHalfCheckDrawable.draw(canvas); } else { - setDrawableBounds(Theme.chat_msgMediaHalfCheckDrawable, layoutWidth - AndroidUtilities.dp(21.5f) - Theme.chat_msgMediaHalfCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.chat_msgMediaHalfCheckDrawable.getIntrinsicHeight()); + setDrawableBounds(Theme.chat_msgMediaHalfCheckDrawable, layoutWidth - AndroidUtilities.dp(21.5f) - Theme.chat_msgMediaHalfCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(13.5f) - Theme.chat_msgMediaHalfCheckDrawable.getIntrinsicHeight()); Theme.chat_msgMediaHalfCheckDrawable.draw(canvas); } } @@ -5119,7 +5549,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate y = layoutHeight - AndroidUtilities.dp(21); } else { x = layoutWidth - AndroidUtilities.dp(34.5f); - y = layoutHeight - AndroidUtilities.dp(25.5f); + y = layoutHeight - AndroidUtilities.dp(26.5f); } rect.set(x, y, x + AndroidUtilities.dp(14), y + AndroidUtilities.dp(14)); canvas.drawRoundRect(rect, AndroidUtilities.dp(1), AndroidUtilities.dp(1), Theme.chat_msgErrorPaint); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/CheckBoxCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/CheckBoxCell.java index aa45016ff..9a04e8055 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/CheckBoxCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/CheckBoxCell.java @@ -79,6 +79,14 @@ public class CheckBoxCell extends FrameLayout { setWillNotDraw(!divider); } + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + textView.setAlpha(enabled ? 1.0f : 0.5f); + valueTextView.setAlpha(enabled ? 1.0f : 0.5f); + checkBox.setAlpha(enabled ? 1.0f : 0.5f); + } + public void setChecked(boolean checked, boolean animated) { checkBox.setChecked(checked, animated); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/CheckBoxUserCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/CheckBoxUserCell.java new file mode 100644 index 000000000..21d1c268a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/CheckBoxUserCell.java @@ -0,0 +1,110 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Cells; + +import android.content.Context; +import android.graphics.Canvas; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.FrameLayout; +import android.widget.TextView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ContactsController; +import org.telegram.messenger.LocaleController; +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.CheckBoxSquare; +import org.telegram.ui.Components.LayoutHelper; + +public class CheckBoxUserCell extends FrameLayout { + + private TextView textView; + private BackupImageView imageView; + private CheckBoxSquare checkBox; + private AvatarDrawable avatarDrawable; + private boolean needDivider; + + private TLRPC.User currentUser; + + public CheckBoxUserCell(Context context, boolean alert) { + super(context); + + textView = new TextView(context); + textView.setTextColor(Theme.getColor(alert ? Theme.key_dialogTextBlack : Theme.key_windowBackgroundWhiteBlackText)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setLines(1); + textView.setMaxLines(1); + textView.setSingleLine(true); + textView.setEllipsize(TextUtils.TruncateAt.END); + 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, (LocaleController.isRTL ? 17 : 94), 0, (LocaleController.isRTL ? 94 : 17), 0)); + + avatarDrawable = new AvatarDrawable(); + imageView = new BackupImageView(context); + imageView.setRoundRadius(AndroidUtilities.dp(36)); + addView(imageView, LayoutHelper.createFrame(36, 36, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, 48, 6, 48, 0)); + + checkBox = new CheckBoxSquare(context, alert); + addView(checkBox, LayoutHelper.createFrame(18, 18, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, (LocaleController.isRTL ? 0 : 17), 15, (LocaleController.isRTL ? 17 : 0), 0)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(48) + (needDivider ? 1 : 0), MeasureSpec.EXACTLY)); + } + + public void setTextColor(int color) { + textView.setTextColor(color); + } + + public TLRPC.User getCurrentUser() { + return currentUser; + } + + public void setUser(TLRPC.User user, boolean checked, boolean divider) { + currentUser = user; + textView.setText(ContactsController.formatName(user.first_name, user.last_name)); + checkBox.setChecked(checked, false); + TLRPC.FileLocation photo = null; + avatarDrawable.setInfo(user); + if (user != null && user.photo != null) { + photo = user.photo.photo_small; + } + imageView.setImage(photo, "50_50", avatarDrawable); + needDivider = divider; + setWillNotDraw(!divider); + } + + public void setChecked(boolean checked, boolean animated) { + checkBox.setChecked(checked, animated); + } + + public boolean isChecked() { + return checkBox.isChecked(); + } + + public TextView getTextView() { + return textView; + } + + public CheckBoxSquare getCheckBox() { + return checkBox; + } + + @Override + protected void onDraw(Canvas canvas) { + if (needDivider) { + canvas.drawLine(getPaddingLeft(), getHeight() - 1, getWidth() - getPaddingRight(), getHeight() - 1, Theme.dividerPaint); + } + } +} 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 42b9a47e0..7f55176c7 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ContextLinkCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ContextLinkCell.java @@ -35,6 +35,7 @@ import org.telegram.tgnet.TLRPC; import org.telegram.ui.Components.LetterDrawable; import org.telegram.ui.Components.RadialProgress; import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.PhotoViewer; import java.io.File; import java.util.ArrayList; @@ -75,6 +76,7 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa private TLRPC.BotInlineResult inlineResult; private TLRPC.Document documentAttach; + private TLRPC.PhotoSize currentPhotoObject; private int documentAttachType; private boolean mediaWebpage; private MessageObject currentMessageObject; @@ -107,6 +109,7 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa descriptionLayout = null; titleLayout = null; linkLayout = null; + currentPhotoObject = null; linkY = AndroidUtilities.dp(27); if (inlineResult == null && documentAttach == null) { @@ -117,7 +120,6 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa int viewWidth = MeasureSpec.getSize(widthMeasureSpec); int maxWidth = viewWidth - AndroidUtilities.dp(AndroidUtilities.leftBaseline) - AndroidUtilities.dp(8); - TLRPC.PhotoSize currentPhotoObject = null; TLRPC.PhotoSize currentPhotoObjectThumb = null; ArrayList photoThumbs = null; String url = null; @@ -517,13 +519,13 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa private void didPressedButton() { if (documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO || documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { if (buttonState == 0) { - if (MediaController.getInstance().playAudio(currentMessageObject)) { + if (MediaController.getInstance().playMessage(currentMessageObject)) { buttonState = 1; radialProgress.setBackground(getDrawableForCurrentState(), false, false); invalidate(); } } else if (buttonState == 1) { - boolean result = MediaController.getInstance().pauseAudio(currentMessageObject); + boolean result = MediaController.getInstance().pauseMessage(currentMessageObject); if (result) { buttonState = 0; radialProgress.setBackground(getDrawableForCurrentState(), false, false); @@ -620,6 +622,9 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa } } if (drawLinkImageView) { + if (inlineResult != null) { + linkImageView.setVisible(!PhotoViewer.getInstance().isShowingImage(inlineResult), false); + } canvas.save(); if (scaled && scale != 0.8f || !scaled && scale != 1.0f) { long newTime = System.currentTimeMillis(); @@ -687,7 +692,7 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa fileName = FileLoader.getAttachFileName(inlineResult.document); cacheFile = FileLoader.getPathToAttach(inlineResult.document); } else if (inlineResult.photo instanceof TLRPC.TL_photo) { - TLRPC.PhotoSize currentPhotoObject = FileLoader.getClosestPhotoSizeWithSize(inlineResult.photo.sizes, AndroidUtilities.getPhotoSize(), true); + currentPhotoObject = FileLoader.getClosestPhotoSizeWithSize(inlineResult.photo.sizes, AndroidUtilities.getPhotoSize(), true); fileName = FileLoader.getAttachFileName(currentPhotoObject); cacheFile = FileLoader.getPathToAttach(currentPhotoObject); } else if (inlineResult.content_url != null) { @@ -743,8 +748,8 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa } else { MediaController.getInstance().removeLoadingFileObserver(this); if (documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC || documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO) { - boolean playing = MediaController.getInstance().isPlayingAudio(currentMessageObject); - if (!playing || playing && MediaController.getInstance().isAudioPaused()) { + boolean playing = MediaController.getInstance().isPlayingMessage(currentMessageObject); + if (!playing || playing && MediaController.getInstance().isMessagePaused()) { buttonState = 0; } else { buttonState = 1; 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 075b71401..9b1a39311 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java @@ -353,7 +353,7 @@ public class DialogCell extends BaseCell { if (isDialogCell) { draftMessage = DraftQuery.getDraft(currentDialogId); if (draftMessage != null && (TextUtils.isEmpty(draftMessage.message) && draftMessage.reply_to_msg_id == 0 || lastDate > draftMessage.date && unreadCount != 0) || - ChatObject.isChannel(chat) && !chat.megagroup && !chat.creator && !chat.editor || + ChatObject.isChannel(chat) && !chat.megagroup && !chat.creator && (chat.admin_rights == null || !chat.admin_rights.post_messages) || chat != null && (chat.left || chat.kicked)) { draftMessage = null; } @@ -766,7 +766,7 @@ public class DialogCell extends BaseCell { return MessagesController.getInstance().dialogs; } else if (dialogsType == 1) { return MessagesController.getInstance().dialogsServerOnly; - } else if (dialogsType == 2) { + } else if (dialogsType == 2) { return MessagesController.getInstance().dialogsGroupsOnly; } return null; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/LanguageCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/LanguageCell.java new file mode 100644 index 000000000..dd7f4a5c7 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/LanguageCell.java @@ -0,0 +1,103 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Cells; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.LayoutHelper; + +public class LanguageCell extends FrameLayout { + + private TextView textView; + private TextView textView2; + private ImageView checkImage; + private boolean needDivider; + private LocaleController.LocaleInfo currentLocale; + private boolean isDialog; + + public LanguageCell(Context context, boolean dialog) { + super(context); + + setWillNotDraw(false); + isDialog = dialog; + + textView = new TextView(context); + textView.setTextColor(Theme.getColor(dialog ? Theme.key_dialogTextBlack : Theme.key_windowBackgroundWhiteBlackText)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setLines(1); + textView.setMaxLines(1); + textView.setSingleLine(true); + textView.setEllipsize(TextUtils.TruncateAt.END); + textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP); + addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 23 + 48 : (dialog ? 23 : 16), (isDialog ? 4 : 6), LocaleController.isRTL ? (dialog ? 23 : 16) : 23 + 48, 0)); + + textView2 = new TextView(context); + textView2.setTextColor(Theme.getColor(dialog ? Theme.key_dialogTextGray3 : Theme.key_windowBackgroundWhiteGrayText3)); + textView2.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); + textView2.setLines(1); + textView2.setMaxLines(1); + textView2.setSingleLine(true); + textView2.setEllipsize(TextUtils.TruncateAt.END); + textView2.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP); + addView(textView2, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 23 + 48 : (dialog ? 23 : 16), (isDialog ? 25 : 28), LocaleController.isRTL ? (dialog ? 23 : 16) : 23 + 48, 0)); + + checkImage = new ImageView(context); + checkImage.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_featuredStickers_addedIcon), PorterDuff.Mode.MULTIPLY)); + checkImage.setImageResource(R.drawable.sticker_added); + addView(checkImage, LayoutHelper.createFrame(19, 14, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, 23, 0, 23, 0)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(isDialog ? 48 : 54) + (needDivider ? 1 : 0), MeasureSpec.EXACTLY)); + } + + public void setLanguage(LocaleController.LocaleInfo language, String desc, boolean divider) { + textView.setText(desc != null ? desc : language.name); + textView2.setText(language.nameEnglish); + currentLocale = language; + needDivider = divider; + } + + public void setValue(String name, String nameEnglish) { + textView.setText(name); + textView2.setText(nameEnglish); + checkImage.setVisibility(INVISIBLE); + currentLocale = null; + needDivider = false; + } + + public LocaleController.LocaleInfo getCurrentLocale() { + return currentLocale; + } + + public void setLanguageSelected(boolean value) { + checkImage.setVisibility(value ? VISIBLE : INVISIBLE); + } + + @Override + protected void onDraw(Canvas canvas) { + if (needDivider) { + canvas.drawLine(getPaddingLeft(), getHeight() - 1, getWidth() - getPaddingRight(), getHeight() - 1, Theme.dividerPaint); + } + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ManageChatTextCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ManageChatTextCell.java new file mode 100644 index 000000000..c15f786ae --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ManageChatTextCell.java @@ -0,0 +1,114 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Cells; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.view.Gravity; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.LocaleController; +import org.telegram.ui.ActionBar.SimpleTextView; +import org.telegram.ui.ActionBar.Theme; + +public class ManageChatTextCell extends FrameLayout { + + private SimpleTextView textView; + private SimpleTextView valueTextView; + private ImageView imageView; + private boolean divider; + + public ManageChatTextCell(Context context) { + super(context); + + textView = new SimpleTextView(context); + textView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + textView.setTextSize(16); + textView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); + addView(textView); + + valueTextView = new SimpleTextView(context); + valueTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteValueText)); + valueTextView.setTextSize(16); + valueTextView.setGravity(LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT); + addView(valueTextView); + + imageView = new ImageView(context); + imageView.setScaleType(ImageView.ScaleType.CENTER); + imageView.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_windowBackgroundWhiteGrayIcon), PorterDuff.Mode.MULTIPLY)); + addView(imageView); + } + + public SimpleTextView getTextView() { + return textView; + } + + public SimpleTextView getValueTextView() { + return valueTextView; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = AndroidUtilities.dp(48); + + valueTextView.measure(MeasureSpec.makeMeasureSpec(width - AndroidUtilities.dp(24), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(20), MeasureSpec.EXACTLY)); + textView.measure(MeasureSpec.makeMeasureSpec(width - AndroidUtilities.dp(71 + 24), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(20), MeasureSpec.EXACTLY)); + imageView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); + + setMeasuredDimension(width, AndroidUtilities.dp(56) + (divider ? 1 : 0)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int height = bottom - top; + int width = right - left; + + int viewTop = (height - valueTextView.getTextHeight()) / 2; + int viewLeft = LocaleController.isRTL ? AndroidUtilities.dp(24) : 0; + valueTextView.layout(viewLeft, viewTop, viewLeft + valueTextView.getMeasuredWidth(), viewTop + valueTextView.getMeasuredHeight()); + + viewTop = (height - textView.getTextHeight()) / 2; + viewLeft = !LocaleController.isRTL ? AndroidUtilities.dp(71) : AndroidUtilities.dp(24); + textView.layout(viewLeft, viewTop, viewLeft + textView.getMeasuredWidth(), viewTop + textView.getMeasuredHeight()); + + viewTop = AndroidUtilities.dp(9); + viewLeft = !LocaleController.isRTL ? AndroidUtilities.dp(16) : width - imageView.getMeasuredWidth() - AndroidUtilities.dp(16); + imageView.layout(viewLeft, viewTop, viewLeft + imageView.getMeasuredWidth(), viewTop + imageView.getMeasuredHeight()); + } + + public void setTextColor(int color) { + textView.setTextColor(color); + } + + public void setText(String text, String value, int resId, boolean needDivider) { + textView.setText(text); + if (value != null) { + valueTextView.setText(value); + valueTextView.setVisibility(VISIBLE); + } else { + valueTextView.setVisibility(INVISIBLE); + } + imageView.setPadding(0, AndroidUtilities.dp(7), 0, 0); + imageView.setImageResource(resId); + divider = needDivider; + setWillNotDraw(!divider); + } + + @Override + protected void onDraw(Canvas canvas) { + if (divider) { + canvas.drawLine(AndroidUtilities.dp(71), getMeasuredHeight() - 1, getMeasuredWidth(), getMeasuredHeight() - 1, Theme.dividerPaint); + } + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ManageChatUserCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ManageChatUserCell.java new file mode 100644 index 000000000..164da6315 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ManageChatUserCell.java @@ -0,0 +1,216 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Cells; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MessagesController; +import org.telegram.messenger.R; +import org.telegram.messenger.UserConfig; +import org.telegram.messenger.UserObject; +import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.TLRPC; +import org.telegram.ui.ActionBar.SimpleTextView; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.AvatarDrawable; +import org.telegram.ui.Components.BackupImageView; +import org.telegram.ui.Components.LayoutHelper; + +public class ManageChatUserCell extends FrameLayout { + + private BackupImageView avatarImageView; + private SimpleTextView nameTextView; + private SimpleTextView statusTextView; + private ImageView optionsButton; + + private AvatarDrawable avatarDrawable; + private TLRPC.User currentUser; + + private CharSequence currentName; + private CharSequence currrntStatus; + + private String lastName; + private int lastStatus; + private TLRPC.FileLocation lastAvatar; + + private int statusColor; + private int statusOnlineColor; + + private ManageChatUserCellDelegate delegate; + + public interface ManageChatUserCellDelegate { + boolean onOptionsButtonCheck(ManageChatUserCell cell, boolean click); + } + + public ManageChatUserCell(Context context, int padding, boolean needOption) { + super(context); + + statusColor = Theme.getColor(Theme.key_windowBackgroundWhiteGrayText); + statusOnlineColor = Theme.getColor(Theme.key_windowBackgroundWhiteBlueText); + + avatarDrawable = new AvatarDrawable(); + + avatarImageView = new BackupImageView(context); + avatarImageView.setRoundRadius(AndroidUtilities.dp(24)); + addView(avatarImageView, LayoutHelper.createFrame(48, 48, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 0 : 7 + padding, 8, LocaleController.isRTL ? 7 + padding : 0, 0)); + + nameTextView = new SimpleTextView(context); + nameTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + nameTextView.setTextSize(17); + nameTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + nameTextView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP); + addView(nameTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 20, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 28 + 18 : (68 + padding), 11.5f, LocaleController.isRTL ? (68 + padding) : 28 + 18, 0)); + + statusTextView = new SimpleTextView(context); + statusTextView.setTextSize(14); + statusTextView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP); + addView(statusTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 20, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 28 : (68 + padding), 34.5f, LocaleController.isRTL ? (68 + padding) : 28, 0)); + + if (needOption) { + optionsButton = new ImageView(context); + optionsButton.setFocusable(false); + optionsButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_stickers_menuSelector))); + optionsButton.setImageResource(R.drawable.ic_ab_other); + optionsButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_stickers_menu), PorterDuff.Mode.MULTIPLY)); + optionsButton.setScaleType(ImageView.ScaleType.CENTER); + addView(optionsButton, LayoutHelper.createFrame(48, 64, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.TOP)); + optionsButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + delegate.onOptionsButtonCheck(ManageChatUserCell.this, true); + } + }); + } + } + + public void setData(TLRPC.User user, CharSequence name, CharSequence status) { + if (user == null) { + currrntStatus = null; + currentName = null; + currentUser = null; + nameTextView.setText(""); + statusTextView.setText(""); + avatarImageView.setImageDrawable(null); + return; + } + currrntStatus = status; + currentName = name; + currentUser = user; + if (optionsButton != null) { + optionsButton.setVisibility(delegate.onOptionsButtonCheck(ManageChatUserCell.this, false) ? VISIBLE : INVISIBLE); + } + update(0); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(64), MeasureSpec.EXACTLY)); + } + + public void setStatusColors(int color, int onlineColor) { + statusColor = color; + statusOnlineColor = onlineColor; + } + + public void update(int mask) { + if (currentUser == null) { + return; + } + TLRPC.FileLocation photo = null; + String newName = null; + if (currentUser.photo != null) { + photo = currentUser.photo.photo_small; + } + + if (mask != 0) { + boolean continueUpdate = false; + if ((mask & MessagesController.UPDATE_MASK_AVATAR) != 0) { + if (lastAvatar != null && photo == null || lastAvatar == null && photo != null && lastAvatar != null && photo != null && (lastAvatar.volume_id != photo.volume_id || lastAvatar.local_id != photo.local_id)) { + continueUpdate = true; + } + } + if (currentUser != null && !continueUpdate && (mask & MessagesController.UPDATE_MASK_STATUS) != 0) { + int newStatus = 0; + if (currentUser.status != null) { + newStatus = currentUser.status.expires; + } + if (newStatus != lastStatus) { + continueUpdate = true; + } + } + if (!continueUpdate && currentName == null && lastName != null && (mask & MessagesController.UPDATE_MASK_NAME) != 0) { + newName = UserObject.getUserName(currentUser); + if (!newName.equals(lastName)) { + continueUpdate = true; + } + } + if (!continueUpdate) { + return; + } + } + + avatarDrawable.setInfo(currentUser); + if (currentUser.status != null) { + lastStatus = currentUser.status.expires; + } else { + lastStatus = 0; + } + + if (currentName != null) { + lastName = null; + nameTextView.setText(currentName); + } else { + lastName = newName == null ? UserObject.getUserName(currentUser) : newName; + nameTextView.setText(lastName); + } + if (currrntStatus != null) { + statusTextView.setTextColor(statusColor); + statusTextView.setText(currrntStatus); + } else if (currentUser != null) { + if (currentUser.bot) { + statusTextView.setTextColor(statusColor); + if (currentUser.bot_chat_history) { + statusTextView.setText(LocaleController.getString("BotStatusRead", R.string.BotStatusRead)); + } else { + statusTextView.setText(LocaleController.getString("BotStatusCantRead", R.string.BotStatusCantRead)); + } + } else { + if (currentUser.id == UserConfig.getClientUserId() || currentUser.status != null && currentUser.status.expires > ConnectionsManager.getInstance().getCurrentTime() || MessagesController.getInstance().onlinePrivacy.containsKey(currentUser.id)) { + statusTextView.setTextColor(statusOnlineColor); + statusTextView.setText(LocaleController.getString("Online", R.string.Online)); + } else { + statusTextView.setTextColor(statusColor); + statusTextView.setText(LocaleController.formatUserStatus(currentUser)); + } + } + } + avatarImageView.setImage(photo, "50_50", avatarDrawable); + } + + public void recycle() { + avatarImageView.getImageReceiver().cancelLoadImage(); + } + + public void setDelegate(ManageChatUserCellDelegate manageChatUserCellDelegate) { + delegate = manageChatUserCellDelegate; + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} 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 bfd36b866..966099349 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoAttachPhotoCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoAttachPhotoCell.java @@ -8,13 +8,21 @@ package org.telegram.ui.Cells; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Rect; +import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.View; +import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.MediaController; @@ -29,6 +37,9 @@ public class PhotoAttachPhotoCell extends FrameLayout { private BackupImageView imageView; private FrameLayout checkFrame; private CheckBox checkBox; + private TextView videoTextView; + private FrameLayout videoInfoContainer; + private AnimatorSet animatorSet; private boolean isLast; private boolean pressed; private static Rect rect = new Rect(); @@ -48,6 +59,20 @@ public class PhotoAttachPhotoCell extends FrameLayout { checkFrame = new FrameLayout(context); addView(checkFrame, LayoutHelper.createFrame(42, 42, Gravity.LEFT | Gravity.TOP, 38, 0, 0, 0)); + videoInfoContainer = new FrameLayout(context); + videoInfoContainer.setBackgroundResource(R.drawable.phototime); + videoInfoContainer.setPadding(AndroidUtilities.dp(3), 0, AndroidUtilities.dp(3), 0); + addView(videoInfoContainer, LayoutHelper.createFrame(80, 16, Gravity.BOTTOM | Gravity.LEFT)); + + ImageView imageView1 = new ImageView(context); + imageView1.setImageResource(R.drawable.ic_video); + videoInfoContainer.addView(imageView1, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL)); + + videoTextView = new TextView(context); + videoTextView.setTextColor(0xffffffff); + videoTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); + videoInfoContainer.addView(videoTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL, 18, -0.7f, 0, 0)); + checkBox = new CheckBox(context, R.drawable.checkbig); checkBox.setSize(30); checkBox.setCheckOffset(AndroidUtilities.dp(1)); @@ -74,6 +99,10 @@ public class PhotoAttachPhotoCell extends FrameLayout { return checkBox; } + public View getVideoInfoContainer() { + return videoInfoContainer; + } + public void setPhotoEntry(MediaController.PhotoEntry entry, boolean last) { pressed = false; photoEntry = entry; @@ -81,8 +110,18 @@ public class PhotoAttachPhotoCell extends FrameLayout { if (photoEntry.thumbPath != null) { imageView.setImage(photoEntry.thumbPath, null, getResources().getDrawable(R.drawable.nophotos)); } else if (photoEntry.path != null) { - imageView.setOrientation(photoEntry.orientation, true); - imageView.setImage("thumb://" + photoEntry.imageId + ":" + photoEntry.path, null, getResources().getDrawable(R.drawable.nophotos)); + if (photoEntry.isVideo) { + imageView.setOrientation(0, true); + videoInfoContainer.setVisibility(VISIBLE); + int minutes = photoEntry.duration / 60; + int seconds = photoEntry.duration - minutes * 60; + videoTextView.setText(String.format("%d:%02d", minutes, seconds)); + imageView.setImage("vthumb://" + photoEntry.imageId + ":" + photoEntry.path, null, getResources().getDrawable(R.drawable.nophotos)); + } else { + videoInfoContainer.setVisibility(INVISIBLE); + imageView.setOrientation(photoEntry.orientation, true); + imageView.setImage("thumb://" + photoEntry.imageId + ":" + photoEntry.path, null, getResources().getDrawable(R.drawable.nophotos)); + } } else { imageView.setImageResource(R.drawable.nophotos); } @@ -104,6 +143,28 @@ public class PhotoAttachPhotoCell extends FrameLayout { this.delegate = delegate; } + public void showCheck(boolean show) { + if (animatorSet != null) { + animatorSet.cancel(); + animatorSet = null; + } + animatorSet = new AnimatorSet(); + animatorSet.setInterpolator(new DecelerateInterpolator()); + animatorSet.setDuration(180); + animatorSet.playTogether( + ObjectAnimator.ofFloat(videoInfoContainer, "alpha", show ? 1.0f : 0.0f), + ObjectAnimator.ofFloat(checkBox, "alpha", show ? 1.0f : 0.0f)); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animation.equals(animatorSet)) { + animatorSet = null; + } + } + }); + animatorSet.start(); + } + @Override public boolean onTouchEvent(MotionEvent event) { boolean result = false; 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 eba5f6953..a1108dbc1 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java @@ -13,8 +13,12 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; +import android.util.TypedValue; import android.view.Gravity; +import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.R; @@ -27,7 +31,10 @@ public class PhotoPickerPhotoCell extends FrameLayout { public BackupImageView photoImage; public FrameLayout checkFrame; public CheckBox checkBox; + public TextView videoTextView; + public FrameLayout videoInfoContainer; private AnimatorSet animator; + private AnimatorSet animatorSet; public int itemWidth; public PhotoPickerPhotoCell(Context context) { @@ -39,6 +46,20 @@ public class PhotoPickerPhotoCell extends FrameLayout { checkFrame = new FrameLayout(context); addView(checkFrame, LayoutHelper.createFrame(42, 42, Gravity.RIGHT | Gravity.TOP)); + videoInfoContainer = new FrameLayout(context); + videoInfoContainer.setBackgroundResource(R.drawable.phototime); + videoInfoContainer.setPadding(AndroidUtilities.dp(3), 0, AndroidUtilities.dp(3), 0); + addView(videoInfoContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 16, Gravity.BOTTOM | Gravity.LEFT)); + + ImageView imageView1 = new ImageView(context); + imageView1.setImageResource(R.drawable.ic_video); + videoInfoContainer.addView(imageView1, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL)); + + videoTextView = new TextView(context); + videoTextView.setTextColor(0xffffffff); + videoTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); + videoInfoContainer.addView(videoTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL, 18, -0.7f, 0, 0)); + checkBox = new CheckBox(context, R.drawable.checkbig); checkBox.setSize(30); checkBox.setCheckOffset(AndroidUtilities.dp(1)); @@ -52,6 +73,28 @@ public class PhotoPickerPhotoCell extends FrameLayout { super.onMeasure(MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY)); } + public void showCheck(boolean show) { + if (animatorSet != null) { + animatorSet.cancel(); + animatorSet = null; + } + animatorSet = new AnimatorSet(); + animatorSet.setInterpolator(new DecelerateInterpolator()); + animatorSet.setDuration(180); + animatorSet.playTogether( + ObjectAnimator.ofFloat(videoInfoContainer, "alpha", show ? 1.0f : 0.0f), + ObjectAnimator.ofFloat(checkBox, "alpha", show ? 1.0f : 0.0f)); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animation.equals(animatorSet)) { + animatorSet = null; + } + } + }); + animatorSet.start(); + } + public void setChecked(final boolean checked, final boolean animated) { checkBox.setChecked(checked, animated); if (animator != null) { 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 877f1b701..4c99be40b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedPhotoVideoCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedPhotoVideoCell.java @@ -53,7 +53,7 @@ public class SharedPhotoVideoCell extends FrameLayout { private BackupImageView imageView; private TextView videoTextView; - private LinearLayout videoInfoContainer; + private FrameLayout videoInfoContainer; private View selector; private CheckBox checkBox; private FrameLayout container; @@ -70,22 +70,19 @@ public class SharedPhotoVideoCell extends FrameLayout { imageView.getImageReceiver().setShouldGenerateQualityThumb(true); container.addView(imageView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); - videoInfoContainer = new LinearLayout(context); - videoInfoContainer.setOrientation(LinearLayout.HORIZONTAL); + videoInfoContainer = new FrameLayout(context); videoInfoContainer.setBackgroundResource(R.drawable.phototime); videoInfoContainer.setPadding(AndroidUtilities.dp(3), 0, AndroidUtilities.dp(3), 0); - videoInfoContainer.setGravity(Gravity.CENTER_VERTICAL); container.addView(videoInfoContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 16, Gravity.BOTTOM | Gravity.LEFT)); ImageView imageView1 = new ImageView(context); imageView1.setImageResource(R.drawable.ic_video); - videoInfoContainer.addView(imageView1, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT)); + videoInfoContainer.addView(imageView1, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL)); videoTextView = new TextView(context); videoTextView.setTextColor(0xffffffff); videoTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); - videoTextView.setGravity(Gravity.CENTER_VERTICAL); - videoInfoContainer.addView(videoTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_VERTICAL, 4, 0, 0, 1)); + videoInfoContainer.addView(videoTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL, 18, -0.7f, 0, 0)); selector = new View(context); selector.setBackgroundDrawable(Theme.getSelectorDrawable(false)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java index da43db30e..a38759dde 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java @@ -44,6 +44,11 @@ public class TextBlockCell extends FrameLayout { setWillNotDraw(!divider); } + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), heightMeasureSpec); + } + @Override protected void onDraw(Canvas canvas) { if (needDivider) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java index 1f200ec1f..ebbdbe0fc 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java @@ -127,6 +127,10 @@ public class TextCheckCell extends FrameLayout { checkBox.setChecked(checked); } + public boolean isChecked() { + return checkBox.isChecked(); + } + @Override protected void onDraw(Canvas canvas) { if (needDivider) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell2.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell2.java new file mode 100644 index 000000000..6c890ac02 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell2.java @@ -0,0 +1,138 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Cells; + +import android.content.Context; +import android.graphics.Canvas; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.FrameLayout; +import android.widget.TextView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.LocaleController; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.Switch2; + +public class TextCheckCell2 extends FrameLayout { + + private TextView textView; + private TextView valueTextView; + private Switch2 checkBox; + private boolean needDivider; + private boolean isMultiline; + + public TextCheckCell2(Context context) { + super(context); + + textView = new TextView(context); + textView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setLines(1); + textView.setMaxLines(1); + textView.setSingleLine(true); + textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + textView.setEllipsize(TextUtils.TruncateAt.END); + addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 64 : 17, 0, LocaleController.isRTL ? 17 : 64, 0)); + + valueTextView = new TextView(context); + valueTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText2)); + valueTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); + valueTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); + valueTextView.setLines(1); + valueTextView.setMaxLines(1); + valueTextView.setSingleLine(true); + valueTextView.setPadding(0, 0, 0, 0); + valueTextView.setEllipsize(TextUtils.TruncateAt.END); + addView(valueTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 64 : 17, 35, LocaleController.isRTL ? 17 : 64, 0)); + + checkBox = new Switch2(context); + addView(checkBox, LayoutHelper.createFrame(44, 40, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, 14, 0, 14, 0)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (isMultiline) { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + } else { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(valueTextView.getVisibility() == VISIBLE ? 64 : 48) + (needDivider ? 1 : 0), MeasureSpec.EXACTLY)); + } + } + + public void setTextAndCheck(String text, boolean checked, boolean divider) { + textView.setText(text); + isMultiline = false; + checkBox.setChecked(checked, false); + needDivider = divider; + valueTextView.setVisibility(GONE); + LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams(); + layoutParams.height = LayoutParams.MATCH_PARENT; + layoutParams.topMargin = 0; + textView.setLayoutParams(layoutParams); + setWillNotDraw(!divider); + } + + public void setTextAndValueAndCheck(String text, String value, boolean checked, boolean multiline, boolean divider) { + textView.setText(text); + valueTextView.setText(value); + checkBox.setChecked(checked, false); + needDivider = divider; + valueTextView.setVisibility(VISIBLE); + isMultiline = multiline; + if (multiline) { + valueTextView.setLines(0); + valueTextView.setMaxLines(0); + valueTextView.setSingleLine(false); + valueTextView.setEllipsize(null); + valueTextView.setPadding(0, 0, 0, AndroidUtilities.dp(11)); + } else { + valueTextView.setLines(1); + valueTextView.setMaxLines(1); + valueTextView.setSingleLine(true); + valueTextView.setEllipsize(TextUtils.TruncateAt.END); + valueTextView.setPadding(0, 0, 0, 0); + } + LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams(); + layoutParams.height = LayoutParams.WRAP_CONTENT; + layoutParams.topMargin = AndroidUtilities.dp(10); + textView.setLayoutParams(layoutParams); + setWillNotDraw(!divider); + } + + @Override + public void setEnabled(boolean value) { + super.setEnabled(value); + if (value) { + textView.setAlpha(1.0f); + valueTextView.setAlpha(1.0f); + checkBox.setAlpha(1.0f); + } else { + checkBox.setAlpha(0.5f); + textView.setAlpha(0.5f); + valueTextView.setAlpha(0.5f); + } + } + + public void setChecked(boolean checked) { + checkBox.setChecked(checked, true); + } + + public boolean isChecked() { + return checkBox.isChecked(); + } + + @Override + protected void onDraw(Canvas canvas) { + if (needDivider) { + canvas.drawLine(getPaddingLeft(), getHeight() - 1, getWidth() - getPaddingRight(), getHeight() - 1, Theme.dividerPaint); + } + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextSettingsCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextSettingsCell.java index 02ed5995a..dcf86b054 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextSettingsCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextSettingsCell.java @@ -14,7 +14,6 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; -import android.support.annotation.ColorInt; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ThemeCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ThemeCell.java index 294ef5378..b7ac7497e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ThemeCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ThemeCell.java @@ -97,7 +97,7 @@ public class ThemeCell extends FrameLayout { public void setTheme(Theme.ThemeInfo themeInfo, boolean divider) { currentThemeInfo = themeInfo; - String text = themeInfo.name; + String text = themeInfo.getName(); if (text.endsWith(".attheme")) { text = text.substring(0, text.lastIndexOf('.')); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChangeChatNameActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChangeChatNameActivity.java index 9bf2e33fb..469a61eb2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChangeChatNameActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChangeChatNameActivity.java @@ -10,39 +10,62 @@ package org.telegram.ui; import android.app.Activity; import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; +import android.os.Vibrator; +import android.text.Editable; +import android.text.InputFilter; import android.text.InputType; +import android.text.TextWatcher; import android.util.TypedValue; import android.view.Gravity; -import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.EditText; +import android.widget.FrameLayout; import android.widget.LinearLayout; -import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.NotificationCenter; +import org.telegram.messenger.UserConfig; import org.telegram.tgnet.TLRPC; import org.telegram.messenger.MessagesController; import org.telegram.messenger.R; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; +import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.Theme; -import org.telegram.ui.ActionBar.ThemeDescription; +import org.telegram.ui.Cells.ShadowSectionCell; +import org.telegram.ui.Cells.TextInfoPrivacyCell; +import org.telegram.ui.Cells.TextSettingsCell; +import org.telegram.ui.Components.AvatarDrawable; +import org.telegram.ui.Components.AvatarUpdater; +import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.LayoutHelper; -public class ChangeChatNameActivity extends BaseFragment { +public class ChangeChatNameActivity extends BaseFragment implements AvatarUpdater.AvatarUpdaterDelegate { - private EditText firstNameField; + private EditText nameTextView; + private BackupImageView avatarImage; + private AvatarDrawable avatarDrawable; + private AvatarUpdater avatarUpdater; private View headerLabelView; - private int chat_id; + private TLRPC.InputFile uploadedAvatar; + private AlertDialog progressDialog; + private int chatId; private View doneButton; + private TLRPC.FileLocation avatar; + private boolean createAfterUpload; + private boolean donePressed; + private TLRPC.Chat currentChat; private final static int done_button = 1; @@ -53,7 +76,11 @@ public class ChangeChatNameActivity extends BaseFragment { @Override public boolean onFragmentCreate() { super.onFragmentCreate(); - chat_id = getArguments().getInt("chat_id", 0); + avatarDrawable = new AvatarDrawable(); + chatId = getArguments().getInt("chat_id", 0); + avatarUpdater = new AvatarUpdater(); + avatarUpdater.parentFragment = this; + avatarUpdater.delegate = this; return true; } @@ -61,14 +88,56 @@ public class ChangeChatNameActivity extends BaseFragment { public View createView(Context context) { actionBar.setBackButtonImage(R.drawable.ic_ab_back); actionBar.setAllowOverlayTitle(true); - actionBar.setTitle(LocaleController.getString("EditName", R.string.EditName)); + actionBar.setTitle(LocaleController.getString("ChannelEdit", R.string.ChannelEdit)); actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { @Override public void onItemClick(int id) { if (id == -1) { finishFragment(); } else if (id == done_button) { - if (firstNameField.getText().length() != 0) { + if (donePressed) { + return; + } + if (nameTextView.length() == 0) { + Vibrator v = (Vibrator) getParentActivity().getSystemService(Context.VIBRATOR_SERVICE); + if (v != null) { + v.vibrate(200); + } + AndroidUtilities.shakeView(nameTextView, 2, 0); + return; + } + donePressed = true; + + if (avatarUpdater.uploadingAvatar != null) { + createAfterUpload = true; + progressDialog = new AlertDialog(getParentActivity(), 1); + progressDialog.setMessage(LocaleController.getString("Loading", R.string.Loading)); + progressDialog.setCanceledOnTouchOutside(false); + progressDialog.setCancelable(false); + progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, LocaleController.getString("Cancel", R.string.Cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + createAfterUpload = false; + progressDialog = null; + donePressed = false; + try { + dialog.dismiss(); + } catch (Exception e) { + FileLog.e(e); + } + } + }); + progressDialog.show(); + return; + } + if (uploadedAvatar != null) { + MessagesController.getInstance().changeChatAvatar(chatId, uploadedAvatar); + } else if (avatar == null && currentChat.photo instanceof TLRPC.TL_chatPhoto) { + MessagesController.getInstance().changeChatAvatar(chatId, null); + } + finishFragment(); + + if (nameTextView.getText().length() != 0) { saveName(); finishFragment(); } @@ -79,10 +148,11 @@ public class ChangeChatNameActivity extends BaseFragment { ActionBarMenu menu = actionBar.createMenu(); doneButton = menu.addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); - TLRPC.Chat currentChat = MessagesController.getInstance().getChat(chat_id); + currentChat = MessagesController.getInstance().getChat(chatId); LinearLayout linearLayout = new LinearLayout(context); fragmentView = linearLayout; + fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); fragmentView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); ((LinearLayout) fragmentView).setOrientation(LinearLayout.VERTICAL); fragmentView.setOnTouchListener(new View.OnTouchListener() { @@ -92,38 +162,147 @@ public class ChangeChatNameActivity extends BaseFragment { } }); - firstNameField = new EditText(context); - firstNameField.setText(currentChat.title); - firstNameField.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); - firstNameField.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); - firstNameField.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); - firstNameField.setBackgroundDrawable(Theme.createEditTextDrawable(context, false)); - firstNameField.setMaxLines(3); - firstNameField.setPadding(0, 0, 0, 0); - firstNameField.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); - firstNameField.setInputType(InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); - firstNameField.setImeOptions(EditorInfo.IME_ACTION_DONE); - firstNameField.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); - AndroidUtilities.clearCursorDrawable(firstNameField); - firstNameField.setOnEditorActionListener(new TextView.OnEditorActionListener() { + LinearLayout linearLayout2 = new LinearLayout(context); + linearLayout2.setOrientation(LinearLayout.VERTICAL); + linearLayout2.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + linearLayout.addView(linearLayout2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + FrameLayout frameLayout = new FrameLayout(context); + linearLayout2.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + avatarImage = new BackupImageView(context); + avatarImage.setRoundRadius(AndroidUtilities.dp(32)); + avatarDrawable.setInfo(5, null, null, false); + avatarDrawable.setDrawPhoto(true); + 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)); + avatarImage.setOnClickListener(new View.OnClickListener() { @Override - public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) { - if (i == EditorInfo.IME_ACTION_DONE && doneButton != null) { - doneButton.performClick(); - return true; + public void onClick(View view) { + if (getParentActivity() == null) { + return; } - return false; + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + + CharSequence[] items; + + if (avatar != null) { + items = new CharSequence[]{LocaleController.getString("FromCamera", R.string.FromCamera), LocaleController.getString("FromGalley", R.string.FromGalley), LocaleController.getString("DeletePhoto", R.string.DeletePhoto)}; + } else { + items = new CharSequence[]{LocaleController.getString("FromCamera", R.string.FromCamera), LocaleController.getString("FromGalley", R.string.FromGalley)}; + } + + builder.setItems(items, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + if (i == 0) { + avatarUpdater.openCamera(); + } else if (i == 1) { + avatarUpdater.openGallery(); + } else if (i == 2) { + avatar = null; + uploadedAvatar = null; + avatarImage.setImage(avatar, "50_50", avatarDrawable); + } + } + }); + showDialog(builder.create()); } }); - linearLayout.addView(firstNameField, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 36, 24, 24, 24, 0)); - - if (chat_id > 0) { - firstNameField.setHint(LocaleController.getString("GroupName", R.string.GroupName)); + nameTextView = new EditText(context); + if (currentChat.megagroup) { + nameTextView.setHint(LocaleController.getString("GroupName", R.string.GroupName)); } else { - firstNameField.setHint(LocaleController.getString("EnterListName", R.string.EnterListName)); + nameTextView.setHint(LocaleController.getString("EnterChannelName", R.string.EnterChannelName)); + } + nameTextView.setMaxLines(4); + nameTextView.setText(currentChat.title); + nameTextView.setGravity(Gravity.CENTER_VERTICAL | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT)); + nameTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + nameTextView.setHint(LocaleController.getString("GroupName", R.string.GroupName)); + nameTextView.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + nameTextView.setBackgroundDrawable(Theme.createEditTextDrawable(context, false)); + nameTextView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + nameTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); + nameTextView.setPadding(0, 0, 0, AndroidUtilities.dp(8)); + nameTextView.setFocusable(nameTextView.isEnabled()); + InputFilter[] inputFilters = new InputFilter[1]; + inputFilters[0] = new InputFilter.LengthFilter(100); + nameTextView.setFilters(inputFilters); + AndroidUtilities.clearCursorDrawable(nameTextView); + nameTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + frameLayout.addView(nameTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_VERTICAL, LocaleController.isRTL ? 16 : 96, 0, LocaleController.isRTL ? 96 : 16, 0)); + nameTextView.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.length() > 0 ? nameTextView.getText().toString() : null, null, false); + avatarImage.invalidate(); + } + }); + + ShadowSectionCell sectionCell = new ShadowSectionCell(context); + sectionCell.setSize(20); + linearLayout.addView(sectionCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + if (currentChat.creator) { + FrameLayout container3 = new FrameLayout(context); + container3.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + linearLayout.addView(container3, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + TextSettingsCell textCell = new TextSettingsCell(context); + textCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText5)); + textCell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + textCell.setText(LocaleController.getString("DeleteMega", R.string.DeleteMega), false); + container3.addView(textCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + textCell.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setMessage(LocaleController.getString("MegaDeleteAlert", R.string.MegaDeleteAlert)); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.closeChats); + if (AndroidUtilities.isTablet()) { + NotificationCenter.getInstance().postNotificationName(NotificationCenter.closeChats, -(long) chatId); + } else { + NotificationCenter.getInstance().postNotificationName(NotificationCenter.closeChats); + } + MessagesController.getInstance().deleteUserFromChat(chatId, MessagesController.getInstance().getUser(UserConfig.getClientUserId()), null); + finishFragment(); + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + showDialog(builder.create()); + } + }); + + TextInfoPrivacyCell infoCell2 = new TextInfoPrivacyCell(context); + infoCell2.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + infoCell2.setText(LocaleController.getString("MegaDeleteInfo", R.string.MegaDeleteInfo)); + linearLayout.addView(infoCell2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + } else { + sectionCell.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + } + + nameTextView.setSelection(nameTextView.length()); + if (currentChat.photo != null) { + avatar = currentChat.photo.photo_small; + avatarImage.setImage(avatar, "50_50", avatarDrawable); + } else { + avatarImage.setImageDrawable(avatarDrawable); } - firstNameField.setSelection(firstNameField.length()); return fragmentView; } @@ -134,8 +313,57 @@ public class ChangeChatNameActivity extends BaseFragment { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); boolean animations = preferences.getBoolean("view_animations", true); if (!animations) { - firstNameField.requestFocus(); - AndroidUtilities.showKeyboard(firstNameField); + nameTextView.requestFocus(); + AndroidUtilities.showKeyboard(nameTextView); + } + } + + @Override + public void didUploadedPhoto(final TLRPC.InputFile file, final TLRPC.PhotoSize small, final TLRPC.PhotoSize big) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + uploadedAvatar = file; + avatar = small.location; + avatarImage.setImage(avatar, "50_50", avatarDrawable); + if (createAfterUpload) { + donePressed = false; + try { + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + progressDialog = null; + } + } catch (Exception e) { + FileLog.e(e); + } + doneButton.performClick(); + } + } + }); + } + + @Override + public void onActivityResultFragment(int requestCode, int resultCode, Intent data) { + avatarUpdater.onActivityResult(requestCode, resultCode, data); + } + + @Override + public void saveSelfArgs(Bundle args) { + if (avatarUpdater != null && avatarUpdater.currentPicturePath != null) { + args.putString("path", avatarUpdater.currentPicturePath); + } + if (nameTextView != null) { + String text = nameTextView.getText().toString(); + if (text != null && text.length() != 0) { + args.putString("nameTextView", text); + } + } + } + + @Override + public void restoreSelfArgs(Bundle args) { + if (avatarUpdater != null) { + avatarUpdater.currentPicturePath = args.getString("path"); } } @@ -145,9 +373,9 @@ public class ChangeChatNameActivity extends BaseFragment { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - if (firstNameField != null) { - firstNameField.requestFocus(); - AndroidUtilities.showKeyboard(firstNameField); + if (nameTextView != null) { + nameTextView.requestFocus(); + AndroidUtilities.showKeyboard(nameTextView); } } }, 100); @@ -155,23 +383,6 @@ public class ChangeChatNameActivity extends BaseFragment { } private void saveName() { - MessagesController.getInstance().changeChatTitle(chat_id, firstNameField.getText().toString()); - } - - @Override - public ThemeDescription[] getThemeDescriptions() { - return new ThemeDescription[]{ - new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), - - new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault), - new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon), - new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle), - new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector), - - new ThemeDescription(firstNameField, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(firstNameField, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText), - new ThemeDescription(firstNameField, ThemeDescription.FLAG_BACKGROUNDFILTER, null, null, null, null, Theme.key_windowBackgroundWhiteInputField), - new ThemeDescription(firstNameField, ThemeDescription.FLAG_BACKGROUNDFILTER | ThemeDescription.FLAG_DRAWABLESELECTEDSTATE, null, null, null, null, Theme.key_windowBackgroundWhiteInputFieldActivated), - }; + MessagesController.getInstance().changeChatTitle(chatId, nameTextView.getText().toString()); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelAdminLogActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelAdminLogActivity.java new file mode 100644 index 000000000..a92d1d98f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelAdminLogActivity.java @@ -0,0 +1,2678 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui; + +import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.app.DatePickerDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.PorterDuffXfermode; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.content.FileProvider; +import android.text.TextUtils; +import android.text.style.CharacterStyle; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.ViewTreeObserver; +import android.widget.DatePicker; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.BuildConfig; +import org.telegram.messenger.BuildVars; +import org.telegram.messenger.ChatObject; +import org.telegram.messenger.ContactsController; +import org.telegram.messenger.FileLoader; +import org.telegram.messenger.FileLog; +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.UserConfig; +import org.telegram.messenger.Utilities; +import org.telegram.messenger.VideoEditedInfo; +import org.telegram.messenger.browser.Browser; +import org.telegram.messenger.exoplayer2.ui.AspectRatioFrameLayout; +import org.telegram.messenger.query.StickersQuery; +import org.telegram.messenger.support.widget.LinearLayoutManager; +import org.telegram.messenger.support.widget.LinearSmoothScrollerMiddle; +import org.telegram.messenger.support.widget.RecyclerView; +import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.RequestDelegate; +import org.telegram.tgnet.TLObject; +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.BackDrawable; +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.BotHelpCell; +import org.telegram.ui.Cells.ChatActionCell; +import org.telegram.ui.Cells.ChatLoadingCell; +import org.telegram.ui.Cells.ChatMessageCell; +import org.telegram.ui.Cells.ChatUnreadCell; +import org.telegram.ui.Components.AdminLogFilterAlert; +import org.telegram.ui.Components.ChatAvatarContainer; +import org.telegram.ui.Components.EmbedBottomSheet; +import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.PipRoundVideoView; +import org.telegram.ui.Components.RadialProgressView; +import org.telegram.ui.Components.RecyclerListView; +import org.telegram.ui.Components.ShareAlert; +import org.telegram.ui.Components.SizeNotifierFrameLayout; +import org.telegram.ui.Components.StickersAlert; +import org.telegram.ui.Components.URLSpanMono; +import org.telegram.ui.Components.URLSpanNoUnderline; +import org.telegram.ui.Components.URLSpanReplacement; +import org.telegram.ui.Components.URLSpanUserMention; + +import java.io.File; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; + +public class ChannelAdminLogActivity extends BaseFragment implements PhotoViewer.PhotoViewerProvider, NotificationCenter.NotificationCenterDelegate { + + protected TLRPC.Chat currentChat; + + private ArrayList chatMessageCellsCache = new ArrayList<>(); + + private FrameLayout progressView; + private View progressView2; + private RadialProgressView progressBar; + private RecyclerListView chatListView; + private LinearLayoutManager chatLayoutManager; + private ChatActivityAdapter chatAdapter; + private TextView bottomOverlayChatText; + private ImageView bottomOverlayImage; + private FrameLayout bottomOverlayChat; + private FrameLayout emptyViewContainer; + private ChatAvatarContainer avatarContainer; + private TextView emptyView; + private ChatActionCell floatingDateView; + private ActionBarMenuItem searchItem; + private long minEventId; + private boolean currentFloatingDateOnScreen; + private boolean currentFloatingTopIsNotMessage; + private AnimatorSet floatingDateAnimation; + private boolean scrollingFloatingDate; + private int[] mid = new int[]{2}; + private boolean searchWas; + + private boolean checkTextureViewPosition; + private SizeNotifierFrameLayout contentView; + + private MessageObject selectedObject; + + private FrameLayout searchContainer; + private ImageView searchCalendarButton; + private ImageView searchUpButton; + private ImageView searchDownButton; + private SimpleTextView searchCountText; + + private FrameLayout roundVideoContainer; + private AspectRatioFrameLayout aspectRatioFrameLayout; + private TextureView videoTextureView; + private Path aspectPath; + private Paint aspectPaint; + + private int scrollToPositionOnRecreate = -1; + private int scrollToOffsetOnRecreate = 0; + + private boolean paused = true; + private boolean wasPaused = false; + + private boolean openAnimationEnded; + + private long dialog_id; + + private HashMap messagesDict = new HashMap<>(); + private HashMap> messagesByDays = new HashMap<>(); + protected ArrayList messages = new ArrayList<>(); + private int minDate; + private boolean endReached; + private boolean loading; + private int loadsCount; + + private ArrayList admins; + private TLRPC.TL_channelAdminLogEventsFilter currentFilter = null; + private String searchQuery = ""; + private HashMap selectedAdmins; + + private MessageObject scrollToMessage; + + public ChannelAdminLogActivity(TLRPC.Chat chat) { + currentChat = chat; + } + + @Override + public boolean onFragmentCreate() { + super.onFragmentCreate(); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.emojiDidLoaded); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidStarted); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingPlayStateChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidReset); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.didSetNewWallpapper); + loadMessages(true); + loadAdmins(); + return true; + } + + @Override + public void onFragmentDestroy() { + super.onFragmentDestroy(); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.emojiDidLoaded); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidStarted); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingPlayStateChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidReset); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didSetNewWallpapper); + } + + private void updateEmptyPlaceholder() { + if (emptyView == null) { + return; + } + if (!TextUtils.isEmpty(searchQuery)) { + emptyView.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(5), AndroidUtilities.dp(8), AndroidUtilities.dp(5)); + emptyView.setText(AndroidUtilities.replaceTags(LocaleController.formatString("EventLogEmptyTextSearch", R.string.EventLogEmptyTextSearch, searchQuery))); + } else if (selectedAdmins != null || currentFilter != null) { + emptyView.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(5), AndroidUtilities.dp(8), AndroidUtilities.dp(5)); + emptyView.setText(AndroidUtilities.replaceTags(LocaleController.getString("EventLogEmptySearch", R.string.EventLogEmptySearch))); + } else { + emptyView.setPadding(AndroidUtilities.dp(16), AndroidUtilities.dp(16), AndroidUtilities.dp(16), AndroidUtilities.dp(16)); + if (currentChat.megagroup) { + emptyView.setText(AndroidUtilities.replaceTags(LocaleController.getString("EventLogEmpty", R.string.EventLogEmpty))); + } else { + emptyView.setText(AndroidUtilities.replaceTags(LocaleController.getString("EventLogEmptyChannel", R.string.EventLogEmptyChannel))); + } + } + } + + private void loadMessages(boolean reset) { + if (loading) { + return; + } + if (reset) { + minEventId = Long.MAX_VALUE; + if (progressView != null) { + progressView.setVisibility(View.VISIBLE); + emptyViewContainer.setVisibility(View.INVISIBLE); + chatListView.setEmptyView(null); + } + messagesDict.clear(); + messages.clear(); + messagesByDays.clear(); + } + loading = true; + TLRPC.TL_channels_getAdminLog req = new TLRPC.TL_channels_getAdminLog(); + req.channel = MessagesController.getInputChannel(currentChat); + req.q = searchQuery; + req.limit = 50; + if (!reset && !messages.isEmpty()) { + req.max_id = minEventId; + } else { + req.max_id = 0; + } + req.min_id = 0; + if (currentFilter != null) { + req.flags |= 1; + req.events_filter = currentFilter; + } + if (selectedAdmins != null) { + req.flags |= 2; + for (HashMap.Entry entry : selectedAdmins.entrySet()) { + req.admins.add(MessagesController.getInputUser(entry.getValue())); + } + } + updateEmptyPlaceholder(); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (response != null) { + final TLRPC.TL_channels_adminLogResults res = (TLRPC.TL_channels_adminLogResults) response; + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().putUsers(res.users, false); + MessagesController.getInstance().putChats(res.chats, false); + boolean added = false; + int oldRowsCount = messages.size(); + for (int a = 0; a < res.events.size(); a++) { + TLRPC.TL_channelAdminLogEvent event = res.events.get(a); + if (messagesDict.containsKey(event.id)) { + continue; + } + minEventId = Math.min(minEventId, event.id); + added = true; + messagesDict.put(event.id, new MessageObject(event, messages, messagesByDays, currentChat, mid)); + } + int newRowsCount = messages.size() - oldRowsCount; + loading = false; + if (!added) { + endReached = true; + } + progressView.setVisibility(View.INVISIBLE); + chatListView.setEmptyView(emptyViewContainer); + if (newRowsCount != 0) { + boolean end = false; + if (endReached) { + end = true; + chatAdapter.notifyItemRangeChanged(0, 2); + } + int firstVisPos = chatLayoutManager.findLastVisibleItemPosition(); + View firstVisView = chatLayoutManager.findViewByPosition(firstVisPos); + int top = ((firstVisView == null) ? 0 : firstVisView.getTop()) - chatListView.getPaddingTop(); + if (newRowsCount - (end ? 1 : 0) > 0) { + int insertStart = 1 + (end ? 0 : 1); + chatAdapter.notifyItemChanged(insertStart); + chatAdapter.notifyItemRangeInserted(insertStart, newRowsCount - (end ? 1 : 0)); + } + if (firstVisPos != -1) { + chatLayoutManager.scrollToPositionWithOffset(firstVisPos + newRowsCount - (end ? 1 : 0), top); + } + } else if (endReached) { + chatAdapter.notifyItemRemoved(0); + } + } + }); + } + } + }); + if (reset && chatAdapter != null) { + chatAdapter.notifyDataSetChanged(); + } + } + + @Override + public void didReceivedNotification(int id, Object... args) { + if (id == NotificationCenter.emojiDidLoaded) { + if (chatListView != null) { + chatListView.invalidateViews(); + } + } else if (id == NotificationCenter.messagePlayingDidStarted) { + MessageObject messageObject = (MessageObject) args[0]; + + if (messageObject.isRoundVideo()) { + MediaController.getInstance().setTextureView(createTextureView(true), aspectRatioFrameLayout, roundVideoContainer, true); + updateTextureViewPosition(); + } + + if (chatListView != null) { + int count = chatListView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = chatListView.getChildAt(a); + if (view instanceof ChatMessageCell) { + ChatMessageCell cell = (ChatMessageCell) view; + MessageObject messageObject1 = cell.getMessageObject(); + if (messageObject1 != null) { + if (messageObject1.isVoice() || messageObject1.isMusic()) { + cell.updateButtonState(false); + } else if (messageObject1.isRoundVideo()) { + cell.checkRoundVideoPlayback(false); + } + } + } + } + } + } else if (id == NotificationCenter.messagePlayingDidReset || id == NotificationCenter.messagePlayingPlayStateChanged) { + if (chatListView != null) { + int count = chatListView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = chatListView.getChildAt(a); + if (view instanceof ChatMessageCell) { + ChatMessageCell cell = (ChatMessageCell) view; + MessageObject messageObject = cell.getMessageObject(); + if (messageObject != null) { + if (messageObject.isVoice() || messageObject.isMusic()) { + cell.updateButtonState(false); + } else if (messageObject.isRoundVideo()) { + if (!MediaController.getInstance().isPlayingMessage(messageObject)) { + cell.checkRoundVideoPlayback(true); + } + } + } + } + } + } + } else if (id == NotificationCenter.messagePlayingProgressDidChanged) { + Integer mid = (Integer) args[0]; + if (chatListView != null) { + int count = chatListView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = chatListView.getChildAt(a); + if (view instanceof ChatMessageCell) { + ChatMessageCell cell = (ChatMessageCell) view; + MessageObject playing = cell.getMessageObject(); + if (playing != null && playing.getId() == mid) { + MessageObject player = MediaController.getInstance().getPlayingMessageObject(); + if (player != null) { + playing.audioProgress = player.audioProgress; + playing.audioProgressSec = player.audioProgressSec; + cell.updatePlayingMessageProgress(); + } + break; + } + } + } + } + } else if (id == NotificationCenter.didSetNewWallpapper) { + if (fragmentView != null) { + ((SizeNotifierFrameLayout) fragmentView).setBackgroundImage(Theme.getCachedWallpaper()); + progressView2.getBackground().setColorFilter(Theme.colorFilter); + if (emptyView != null) { + emptyView.getBackground().setColorFilter(Theme.colorFilter); + } + chatListView.invalidateViews(); + } + } + } + + private void updateBottomOverlay() { + /*if (searchItem != null && searchItem.getVisibility() == View.VISIBLE) { + searchContainer.setVisibility(View.VISIBLE); + bottomOverlayChat.setVisibility(View.INVISIBLE); + } else { + searchContainer.setVisibility(View.INVISIBLE); + bottomOverlayChat.setVisibility(View.VISIBLE); + }*/ + } + + @Override + public View createView(Context context) { + if (chatMessageCellsCache.isEmpty()) { + for (int a = 0; a < 8; a++) { + chatMessageCellsCache.add(new ChatMessageCell(context)); + } + } + + searchWas = false; + hasOwnBackground = true; + + Theme.createChatResources(context, false); + + actionBar.setAddToContainer(false); + actionBar.setBackButtonDrawable(new BackDrawable(false)); + actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { + @Override + public void onItemClick(final int id) { + if (id == -1) { + finishFragment(); + } + } + }); + + avatarContainer = new ChatAvatarContainer(context, null, false); + actionBar.addView(avatarContainer, 0, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 56, 0, 40, 0)); + + ActionBarMenu menu = actionBar.createMenu(); + searchItem = menu.addItem(0, R.drawable.ic_ab_search).setIsSearchField(true).setActionBarMenuItemSearchListener(new ActionBarMenuItem.ActionBarMenuItemSearchListener() { + + @Override + public void onSearchCollapse() { + searchQuery = ""; + avatarContainer.setVisibility(View.VISIBLE); + if (searchWas) { + searchWas = false; + loadMessages(true); + } + /*highlightMessageId = Integer.MAX_VALUE; + updateVisibleRows(); + scrollToLastMessage(false); + */ + updateBottomOverlay(); + } + + @Override + public void onSearchExpand() { + avatarContainer.setVisibility(View.GONE); + updateBottomOverlay(); + } + + @Override + public void onSearchPressed(EditText editText) { + searchWas = true; + searchQuery = editText.getText().toString(); + loadMessages(true); + //updateSearchButtons(0, 0, 0); + } + }); + searchItem.getSearchField().setHint(LocaleController.getString("Search", R.string.Search)); + + avatarContainer.setEnabled(false); + + avatarContainer.setTitle(currentChat.title); + avatarContainer.setSubtitle(LocaleController.getString("EventLogAllEvents", R.string.EventLogAllEvents)); + avatarContainer.setChatAvatar(currentChat); + + fragmentView = new SizeNotifierFrameLayout(context) { + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject != null && messageObject.isRoundVideo() && messageObject.getDialogId() == dialog_id) { + MediaController.getInstance().setTextureView(createTextureView(false), aspectRatioFrameLayout, roundVideoContainer, true); + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean result = super.drawChild(canvas, child, drawingTime); + if (child == actionBar && parentLayout != null) { + parentLayout.drawHeaderShadow(canvas, actionBar.getVisibility() == VISIBLE ? actionBar.getMeasuredHeight() : 0); + } + return result; + } + + @Override + protected boolean isActionBarVisible() { + return actionBar.getVisibility() == VISIBLE; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int allHeight; + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + setMeasuredDimension(widthSize, heightSize); + heightSize -= getPaddingTop(); + + measureChildWithMargins(actionBar, widthMeasureSpec, 0, heightMeasureSpec, 0); + int actionBarHeight = actionBar.getMeasuredHeight(); + if (actionBar.getVisibility() == VISIBLE) { + heightSize -= actionBarHeight; + } + + int keyboardSize = getKeyboardHeight(); + + int childCount = getChildCount(); + + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child == null || child.getVisibility() == GONE || child == actionBar) { + continue; + } + if (child == chatListView || child == progressView) { + int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + int contentHeightSpec = MeasureSpec.makeMeasureSpec(Math.max(AndroidUtilities.dp(10), heightSize - AndroidUtilities.dp(48 + 2)), MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); + } else if (child == emptyViewContainer) { + int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + int contentHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); + } else { + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final int width = child.getMeasuredWidth(); + final int height = child.getMeasuredHeight(); + + int childLeft; + int childTop; + + int gravity = lp.gravity; + if (gravity == -1) { + gravity = Gravity.TOP | Gravity.LEFT; + } + + final int absoluteGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + childLeft = (r - l - width) / 2 + lp.leftMargin - lp.rightMargin; + break; + case Gravity.RIGHT: + childLeft = r - width - lp.rightMargin; + break; + case Gravity.LEFT: + default: + childLeft = lp.leftMargin; + } + + switch (verticalGravity) { + case Gravity.TOP: + childTop = lp.topMargin + getPaddingTop(); + if (child != actionBar && actionBar.getVisibility() == VISIBLE) { + childTop += actionBar.getMeasuredHeight(); + } + break; + case Gravity.CENTER_VERTICAL: + childTop = (b - t - height) / 2 + lp.topMargin - lp.bottomMargin; + break; + case Gravity.BOTTOM: + childTop = (b - t) - height - lp.bottomMargin; + break; + default: + childTop = lp.topMargin; + } + + if (child == emptyViewContainer) { + childTop -= AndroidUtilities.dp(24) - (actionBar.getVisibility() == VISIBLE ? actionBar.getMeasuredHeight() / 2 : 0); + } else if (child == actionBar) { + childTop -= getPaddingTop(); + } + child.layout(childLeft, childTop, childLeft + width, childTop + height); + } + + updateMessagesVisisblePart(); + notifyHeightChanged(); + } + }; + + contentView = (SizeNotifierFrameLayout) fragmentView; + + contentView.setBackgroundImage(Theme.getCachedWallpaper()); + + emptyViewContainer = new FrameLayout(context); + emptyViewContainer.setVisibility(View.INVISIBLE); + contentView.addView(emptyViewContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER)); + emptyViewContainer.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return true; + } + }); + + + emptyView = new TextView(context); + emptyView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + emptyView.setGravity(Gravity.CENTER); + emptyView.setTextColor(Theme.getColor(Theme.key_chat_serviceText)); + emptyView.setBackgroundDrawable(Theme.createRoundRectDrawable(AndroidUtilities.dp(10), Theme.getServiceMessageColor())); + emptyView.setPadding(AndroidUtilities.dp(16), AndroidUtilities.dp(16), AndroidUtilities.dp(16), AndroidUtilities.dp(16)); + emptyViewContainer.addView(emptyView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER, 16, 0, 16, 0)); + + chatListView = new RecyclerListView(context) { + + @Override + public boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean result = super.drawChild(canvas, child, drawingTime); + if (child instanceof ChatMessageCell) { + ChatMessageCell chatMessageCell = (ChatMessageCell) child; + ImageReceiver imageReceiver = chatMessageCell.getAvatarImage(); + if (imageReceiver != null) { + int top = child.getTop(); + if (chatMessageCell.isPinnedBottom()) { + ViewHolder holder = chatListView.getChildViewHolder(child); + if (holder != null) { + holder = chatListView.findViewHolderForAdapterPosition(holder.getAdapterPosition() + 1); + if (holder != null) { + imageReceiver.setImageY(-AndroidUtilities.dp(1000)); + imageReceiver.draw(canvas); + return result; + } + } + } + if (chatMessageCell.isPinnedTop()) { + ViewHolder holder = chatListView.getChildViewHolder(child); + if (holder != null) { + while (true) { + holder = chatListView.findViewHolderForAdapterPosition(holder.getAdapterPosition() - 1); + if (holder != null) { + top = holder.itemView.getTop(); + if (!(holder.itemView instanceof ChatMessageCell) || !((ChatMessageCell) holder.itemView).isPinnedTop()) { + break; + } + } else { + break; + } + } + } + } + int y = child.getTop() + chatMessageCell.getLayoutHeight(); + int maxY = chatListView.getHeight() - chatListView.getPaddingBottom(); + if (y > maxY) { + y = maxY; + } + if (y - AndroidUtilities.dp(48) < top) { + y = top + AndroidUtilities.dp(48); + } + imageReceiver.setImageY(y - AndroidUtilities.dp(44)); + imageReceiver.draw(canvas); + } + } + return result; + } + }; + chatListView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + createMenu(view); + } + }); + chatListView.setTag(1); + chatListView.setVerticalScrollBarEnabled(true); + chatListView.setAdapter(chatAdapter = new ChatActivityAdapter(context)); + chatListView.setClipToPadding(false); + chatListView.setPadding(0, AndroidUtilities.dp(4), 0, AndroidUtilities.dp(3)); + chatListView.setItemAnimator(null); + chatListView.setLayoutAnimation(null); + chatLayoutManager = new LinearLayoutManager(context) { + @Override + public boolean supportsPredictiveItemAnimations() { + return false; + } + + @Override + public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { + LinearSmoothScrollerMiddle linearSmoothScroller = new LinearSmoothScrollerMiddle(recyclerView.getContext()); + linearSmoothScroller.setTargetPosition(position); + startSmoothScroll(linearSmoothScroller); + } + }; + chatLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); + chatLayoutManager.setStackFromEnd(true); + chatListView.setLayoutManager(chatLayoutManager); + contentView.addView(chatListView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + chatListView.setOnScrollListener(new RecyclerView.OnScrollListener() { + + private float totalDy = 0; + private final int scrollValue = AndroidUtilities.dp(100); + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { + scrollingFloatingDate = true; + checkTextureViewPosition = true; + } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { + scrollingFloatingDate = false; + checkTextureViewPosition = false; + hideFloatingDateView(true); + } + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + chatListView.invalidate(); + if (dy != 0 && scrollingFloatingDate && !currentFloatingTopIsNotMessage) { + if (floatingDateView.getTag() == null) { + if (floatingDateAnimation != null) { + floatingDateAnimation.cancel(); + } + floatingDateView.setTag(1); + floatingDateAnimation = new AnimatorSet(); + floatingDateAnimation.setDuration(150); + floatingDateAnimation.playTogether(ObjectAnimator.ofFloat(floatingDateView, "alpha", 1.0f)); + floatingDateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animation.equals(floatingDateAnimation)) { + floatingDateAnimation = null; + } + } + }); + floatingDateAnimation.start(); + } + } + checkScrollForLoad(true); + updateMessagesVisisblePart(); + } + }); + if (scrollToPositionOnRecreate != -1) { + chatLayoutManager.scrollToPositionWithOffset(scrollToPositionOnRecreate, scrollToOffsetOnRecreate); + scrollToPositionOnRecreate = -1; + } + + progressView = new FrameLayout(context); + progressView.setVisibility(View.INVISIBLE); + contentView.addView(progressView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); + + progressView2 = new View(context); + progressView2.setBackgroundResource(R.drawable.system_loader); + progressView2.getBackground().setColorFilter(Theme.colorFilter); + progressView.addView(progressView2, LayoutHelper.createFrame(36, 36, Gravity.CENTER)); + + progressBar = new RadialProgressView(context); + progressBar.setSize(AndroidUtilities.dp(28)); + progressBar.setProgressColor(Theme.getColor(Theme.key_chat_serviceText)); + progressView.addView(progressBar, LayoutHelper.createFrame(32, 32, Gravity.CENTER)); + + floatingDateView = new ChatActionCell(context); + floatingDateView.setAlpha(0.0f); + contentView.addView(floatingDateView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 4, 0, 0)); + + contentView.addView(actionBar); + + bottomOverlayChat = new FrameLayout(context) { + @Override + public void onDraw(Canvas canvas) { + int bottom = Theme.chat_composeShadowDrawable.getIntrinsicHeight(); + Theme.chat_composeShadowDrawable.setBounds(0, 0, getMeasuredWidth(), bottom); + Theme.chat_composeShadowDrawable.draw(canvas); + canvas.drawRect(0, bottom, getMeasuredWidth(), getMeasuredHeight(), Theme.chat_composeBackgroundPaint); + } + }; + bottomOverlayChat.setWillNotDraw(false); + bottomOverlayChat.setPadding(0, AndroidUtilities.dp(3), 0, 0); + contentView.addView(bottomOverlayChat, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 51, Gravity.BOTTOM)); + bottomOverlayChat.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (getParentActivity() == null) { + return; + } + AdminLogFilterAlert adminLogFilterAlert = new AdminLogFilterAlert(getParentActivity(), currentFilter, selectedAdmins, currentChat.megagroup); + adminLogFilterAlert.setCurrentAdmins(admins); + adminLogFilterAlert.setAdminLogFilterAlertDelegate(new AdminLogFilterAlert.AdminLogFilterAlertDelegate() { + @Override + public void didSelectRights(TLRPC.TL_channelAdminLogEventsFilter filter, HashMap admins) { + currentFilter = filter; + selectedAdmins = admins; + if (currentFilter != null || selectedAdmins != null) { + avatarContainer.setSubtitle(LocaleController.getString("EventLogSelectedEvents", R.string.EventLogSelectedEvents)); + } else { + avatarContainer.setSubtitle(LocaleController.getString("EventLogAllEvents", R.string.EventLogAllEvents)); + } + loadMessages(true); + } + }); + showDialog(adminLogFilterAlert); + } + }); + + bottomOverlayChatText = new TextView(context); + bottomOverlayChatText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + bottomOverlayChatText.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + bottomOverlayChatText.setTextColor(Theme.getColor(Theme.key_chat_fieldOverlayText)); + bottomOverlayChatText.setText(LocaleController.getString("SETTINGS", R.string.SETTINGS).toUpperCase()); + bottomOverlayChat.addView(bottomOverlayChatText, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER)); + + bottomOverlayImage = new ImageView(context); + bottomOverlayImage.setImageResource(R.drawable.log_info); + bottomOverlayImage.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_fieldOverlayText), PorterDuff.Mode.MULTIPLY)); + bottomOverlayImage.setScaleType(ImageView.ScaleType.CENTER); + bottomOverlayChat.addView(bottomOverlayImage, LayoutHelper.createFrame(48, 48, Gravity.RIGHT | Gravity.TOP, 3, 0, 0, 0)); + bottomOverlayImage.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + if (currentChat.megagroup) { + builder.setMessage(AndroidUtilities.replaceTags(LocaleController.getString("EventLogInfoDetail", R.string.EventLogInfoDetail))); + } else { + builder.setMessage(AndroidUtilities.replaceTags(LocaleController.getString("EventLogInfoDetailChannel", R.string.EventLogInfoDetailChannel))); + } + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); + builder.setTitle(LocaleController.getString("EventLogInfoTitle", R.string.EventLogInfoTitle)); + showDialog(builder.create()); + } + }); + + searchContainer = new FrameLayout(context) { + @Override + public void onDraw(Canvas canvas) { + int bottom = Theme.chat_composeShadowDrawable.getIntrinsicHeight(); + Theme.chat_composeShadowDrawable.setBounds(0, 0, getMeasuredWidth(), bottom); + Theme.chat_composeShadowDrawable.draw(canvas); + canvas.drawRect(0, bottom, getMeasuredWidth(), getMeasuredHeight(), Theme.chat_composeBackgroundPaint); + } + }; + searchContainer.setWillNotDraw(false); + searchContainer.setVisibility(View.INVISIBLE); + searchContainer.setFocusable(true); + searchContainer.setFocusableInTouchMode(true); + searchContainer.setClickable(true); + searchContainer.setPadding(0, AndroidUtilities.dp(3), 0, 0); + contentView.addView(searchContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 51, Gravity.BOTTOM)); + + /*searchUpButton = new ImageView(context); + searchUpButton.setScaleType(ImageView.ScaleType.CENTER); + searchUpButton.setImageResource(R.drawable.search_up); + searchUpButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_searchPanelIcons), PorterDuff.Mode.MULTIPLY)); + searchContainer.addView(searchUpButton, LayoutHelper.createFrame(48, 48)); + searchUpButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + MessagesSearchQuery.searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 1); + } + }); + + searchDownButton = new ImageView(context); + searchDownButton.setScaleType(ImageView.ScaleType.CENTER); + searchDownButton.setImageResource(R.drawable.search_down); + searchDownButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_searchPanelIcons), PorterDuff.Mode.MULTIPLY)); + searchContainer.addView(searchDownButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.TOP, 48, 0, 0, 0)); + searchDownButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + MessagesSearchQuery.searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 2); + } + });*/ + + searchCalendarButton = new ImageView(context); + searchCalendarButton.setScaleType(ImageView.ScaleType.CENTER); + searchCalendarButton.setImageResource(R.drawable.search_calendar); + searchCalendarButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_searchPanelIcons), PorterDuff.Mode.MULTIPLY)); + searchContainer.addView(searchCalendarButton, LayoutHelper.createFrame(48, 48, Gravity.RIGHT | Gravity.TOP)); + searchCalendarButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (getParentActivity() == null) { + return; + } + AndroidUtilities.hideKeyboard(searchItem.getSearchField()); + Calendar calendar = Calendar.getInstance(); + int year = calendar.get(Calendar.YEAR); + int monthOfYear = calendar.get(Calendar.MONTH); + int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + try { + DatePickerDialog dialog = new DatePickerDialog(getParentActivity(), new DatePickerDialog.OnDateSetListener() { + @Override + public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) { + Calendar calendar = Calendar.getInstance(); + calendar.clear(); + calendar.set(year, month, dayOfMonth); + int date = (int) (calendar.getTime().getTime() / 1000); + loadMessages(true); + } + }, year, monthOfYear, dayOfMonth); + final DatePicker datePicker = dialog.getDatePicker(); + datePicker.setMinDate(1375315200000L); + datePicker.setMaxDate(System.currentTimeMillis()); + dialog.setButton(DialogInterface.BUTTON_POSITIVE, LocaleController.getString("JumpToDate", R.string.JumpToDate), dialog); + dialog.setButton(DialogInterface.BUTTON_NEGATIVE, LocaleController.getString("Cancel", R.string.Cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }); + if (Build.VERSION.SDK_INT >= 21) { + dialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + int count = datePicker.getChildCount(); + for (int a = 0; a < count; a++) { + View child = datePicker.getChildAt(a); + ViewGroup.LayoutParams layoutParams = child.getLayoutParams(); + layoutParams.width = LayoutHelper.MATCH_PARENT; + child.setLayoutParams(layoutParams); + } + } + }); + } + showDialog(dialog); + } catch (Exception e) { + FileLog.e(e); + } + } + }); + + searchCountText = new SimpleTextView(context); + searchCountText.setTextColor(Theme.getColor(Theme.key_chat_searchPanelText)); + searchCountText.setTextSize(15); + searchCountText.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + searchContainer.addView(searchCountText, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL, 108, 0, 0, 0)); + + chatAdapter.updateRows(); + if (loading && messages.isEmpty()) { + progressView.setVisibility(View.VISIBLE); + chatListView.setEmptyView(null); + } else { + progressView.setVisibility(View.INVISIBLE); + chatListView.setEmptyView(emptyViewContainer); + } + + updateEmptyPlaceholder(); + + return fragmentView; + } + + private void createMenu(View v) { + MessageObject message = null; + if (v instanceof ChatMessageCell) { + message = ((ChatMessageCell) v).getMessageObject(); + } else if (v instanceof ChatActionCell) { + message = ((ChatActionCell) v).getMessageObject(); + } + if (message == null) { + return; + } + final int type = getMessageType(message); + selectedObject = message; + if (getParentActivity() == null) { + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + + ArrayList items = new ArrayList<>(); + final ArrayList options = new ArrayList<>(); + + if (selectedObject.type == 0 || selectedObject.caption != null) { + items.add(LocaleController.getString("Copy", R.string.Copy)); + options.add(3); + } + if (type == 3) { + if (selectedObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage && MessageObject.isNewGifDocument(selectedObject.messageOwner.media.webpage.document)) { + items.add(LocaleController.getString("SaveToGIFs", R.string.SaveToGIFs)); + options.add(11); + } + } else if (type == 4) { + if (selectedObject.isVideo()) { + items.add(LocaleController.getString("SaveToGallery", R.string.SaveToGallery)); + options.add(4); + items.add(LocaleController.getString("ShareFile", R.string.ShareFile)); + options.add(6); + } else if (selectedObject.isMusic()) { + items.add(LocaleController.getString("SaveToMusic", R.string.SaveToMusic)); + options.add(10); + items.add(LocaleController.getString("ShareFile", R.string.ShareFile)); + options.add(6); + } else if (selectedObject.getDocument() != null) { + if (MessageObject.isNewGifDocument(selectedObject.getDocument())) { + items.add(LocaleController.getString("SaveToGIFs", R.string.SaveToGIFs)); + options.add(11); + } + items.add(LocaleController.getString("SaveToDownloads", R.string.SaveToDownloads)); + options.add(10); + items.add(LocaleController.getString("ShareFile", R.string.ShareFile)); + options.add(6); + } else { + items.add(LocaleController.getString("SaveToGallery", R.string.SaveToGallery)); + options.add(4); + } + } else if (type == 5) { + items.add(LocaleController.getString("ApplyLocalizationFile", R.string.ApplyLocalizationFile)); + options.add(5); + items.add(LocaleController.getString("SaveToDownloads", R.string.SaveToDownloads)); + options.add(10); + items.add(LocaleController.getString("ShareFile", R.string.ShareFile)); + options.add(6); + } else if (type == 10) { + items.add(LocaleController.getString("ApplyThemeFile", R.string.ApplyThemeFile)); + options.add(5); + items.add(LocaleController.getString("SaveToDownloads", R.string.SaveToDownloads)); + options.add(10); + items.add(LocaleController.getString("ShareFile", R.string.ShareFile)); + options.add(6); + } else if (type == 6) { + items.add(LocaleController.getString("SaveToGallery", R.string.SaveToGallery)); + options.add(7); + items.add(LocaleController.getString("SaveToDownloads", R.string.SaveToDownloads)); + options.add(10); + items.add(LocaleController.getString("ShareFile", R.string.ShareFile)); + options.add(6); + } else if (type == 7) { + if (selectedObject.isMask()) { + items.add(LocaleController.getString("AddToMasks", R.string.AddToMasks)); + } else { + items.add(LocaleController.getString("AddToStickers", R.string.AddToStickers)); + } + options.add(9); + } else if (type == 8) { + TLRPC.User user = MessagesController.getInstance().getUser(selectedObject.messageOwner.media.user_id); + if (user != null && user.id != UserConfig.getClientUserId() && ContactsController.getInstance().contactsDict.get(user.id) == null) { + items.add(LocaleController.getString("AddContactTitle", R.string.AddContactTitle)); + options.add(15); + } + if (selectedObject.messageOwner.media.phone_number != null || selectedObject.messageOwner.media.phone_number.length() != 0) { + items.add(LocaleController.getString("Copy", R.string.Copy)); + options.add(16); + items.add(LocaleController.getString("Call", R.string.Call)); + options.add(17); + } + } + + if (options.isEmpty()) { + return; + } + final CharSequence[] finalItems = items.toArray(new CharSequence[items.size()]); + builder.setItems(finalItems, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + if (selectedObject == null || i < 0 || i >= options.size()) { + return; + } + processSelectedOption(options.get(i)); + } + }); + + builder.setTitle(LocaleController.getString("Message", R.string.Message)); + showDialog(builder.create()); + } + + private String getMessageContent(MessageObject messageObject, int previousUid, boolean name) { + String str = ""; + if (name) { + if (previousUid != messageObject.messageOwner.from_id) { + if (messageObject.messageOwner.from_id > 0) { + TLRPC.User user = MessagesController.getInstance().getUser(messageObject.messageOwner.from_id); + if (user != null) { + str = ContactsController.formatName(user.first_name, user.last_name) + ":\n"; + } + } else if (messageObject.messageOwner.from_id < 0) { + TLRPC.Chat chat = MessagesController.getInstance().getChat(-messageObject.messageOwner.from_id); + if (chat != null) { + str = chat.title + ":\n"; + } + } + } + } + if (messageObject.type == 0 && messageObject.messageOwner.message != null) { + str += messageObject.messageOwner.message; + } else if (messageObject.messageOwner.media != null && messageObject.messageOwner.media.caption != null) { + str += messageObject.messageOwner.media.caption; + } else { + str += messageObject.messageText; + } + return str; + } + + private TextureView createTextureView(boolean add) { + if (parentLayout == null) { + return null; + } + if (roundVideoContainer == null) { + if (Build.VERSION.SDK_INT >= 21) { + roundVideoContainer = new FrameLayout(getParentActivity()) { + @Override + public void setTranslationY(float translationY) { + super.setTranslationY(translationY); + contentView.invalidate(); + } + }; + roundVideoContainer.setOutlineProvider(new ViewOutlineProvider() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void getOutline(View view, Outline outline) { + outline.setOval(0, 0, AndroidUtilities.roundMessageSize, AndroidUtilities.roundMessageSize); + } + }); + roundVideoContainer.setClipToOutline(true); + } else { + roundVideoContainer = new FrameLayout(getParentActivity()) { + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + aspectPath.reset(); + aspectPath.addCircle(w / 2, h / 2, w / 2, Path.Direction.CW); + aspectPath.toggleInverseFillType(); + } + + @Override + public void setTranslationY(float translationY) { + super.setTranslationY(translationY); + contentView.invalidate(); + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (visibility == VISIBLE) { + setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + canvas.drawPath(aspectPath, aspectPaint); + } + }; + aspectPath = new Path(); + aspectPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + aspectPaint.setColor(0xff000000); + aspectPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + } + roundVideoContainer.setWillNotDraw(false); + roundVideoContainer.setVisibility(View.INVISIBLE); + + aspectRatioFrameLayout = new AspectRatioFrameLayout(getParentActivity()); + aspectRatioFrameLayout.setBackgroundColor(0); + if (add) { + roundVideoContainer.addView(aspectRatioFrameLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + } + + videoTextureView = new TextureView(getParentActivity()); + videoTextureView.setOpaque(false); + aspectRatioFrameLayout.addView(videoTextureView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + } + if (roundVideoContainer.getParent() == null) { + contentView.addView(roundVideoContainer, 1, new FrameLayout.LayoutParams(AndroidUtilities.roundMessageSize, AndroidUtilities.roundMessageSize)); + } + roundVideoContainer.setVisibility(View.INVISIBLE); + aspectRatioFrameLayout.setDrawingReady(false); + return videoTextureView; + } + + private void destroyTextureView() { + if (roundVideoContainer == null || roundVideoContainer.getParent() == null) { + return; + } + contentView.removeView(roundVideoContainer); + aspectRatioFrameLayout.setDrawingReady(false); + roundVideoContainer.setVisibility(View.INVISIBLE); + if (Build.VERSION.SDK_INT < 21) { + roundVideoContainer.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + + private void processSelectedOption(int option) { + if (selectedObject == null) { + return; + } + switch (option) { + case 3: { + AndroidUtilities.addToClipboard(getMessageContent(selectedObject, 0, true)); + break; + } + case 4: { + String path = selectedObject.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(selectedObject.messageOwner).toString(); + } + if (selectedObject.type == 3 || selectedObject.type == 1) { + if (Build.VERSION.SDK_INT >= 23 && getParentActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + getParentActivity().requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 4); + selectedObject = null; + return; + } + MediaController.saveFile(path, getParentActivity(), selectedObject.type == 3 ? 1 : 0, null, null); + } + break; + } + case 5: { + File locFile = null; + if (selectedObject.messageOwner.attachPath != null && selectedObject.messageOwner.attachPath.length() != 0) { + File f = new File(selectedObject.messageOwner.attachPath); + if (f.exists()) { + locFile = f; + } + } + if (locFile == null) { + File f = FileLoader.getPathToMessage(selectedObject.messageOwner); + if (f.exists()) { + locFile = f; + } + } + if (locFile != null) { + if (locFile.getName().endsWith("attheme")) { + if (chatLayoutManager != null) { + int lastPosition = chatLayoutManager.findLastVisibleItemPosition(); + if (lastPosition < chatLayoutManager.getItemCount() - 1) { + scrollToPositionOnRecreate = chatLayoutManager.findFirstVisibleItemPosition(); + RecyclerListView.Holder holder = (RecyclerListView.Holder) chatListView.findViewHolderForAdapterPosition(scrollToPositionOnRecreate); + if (holder != null) { + scrollToOffsetOnRecreate = holder.itemView.getTop(); + } else { + scrollToPositionOnRecreate = -1; + } + } else { + scrollToPositionOnRecreate = -1; + } + } + Theme.ThemeInfo themeInfo = Theme.applyThemeFile(locFile, selectedObject.getDocumentName(), true); + if (themeInfo != null) { + presentFragment(new ThemePreviewActivity(locFile, themeInfo)); + } else { + scrollToPositionOnRecreate = -1; + if (getParentActivity() == null) { + selectedObject = null; + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setMessage(LocaleController.getString("IncorrectTheme", R.string.IncorrectTheme)); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); + showDialog(builder.create()); + } + } else { + if (LocaleController.getInstance().applyLanguageFile(locFile)) { + presentFragment(new LanguageSelectActivity()); + } else { + if (getParentActivity() == null) { + selectedObject = null; + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setMessage(LocaleController.getString("IncorrectLocalization", R.string.IncorrectLocalization)); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); + showDialog(builder.create()); + } + } + } + break; + } + case 6: { + String path = selectedObject.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(selectedObject.messageOwner).toString(); + } + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(selectedObject.getDocument().mime_type); + intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(path))); + getParentActivity().startActivityForResult(Intent.createChooser(intent, LocaleController.getString("ShareFile", R.string.ShareFile)), 500); + break; + } + case 7: { + String path = selectedObject.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(selectedObject.messageOwner).toString(); + } + if (Build.VERSION.SDK_INT >= 23 && getParentActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + getParentActivity().requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 4); + selectedObject = null; + return; + } + MediaController.saveFile(path, getParentActivity(), 0, null, null); + break; + } + case 9: { + showDialog(new StickersAlert(getParentActivity(), this, selectedObject.getInputStickerSet(), null, null)); + break; + } + case 10: { + if (Build.VERSION.SDK_INT >= 23 && getParentActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + getParentActivity().requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 4); + selectedObject = null; + return; + } + String fileName = FileLoader.getDocumentFileName(selectedObject.getDocument()); + if (TextUtils.isEmpty(fileName)) { + fileName = selectedObject.getFileName(); + } + String path = selectedObject.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(selectedObject.messageOwner).toString(); + } + MediaController.saveFile(path, getParentActivity(), selectedObject.isMusic() ? 3 : 2, fileName, selectedObject.getDocument() != null ? selectedObject.getDocument().mime_type : ""); + break; + } + case 11: { + TLRPC.Document document = selectedObject.getDocument(); + MessagesController.getInstance().saveGif(document); + break; + } + case 15: { + Bundle args = new Bundle(); + args.putInt("user_id", selectedObject.messageOwner.media.user_id); + args.putString("phone", selectedObject.messageOwner.media.phone_number); + args.putBoolean("addContact", true); + presentFragment(new ContactAddActivity(args)); + break; + } + case 16: { + AndroidUtilities.addToClipboard(selectedObject.messageOwner.media.phone_number); + break; + } + case 17: { + try { + Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + selectedObject.messageOwner.media.phone_number)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getParentActivity().startActivityForResult(intent, 500); + } catch (Exception e) { + FileLog.e(e); + } + break; + } + } + selectedObject = null; + } + + private int getMessageType(MessageObject messageObject) { + if (messageObject == null) { + return -1; + } + if (messageObject.type == 6) { + return -1; + } else if (messageObject.type == 10 || messageObject.type == 11 || messageObject.type == 16) { + if (messageObject.getId() == 0) { + return -1; + } + return 1; + } else { + if (messageObject.isVoice()) { + return 2; + } else if (messageObject.isSticker()) { + TLRPC.InputStickerSet inputStickerSet = messageObject.getInputStickerSet(); + if (inputStickerSet instanceof TLRPC.TL_inputStickerSetID) { + if (!StickersQuery.isStickerPackInstalled(inputStickerSet.id)) { + return 7; + } + } else if (inputStickerSet instanceof TLRPC.TL_inputStickerSetShortName) { + if (!StickersQuery.isStickerPackInstalled(inputStickerSet.short_name)) { + return 7; + } + } + } else if ((!messageObject.isRoundVideo() || messageObject.isRoundVideo() && BuildVars.DEBUG_VERSION) && (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaPhoto || messageObject.getDocument() != null || messageObject.isMusic() || messageObject.isVideo())) { + boolean canSave = false; + if (messageObject.messageOwner.attachPath != null && messageObject.messageOwner.attachPath.length() != 0) { + File f = new File(messageObject.messageOwner.attachPath); + if (f.exists()) { + canSave = true; + } + } + if (!canSave) { + File f = FileLoader.getPathToMessage(messageObject.messageOwner); + if (f.exists()) { + canSave = true; + } + } + if (canSave) { + if (messageObject.getDocument() != null) { + String mime = messageObject.getDocument().mime_type; + if (mime != null) { + if (messageObject.getDocumentName().endsWith("attheme")) { + return 10; + } else if (mime.endsWith("/xml")) { + return 5; + } else if (mime.endsWith("/png") || mime.endsWith("/jpg") || mime.endsWith("/jpeg")) { + return 6; + } + } + } + return 4; + } + } else if (messageObject.type == 12) { + return 8; + } else if (messageObject.isMediaEmpty()) { + return 3; + } + return 2; + } + } + + private void loadAdmins() { + TLRPC.TL_channels_getParticipants req = new TLRPC.TL_channels_getParticipants(); + req.channel = MessagesController.getInputChannel(currentChat); + req.filter = new TLRPC.TL_channelParticipantsAdmins(); + req.offset = 0; + req.limit = 200; + int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (error == null) { + TLRPC.TL_channels_channelParticipants res = (TLRPC.TL_channels_channelParticipants) response; + MessagesController.getInstance().putUsers(res.users, false); + admins = res.participants; + if (visibleDialog instanceof AdminLogFilterAlert) { + ((AdminLogFilterAlert) visibleDialog).setCurrentAdmins(admins); + } + } + } + }); + } + }); + ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid); + } + + private void hideFloatingDateView(boolean animated) { + if (floatingDateView.getTag() != null && !currentFloatingDateOnScreen && (!scrollingFloatingDate || currentFloatingTopIsNotMessage)) { + floatingDateView.setTag(null); + if (animated) { + floatingDateAnimation = new AnimatorSet(); + floatingDateAnimation.setDuration(150); + floatingDateAnimation.playTogether(ObjectAnimator.ofFloat(floatingDateView, "alpha", 0.0f)); + floatingDateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animation.equals(floatingDateAnimation)) { + floatingDateAnimation = null; + } + } + }); + floatingDateAnimation.setStartDelay(500); + floatingDateAnimation.start(); + } else { + if (floatingDateAnimation != null) { + floatingDateAnimation.cancel(); + floatingDateAnimation = null; + } + floatingDateView.setAlpha(0.0f); + } + } + } + + private void checkScrollForLoad(boolean scroll) { + if (chatLayoutManager == null || paused) { + return; + } + int firstVisibleItem = chatLayoutManager.findFirstVisibleItemPosition(); + int visibleItemCount = firstVisibleItem == RecyclerView.NO_POSITION ? 0 : Math.abs(chatLayoutManager.findLastVisibleItemPosition() - firstVisibleItem) + 1; + if (visibleItemCount > 0) { + int totalItemCount = chatAdapter.getItemCount(); + int checkLoadCount; + if (scroll) { + checkLoadCount = 25; + } else { + checkLoadCount = 5; + } + if (firstVisibleItem <= checkLoadCount && !loading && !endReached) { + loadMessages(false); + } + } + } + + private void moveScrollToLastMessage() { + if (chatListView != null && !messages.isEmpty()) { + chatLayoutManager.scrollToPositionWithOffset(messages.size() - 1, -100000 - chatListView.getPaddingTop()); + } + } + + private void updateTextureViewPosition() { + boolean foundTextureViewMessage = false; + int count = chatListView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = chatListView.getChildAt(a); + if (view instanceof ChatMessageCell) { + ChatMessageCell messageCell = (ChatMessageCell) view; + MessageObject messageObject = messageCell.getMessageObject(); + if (roundVideoContainer != null && messageObject.isRoundVideo() && MediaController.getInstance().isPlayingMessage(messageObject)) { + ImageReceiver imageReceiver = messageCell.getPhotoImage(); + roundVideoContainer.setTranslationX(imageReceiver.getImageX()); + roundVideoContainer.setTranslationY(fragmentView.getPaddingTop() + messageCell.getTop() + imageReceiver.getImageY()); + fragmentView.invalidate(); + roundVideoContainer.invalidate(); + foundTextureViewMessage = true; + break; + } + } + } + if (roundVideoContainer != null) { + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (!foundTextureViewMessage) { + roundVideoContainer.setTranslationY(-AndroidUtilities.roundMessageSize - 100); + fragmentView.invalidate(); + if (messageObject != null && messageObject.isRoundVideo()) { + if (checkTextureViewPosition || PipRoundVideoView.getInstance() != null) { + MediaController.getInstance().setCurrentRoundVisible(false); + } + } + } else { + MediaController.getInstance().setCurrentRoundVisible(true); + } + } + } + + private void updateMessagesVisisblePart() { + if (chatListView == null) { + return; + } + int count = chatListView.getChildCount(); + int height = chatListView.getMeasuredHeight(); + int minPositionHolder = Integer.MAX_VALUE; + int minPositionDateHolder = Integer.MAX_VALUE; + View minDateChild = null; + View minChild = null; + View minMessageChild = null; + boolean foundTextureViewMessage = false; + for (int a = 0; a < count; a++) { + View view = chatListView.getChildAt(a); + if (view instanceof ChatMessageCell) { + ChatMessageCell messageCell = (ChatMessageCell) view; + int top = messageCell.getTop(); + int bottom = messageCell.getBottom(); + int viewTop = top >= 0 ? 0 : -top; + int viewBottom = messageCell.getMeasuredHeight(); + if (viewBottom > height) { + viewBottom = viewTop + height; + } + messageCell.setVisiblePart(viewTop, viewBottom - viewTop); + + MessageObject messageObject = messageCell.getMessageObject(); + if (roundVideoContainer != null && messageObject.isRoundVideo() && MediaController.getInstance().isPlayingMessage(messageObject)) { + ImageReceiver imageReceiver = messageCell.getPhotoImage(); + roundVideoContainer.setTranslationX(imageReceiver.getImageX()); + roundVideoContainer.setTranslationY(fragmentView.getPaddingTop() + top + imageReceiver.getImageY()); + fragmentView.invalidate(); + roundVideoContainer.invalidate(); + foundTextureViewMessage = true; + } + } + if (view.getBottom() <= chatListView.getPaddingTop()) { + continue; + } + int position = view.getBottom(); + if (position < minPositionHolder) { + minPositionHolder = position; + if (view instanceof ChatMessageCell || view instanceof ChatActionCell) { + minMessageChild = view; + } + minChild = view; + } + if (view instanceof ChatActionCell && ((ChatActionCell) view).getMessageObject().isDateObject) { + if (view.getAlpha() != 1.0f) { + view.setAlpha(1.0f); + } + if (position < minPositionDateHolder) { + minPositionDateHolder = position; + minDateChild = view; + } + } + } + if (roundVideoContainer != null) { + if (!foundTextureViewMessage) { + roundVideoContainer.setTranslationY(-AndroidUtilities.roundMessageSize - 100); + fragmentView.invalidate(); + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject != null && messageObject.isRoundVideo() && checkTextureViewPosition) { + MediaController.getInstance().setCurrentRoundVisible(false); + } + } else { + MediaController.getInstance().setCurrentRoundVisible(true); + } + } + if (minMessageChild != null) { + MessageObject messageObject; + if (minMessageChild instanceof ChatMessageCell) { + messageObject = ((ChatMessageCell) minMessageChild).getMessageObject(); + } else { + messageObject = ((ChatActionCell) minMessageChild).getMessageObject(); + } + floatingDateView.setCustomDate(messageObject.messageOwner.date); + } + currentFloatingDateOnScreen = false; + currentFloatingTopIsNotMessage = !(minChild instanceof ChatMessageCell || minChild instanceof ChatActionCell); + if (minDateChild != null) { + if (minDateChild.getTop() > chatListView.getPaddingTop() || currentFloatingTopIsNotMessage) { + if (minDateChild.getAlpha() != 1.0f) { + minDateChild.setAlpha(1.0f); + } + hideFloatingDateView(!currentFloatingTopIsNotMessage); + } else { + if (minDateChild.getAlpha() != 0.0f) { + minDateChild.setAlpha(0.0f); + } + if (floatingDateAnimation != null) { + floatingDateAnimation.cancel(); + floatingDateAnimation = null; + } + if (floatingDateView.getTag() == null) { + floatingDateView.setTag(1); + } + if (floatingDateView.getAlpha() != 1.0f) { + floatingDateView.setAlpha(1.0f); + } + currentFloatingDateOnScreen = true; + } + int offset = minDateChild.getBottom() - chatListView.getPaddingTop(); + if (offset > floatingDateView.getMeasuredHeight() && offset < floatingDateView.getMeasuredHeight() * 2) { + floatingDateView.setTranslationY(-floatingDateView.getMeasuredHeight() * 2 + offset); + } else { + floatingDateView.setTranslationY(0); + } + } else { + hideFloatingDateView(true); + floatingDateView.setTranslationY(0); + } + } + + @Override + public void onTransitionAnimationStart(boolean isOpen, boolean backward) { + NotificationCenter.getInstance().setAllowedNotificationsDutingAnimation(new int[]{NotificationCenter.chatInfoDidLoaded, NotificationCenter.dialogsNeedReload, + NotificationCenter.closeChats, NotificationCenter.messagesDidLoaded, NotificationCenter.botKeyboardDidLoaded/*, NotificationCenter.botInfoDidLoaded*/}); + NotificationCenter.getInstance().setAnimationInProgress(true); + if (isOpen) { + openAnimationEnded = false; + } + } + + @Override + public void onTransitionAnimationEnd(boolean isOpen, boolean backward) { + NotificationCenter.getInstance().setAnimationInProgress(false); + if (isOpen) { + openAnimationEnded = true; + } + } + + @Override + public void onResume() { + super.onResume(); + + paused = false; + checkScrollForLoad(false); + if (wasPaused) { + wasPaused = false; + if (chatAdapter != null) { + chatAdapter.notifyDataSetChanged(); + } + } + + fixLayout(); + } + + @Override + public void onPause() { + super.onPause(); + paused = true; + wasPaused = true; + } + + private void fixLayout() { + if (avatarContainer != null) { + avatarContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (avatarContainer != null) { + avatarContainer.getViewTreeObserver().removeOnPreDrawListener(this); + } + return true; + } + }); + } + } + + @Override + public void onConfigurationChanged(android.content.res.Configuration newConfig) { + fixLayout(); + if (visibleDialog instanceof DatePickerDialog) { + visibleDialog.dismiss(); + } + } + + private void alertUserOpenError(MessageObject message) { + if (getParentActivity() == null) { + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); + if (message.type == 3) { + builder.setMessage(LocaleController.getString("NoPlayerInstalled", R.string.NoPlayerInstalled)); + } else { + builder.setMessage(LocaleController.formatString("NoHandleAppInstalled", R.string.NoHandleAppInstalled, message.getDocument().mime_type)); + } + showDialog(builder.create()); + } + + @Override + public void updatePhotoAtIndex(int index) { + + } + + public TLRPC.Chat getCurrentChat() { + return currentChat; + } + + @Override + public boolean allowCaption() { + return true; + } + + @Override + public boolean scaleToFill() { + return false; + } + + @Override + public PhotoViewer.PlaceProviderObject getPlaceForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { + int count = chatListView.getChildCount(); + + for (int a = 0; a < count; a++) { + ImageReceiver imageReceiver = null; + View view = chatListView.getChildAt(a); + if (view instanceof ChatMessageCell) { + if (messageObject != null) { + ChatMessageCell cell = (ChatMessageCell) view; + MessageObject message = cell.getMessageObject(); + if (message != null && message.getId() == messageObject.getId()) { + imageReceiver = cell.getPhotoImage(); + } + } + } else if (view instanceof ChatActionCell) { + ChatActionCell cell = (ChatActionCell) view; + MessageObject message = cell.getMessageObject(); + if (message != null) { + if (messageObject != null) { + if (message.getId() == messageObject.getId()) { + imageReceiver = cell.getPhotoImage(); + } + } else if (fileLocation != null && message.photoThumbs != null) { + for (int b = 0; b < message.photoThumbs.size(); b++) { + TLRPC.PhotoSize photoSize = message.photoThumbs.get(b); + if (photoSize.location.volume_id == fileLocation.volume_id && photoSize.location.local_id == fileLocation.local_id) { + imageReceiver = cell.getPhotoImage(); + break; + } + } + } + } + } + + if (imageReceiver != null) { + int coords[] = new int[2]; + view.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 = chatListView; + object.imageReceiver = imageReceiver; + object.thumb = imageReceiver.getBitmap(); + object.radius = imageReceiver.getRoundRadius(); + object.isEvent = true; + return object; + } + } + return null; + } + + private void addCanBanUser(Bundle bundle, int uid) { + if (!currentChat.megagroup || admins == null || !ChatObject.canBlockUsers(currentChat)) { + return; + } + for (int a = 0; a < admins.size(); a++) { + TLRPC.ChannelParticipant channelParticipant = admins.get(a); + if (channelParticipant.user_id == uid) { + if (!channelParticipant.can_edit) { + return; + } + break; + } + } + bundle.putInt("ban_chat_id", currentChat.id); + } + + public void showOpenUrlAlert(final String url, boolean ask) { + if (Browser.isInternalUrl(url) || !ask) { + Browser.openUrl(getParentActivity(), url, true); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setMessage(LocaleController.formatString("OpenUrlAlert", R.string.OpenUrlAlert, url)); + builder.setPositiveButton(LocaleController.getString("Open", R.string.Open), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + Browser.openUrl(getParentActivity(), url, true); + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + showDialog(builder.create()); + } + } + + @Override + public Bitmap getThumbForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { + return null; + } + + @Override + public void willSwitchFromPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { + } + + @Override + public void willHidePhotoViewer() { + } + + @Override + public boolean isPhotoChecked(int index) { + return false; + } + + @Override + public void setPhotoChecked(int index, VideoEditedInfo videoEditedInfo) { + } + + @Override + public boolean cancelButtonPressed() { + return true; + } + + @Override + public void sendButtonPressed(int index, VideoEditedInfo videoEditedInfo) { + } + + @Override + public int getSelectedCount() { + return 0; + } + + private void removeMessageObject(MessageObject messageObject) { + int index = messages.indexOf(messageObject); + if (index == -1) { + return; + } + messages.remove(index); + if (chatAdapter != null) { + chatAdapter.notifyItemRemoved(chatAdapter.messagesStartRow + messages.size() - index - 1); + } + } + + public class ChatActivityAdapter extends RecyclerView.Adapter { + + private Context mContext; + private int rowCount; + private int loadingUpRow; + private int messagesStartRow; + private int messagesEndRow; + + public ChatActivityAdapter(Context context) { + mContext = context; + } + + public void updateRows() { + rowCount = 0; + if (!messages.isEmpty()) { + if (!endReached) { + loadingUpRow = rowCount++; + } else { + loadingUpRow = -1; + } + messagesStartRow = rowCount; + rowCount += messages.size(); + messagesEndRow = rowCount; + } else { + loadingUpRow = -1; + messagesStartRow = -1; + messagesEndRow = -1; + } + } + + @Override + public int getItemCount() { + return rowCount; + } + + @Override + public long getItemId(int i) { + return RecyclerListView.NO_ID; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = null; + if (viewType == 0) { + if (!chatMessageCellsCache.isEmpty()) { + view = chatMessageCellsCache.get(0); + chatMessageCellsCache.remove(0); + } else { + view = new ChatMessageCell(mContext); + } + ChatMessageCell chatMessageCell = (ChatMessageCell) view; + chatMessageCell.setDelegate(new ChatMessageCell.ChatMessageCellDelegate() { + @Override + public void didPressedShare(ChatMessageCell cell) { + if (getParentActivity() == null) { + return; + } + showDialog(new ShareAlert(mContext, cell.getMessageObject(), null, ChatObject.isChannel(currentChat) && !currentChat.megagroup && currentChat.username != null && currentChat.username.length() > 0, null, false)); + } + + @Override + public boolean needPlayMessage(MessageObject messageObject) { + if (messageObject.isVoice() || messageObject.isRoundVideo()) { + boolean result = MediaController.getInstance().playMessage(messageObject); + MediaController.getInstance().setVoiceMessagesPlaylist(null, false); + return result; + } else if (messageObject.isMusic()) { + return MediaController.getInstance().setPlaylist(messages, messageObject); + } + return false; + } + + @Override + public void didPressedChannelAvatar(ChatMessageCell cell, TLRPC.Chat chat, int postId) { + if (chat != null && chat != currentChat) { + Bundle args = new Bundle(); + args.putInt("chat_id", chat.id); + if (postId != 0) { + args.putInt("message_id", postId); + } + if (MessagesController.checkCanOpenChat(args, ChannelAdminLogActivity.this)) { + presentFragment(new ChatActivity(args), true); + } + } + } + + @Override + public void didPressedOther(ChatMessageCell cell) { + createMenu(cell); + } + + @Override + public void didPressedUserAvatar(ChatMessageCell cell, TLRPC.User user) { + if (user != null && user.id != UserConfig.getClientUserId()) { + Bundle args = new Bundle(); + args.putInt("user_id", user.id); + addCanBanUser(args, user.id); + ProfileActivity fragment = new ProfileActivity(args); + fragment.setPlayProfileAnimation(false); + presentFragment(fragment); + } + } + + @Override + public void didPressedBotButton(ChatMessageCell cell, TLRPC.KeyboardButton button) { + + } + + @Override + public void didPressedCancelSendButton(ChatMessageCell cell) { + + } + + @Override + public void didLongPressed(ChatMessageCell cell) { + createMenu(cell); + } + + @Override + public boolean canPerformActions() { + return true; + } + + @Override + public void didPressedUrl(MessageObject messageObject, final CharacterStyle url, boolean longPress) { + if (url == null) { + return; + } + if (url instanceof URLSpanMono) { + ((URLSpanMono) url).copyToClipboard(); + Toast.makeText(getParentActivity(), LocaleController.getString("TextCopied", R.string.TextCopied), Toast.LENGTH_SHORT).show(); + } else if (url instanceof URLSpanUserMention) { + TLRPC.User user = MessagesController.getInstance().getUser(Utilities.parseInt(((URLSpanUserMention) url).getURL())); + if (user != null) { + MessagesController.openChatOrProfileWith(user, null, ChannelAdminLogActivity.this, 0, false); + } + } else if (url instanceof URLSpanNoUnderline) { + String str = ((URLSpanNoUnderline) url).getURL(); + if (str.startsWith("@")) { + MessagesController.openByUserName(str.substring(1), ChannelAdminLogActivity.this, 0); + } else if (str.startsWith("#")) { + DialogsActivity fragment = new DialogsActivity(null); + fragment.setSearchString(str); + presentFragment(fragment); + } + } else { + final String urlFinal = ((URLSpan) url).getURL(); + if (longPress) { + BottomSheet.Builder builder = new BottomSheet.Builder(getParentActivity()); + builder.setTitle(urlFinal); + builder.setItems(new CharSequence[]{LocaleController.getString("Open", R.string.Open), LocaleController.getString("Copy", R.string.Copy)}, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, final int which) { + if (which == 0) { + Browser.openUrl(getParentActivity(), urlFinal, true); + } else if (which == 1) { + String url = urlFinal; + if (url.startsWith("mailto:")) { + url = url.substring(7); + } else if (url.startsWith("tel:")) { + url = url.substring(4); + } + AndroidUtilities.addToClipboard(url); + } + } + }); + showDialog(builder.create()); + } else { + if (url instanceof URLSpanReplacement) { + showOpenUrlAlert(((URLSpanReplacement) url).getURL(), true); + } else if (url instanceof URLSpan) { + 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))) { + ArticleViewer.getInstance().setParentActivity(getParentActivity(), ChannelAdminLogActivity.this); + ArticleViewer.getInstance().open(messageObject); + return; + } + } + Browser.openUrl(getParentActivity(), urlFinal, true); + } else if (url instanceof ClickableSpan) { + ((ClickableSpan) url).onClick(fragmentView); + } + } + } + } + + @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); + } + + @Override + public void didPressedReplyMessage(ChatMessageCell cell, int id) { + + } + + @Override + public void didPressedViaBot(ChatMessageCell cell, String username) { + + } + + @Override + public void didPressedImage(ChatMessageCell cell) { + MessageObject message = cell.getMessageObject(); + if (message.type == 13) { + showDialog(new StickersAlert(getParentActivity(), ChannelAdminLogActivity.this, message.getInputStickerSet(), null, null)); + } else if (Build.VERSION.SDK_INT >= 16 && message.isVideo() || message.type == 1 || message.type == 0 && !message.isWebpageDocument() || message.isGif()) { + PhotoViewer.getInstance().setParentActivity(getParentActivity()); + PhotoViewer.getInstance().openPhoto(message, message.type != 0 ? dialog_id : 0, 0, ChannelAdminLogActivity.this); + } else if (message.type == 3) { + try { + File f = null; + if (message.messageOwner.attachPath != null && message.messageOwner.attachPath.length() != 0) { + f = new File(message.messageOwner.attachPath); + } + if (f == null || !f.exists()) { + f = FileLoader.getPathToMessage(message.messageOwner); + } + Intent intent = new Intent(Intent.ACTION_VIEW); + if (Build.VERSION.SDK_INT >= 24) { + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setDataAndType(FileProvider.getUriForFile(getParentActivity(), BuildConfig.APPLICATION_ID + ".provider", f), "video/mp4"); + } else { + intent.setDataAndType(Uri.fromFile(f), "video/mp4"); + } + getParentActivity().startActivityForResult(intent, 500); + } catch (Exception e) { + alertUserOpenError(message); + } + } else if (message.type == 4) { + if (!AndroidUtilities.isGoogleMapsInstalled(ChannelAdminLogActivity.this)) { + return; + } + LocationActivity fragment = new LocationActivity(); + fragment.setMessageObject(message); + presentFragment(fragment); + } else if (message.type == 9 || message.type == 0) { + if (message.getDocumentName().endsWith("attheme")) { + File locFile = null; + if (message.messageOwner.attachPath != null && message.messageOwner.attachPath.length() != 0) { + File f = new File(message.messageOwner.attachPath); + if (f.exists()) { + locFile = f; + } + } + if (locFile == null) { + File f = FileLoader.getPathToMessage(message.messageOwner); + if (f.exists()) { + locFile = f; + } + } + if (chatLayoutManager != null) { + int lastPosition = chatLayoutManager.findLastVisibleItemPosition(); + if (lastPosition < chatLayoutManager.getItemCount() - 1) { + scrollToPositionOnRecreate = chatLayoutManager.findFirstVisibleItemPosition(); + RecyclerListView.Holder holder = (RecyclerListView.Holder) chatListView.findViewHolderForAdapterPosition(scrollToPositionOnRecreate); + if (holder != null) { + scrollToOffsetOnRecreate = holder.itemView.getTop(); + } else { + scrollToPositionOnRecreate = -1; + } + } else { + scrollToPositionOnRecreate = -1; + } + } + Theme.ThemeInfo themeInfo = Theme.applyThemeFile(locFile, message.getDocumentName(), true); + if (themeInfo != null) { + presentFragment(new ThemePreviewActivity(locFile, themeInfo)); + return; + } else { + scrollToPositionOnRecreate = -1; + } + } + try { + AndroidUtilities.openForView(message, getParentActivity()); + } catch (Exception e) { + alertUserOpenError(message); + } + } + } + + @Override + public void didPressedInstantButton(ChatMessageCell cell, int type) { + MessageObject messageObject = cell.getMessageObject(); + if (type == 0) { + if (messageObject.messageOwner.media != null && messageObject.messageOwner.media.webpage != null && messageObject.messageOwner.media.webpage.cached_page != null) { + ArticleViewer.getInstance().setParentActivity(getParentActivity(), ChannelAdminLogActivity.this); + ArticleViewer.getInstance().open(messageObject); + } + } else { + Browser.openUrl(getParentActivity(), messageObject.messageOwner.media.webpage.url); + } + } + }); + chatMessageCell.setAllowAssistant(true); + } else if (viewType == 1) { + view = new ChatActionCell(mContext); + ((ChatActionCell) view).setDelegate(new ChatActionCell.ChatActionCellDelegate() { + @Override + public void didClickedImage(ChatActionCell cell) { + MessageObject message = cell.getMessageObject(); + PhotoViewer.getInstance().setParentActivity(getParentActivity()); + TLRPC.PhotoSize photoSize = FileLoader.getClosestPhotoSizeWithSize(message.photoThumbs, 640); + if (photoSize != null) { + PhotoViewer.getInstance().openPhoto(photoSize.location, ChannelAdminLogActivity.this); + } else { + PhotoViewer.getInstance().openPhoto(message, 0, 0, ChannelAdminLogActivity.this); + } + } + + @Override + public void didLongPressed(ChatActionCell cell) { + createMenu(cell); + } + + @Override + public void needOpenUserProfile(int uid) { + if (uid < 0) { + Bundle args = new Bundle(); + args.putInt("chat_id", -uid); + if (MessagesController.checkCanOpenChat(args, ChannelAdminLogActivity.this)) { + presentFragment(new ChatActivity(args), true); + } + } else if (uid != UserConfig.getClientUserId()) { + Bundle args = new Bundle(); + args.putInt("user_id", uid); + addCanBanUser(args, uid); + ProfileActivity fragment = new ProfileActivity(args); + fragment.setPlayProfileAnimation(false); + presentFragment(fragment); + } + } + + @Override + public void didPressedReplyMessage(ChatActionCell cell, int id) { + + } + + @Override + public void didPressedBotButton(MessageObject messageObject, TLRPC.KeyboardButton button) { + + } + }); + } else if (viewType == 2) { + view = new ChatUnreadCell(mContext); + } else if (viewType == 3) { + view = new BotHelpCell(mContext); + ((BotHelpCell) view).setDelegate(new BotHelpCell.BotHelpCellDelegate() { + @Override + public void didPressUrl(String url) { + if (url.startsWith("@")) { + MessagesController.openByUserName(url.substring(1), ChannelAdminLogActivity.this, 0); + } else if (url.startsWith("#")) { + DialogsActivity fragment = new DialogsActivity(null); + fragment.setSearchString(url); + presentFragment(fragment); + } + } + }); + } else if (viewType == 4) { + view = new ChatLoadingCell(mContext); + } + view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)); + return new RecyclerListView.Holder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + if (position == loadingUpRow) { + ChatLoadingCell loadingCell = (ChatLoadingCell) holder.itemView; + loadingCell.setProgressVisible(loadsCount > 1); + } else if (position >= messagesStartRow && position < messagesEndRow) { + MessageObject message = messages.get(messages.size() - (position - messagesStartRow) - 1); + View view = holder.itemView; + + if (view instanceof ChatMessageCell) { + final ChatMessageCell messageCell = (ChatMessageCell) view; + messageCell.isChat = true; + int nextType = getItemViewType(position + 1); + int prevType = getItemViewType(position - 1); + boolean pinnedBotton; + boolean pinnedTop; + if (!(message.messageOwner.reply_markup instanceof TLRPC.TL_replyInlineMarkup) && nextType == holder.getItemViewType()) { + MessageObject nextMessage = messages.get(messages.size() - (position + 1 - messagesStartRow) - 1); + pinnedBotton = nextMessage.isOutOwner() == message.isOutOwner() && (nextMessage.messageOwner.from_id == message.messageOwner.from_id) && Math.abs(nextMessage.messageOwner.date - message.messageOwner.date) <= 5 * 60; + } else { + pinnedBotton = false; + } + if (prevType == holder.getItemViewType()) { + MessageObject prevMessage = messages.get(messages.size() - (position - messagesStartRow)); + pinnedTop = !(prevMessage.messageOwner.reply_markup instanceof TLRPC.TL_replyInlineMarkup) && prevMessage.isOutOwner() == message.isOutOwner() && (prevMessage.messageOwner.from_id == message.messageOwner.from_id) && Math.abs(prevMessage.messageOwner.date - message.messageOwner.date) <= 5 * 60; + } else { + pinnedTop = false; + } + messageCell.setMessageObject(message, pinnedBotton, pinnedTop); + if (view instanceof ChatMessageCell && MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_AUDIO)) { + ((ChatMessageCell) view).downloadAudioIfNeed(); + } + messageCell.setHighlighted(false); + messageCell.setHighlightedText(null); + } else if (view instanceof ChatActionCell) { + ChatActionCell actionCell = (ChatActionCell) view; + actionCell.setMessageObject(message); + actionCell.setAlpha(1.0f); + } + } + } + + @Override + public int getItemViewType(int position) { + if (position >= messagesStartRow && position < messagesEndRow) { + return messages.get(messages.size() - (position - messagesStartRow) - 1).contentType; + } + return 4; + } + + @Override + public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { + if (holder.itemView instanceof ChatMessageCell) { + final ChatMessageCell messageCell = (ChatMessageCell) holder.itemView; + MessageObject message = messageCell.getMessageObject(); + + boolean selected = false; + boolean disableSelection = false; + messageCell.setBackgroundDrawable(null); + messageCell.setCheckPressed(!disableSelection, disableSelection && selected); + + messageCell.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + messageCell.getViewTreeObserver().removeOnPreDrawListener(this); + + int height = chatListView.getMeasuredHeight(); + int top = messageCell.getTop(); + int bottom = messageCell.getBottom(); + int viewTop = top >= 0 ? 0 : -top; + int viewBottom = messageCell.getMeasuredHeight(); + if (viewBottom > height) { + viewBottom = viewTop + height; + } + messageCell.setVisiblePart(viewTop, viewBottom - viewTop); + + return true; + } + }); + messageCell.setHighlighted(false); + } + } + + public void updateRowWithMessageObject(MessageObject messageObject) { + int index = messages.indexOf(messageObject); + if (index == -1) { + return; + } + notifyItemChanged(messagesStartRow + messages.size() - index - 1); + } + + @Override + public void notifyDataSetChanged() { + updateRows(); + try { + super.notifyDataSetChanged(); + } catch (Exception e) { + FileLog.e(e); + } + } + + @Override + public void notifyItemChanged(int position) { + updateRows(); + try { + super.notifyItemChanged(position); + } catch (Exception e) { + FileLog.e(e); + } + } + + @Override + public void notifyItemRangeChanged(int positionStart, int itemCount) { + updateRows(); + try { + super.notifyItemRangeChanged(positionStart, itemCount); + } catch (Exception e) { + FileLog.e(e); + } + } + + @Override + public void notifyItemInserted(int position) { + updateRows(); + try { + super.notifyItemInserted(position); + } catch (Exception e) { + FileLog.e(e); + } + } + + @Override + public void notifyItemMoved(int fromPosition, int toPosition) { + updateRows(); + try { + super.notifyItemMoved(fromPosition, toPosition); + } catch (Exception e) { + FileLog.e(e); + } + } + + @Override + public void notifyItemRangeInserted(int positionStart, int itemCount) { + updateRows(); + try { + super.notifyItemRangeInserted(positionStart, itemCount); + } catch (Exception e) { + FileLog.e(e); + } + } + + @Override + public void notifyItemRemoved(int position) { + updateRows(); + try { + super.notifyItemRemoved(position); + } catch (Exception e) { + FileLog.e(e); + } + } + + @Override + public void notifyItemRangeRemoved(int positionStart, int itemCount) { + updateRows(); + try { + super.notifyItemRangeRemoved(positionStart, itemCount); + } catch (Exception e) { + FileLog.e(e); + } + } + } + + @Override + public ThemeDescription[] getThemeDescriptions() { + return new ThemeDescription[]{ + new ThemeDescription(fragmentView, 0, null, null, null, null, Theme.key_chat_wallpaper), + + new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault), + new ThemeDescription(chatListView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_actionBarDefault), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SUBMENUBACKGROUND, null, null, null, null, Theme.key_actionBarDefaultSubmenuBackground), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SUBMENUITEM, null, null, null, null, Theme.key_actionBarDefaultSubmenuItem), + + new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault), + new ThemeDescription(chatListView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_actionBarDefault), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon), + new ThemeDescription(avatarContainer.getTitleTextView(), ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle), + new ThemeDescription(avatarContainer.getSubtitleTextView(), ThemeDescription.FLAG_TEXTCOLOR, null, new Paint[]{Theme.chat_statusPaint, Theme.chat_statusRecordPaint}, null, null, Theme.key_actionBarDefaultSubtitle, null), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector), + + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.avatar_photoDrawable, Theme.avatar_broadcastDrawable}, null, Theme.key_avatar_text), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_backgroundRed), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_backgroundOrange), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_backgroundViolet), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_backgroundGreen), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_backgroundCyan), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_backgroundBlue), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_backgroundPink), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_nameInMessageRed), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_nameInMessageOrange), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_nameInMessageViolet), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_nameInMessageGreen), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_nameInMessageCyan), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_nameInMessageBlue), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_avatar_nameInMessagePink), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInDrawable, Theme.chat_msgInMediaDrawable}, null, Theme.key_chat_inBubble), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInSelectedDrawable, Theme.chat_msgInMediaSelectedDrawable}, null, Theme.key_chat_inBubbleSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInShadowDrawable, Theme.chat_msgInMediaShadowDrawable}, null, Theme.key_chat_inBubbleShadow), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutDrawable, Theme.chat_msgOutMediaDrawable}, null, Theme.key_chat_outBubble), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutSelectedDrawable, Theme.chat_msgOutMediaSelectedDrawable}, null, Theme.key_chat_outBubbleSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutShadowDrawable, Theme.chat_msgOutMediaShadowDrawable}, null, Theme.key_chat_outBubbleShadow), + new ThemeDescription(chatListView, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{ChatActionCell.class}, Theme.chat_actionTextPaint, null, null, Theme.key_chat_serviceText), + new ThemeDescription(chatListView, ThemeDescription.FLAG_LINKCOLOR, new Class[]{ChatActionCell.class}, Theme.chat_actionTextPaint, null, null, Theme.key_chat_serviceLink), + + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_shareIconDrawable, Theme.chat_botInlineDrawable, Theme.chat_botLinkDrawalbe}, null, Theme.key_chat_serviceIcon), + + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class, ChatActionCell.class}, null, null, null, Theme.key_chat_serviceBackground), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class, ChatActionCell.class}, null, null, null, Theme.key_chat_serviceBackgroundSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_messageTextIn), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_messageTextOut), + new ThemeDescription(chatListView, ThemeDescription.FLAG_LINKCOLOR, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_messageLinkIn, null), + new ThemeDescription(chatListView, ThemeDescription.FLAG_LINKCOLOR, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_messageLinkOut, null), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutCheckDrawable, Theme.chat_msgOutHalfCheckDrawable}, null, Theme.key_chat_outSentCheck), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutCheckSelectedDrawable, Theme.chat_msgOutHalfCheckSelectedDrawable}, null, Theme.key_chat_outSentCheckSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutClockDrawable}, null, Theme.key_chat_outSentClock), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutSelectedClockDrawable}, null, Theme.key_chat_outSentClockSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInClockDrawable}, null, Theme.key_chat_inSentClock), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInSelectedClockDrawable}, null, Theme.key_chat_inSentClockSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgMediaCheckDrawable, Theme.chat_msgMediaHalfCheckDrawable}, null, Theme.key_chat_mediaSentCheck), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgStickerHalfCheckDrawable, Theme.chat_msgStickerCheckDrawable, Theme.chat_msgStickerClockDrawable, Theme.chat_msgStickerViewsDrawable}, null, Theme.key_chat_serviceText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgMediaClockDrawable}, null, Theme.key_chat_mediaSentClock), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutViewsDrawable}, null, Theme.key_chat_outViews), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutViewsSelectedDrawable}, null, Theme.key_chat_outViewsSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInViewsDrawable}, null, Theme.key_chat_inViews), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInViewsSelectedDrawable}, null, Theme.key_chat_inViewsSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgMediaViewsDrawable}, null, Theme.key_chat_mediaViews), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutMenuDrawable}, null, Theme.key_chat_outMenu), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutMenuSelectedDrawable}, null, Theme.key_chat_outMenuSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInMenuDrawable}, null, Theme.key_chat_inMenu), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInMenuSelectedDrawable}, null, Theme.key_chat_inMenuSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgMediaMenuDrawable}, null, Theme.key_chat_mediaMenu), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutInstantDrawable, Theme.chat_msgOutCallDrawable}, null, Theme.key_chat_outInstant), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutCallSelectedDrawable}, null, Theme.key_chat_outInstantSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInInstantDrawable, Theme.chat_msgInCallDrawable}, null, Theme.key_chat_inInstant), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInCallSelectedDrawable}, null, Theme.key_chat_inInstantSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgCallUpRedDrawable, Theme.chat_msgCallDownRedDrawable}, null, Theme.key_calls_callReceivedRedIcon), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgCallUpGreenDrawable, Theme.chat_msgCallDownGreenDrawable}, null, Theme.key_calls_callReceivedGreenIcon), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, Theme.chat_msgErrorPaint, null, null, Theme.key_chat_sentError), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgErrorDrawable}, null, Theme.key_chat_sentErrorIcon), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, Theme.chat_durationPaint, null, null, Theme.key_chat_previewDurationText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, Theme.chat_gamePaint, null, null, Theme.key_chat_previewGameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inPreviewInstantText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outPreviewInstantText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inPreviewInstantSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outPreviewInstantSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, Theme.chat_deleteProgressPaint, null, null, Theme.key_chat_secretTimeText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_stickerNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, Theme.chat_botButtonPaint, null, null, Theme.key_chat_botButtonText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, Theme.chat_botProgressPaint, null, null, Theme.key_chat_botProgress), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inForwardedNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outForwardedNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inViaBotNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outViaBotNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_stickerViaBotNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inReplyLine), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outReplyLine), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_stickerReplyLine), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inReplyNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outReplyNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_stickerReplyNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inReplyMessageText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outReplyMessageText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inReplyMediaMessageText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outReplyMediaMessageText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inReplyMediaMessageSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outReplyMediaMessageSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_stickerReplyMessageText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inPreviewLine), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outPreviewLine), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inSiteNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outSiteNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inContactNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outContactNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inContactPhoneText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outContactPhoneText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_mediaProgress), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inAudioProgress), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outAudioProgress), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inAudioSelectedProgress), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outAudioSelectedProgress), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_mediaTimeText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inTimeText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outTimeText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inTimeSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outTimeSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inAudioPerfomerText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outAudioPerfomerText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inAudioTitleText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outAudioTitleText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inAudioDurationText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outAudioDurationText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inAudioDurationSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outAudioDurationSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inAudioSeekbar), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outAudioSeekbar), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inAudioSeekbarSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outAudioSeekbarSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inAudioSeekbarFill), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outAudioSeekbarFill), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inVoiceSeekbar), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outVoiceSeekbar), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inVoiceSeekbarSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outVoiceSeekbarSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inVoiceSeekbarFill), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outVoiceSeekbarFill), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inFileProgress), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outFileProgress), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inFileProgressSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outFileProgressSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inFileNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outFileNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inFileInfoText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outFileInfoText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inFileInfoSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outFileInfoSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inFileBackground), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outFileBackground), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inFileBackgroundSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outFileBackgroundSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inVenueNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outVenueNameText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inVenueInfoText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outVenueInfoText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_inVenueInfoSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_outVenueInfoSelectedText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, null, null, Theme.key_chat_mediaInfoText), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, Theme.chat_urlPaint, null, null, Theme.key_chat_linkSelectBackground), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, Theme.chat_textSearchSelectionPaint, null, null, Theme.key_chat_textSelectBackground), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_fileStatesDrawable[0][0], Theme.chat_fileStatesDrawable[1][0], Theme.chat_fileStatesDrawable[2][0], Theme.chat_fileStatesDrawable[3][0], Theme.chat_fileStatesDrawable[4][0]}, null, Theme.key_chat_outLoader), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_fileStatesDrawable[0][0], Theme.chat_fileStatesDrawable[1][0], Theme.chat_fileStatesDrawable[2][0], Theme.chat_fileStatesDrawable[3][0], Theme.chat_fileStatesDrawable[4][0]}, null, Theme.key_chat_outBubble), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_fileStatesDrawable[0][1], Theme.chat_fileStatesDrawable[1][1], Theme.chat_fileStatesDrawable[2][1], Theme.chat_fileStatesDrawable[3][1], Theme.chat_fileStatesDrawable[4][1]}, null, Theme.key_chat_outLoaderSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_fileStatesDrawable[0][1], Theme.chat_fileStatesDrawable[1][1], Theme.chat_fileStatesDrawable[2][1], Theme.chat_fileStatesDrawable[3][1], Theme.chat_fileStatesDrawable[4][1]}, null, Theme.key_chat_outBubbleSelected), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_fileStatesDrawable[5][0], Theme.chat_fileStatesDrawable[6][0], Theme.chat_fileStatesDrawable[7][0], Theme.chat_fileStatesDrawable[8][0], Theme.chat_fileStatesDrawable[9][0]}, null, Theme.key_chat_inLoader), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_fileStatesDrawable[5][0], Theme.chat_fileStatesDrawable[6][0], Theme.chat_fileStatesDrawable[7][0], Theme.chat_fileStatesDrawable[8][0], Theme.chat_fileStatesDrawable[9][0]}, null, Theme.key_chat_inBubble), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_fileStatesDrawable[5][1], Theme.chat_fileStatesDrawable[6][1], Theme.chat_fileStatesDrawable[7][1], Theme.chat_fileStatesDrawable[8][1], Theme.chat_fileStatesDrawable[9][1]}, null, Theme.key_chat_inLoaderSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_fileStatesDrawable[5][1], Theme.chat_fileStatesDrawable[6][1], Theme.chat_fileStatesDrawable[7][1], Theme.chat_fileStatesDrawable[8][1], Theme.chat_fileStatesDrawable[9][1]}, null, Theme.key_chat_inBubbleSelected), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[0][0], Theme.chat_photoStatesDrawables[1][0], Theme.chat_photoStatesDrawables[2][0], Theme.chat_photoStatesDrawables[3][0]}, null, Theme.key_chat_mediaLoaderPhoto), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[0][0], Theme.chat_photoStatesDrawables[1][0], Theme.chat_photoStatesDrawables[2][0], Theme.chat_photoStatesDrawables[3][0]}, null, Theme.key_chat_mediaLoaderPhotoIcon), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[0][1], Theme.chat_photoStatesDrawables[1][1], Theme.chat_photoStatesDrawables[2][1], Theme.chat_photoStatesDrawables[3][1]}, null, Theme.key_chat_mediaLoaderPhotoSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[0][1], Theme.chat_photoStatesDrawables[1][1], Theme.chat_photoStatesDrawables[2][1], Theme.chat_photoStatesDrawables[3][1]}, null, Theme.key_chat_mediaLoaderPhotoIconSelected), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[7][0], Theme.chat_photoStatesDrawables[8][0]}, null, Theme.key_chat_outLoaderPhoto), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[7][0], Theme.chat_photoStatesDrawables[8][0]}, null, Theme.key_chat_outLoaderPhotoIcon), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[7][1], Theme.chat_photoStatesDrawables[8][1]}, null, Theme.key_chat_outLoaderPhotoSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[7][1], Theme.chat_photoStatesDrawables[8][1]}, null, Theme.key_chat_outLoaderPhotoIconSelected), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[10][0], Theme.chat_photoStatesDrawables[11][0]}, null, Theme.key_chat_inLoaderPhoto), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[10][0], Theme.chat_photoStatesDrawables[11][0]}, null, Theme.key_chat_inLoaderPhotoIcon), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[10][1], Theme.chat_photoStatesDrawables[11][1]}, null, Theme.key_chat_inLoaderPhotoSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[10][1], Theme.chat_photoStatesDrawables[11][1]}, null, Theme.key_chat_inLoaderPhotoIconSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[9][0]}, null, Theme.key_chat_outFileIcon), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[9][1]}, null, Theme.key_chat_outFileSelectedIcon), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[12][0]}, null, Theme.key_chat_inFileIcon), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_photoStatesDrawables[12][1]}, null, Theme.key_chat_inFileSelectedIcon), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_contactDrawable[0]}, null, Theme.key_chat_inContactBackground), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_contactDrawable[0]}, null, Theme.key_chat_inContactIcon), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_contactDrawable[1]}, null, Theme.key_chat_outContactBackground), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_contactDrawable[1]}, null, Theme.key_chat_outContactIcon), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_locationDrawable[0]}, null, Theme.key_chat_inLocationBackground), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_locationDrawable[0]}, null, Theme.key_chat_inLocationIcon), + new ThemeDescription(chatListView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_locationDrawable[1]}, null, Theme.key_chat_outLocationBackground), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_locationDrawable[1]}, null, Theme.key_chat_outLocationIcon), + + new ThemeDescription(bottomOverlayChat, 0, null, Theme.chat_composeBackgroundPaint, null, null, Theme.key_chat_messagePanelBackground), + new ThemeDescription(bottomOverlayChat, 0, null, null, new Drawable[]{Theme.chat_composeShadowDrawable}, null, Theme.key_chat_messagePanelShadow), + + new ThemeDescription(bottomOverlayChatText, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_chat_fieldOverlayText), + + new ThemeDescription(emptyView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_chat_serviceText), + + new ThemeDescription(progressBar, ThemeDescription.FLAG_PROGRESSBAR, null, null, null, null, Theme.key_chat_serviceText), + + new ThemeDescription(chatListView, ThemeDescription.FLAG_USEBACKGROUNDDRAWABLE, new Class[]{ChatUnreadCell.class}, new String[]{"backgroundLayout"}, null, null, null, Theme.key_chat_unreadMessagesStartBackground), + new ThemeDescription(chatListView, ThemeDescription.FLAG_IMAGECOLOR, new Class[]{ChatUnreadCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_chat_unreadMessagesStartArrowIcon), + new ThemeDescription(chatListView, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{ChatUnreadCell.class}, new String[]{"textView"}, null, null, null, Theme.key_chat_unreadMessagesStartText), + + new ThemeDescription(progressView2, ThemeDescription.FLAG_SERVICEBACKGROUND, null, null, null, null, Theme.key_chat_serviceBackground), + new ThemeDescription(emptyView, ThemeDescription.FLAG_SERVICEBACKGROUND, null, null, null, null, Theme.key_chat_serviceBackground), + + new ThemeDescription(chatListView, ThemeDescription.FLAG_SERVICEBACKGROUND, new Class[]{ChatLoadingCell.class}, new String[]{"textView"}, null, null, null, Theme.key_chat_serviceBackground), + new ThemeDescription(chatListView, ThemeDescription.FLAG_PROGRESSBAR, new Class[]{ChatLoadingCell.class}, new String[]{"textView"}, null, null, null, Theme.key_chat_serviceText), + + new ThemeDescription(avatarContainer.getTimeItem(), 0, null, null, null, null, Theme.key_chat_secretTimerBackground), + new ThemeDescription(avatarContainer.getTimeItem(), 0, null, null, null, null, Theme.key_chat_secretTimerText), + }; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java index 04866b684..5ebd39ebd 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java @@ -10,27 +10,19 @@ package org.telegram.ui; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.os.Vibrator; -import android.text.Editable; -import android.text.InputFilter; -import android.text.InputType; -import android.text.TextWatcher; -import android.util.TypedValue; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; import android.view.Gravity; -import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.ScrollView; -import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ChatObject; import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessagesController; @@ -38,78 +30,82 @@ import org.telegram.messenger.MessagesStorage; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; import org.telegram.messenger.UserConfig; +import org.telegram.messenger.UserObject; +import org.telegram.messenger.support.widget.LinearLayoutManager; +import org.telegram.messenger.support.widget.RecyclerView; +import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.RequestDelegate; +import org.telegram.tgnet.TLObject; 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.Theme; import org.telegram.ui.ActionBar.ThemeDescription; +import org.telegram.ui.Adapters.SearchAdapterHelper; +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.TextCheckCell; import org.telegram.ui.Cells.TextInfoPrivacyCell; -import org.telegram.ui.Cells.TextSettingsCell; -import org.telegram.ui.Components.AvatarDrawable; -import org.telegram.ui.Components.AvatarUpdater; -import org.telegram.ui.Components.BackupImageView; +import org.telegram.ui.Components.EmptyTextProgressView; import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.RecyclerListView; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.Semaphore; -public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.AvatarUpdaterDelegate, NotificationCenter.NotificationCenterDelegate { +public class ChannelEditActivity extends BaseFragment implements NotificationCenter.NotificationCenterDelegate { - private View doneButton; - private EditText nameTextView; - private EditText descriptionTextView; - private BackupImageView avatarImage; - private AvatarDrawable avatarDrawable; - private AvatarUpdater avatarUpdater; - private AlertDialog progressDialog; - private TextSettingsCell typeCell; - private TextSettingsCell adminCell; - private LinearLayout linearLayout2; - private LinearLayout linearLayout3; - private View lineView; - private View lineView2; - private FrameLayout container1; - private FrameLayout container2; - private FrameLayout container3; - private ShadowSectionCell sectionCell; - private ShadowSectionCell sectionCell2; - private TextCheckCell textCheckCell; - private TextInfoPrivacyCell infoCell; - private TextSettingsCell textCell; - private TextInfoPrivacyCell infoCell2; + private RecyclerListView listView; + private ListAdapter listViewAdapter; + private SearchAdapter searchListViewAdapter; + private int chat_id; + + private boolean loadingUsers; + private HashMap participantsMap = new HashMap<>(); + private boolean usersEndReached; - private TLRPC.FileLocation avatar; - private TLRPC.Chat currentChat; private TLRPC.ChatFull info; - private int chatId; - private TLRPC.InputFile uploadedAvatar; - private boolean signMessages; + private ArrayList sortedUsers; - private boolean createAfterUpload; - private boolean donePressed; + private TLRPC.Chat currentChat; - private final static int done_button = 1; + private final static int search_button = 1; + + private boolean searchWas; + private boolean searching; + + private int infoRow; + private int eventLogRow; + private int blockedUsersRow; + private int managementRow; + private int membersSectionRow; + private int membersStartRow; + private int membersEndRow; + private int membersSection2Row; + private int loadMoreMembersRow; + private int rowCount = 0; public ChannelEditActivity(Bundle args) { super(args); - avatarDrawable = new AvatarDrawable(); - avatarUpdater = new AvatarUpdater(); - chatId = args.getInt("chat_id", 0); } - @SuppressWarnings("unchecked") @Override public boolean onFragmentCreate() { - currentChat = MessagesController.getInstance().getChat(chatId); + chat_id = getArguments().getInt("chat_id", 0); + currentChat = MessagesController.getInstance().getChat(chat_id); if (currentChat == null) { final Semaphore semaphore = new Semaphore(0); MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override public void run() { - currentChat = MessagesStorage.getInstance().getChat(chatId); + currentChat = MessagesStorage.getInstance().getChat(chat_id); semaphore.release(); } }); @@ -123,501 +119,690 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A } else { return false; } - if (info == null) { - MessagesStorage.getInstance().loadChatInfo(chatId, semaphore, false, false); - try { - semaphore.acquire(); - } catch (Exception e) { - FileLog.e(e); - } - if (info == null) { - return false; - } - } } - avatarUpdater.parentFragment = this; - avatarUpdater.delegate = this; - signMessages = currentChat.signatures; + + getChannelParticipants(true); NotificationCenter.getInstance().addObserver(this, NotificationCenter.chatInfoDidLoaded); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.updateInterfaces); - return super.onFragmentCreate(); + + sortedUsers = new ArrayList<>(); + updateRowsIds(); + + return true; } @Override public void onFragmentDestroy() { super.onFragmentDestroy(); - if (avatarUpdater != null) { - avatarUpdater.clear(); - } NotificationCenter.getInstance().removeObserver(this, NotificationCenter.chatInfoDidLoaded); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.updateInterfaces); - AndroidUtilities.removeAdjustResize(getParentActivity(), classGuid); + } + + @Override + public View createView(Context context) { + Theme.createProfileResources(context); + + searching = false; + searchWas = false; + + actionBar.setBackButtonImage(R.drawable.ic_ab_back); + actionBar.setAllowOverlayTitle(true); + if (currentChat.megagroup) { + actionBar.setTitle(LocaleController.getString("ManageGroup", R.string.ManageGroup)); + } else { + actionBar.setTitle(LocaleController.getString("ManageChannel", R.string.ManageChannel)); + } + actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { + @Override + public void onItemClick(final int id) { + if (getParentActivity() == null) { + return; + } + if (id == -1) { + finishFragment(); + } + } + }); + + searchListViewAdapter = new SearchAdapter(context); + ActionBarMenu menu = actionBar.createMenu(); + ActionBarMenuItem searchItem = menu.addItem(search_button, R.drawable.ic_ab_search).setIsSearchField(true).setActionBarMenuItemSearchListener(new ActionBarMenuItem.ActionBarMenuItemSearchListener() { + @Override + public void onSearchExpand() { + searching = true; + } + + @Override + public void onSearchCollapse() { + searchListViewAdapter.searchDialogs(null); + searching = false; + searchWas = false; + listView.setAdapter(listViewAdapter); + listViewAdapter.notifyDataSetChanged(); + listView.setFastScrollVisible(true); + listView.setVerticalScrollBarEnabled(false); + } + + @Override + public void onTextChanged(EditText editText) { + if (searchListViewAdapter == null) { + return; + } + String text = editText.getText().toString(); + if (text.length() != 0) { + searchWas = true; + if (listView != null) { + listView.setAdapter(searchListViewAdapter); + searchListViewAdapter.notifyDataSetChanged(); + listView.setFastScrollVisible(false); + listView.setVerticalScrollBarEnabled(true); + } + } + searchListViewAdapter.searchDialogs(text); + } + }); + searchItem.getSearchField().setHint(LocaleController.getString("Search", R.string.Search)); + + listViewAdapter = new ListAdapter(context); + fragmentView = new FrameLayout(context); + fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); + FrameLayout frameLayout = (FrameLayout) fragmentView; + + EmptyTextProgressView emptyView = new EmptyTextProgressView(context); + emptyView.setShowAtCenter(true); + emptyView.setText(LocaleController.getString("NoResult", R.string.NoResult)); + emptyView.showTextView(); + frameLayout.addView(emptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + listView = new RecyclerListView(context) { + @Override + public boolean hasOverlappingRendering() { + return false; + } + }; + listView.setVerticalScrollBarEnabled(false); + listView.setEmptyView(emptyView); + listView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); + frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); + + listView.setAdapter(listViewAdapter); + listView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { + @Override + public void onItemClick(View view, final int position) { + if (getParentActivity() == null) { + return; + } + if (listView.getAdapter() == searchListViewAdapter) { + Bundle args = new Bundle(); + args.putInt("user_id", searchListViewAdapter.getItem(position).user_id); + presentFragment(new ProfileActivity(args)); + } else { + if (position >= membersStartRow && position < membersEndRow) { + int user_id; + if (!sortedUsers.isEmpty()) { + user_id = info.participants.participants.get(sortedUsers.get(position - membersStartRow)).user_id; + } else { + user_id = info.participants.participants.get(position - membersStartRow).user_id; + } + Bundle args = new Bundle(); + args.putInt("user_id", user_id); + presentFragment(new ProfileActivity(args)); + } else if (position == blockedUsersRow || position == managementRow) { + Bundle args = new Bundle(); + args.putInt("chat_id", chat_id); + if (position == blockedUsersRow) { + args.putInt("type", 0); + } else if (position == managementRow) { + args.putInt("type", 1); + } + presentFragment(new ChannelUsersActivity(args)); + } else if (position == eventLogRow) { + presentFragment(new ChannelAdminLogActivity(currentChat)); + } else if (position == infoRow) { + Bundle args = new Bundle(); + args.putInt("chat_id", chat_id); + ChannelEditInfoActivity fragment = new ChannelEditInfoActivity(args); + fragment.setInfo(info); + presentFragment(fragment); + } + } + } + }); + + listView.setOnItemLongClickListener(new RecyclerListView.OnItemLongClickListener() { + @Override + public boolean onItemClick(View view, int position) { + if (position >= membersStartRow && position < membersEndRow) { + if (getParentActivity() == null) { + return false; + } + final TLRPC.TL_chatChannelParticipant user; + if (!sortedUsers.isEmpty()) { + user = (TLRPC.TL_chatChannelParticipant) info.participants.participants.get(sortedUsers.get(position - membersStartRow)); + } else { + user = (TLRPC.TL_chatChannelParticipant) info.participants.participants.get(position - membersStartRow); + } + return createMenuForParticipant(user, null, false); + } + return false; + } + }); + + return fragmentView; + } + + @SuppressWarnings("unchecked") + @Override + public void didReceivedNotification(int id, final Object... args) { + if (id == NotificationCenter.chatInfoDidLoaded) { + TLRPC.ChatFull chatFull = (TLRPC.ChatFull) args[0]; + if (chatFull.id == chat_id) { + boolean byChannelUsers = (Boolean) args[2]; + if (info instanceof TLRPC.TL_channelFull) { + if (chatFull.participants == null && info != null) { + chatFull.participants = info.participants; + } + } + boolean loadChannelParticipants = info == null && chatFull instanceof TLRPC.TL_channelFull; + info = chatFull; + fetchUsersFromChannelInfo(); + updateRowsIds(); + if (listViewAdapter != null) { + listViewAdapter.notifyDataSetChanged(); + } + TLRPC.Chat newChat = MessagesController.getInstance().getChat(chat_id); + if (newChat != null) { + currentChat = newChat; + } + if (loadChannelParticipants || !byChannelUsers) { + getChannelParticipants(true); + } + } + } } @Override public void onResume() { super.onResume(); - AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); - } - - @Override - public View createView(Context context) { - actionBar.setBackButtonImage(R.drawable.ic_ab_back); - actionBar.setAllowOverlayTitle(true); - - actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { - @Override - public void onItemClick(int id) { - if (id == -1) { - finishFragment(); - } else if (id == done_button) { - if (donePressed) { - return; - } - if (nameTextView.length() == 0) { - Vibrator v = (Vibrator) getParentActivity().getSystemService(Context.VIBRATOR_SERVICE); - if (v != null) { - v.vibrate(200); - } - AndroidUtilities.shakeView(nameTextView, 2, 0); - return; - } - donePressed = true; - - if (avatarUpdater.uploadingAvatar != null) { - createAfterUpload = true; - progressDialog = new AlertDialog(getParentActivity(), 1); - progressDialog.setMessage(LocaleController.getString("Loading", R.string.Loading)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.setCancelable(false); - progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, LocaleController.getString("Cancel", R.string.Cancel), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - createAfterUpload = false; - progressDialog = null; - donePressed = false; - try { - dialog.dismiss(); - } catch (Exception e) { - FileLog.e(e); - } - } - }); - progressDialog.show(); - return; - } - if (!currentChat.title.equals(nameTextView.getText().toString())) { - MessagesController.getInstance().changeChatTitle(chatId, nameTextView.getText().toString()); - } - if (info != null && !info.about.equals(descriptionTextView.getText().toString())) { - MessagesController.getInstance().updateChannelAbout(chatId, descriptionTextView.getText().toString(), info); - } - if (signMessages != currentChat.signatures) { - currentChat.signatures = true; - MessagesController.getInstance().toogleChannelSignatures(chatId, signMessages); - } - if (uploadedAvatar != null) { - MessagesController.getInstance().changeChatAvatar(chatId, uploadedAvatar); - } else if (avatar == null && currentChat.photo instanceof TLRPC.TL_chatPhoto) { - MessagesController.getInstance().changeChatAvatar(chatId, null); - } - finishFragment(); - } - } - }); - - ActionBarMenu menu = actionBar.createMenu(); - doneButton = menu.addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); - - LinearLayout linearLayout; - - fragmentView = new ScrollView(context); - fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); - ScrollView scrollView = (ScrollView) fragmentView; - scrollView.setFillViewport(true); - linearLayout = new LinearLayout(context); - scrollView.addView(linearLayout, new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - - linearLayout.setOrientation(LinearLayout.VERTICAL); - - actionBar.setTitle(LocaleController.getString("ChannelEdit", R.string.ChannelEdit)); - - linearLayout2 = new LinearLayout(context); - linearLayout2.setOrientation(LinearLayout.VERTICAL); - linearLayout2.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - linearLayout.addView(linearLayout2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - FrameLayout frameLayout = new FrameLayout(context); - linearLayout2.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - avatarImage = new BackupImageView(context); - avatarImage.setRoundRadius(AndroidUtilities.dp(32)); - avatarDrawable.setInfo(5, null, null, false); - avatarDrawable.setDrawPhoto(true); - 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)); - avatarImage.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (getParentActivity() == null) { - return; - } - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - - CharSequence[] items; - - if (avatar != null) { - items = new CharSequence[]{LocaleController.getString("FromCamera", R.string.FromCamera), LocaleController.getString("FromGalley", R.string.FromGalley), LocaleController.getString("DeletePhoto", R.string.DeletePhoto)}; - } else { - items = new CharSequence[]{LocaleController.getString("FromCamera", R.string.FromCamera), LocaleController.getString("FromGalley", R.string.FromGalley)}; - } - - builder.setItems(items, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - if (i == 0) { - avatarUpdater.openCamera(); - } else if (i == 1) { - avatarUpdater.openGallery(); - } else if (i == 2) { - avatar = null; - uploadedAvatar = null; - avatarImage.setImage(avatar, "50_50", avatarDrawable); - } - } - }); - showDialog(builder.create()); - } - }); - - nameTextView = new EditText(context); - if (currentChat.megagroup) { - nameTextView.setHint(LocaleController.getString("GroupName", R.string.GroupName)); - } else { - nameTextView.setHint(LocaleController.getString("EnterChannelName", R.string.EnterChannelName)); - } - nameTextView.setMaxLines(4); - nameTextView.setGravity(Gravity.CENTER_VERTICAL | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT)); - nameTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); - nameTextView.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); - nameTextView.setBackgroundDrawable(Theme.createEditTextDrawable(context, false)); - nameTextView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); - nameTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); - nameTextView.setPadding(0, 0, 0, AndroidUtilities.dp(8)); - InputFilter[] inputFilters = new InputFilter[1]; - inputFilters[0] = new InputFilter.LengthFilter(100); - nameTextView.setFilters(inputFilters); - AndroidUtilities.clearCursorDrawable(nameTextView); - nameTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); - frameLayout.addView(nameTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_VERTICAL, LocaleController.isRTL ? 16 : 96, 0, LocaleController.isRTL ? 96 : 16, 0)); - nameTextView.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.length() > 0 ? nameTextView.getText().toString() : null, null, false); - avatarImage.invalidate(); - } - }); - - lineView = new View(context); - lineView.setBackgroundColor(Theme.getColor(Theme.key_divider)); - linearLayout.addView(lineView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); - - linearLayout3 = new LinearLayout(context); - linearLayout3.setOrientation(LinearLayout.VERTICAL); - linearLayout3.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - linearLayout.addView(linearLayout3, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - descriptionTextView = new EditText(context); - descriptionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); - descriptionTextView.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); - descriptionTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); - descriptionTextView.setPadding(0, 0, 0, AndroidUtilities.dp(6)); - descriptionTextView.setBackgroundDrawable(null); - descriptionTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); - descriptionTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); - descriptionTextView.setImeOptions(EditorInfo.IME_ACTION_DONE); - inputFilters = new InputFilter[1]; - inputFilters[0] = new InputFilter.LengthFilter(255); - descriptionTextView.setFilters(inputFilters); - descriptionTextView.setHint(LocaleController.getString("DescriptionOptionalPlaceholder", R.string.DescriptionOptionalPlaceholder)); - AndroidUtilities.clearCursorDrawable(descriptionTextView); - linearLayout3.addView(descriptionTextView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 17, 12, 17, 6)); - descriptionTextView.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) { - if (i == EditorInfo.IME_ACTION_DONE && doneButton != null) { - doneButton.performClick(); - return true; - } - return false; - } - }); - descriptionTextView.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - - } - - @Override - public void afterTextChanged(Editable editable) { - - } - }); - - sectionCell = new ShadowSectionCell(context); - sectionCell.setSize(20); - linearLayout.addView(sectionCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - container1 = new FrameLayout(context); - container1.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - linearLayout.addView(container1, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - typeCell = new TextSettingsCell(context); - updateTypeCell(); - typeCell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); - container1.addView(typeCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - lineView2 = new View(context); - lineView2.setBackgroundColor(Theme.getColor(Theme.key_divider)); - linearLayout.addView(lineView2, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); - - container2 = new FrameLayout(context); - container2.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - linearLayout.addView(container2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - if (!currentChat.megagroup) { - textCheckCell = new TextCheckCell(context); - textCheckCell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); - textCheckCell.setTextAndCheck(LocaleController.getString("ChannelSignMessages", R.string.ChannelSignMessages), signMessages, false); - container2.addView(textCheckCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - textCheckCell.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - signMessages = !signMessages; - ((TextCheckCell) v).setChecked(signMessages); - } - }); - - infoCell = new TextInfoPrivacyCell(context); - infoCell.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); - infoCell.setText(LocaleController.getString("ChannelSignMessagesInfo", R.string.ChannelSignMessagesInfo)); - linearLayout.addView(infoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - } else { - adminCell = new TextSettingsCell(context); - updateAdminCell(); - adminCell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); - container2.addView(adminCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - adminCell.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Bundle args = new Bundle(); - args.putInt("chat_id", chatId); - args.putInt("type", 1); - presentFragment(new ChannelUsersActivity(args)); - } - }); - - sectionCell2 = new ShadowSectionCell(context); - sectionCell2.setSize(20); - linearLayout.addView(sectionCell2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - if (!currentChat.creator) { - sectionCell2.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); - } - } - - if (currentChat.creator) { - container3 = new FrameLayout(context); - container3.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - linearLayout.addView(container3, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - textCell = new TextSettingsCell(context); - textCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText5)); - textCell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); - if (currentChat.megagroup) { - textCell.setText(LocaleController.getString("DeleteMega", R.string.DeleteMega), false); - } else { - textCell.setText(LocaleController.getString("ChannelDelete", R.string.ChannelDelete), false); - } - container3.addView(textCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - textCell.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - if (currentChat.megagroup) { - builder.setMessage(LocaleController.getString("MegaDeleteAlert", R.string.MegaDeleteAlert)); - } else { - builder.setMessage(LocaleController.getString("ChannelDeleteAlert", R.string.ChannelDeleteAlert)); - } - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); - builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.closeChats); - if (AndroidUtilities.isTablet()) { - NotificationCenter.getInstance().postNotificationName(NotificationCenter.closeChats, -(long) chatId); - } else { - NotificationCenter.getInstance().postNotificationName(NotificationCenter.closeChats); - } - MessagesController.getInstance().deleteUserFromChat(chatId, MessagesController.getInstance().getUser(UserConfig.getClientUserId()), info); - finishFragment(); - } - }); - builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); - showDialog(builder.create()); - } - }); - - infoCell2 = new TextInfoPrivacyCell(context); - infoCell2.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); - if (currentChat.megagroup) { - infoCell2.setText(LocaleController.getString("MegaDeleteInfo", R.string.MegaDeleteInfo)); - } else { - infoCell2.setText(LocaleController.getString("ChannelDeleteInfo", R.string.ChannelDeleteInfo)); - } - linearLayout.addView(infoCell2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - } - - nameTextView.setText(currentChat.title); - nameTextView.setSelection(nameTextView.length()); - if (info != null) { - descriptionTextView.setText(info.about); - } - if (currentChat.photo != null) { - avatar = currentChat.photo.photo_small; - avatarImage.setImage(avatar, "50_50", avatarDrawable); - } else { - avatarImage.setImageDrawable(avatarDrawable); - } - - return fragmentView; - } - - @Override - public void didReceivedNotification(int id, Object... args) { - if (id == NotificationCenter.chatInfoDidLoaded) { - TLRPC.ChatFull chatFull = (TLRPC.ChatFull) args[0]; - if (chatFull.id == chatId) { - if (info == null) { - descriptionTextView.setText(chatFull.about); - } - info = chatFull; - updateAdminCell(); - updateTypeCell(); - } - } else if (id == NotificationCenter.updateInterfaces) { - int updateMask = (Integer) args[0]; - if ((updateMask & MessagesController.UPDATE_MASK_CHANNEL) != 0) { - updateTypeCell(); - } + if (listViewAdapter != null) { + listViewAdapter.notifyDataSetChanged(); } } - @Override - public void didUploadedPhoto(final TLRPC.InputFile file, final TLRPC.PhotoSize small, final TLRPC.PhotoSize big) { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - uploadedAvatar = file; - avatar = small.location; - avatarImage.setImage(avatar, "50_50", avatarDrawable); - if (createAfterUpload) { - try { - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - progressDialog = null; - } - } catch (Exception e) { - FileLog.e(e); - } - doneButton.performClick(); - } - } - }); - } - - @Override - public void onActivityResultFragment(int requestCode, int resultCode, Intent data) { - avatarUpdater.onActivityResult(requestCode, resultCode, data); - } - - @Override - public void saveSelfArgs(Bundle args) { - if (avatarUpdater != null && avatarUpdater.currentPicturePath != null) { - args.putString("path", avatarUpdater.currentPicturePath); - } - if (nameTextView != null) { - String text = nameTextView.getText().toString(); - if (text != null && text.length() != 0) { - args.putString("nameTextView", text); - } - } - } - - @Override - public void restoreSelfArgs(Bundle args) { - if (avatarUpdater != null) { - avatarUpdater.currentPicturePath = args.getString("path"); - } - } - - public void setInfo(TLRPC.ChatFull chatFull) { - info = chatFull; - } - - private void updateTypeCell() { - String type = currentChat.username == null || currentChat.username.length() == 0 ? LocaleController.getString("ChannelTypePrivate", R.string.ChannelTypePrivate) : LocaleController.getString("ChannelTypePublic", R.string.ChannelTypePublic); - if (currentChat.megagroup) { - typeCell.setTextAndValue(LocaleController.getString("GroupType", R.string.GroupType), type, false); - } else { - typeCell.setTextAndValue(LocaleController.getString("ChannelType", R.string.ChannelType), type, false); - } - - if (currentChat.creator && (info == null || info.can_set_username)) { - typeCell.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Bundle args = new Bundle(); - args.putInt("chat_id", chatId); - ChannelEditTypeActivity fragment = new ChannelEditTypeActivity(args); - fragment.setInfo(info); - presentFragment(fragment); - } - }); - typeCell.getTextView().setTag(Theme.key_windowBackgroundWhiteBlackText); - typeCell.getValueTextView().setTag(Theme.key_windowBackgroundWhiteValueText); - typeCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); - typeCell.setTextValueColor(Theme.getColor(Theme.key_windowBackgroundWhiteValueText)); - } else { - typeCell.setOnClickListener(null); - typeCell.getTextView().setTag(Theme.key_windowBackgroundWhiteGrayText); - typeCell.getValueTextView().setTag(Theme.key_windowBackgroundWhiteGrayText); - typeCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText)); - typeCell.setTextValueColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText)); - } - } - - private void updateAdminCell() { - if (adminCell == null) { + private void getChannelParticipants(boolean reload) { + if (loadingUsers || participantsMap == null || info == null) { return; } - if (info != null) { - adminCell.setTextAndValue(LocaleController.getString("ChannelAdministrators", R.string.ChannelAdministrators), String.format("%d", info.admins_count), false); + loadingUsers = true; + final int delay = !participantsMap.isEmpty() && reload ? 300 : 0; + + final TLRPC.TL_channels_getParticipants req = new TLRPC.TL_channels_getParticipants(); + req.channel = MessagesController.getInputChannel(chat_id); + req.filter = new TLRPC.TL_channelParticipantsRecent(); + req.offset = reload ? 0 : participantsMap.size(); + req.limit = 200; + int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (error == null) { + TLRPC.TL_channels_channelParticipants res = (TLRPC.TL_channels_channelParticipants) response; + MessagesController.getInstance().putUsers(res.users, false); + if (res.users.size() != 200) { + usersEndReached = true; + } + if (req.offset == 0) { + participantsMap.clear(); + info.participants = new TLRPC.TL_chatParticipants(); + MessagesStorage.getInstance().putUsersAndChats(res.users, null, true, true); + MessagesStorage.getInstance().updateChannelUsers(chat_id, res.participants); + } + for (int a = 0; a < res.participants.size(); a++) { + TLRPC.TL_chatChannelParticipant participant = new TLRPC.TL_chatChannelParticipant(); + participant.channelParticipant = res.participants.get(a); + participant.inviter_id = participant.channelParticipant.inviter_id; + participant.user_id = participant.channelParticipant.user_id; + participant.date = participant.channelParticipant.date; + if (!participantsMap.containsKey(participant.user_id)) { + info.participants.participants.add(participant); + participantsMap.put(participant.user_id, participant); + } + } + } + loadingUsers = false; + NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatInfoDidLoaded, info, 0, true, null); + } + }, delay); + } + }); + ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid); + } + + public void setInfo(TLRPC.ChatFull chatInfo) { + info = chatInfo; + fetchUsersFromChannelInfo(); + } + + private void fetchUsersFromChannelInfo() { + if (info instanceof TLRPC.TL_channelFull && info.participants != null) { + for (int a = 0; a < info.participants.participants.size(); a++) { + TLRPC.ChatParticipant chatParticipant = info.participants.participants.get(a); + participantsMap.put(chatParticipant.user_id, chatParticipant); + } + } + } + + private void updateRowsIds() { + rowCount = 0; + if (ChatObject.canEditInfo(currentChat)) { + infoRow = rowCount++; } else { - adminCell.setText(LocaleController.getString("ChannelAdministrators", R.string.ChannelAdministrators), false); + infoRow = -1; + } + eventLogRow = rowCount++; + managementRow = rowCount++; + if (currentChat.megagroup) { + blockedUsersRow = rowCount++; + } else { + blockedUsersRow = -1; + } + membersSectionRow = rowCount++; + if (info != null && info.participants != null && !info.participants.participants.isEmpty()) { + membersStartRow = rowCount; + rowCount += info.participants.participants.size(); + membersEndRow = rowCount; + membersSection2Row = rowCount++; + if (!usersEndReached) { + loadMoreMembersRow = rowCount++; + } else { + loadMoreMembersRow = -1; + } + } else { + membersStartRow = -1; + membersEndRow = -1; + loadMoreMembersRow = -1; + membersSection2Row = -1; + } + + /* + if (!ChatObject.isNotInChat(currentChat) && !currentChat.megagroup && (currentChat.creator || currentChat.admin_rights != null && currentChat.admin_rights.add_admins)) { + managementRow = rowCount++; + } + if (!ChatObject.isNotInChat(currentChat) && currentChat.megagroup && (currentChat.creator || currentChat.admin_rights != null && currentChat.admin_rights.ban_users)) { + blockedUsersRow = rowCount++; + }*/ + } + + private boolean createMenuForParticipant(TLRPC.TL_chatChannelParticipant user, TLRPC.ChannelParticipant channelParticipant, boolean resultOnly) { + if (user == null && channelParticipant == null) { + return false; + } + int currentUserId = UserConfig.getClientUserId(); + final int uid; + if (channelParticipant != null) { + if (currentUserId == channelParticipant.user_id) { + return false; + } + uid = channelParticipant.user_id; + user = (TLRPC.TL_chatChannelParticipant) participantsMap.get(channelParticipant.user_id); + if (user != null) { + channelParticipant = user.channelParticipant; + } + } else { + if (user.user_id == UserConfig.getClientUserId()) { + return false; + } + uid = user.user_id; + channelParticipant = user.channelParticipant; + } + + + TLRPC.User u = MessagesController.getInstance().getUser(uid); + boolean allowSetAdmin = channelParticipant instanceof TLRPC.TL_channelParticipant || channelParticipant instanceof TLRPC.TL_channelParticipantBanned; + boolean canEditAdmin = !(channelParticipant instanceof TLRPC.TL_channelParticipantAdmin || channelParticipant instanceof TLRPC.TL_channelParticipantCreator) || channelParticipant.can_edit; + + ArrayList items; + final ArrayList actions; + if (resultOnly) { + items = null; + actions = null; + } else { + items = new ArrayList<>(); + actions = new ArrayList<>(); + } + if (allowSetAdmin && ChatObject.canAddAdmins(currentChat)) { + if (resultOnly) { + return true; + } + items.add(LocaleController.getString("SetAsAdmin", R.string.SetAsAdmin)); + actions.add(0); + } + if (ChatObject.canBlockUsers(currentChat) && canEditAdmin) { + if (resultOnly) { + return true; + } + if (currentChat.megagroup) { + items.add(LocaleController.getString("KickFromSupergroup", R.string.KickFromSupergroup)); + actions.add(1); + items.add(LocaleController.getString("KickFromGroup", R.string.KickFromGroup)); + actions.add(2); + } else { + items.add(LocaleController.getString("ChannelRemoveUser", R.string.ChannelRemoveUser)); + actions.add(2); + } + } + if (items == null || items.isEmpty()) { + return false; + } + final TLRPC.ChannelParticipant channelParticipantFinal = channelParticipant; + final TLRPC.TL_chatChannelParticipant userFinal = user; + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setItems(items.toArray(new CharSequence[items.size()]), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, final int i) { + if (actions.get(i) == 2) { + MessagesController.getInstance().deleteUserFromChat(chat_id, MessagesController.getInstance().getUser(uid), info); + } else { + ChannelRightsEditActivity fragment = new ChannelRightsEditActivity(channelParticipantFinal.user_id, chat_id, channelParticipantFinal.admin_rights, channelParticipantFinal.banned_rights, actions.get(i), true); + fragment.setDelegate(new ChannelRightsEditActivity.ChannelRightsEditActivityDelegate() { + @Override + public void didSetRights(int rights, TLRPC.TL_channelAdminRights rightsAdmin, TLRPC.TL_channelBannedRights rightsBanned) { + channelParticipantFinal.admin_rights = rightsAdmin; + channelParticipantFinal.banned_rights = rightsBanned; + if (actions.get(i) == 0) { + if (userFinal != null) { + if (rights == 1) { + userFinal.channelParticipant = new TLRPC.TL_channelParticipantAdmin(); + } else { + userFinal.channelParticipant = new TLRPC.TL_channelParticipant(); + } + userFinal.channelParticipant.inviter_id = UserConfig.getClientUserId(); + userFinal.channelParticipant.user_id = userFinal.user_id; + userFinal.channelParticipant.date = userFinal.date; + } + } else if (actions.get(i) == 1) { + if (rights == 0) { + if (currentChat.megagroup && info != null && info.participants != null) { + boolean changed = false; + for (int a = 0; a < info.participants.participants.size(); a++) { + TLRPC.ChannelParticipant p = ((TLRPC.TL_chatChannelParticipant) info.participants.participants.get(a)).channelParticipant; + if (p.user_id == uid) { + if (info != null) { + info.participants_count--; + } + info.participants.participants.remove(a); + changed = true; + break; + } + } + if (info != null && info.participants != null) { + for (int a = 0; a < info.participants.participants.size(); a++) { + TLRPC.ChatParticipant p = info.participants.participants.get(a); + if (p.user_id == uid) { + info.participants.participants.remove(a); + changed = true; + break; + } + } + } + if (changed) { + NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatInfoDidLoaded, info, 0, true, null); + } + } + } + } + } + }); + presentFragment(fragment); + } + } + }); + showDialog(builder.create()); + return true; + } + + private class SearchAdapter extends RecyclerListView.SelectionAdapter { + + private Context mContext; + private SearchAdapterHelper searchAdapterHelper; + private Timer searchTimer; + + public SearchAdapter(Context context) { + mContext = context; + searchAdapterHelper = new SearchAdapterHelper(); + searchAdapterHelper.setDelegate(new SearchAdapterHelper.SearchAdapterHelperDelegate() { + @Override + public void onDataSetChanged() { + notifyDataSetChanged(); + } + + @Override + public void onSetHashtags(ArrayList arrayList, HashMap hashMap) { + + } + }); + } + + public void searchDialogs(final String query) { + try { + if (searchTimer != null) { + searchTimer.cancel(); + } + } catch (Exception e) { + FileLog.e(e); + } + if (query == null) { + searchAdapterHelper.queryServerSearch(null, false, false, true, true, chat_id, false); + 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); + } + } + + private void processSearch(final String query) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + searchAdapterHelper.queryServerSearch(query, false, false, true, true, chat_id, false); + } + }); + } + + @Override + public boolean isEnabled(RecyclerView.ViewHolder holder) { + return holder.getItemViewType() != 1; + } + + @Override + public int getItemCount() { + return searchAdapterHelper.getGroupSearch().size(); + } + + public TLRPC.ChannelParticipant getItem(int i) { + return searchAdapterHelper.getGroupSearch().get(i); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = new ManageChatUserCell(mContext, 8, true); + view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + ((ManageChatUserCell) view).setDelegate(new ManageChatUserCell.ManageChatUserCellDelegate() { + @Override + public boolean onOptionsButtonCheck(ManageChatUserCell cell, boolean click) { + return createMenuForParticipant(null, getItem((Integer) cell.getTag()), !click); + } + }); + return new RecyclerListView.Holder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + switch (holder.getItemViewType()) { + case 0: { + TLObject object = getItem(position); + TLRPC.User user; + if (object instanceof TLRPC.User) { + user = (TLRPC.User) object; + } else { + user = MessagesController.getInstance().getUser(((TLRPC.ChannelParticipant) object).user_id); + } + String un = user.username; + CharSequence username = null; + CharSequence name = null; + + String nameSearch = searchAdapterHelper.getLastFoundChannel(); + + if (nameSearch != null) { + String u = UserObject.getUserName(user); + name = new SpannableStringBuilder(u); + int idx = u.toLowerCase().indexOf(nameSearch); + if (idx != -1) { + ((SpannableStringBuilder) name).setSpan(new ForegroundColorSpan(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText4)), idx, idx + nameSearch.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + ManageChatUserCell userCell = (ManageChatUserCell) holder.itemView; + userCell.setTag(position); + userCell.setData(user, name, username); + break; + } + } + } + + @Override + public int getItemViewType(int i) { + return 0; + } + } + + private class ListAdapter extends RecyclerListView.SelectionAdapter { + + private Context mContext; + + public ListAdapter(Context context) { + mContext = context; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = null; + switch (viewType) { + case 0: + view = new ManageChatTextCell(mContext); + view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + break; + case 1: + view = new ManageChatUserCell(mContext, 8, true); + view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + ((ManageChatUserCell) view).setDelegate(new ManageChatUserCell.ManageChatUserCellDelegate() { + @Override + public boolean onOptionsButtonCheck(ManageChatUserCell cell, boolean click) { + int i = (Integer) cell.getTag(); + TLRPC.ChatParticipant part; + if (!sortedUsers.isEmpty()) { + part = info.participants.participants.get(sortedUsers.get(i - membersStartRow)); + } else { + part = info.participants.participants.get(i - membersStartRow); + } + return createMenuForParticipant((TLRPC.TL_chatChannelParticipant) part, null, !click); + } + }); + break; + case 2: + view = new ShadowSectionCell(mContext); + break; + case 3: + view = new LoadingCell(mContext); + break; + } + view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)); + return new RecyclerListView.Holder(view); + } + + @Override + public void onViewRecycled(RecyclerView.ViewHolder holder) { + if (holder.itemView instanceof ManageChatUserCell) { + ((ManageChatUserCell) holder.itemView).recycle(); + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) { + boolean checkBackground = true; + switch (holder.getItemViewType()) { + case 0: + ManageChatTextCell textCell = (ManageChatTextCell) holder.itemView; + textCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + textCell.setTag(Theme.key_windowBackgroundWhiteBlackText); + + if (i == managementRow) { + textCell.setText(LocaleController.getString("ChannelAdministrators", R.string.ChannelAdministrators), info != null ? String.format("%d", info.admins_count) : null, R.drawable.group_admin, blockedUsersRow != -1); + } else if (i == blockedUsersRow) { + textCell.setText(LocaleController.getString("ChannelBlacklist", R.string.ChannelBlacklist), info != null ? String.format("%d", info.kicked_count + info.banned_count) : null, R.drawable.group_banned, false); + } else if (i == eventLogRow) { + textCell.setText(LocaleController.getString("EventLog", R.string.EventLog), null, R.drawable.group_log, true); + } else if (i == infoRow) { + textCell.setText(currentChat.megagroup ? LocaleController.getString("EventLogFilterGroupInfo", R.string.EventLogFilterGroupInfo) : LocaleController.getString("EventLogFilterChannelInfo", R.string.EventLogFilterChannelInfo), null, R.drawable.group_edit, true); + } + break; + case 1: + ManageChatUserCell userCell = ((ManageChatUserCell) holder.itemView); + userCell.setTag(i); + TLRPC.ChatParticipant part; + if (!sortedUsers.isEmpty()) { + part = info.participants.participants.get(sortedUsers.get(i - membersStartRow)); + } else { + part = info.participants.participants.get(i - membersStartRow); + } + if (part != null) { + userCell.setData(MessagesController.getInstance().getUser(part.user_id), null, null); + } + break; + case 2: + if (i == membersSectionRow && membersStartRow != -1) { + holder.itemView.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); + } else { + holder.itemView.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + } + break; + } + } + + @Override + public boolean isEnabled(RecyclerView.ViewHolder holder) { + int type = holder.getItemViewType(); + return type == 0 || type == 1; + } + + @Override + public int getItemCount() { + return rowCount; + } + + @Override + public int getItemViewType(int i) { + if (i == managementRow || i == blockedUsersRow || i == infoRow || i == eventLogRow) { + return 0; + } else if (i >= membersStartRow && i < membersEndRow) { + return 1; + } else if (i == membersSectionRow || i == membersSection2Row) { + return 2; + } else if (i == loadMoreMembersRow) { + return 3; + } + return 0; } } @@ -626,68 +811,53 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A ThemeDescription.ThemeDescriptionDelegate сellDelegate = new ThemeDescription.ThemeDescriptionDelegate() { @Override public void didSetColor(int color) { - if (avatarImage != null) { - avatarDrawable.setInfo(5, nameTextView.length() > 0 ? nameTextView.getText().toString() : null, null, false); - avatarImage.invalidate(); + int count = listView.getChildCount(); + for (int a = 0; a < count; a++) { + View child = listView.getChildAt(a); + if (child instanceof ManageChatUserCell) { + ((ManageChatUserCell) child).update(0); + } } } }; return new ThemeDescription[]{ - new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray), + new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SUBMENUBACKGROUND, null, null, null, null, Theme.key_actionBarDefaultSubmenuBackground), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SUBMENUITEM, null, null, null, null, Theme.key_actionBarDefaultSubmenuItem), - new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault), - new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon), - new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle), - new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector), + new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_avatar_backgroundActionBarBlue), + new ThemeDescription(listView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_avatar_backgroundActionBarBlue), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_avatar_actionBarSelectorBlue), - new ThemeDescription(nameTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(nameTextView, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText), - new ThemeDescription(nameTextView, ThemeDescription.FLAG_BACKGROUNDFILTER, null, null, null, null, Theme.key_windowBackgroundWhiteInputField), - new ThemeDescription(nameTextView, ThemeDescription.FLAG_BACKGROUNDFILTER | ThemeDescription.FLAG_DRAWABLESELECTEDSTATE, null, null, null, null, Theme.key_windowBackgroundWhiteInputFieldActivated), - new ThemeDescription(descriptionTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(descriptionTextView, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText), + new ThemeDescription(listView, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), + new ThemeDescription(listView, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider), + new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), - new ThemeDescription(linearLayout2, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), - new ThemeDescription(linearLayout3, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), - new ThemeDescription(container1, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), - new ThemeDescription(container2, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), - new ThemeDescription(container3, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(listView, ThemeDescription.FLAG_CHECKTAG, new Class[]{ManageChatTextCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(listView, ThemeDescription.FLAG_CHECKTAG, new Class[]{ManageChatTextCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGreenText2), + new ThemeDescription(listView, ThemeDescription.FLAG_CHECKTAG, new Class[]{ManageChatTextCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteRedText5), + new ThemeDescription(listView, 0, new Class[]{ManageChatTextCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayIcon), - new ThemeDescription(null, 0, null, null, new Drawable[]{Theme.avatar_photoDrawable, Theme.avatar_broadcastDrawable}, сellDelegate, Theme.key_avatar_text), + new ThemeDescription(listView, 0, new Class[]{ManageChatUserCell.class}, new String[]{"nameTextView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(listView, 0, new Class[]{ManageChatUserCell.class}, new String[]{"statusColor"}, null, null, сellDelegate, Theme.key_windowBackgroundWhiteGrayText), + new ThemeDescription(listView, 0, new Class[]{ManageChatUserCell.class}, new String[]{"statusOnlineColor"}, null, null, сellDelegate, Theme.key_windowBackgroundWhiteBlueText), + new ThemeDescription(listView, 0, new Class[]{ManageChatUserCell.class}, null, new Drawable[]{Theme.avatar_photoDrawable, Theme.avatar_broadcastDrawable}, null, Theme.key_avatar_text), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundRed), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundOrange), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundViolet), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundGreen), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundCyan), new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundBlue), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundPink), - new ThemeDescription(lineView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_divider), - new ThemeDescription(lineView2, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_divider), + new ThemeDescription(listView, 0, new Class[]{LoadingCell.class}, new String[]{"progressBar"}, null, null, null, Theme.key_progressCircle), - new ThemeDescription(sectionCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), + new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), + new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER | ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGray), - new ThemeDescription(typeCell, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), - new ThemeDescription(typeCell, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, new Class[]{TextSettingsCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(typeCell, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, new Class[]{TextSettingsCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteValueText), - new ThemeDescription(typeCell, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, new Class[]{TextSettingsCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText), - new ThemeDescription(typeCell, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, new Class[]{TextSettingsCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText), - - new ThemeDescription(textCheckCell, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), - new ThemeDescription(textCheckCell, 0, new Class[]{TextCheckCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(textCheckCell, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchThumb), - new ThemeDescription(textCheckCell, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchTrack), - new ThemeDescription(textCheckCell, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchThumbChecked), - new ThemeDescription(textCheckCell, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchTrackChecked), - - new ThemeDescription(infoCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), - new ThemeDescription(infoCell, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4), - - new ThemeDescription(adminCell, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), - new ThemeDescription(adminCell, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{TextSettingsCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(adminCell, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{TextSettingsCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteValueText), - - new ThemeDescription(sectionCell2, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), - - new ThemeDescription(textCell, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), - new ThemeDescription(textCell, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{TextSettingsCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteRedText5), - - new ThemeDescription(infoCell2, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), - new ThemeDescription(infoCell2, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4), + new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), + new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER | ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGray), + new ThemeDescription(listView, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4), }; } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditInfoActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditInfoActivity.java new file mode 100644 index 000000000..94fcc5ec9 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditInfoActivity.java @@ -0,0 +1,1188 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Vibrator; +import android.text.Editable; +import android.text.InputFilter; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.ChatObject; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MessagesController; +import org.telegram.messenger.MessagesStorage; +import org.telegram.messenger.NotificationCenter; +import org.telegram.messenger.R; +import org.telegram.messenger.UserConfig; +import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.RequestDelegate; +import org.telegram.tgnet.TLObject; +import org.telegram.tgnet.TLRPC; +import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.ActionBarMenu; +import org.telegram.ui.ActionBar.AlertDialog; +import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.ActionBar.ThemeDescription; +import org.telegram.ui.Cells.AdminedChannelCell; +import org.telegram.ui.Cells.HeaderCell; +import org.telegram.ui.Cells.LoadingCell; +import org.telegram.ui.Cells.RadioButtonCell; +import org.telegram.ui.Cells.ShadowSectionCell; +import org.telegram.ui.Cells.TextBlockCell; +import org.telegram.ui.Cells.TextCheckCell; +import org.telegram.ui.Cells.TextInfoPrivacyCell; +import org.telegram.ui.Cells.TextSettingsCell; +import org.telegram.ui.Components.AvatarDrawable; +import org.telegram.ui.Components.AvatarUpdater; +import org.telegram.ui.Components.BackupImageView; +import org.telegram.ui.Components.LayoutHelper; + +import java.util.ArrayList; +import java.util.concurrent.Semaphore; + +public class ChannelEditInfoActivity extends BaseFragment implements AvatarUpdater.AvatarUpdaterDelegate, NotificationCenter.NotificationCenterDelegate { + + private View doneButton; + private EditText nameTextView; + private EditText usernameTextView; + private EditText descriptionTextView; + private EditText editText; + private TextInfoPrivacyCell typeInfoCell; + private HeaderCell headerCell; + private TextView checkTextView; + private LinearLayout linearLayout; + private BackupImageView avatarImage; + private AvatarDrawable avatarDrawable; + private AvatarUpdater avatarUpdater; + private AlertDialog progressDialog; + private LinearLayout linearLayout2; + private LinearLayout linearLayout3; + private LinearLayout linearLayoutTypeContainer; + private RadioButtonCell radioButtonCell1; + private RadioButtonCell radioButtonCell2; + private LinearLayout adminnedChannelsLayout; + private LinearLayout linkContainer; + private LinearLayout publicContainer; + private TextBlockCell privateContainer; + private View lineView; + private View lineView2; + private View lineView3; + private FrameLayout container1; + private FrameLayout container2; + private FrameLayout container3; + private FrameLayout container4; + private ShadowSectionCell sectionCell; + private ShadowSectionCell sectionCell2; + private TextCheckCell textCheckCell; + private TextInfoPrivacyCell infoCell; + private TextSettingsCell textCell; + private TextInfoPrivacyCell infoCell2; + + private boolean isPrivate; + + private TLRPC.FileLocation avatar; + private TLRPC.Chat currentChat; + private TLRPC.ChatFull info; + private int chatId; + private TLRPC.InputFile uploadedAvatar; + private boolean signMessages; + + private boolean canCreatePublic = true; + private boolean loadingAdminedChannels; + private ShadowSectionCell adminedInfoCell; + private ArrayList adminedChannelCells = new ArrayList<>(); + private LoadingCell loadingAdminedCell; + + private int checkReqId; + private String lastCheckName; + private Runnable checkRunnable; + private boolean lastNameAvailable; + private boolean loadingInvite; + private TLRPC.ExportedChatInvite invite; + + private boolean createAfterUpload; + private boolean donePressed; + + private final static int done_button = 1; + + public ChannelEditInfoActivity(Bundle args) { + super(args); + avatarDrawable = new AvatarDrawable(); + avatarUpdater = new AvatarUpdater(); + chatId = args.getInt("chat_id", 0); + } + + @SuppressWarnings("unchecked") + @Override + public boolean onFragmentCreate() { + currentChat = MessagesController.getInstance().getChat(chatId); + if (currentChat == null) { + final Semaphore semaphore = new Semaphore(0); + MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { + @Override + public void run() { + currentChat = MessagesStorage.getInstance().getChat(chatId); + semaphore.release(); + } + }); + try { + semaphore.acquire(); + } catch (Exception e) { + FileLog.e(e); + } + if (currentChat != null) { + MessagesController.getInstance().putChat(currentChat, true); + } else { + return false; + } + if (info == null) { + MessagesStorage.getInstance().loadChatInfo(chatId, semaphore, false, false); + try { + semaphore.acquire(); + } catch (Exception e) { + FileLog.e(e); + } + if (info == null) { + return false; + } + } + } + isPrivate = currentChat.username == null || currentChat.username.length() == 0; + if (isPrivate && currentChat.creator) { + TLRPC.TL_channels_checkUsername req = new TLRPC.TL_channels_checkUsername(); + req.username = "1"; + req.channel = new TLRPC.TL_inputChannelEmpty(); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + canCreatePublic = error == null || !error.text.equals("CHANNELS_ADMIN_PUBLIC_TOO_MUCH"); + if (!canCreatePublic) { + loadAdminedChannels(); + } + } + }); + } + }); + } + avatarUpdater.parentFragment = this; + avatarUpdater.delegate = this; + signMessages = currentChat.signatures; + NotificationCenter.getInstance().addObserver(this, NotificationCenter.chatInfoDidLoaded); + return super.onFragmentCreate(); + } + + @Override + public void onFragmentDestroy() { + super.onFragmentDestroy(); + if (avatarUpdater != null) { + avatarUpdater.clear(); + } + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.chatInfoDidLoaded); + AndroidUtilities.removeAdjustResize(getParentActivity(), classGuid); + } + + @Override + public void onResume() { + super.onResume(); + AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); + } + + @Override + public View createView(Context context) { + actionBar.setBackButtonImage(R.drawable.ic_ab_back); + actionBar.setAllowOverlayTitle(true); + + actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { + @Override + public void onItemClick(int id) { + if (id == -1) { + finishFragment(); + } else if (id == done_button) { + if (donePressed) { + return; + } + if (nameTextView.length() == 0) { + Vibrator v = (Vibrator) getParentActivity().getSystemService(Context.VIBRATOR_SERVICE); + if (v != null) { + v.vibrate(200); + } + AndroidUtilities.shakeView(nameTextView, 2, 0); + return; + } + if (usernameTextView != null) { + if (!isPrivate && ((currentChat.username == null && usernameTextView.length() != 0) || (currentChat.username != null && !currentChat.username.equalsIgnoreCase(usernameTextView.getText().toString())))) { + if (nameTextView.length() != 0 && !lastNameAvailable) { + Vibrator v = (Vibrator) getParentActivity().getSystemService(Context.VIBRATOR_SERVICE); + if (v != null) { + v.vibrate(200); + } + AndroidUtilities.shakeView(checkTextView, 2, 0); + return; + } + } + } + donePressed = true; + + if (avatarUpdater.uploadingAvatar != null) { + createAfterUpload = true; + progressDialog = new AlertDialog(getParentActivity(), 1); + progressDialog.setMessage(LocaleController.getString("Loading", R.string.Loading)); + progressDialog.setCanceledOnTouchOutside(false); + progressDialog.setCancelable(false); + progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, LocaleController.getString("Cancel", R.string.Cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + createAfterUpload = false; + progressDialog = null; + donePressed = false; + try { + dialog.dismiss(); + } catch (Exception e) { + FileLog.e(e); + } + } + }); + progressDialog.show(); + return; + } + if (usernameTextView != null) { + String oldUserName = currentChat.username != null ? currentChat.username : ""; + String newUserName = isPrivate ? "" : usernameTextView.getText().toString(); + if (!oldUserName.equals(newUserName)) { + MessagesController.getInstance().updateChannelUserName(chatId, newUserName); + } + } + if (!currentChat.title.equals(nameTextView.getText().toString())) { + MessagesController.getInstance().changeChatTitle(chatId, nameTextView.getText().toString()); + } + if (info != null && !info.about.equals(descriptionTextView.getText().toString())) { + MessagesController.getInstance().updateChannelAbout(chatId, descriptionTextView.getText().toString(), info); + } + if (signMessages != currentChat.signatures) { + currentChat.signatures = true; + MessagesController.getInstance().toogleChannelSignatures(chatId, signMessages); + } + if (uploadedAvatar != null) { + MessagesController.getInstance().changeChatAvatar(chatId, uploadedAvatar); + } else if (avatar == null && currentChat.photo instanceof TLRPC.TL_chatPhoto) { + MessagesController.getInstance().changeChatAvatar(chatId, null); + } + finishFragment(); + } + } + }); + + ActionBarMenu menu = actionBar.createMenu(); + doneButton = menu.addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); + + fragmentView = new ScrollView(context); + fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); + ScrollView scrollView = (ScrollView) fragmentView; + scrollView.setFillViewport(true); + linearLayout = new LinearLayout(context); + scrollView.addView(linearLayout, new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + linearLayout.setOrientation(LinearLayout.VERTICAL); + + actionBar.setTitle(LocaleController.getString("ChannelEdit", R.string.ChannelEdit)); + + linearLayout2 = new LinearLayout(context); + linearLayout2.setOrientation(LinearLayout.VERTICAL); + linearLayout2.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + linearLayout.addView(linearLayout2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + FrameLayout frameLayout = new FrameLayout(context); + linearLayout2.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + avatarImage = new BackupImageView(context); + avatarImage.setRoundRadius(AndroidUtilities.dp(32)); + avatarDrawable.setInfo(5, null, null, false); + avatarDrawable.setDrawPhoto(true); + 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)); + avatarImage.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (getParentActivity() == null) { + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + + CharSequence[] items; + + if (avatar != null) { + items = new CharSequence[]{LocaleController.getString("FromCamera", R.string.FromCamera), LocaleController.getString("FromGalley", R.string.FromGalley), LocaleController.getString("DeletePhoto", R.string.DeletePhoto)}; + } else { + items = new CharSequence[]{LocaleController.getString("FromCamera", R.string.FromCamera), LocaleController.getString("FromGalley", R.string.FromGalley)}; + } + + builder.setItems(items, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + if (i == 0) { + avatarUpdater.openCamera(); + } else if (i == 1) { + avatarUpdater.openGallery(); + } else if (i == 2) { + avatar = null; + uploadedAvatar = null; + avatarImage.setImage(avatar, "50_50", avatarDrawable); + } + } + }); + showDialog(builder.create()); + } + }); + + nameTextView = new EditText(context); + if (currentChat.megagroup) { + nameTextView.setHint(LocaleController.getString("GroupName", R.string.GroupName)); + } else { + nameTextView.setHint(LocaleController.getString("EnterChannelName", R.string.EnterChannelName)); + } + nameTextView.setMaxLines(4); + nameTextView.setGravity(Gravity.CENTER_VERTICAL | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT)); + nameTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + nameTextView.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + nameTextView.setBackgroundDrawable(Theme.createEditTextDrawable(context, false)); + nameTextView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + nameTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); + nameTextView.setPadding(0, 0, 0, AndroidUtilities.dp(8)); + nameTextView.setEnabled(ChatObject.canChangeChatInfo(currentChat)); + nameTextView.setFocusable(nameTextView.isEnabled()); + InputFilter[] inputFilters = new InputFilter[1]; + inputFilters[0] = new InputFilter.LengthFilter(100); + nameTextView.setFilters(inputFilters); + AndroidUtilities.clearCursorDrawable(nameTextView); + nameTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + frameLayout.addView(nameTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_VERTICAL, LocaleController.isRTL ? 16 : 96, 0, LocaleController.isRTL ? 96 : 16, 0)); + nameTextView.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.length() > 0 ? nameTextView.getText().toString() : null, null, false); + avatarImage.invalidate(); + } + }); + + lineView = new View(context); + lineView.setBackgroundColor(Theme.getColor(Theme.key_divider)); + linearLayout.addView(lineView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); + + linearLayout3 = new LinearLayout(context); + linearLayout3.setOrientation(LinearLayout.VERTICAL); + linearLayout3.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + linearLayout.addView(linearLayout3, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + descriptionTextView = new EditText(context); + descriptionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + descriptionTextView.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + descriptionTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + descriptionTextView.setPadding(0, 0, 0, AndroidUtilities.dp(6)); + descriptionTextView.setBackgroundDrawable(null); + descriptionTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); + descriptionTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); + descriptionTextView.setImeOptions(EditorInfo.IME_ACTION_DONE); + descriptionTextView.setEnabled(ChatObject.canChangeChatInfo(currentChat)); + descriptionTextView.setFocusable(descriptionTextView.isEnabled()); + inputFilters = new InputFilter[1]; + inputFilters[0] = new InputFilter.LengthFilter(255); + descriptionTextView.setFilters(inputFilters); + descriptionTextView.setHint(LocaleController.getString("DescriptionOptionalPlaceholder", R.string.DescriptionOptionalPlaceholder)); + AndroidUtilities.clearCursorDrawable(descriptionTextView); + linearLayout3.addView(descriptionTextView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 17, 12, 17, 6)); + descriptionTextView.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) { + if (i == EditorInfo.IME_ACTION_DONE && doneButton != null) { + doneButton.performClick(); + return true; + } + return false; + } + }); + descriptionTextView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { + + } + + @Override + public void afterTextChanged(Editable editable) { + + } + }); + + sectionCell = new ShadowSectionCell(context); + sectionCell.setSize(20); + linearLayout.addView(sectionCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + container1 = new FrameLayout(context); + container1.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + linearLayout.addView(container1, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + if (currentChat.creator && (info == null || info.can_set_username)) { + linearLayoutTypeContainer = new LinearLayout(context); + linearLayoutTypeContainer.setOrientation(LinearLayout.VERTICAL); + linearLayoutTypeContainer.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + linearLayout.addView(linearLayoutTypeContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + radioButtonCell1 = new RadioButtonCell(context); + radioButtonCell1.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + if (currentChat.megagroup) { + radioButtonCell1.setTextAndValue(LocaleController.getString("MegaPublic", R.string.MegaPublic), LocaleController.getString("MegaPublicInfo", R.string.MegaPublicInfo), !isPrivate); + } else { + radioButtonCell1.setTextAndValue(LocaleController.getString("ChannelPublic", R.string.ChannelPublic), LocaleController.getString("ChannelPublicInfo", R.string.ChannelPublicInfo), !isPrivate); + } + linearLayoutTypeContainer.addView(radioButtonCell1, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + radioButtonCell1.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (!isPrivate) { + return; + } + isPrivate = false; + updatePrivatePublic(); + } + }); + + radioButtonCell2 = new RadioButtonCell(context); + radioButtonCell2.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + if (currentChat.megagroup) { + radioButtonCell2.setTextAndValue(LocaleController.getString("MegaPrivate", R.string.MegaPrivate), LocaleController.getString("MegaPrivateInfo", R.string.MegaPrivateInfo), isPrivate); + } else { + radioButtonCell2.setTextAndValue(LocaleController.getString("ChannelPrivate", R.string.ChannelPrivate), LocaleController.getString("ChannelPrivateInfo", R.string.ChannelPrivateInfo), isPrivate); + } + linearLayoutTypeContainer.addView(radioButtonCell2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + radioButtonCell2.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (isPrivate) { + return; + } + isPrivate = true; + updatePrivatePublic(); + } + }); + + sectionCell2 = new ShadowSectionCell(context); + linearLayout.addView(sectionCell2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + linkContainer = new LinearLayout(context); + linkContainer.setOrientation(LinearLayout.VERTICAL); + linkContainer.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + linearLayout.addView(linkContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + headerCell = new HeaderCell(context); + linkContainer.addView(headerCell); + + publicContainer = new LinearLayout(context); + publicContainer.setOrientation(LinearLayout.HORIZONTAL); + linkContainer.addView(publicContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 36, 17, 7, 17, 0)); + + editText = new EditText(context); + editText.setText(MessagesController.getInstance().linkPrefix + "/"); + editText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + editText.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + editText.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + editText.setMaxLines(1); + editText.setLines(1); + editText.setEnabled(false); + editText.setBackgroundDrawable(null); + editText.setPadding(0, 0, 0, 0); + editText.setSingleLine(true); + editText.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); + editText.setImeOptions(EditorInfo.IME_ACTION_DONE); + publicContainer.addView(editText, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, 36)); + + usernameTextView = new EditText(context); + usernameTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + if (!isPrivate) { + usernameTextView.setText(currentChat.username); + } + usernameTextView.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + usernameTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + usernameTextView.setMaxLines(1); + usernameTextView.setLines(1); + usernameTextView.setBackgroundDrawable(null); + usernameTextView.setPadding(0, 0, 0, 0); + usernameTextView.setSingleLine(true); + usernameTextView.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); + usernameTextView.setImeOptions(EditorInfo.IME_ACTION_DONE); + usernameTextView.setHint(LocaleController.getString("ChannelUsernamePlaceholder", R.string.ChannelUsernamePlaceholder)); + AndroidUtilities.clearCursorDrawable(usernameTextView); + publicContainer.addView(usernameTextView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 36)); + usernameTextView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { + checkUserName(usernameTextView.getText().toString()); + } + + @Override + public void afterTextChanged(Editable editable) { + + } + }); + + privateContainer = new TextBlockCell(context); + privateContainer.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + linkContainer.addView(privateContainer); + privateContainer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (invite == null) { + return; + } + try { + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); + clipboard.setPrimaryClip(clip); + Toast.makeText(getParentActivity(), LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + FileLog.e(e); + } + } + }); + + checkTextView = new TextView(context); + checkTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + checkTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); + checkTextView.setVisibility(View.GONE); + linkContainer.addView(checkTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 17, 3, 17, 7)); + + typeInfoCell = new TextInfoPrivacyCell(context); + typeInfoCell.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + linearLayout.addView(typeInfoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + loadingAdminedCell = new LoadingCell(context); + linearLayout.addView(loadingAdminedCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + adminnedChannelsLayout = new LinearLayout(context); + adminnedChannelsLayout.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + adminnedChannelsLayout.setOrientation(LinearLayout.VERTICAL); + linearLayout.addView(adminnedChannelsLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + adminedInfoCell = new ShadowSectionCell(context); + adminedInfoCell.setSize(20); + linearLayout.addView(adminedInfoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + updatePrivatePublic(); + } + + lineView2 = new View(context); + lineView2.setBackgroundColor(Theme.getColor(Theme.key_divider)); + linearLayout.addView(lineView2, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); + + container2 = new FrameLayout(context); + container2.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + + container3 = new FrameLayout(context); + container3.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + linearLayout.addView(container3, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + lineView3 = new View(context); + lineView3.setBackgroundColor(Theme.getColor(Theme.key_divider)); + linearLayout.addView(lineView3, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); + + linearLayout.addView(container2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + if (!currentChat.megagroup) { + textCheckCell = new TextCheckCell(context); + textCheckCell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + textCheckCell.setTextAndCheck(LocaleController.getString("ChannelSignMessages", R.string.ChannelSignMessages), signMessages, false); + container2.addView(textCheckCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + textCheckCell.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + signMessages = !signMessages; + ((TextCheckCell) v).setChecked(signMessages); + } + }); + + infoCell = new TextInfoPrivacyCell(context); + infoCell.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); + infoCell.setText(LocaleController.getString("ChannelSignMessagesInfo", R.string.ChannelSignMessagesInfo)); + linearLayout.addView(infoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + } + + if (currentChat.creator) { + container3 = new FrameLayout(context); + container3.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + linearLayout.addView(container3, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + textCell = new TextSettingsCell(context); + textCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText5)); + textCell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + if (currentChat.megagroup) { + textCell.setText(LocaleController.getString("DeleteMega", R.string.DeleteMega), false); + } else { + textCell.setText(LocaleController.getString("ChannelDelete", R.string.ChannelDelete), false); + } + container3.addView(textCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + textCell.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + if (currentChat.megagroup) { + builder.setMessage(LocaleController.getString("MegaDeleteAlert", R.string.MegaDeleteAlert)); + } else { + builder.setMessage(LocaleController.getString("ChannelDeleteAlert", R.string.ChannelDeleteAlert)); + } + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.closeChats); + if (AndroidUtilities.isTablet()) { + NotificationCenter.getInstance().postNotificationName(NotificationCenter.closeChats, -(long) chatId); + } else { + NotificationCenter.getInstance().postNotificationName(NotificationCenter.closeChats); + } + MessagesController.getInstance().deleteUserFromChat(chatId, MessagesController.getInstance().getUser(UserConfig.getClientUserId()), info); + finishFragment(); + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + showDialog(builder.create()); + } + }); + + infoCell2 = new TextInfoPrivacyCell(context); + infoCell2.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + if (currentChat.megagroup) { + infoCell2.setText(LocaleController.getString("MegaDeleteInfo", R.string.MegaDeleteInfo)); + } else { + infoCell2.setText(LocaleController.getString("ChannelDeleteInfo", R.string.ChannelDeleteInfo)); + } + linearLayout.addView(infoCell2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + } else { + if (currentChat.megagroup) { + sectionCell.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + } else { + infoCell.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + } + lineView3.setVisibility(View.GONE); + lineView2.setVisibility(View.GONE); + } + + nameTextView.setText(currentChat.title); + nameTextView.setSelection(nameTextView.length()); + if (info != null) { + descriptionTextView.setText(info.about); + } + if (currentChat.photo != null) { + avatar = currentChat.photo.photo_small; + avatarImage.setImage(avatar, "50_50", avatarDrawable); + } else { + avatarImage.setImageDrawable(avatarDrawable); + } + + return fragmentView; + } + + @Override + public void didReceivedNotification(int id, Object... args) { + if (id == NotificationCenter.chatInfoDidLoaded) { + TLRPC.ChatFull chatFull = (TLRPC.ChatFull) args[0]; + if (chatFull.id == chatId) { + if (info == null) { + descriptionTextView.setText(chatFull.about); + } + info = chatFull; + invite = chatFull.exported_invite; + updatePrivatePublic(); + } + } + } + + @Override + public void didUploadedPhoto(final TLRPC.InputFile file, final TLRPC.PhotoSize small, final TLRPC.PhotoSize big) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + uploadedAvatar = file; + avatar = small.location; + avatarImage.setImage(avatar, "50_50", avatarDrawable); + if (createAfterUpload) { + try { + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + progressDialog = null; + } + } catch (Exception e) { + FileLog.e(e); + } + donePressed = false; + doneButton.performClick(); + } + } + }); + } + + @Override + public void onActivityResultFragment(int requestCode, int resultCode, Intent data) { + avatarUpdater.onActivityResult(requestCode, resultCode, data); + } + + @Override + public void saveSelfArgs(Bundle args) { + if (avatarUpdater != null && avatarUpdater.currentPicturePath != null) { + args.putString("path", avatarUpdater.currentPicturePath); + } + if (nameTextView != null) { + String text = nameTextView.getText().toString(); + if (text != null && text.length() != 0) { + args.putString("nameTextView", text); + } + } + } + + @Override + public void restoreSelfArgs(Bundle args) { + if (avatarUpdater != null) { + avatarUpdater.currentPicturePath = args.getString("path"); + } + } + + public void setInfo(TLRPC.ChatFull chatFull) { + info = chatFull; + if (chatFull != null) { + if (chatFull.exported_invite instanceof TLRPC.TL_chatInviteExported) { + invite = chatFull.exported_invite; + } else { + generateLink(); + } + } + } + + private void loadAdminedChannels() { + if (loadingAdminedChannels || adminnedChannelsLayout == null) { + return; + } + loadingAdminedChannels = true; + updatePrivatePublic(); + TLRPC.TL_channels_getAdminedPublicChannels req = new TLRPC.TL_channels_getAdminedPublicChannels(); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + loadingAdminedChannels = false; + if (response != null) { + if (getParentActivity() == null) { + return; + } + for (int a = 0; a < adminedChannelCells.size(); a++) { + linearLayout.removeView(adminedChannelCells.get(a)); + } + adminedChannelCells.clear(); + TLRPC.TL_messages_chats res = (TLRPC.TL_messages_chats) response; + + for (int a = 0; a < res.chats.size(); a++) { + AdminedChannelCell adminedChannelCell = new AdminedChannelCell(getParentActivity(), new View.OnClickListener() { + @Override + public void onClick(View view) { + AdminedChannelCell cell = (AdminedChannelCell) view.getParent(); + final TLRPC.Chat channel = cell.getCurrentChannel(); + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + if (channel.megagroup) { + builder.setMessage(AndroidUtilities.replaceTags(LocaleController.formatString("RevokeLinkAlert", R.string.RevokeLinkAlert, MessagesController.getInstance().linkPrefix + "/" + channel.username, channel.title))); + } else { + builder.setMessage(AndroidUtilities.replaceTags(LocaleController.formatString("RevokeLinkAlertChannel", R.string.RevokeLinkAlertChannel, MessagesController.getInstance().linkPrefix + "/" + channel.username, channel.title))); + } + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + builder.setPositiveButton(LocaleController.getString("RevokeButton", R.string.RevokeButton), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + TLRPC.TL_channels_updateUsername req = new TLRPC.TL_channels_updateUsername(); + req.channel = MessagesController.getInputChannel(channel); + req.username = ""; + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (response instanceof TLRPC.TL_boolTrue) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + canCreatePublic = true; + if (nameTextView.length() > 0) { + checkUserName(nameTextView.getText().toString()); + } + updatePrivatePublic(); + } + }); + } + } + }, ConnectionsManager.RequestFlagInvokeAfter); + } + }); + showDialog(builder.create()); + } + }); + adminedChannelCell.setChannel(res.chats.get(a), a == res.chats.size() - 1); + adminedChannelCells.add(adminedChannelCell); + adminnedChannelsLayout.addView(adminedChannelCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 72)); + } + updatePrivatePublic(); + } + } + }); + } + }); + } + + private void updatePrivatePublic() { + if (sectionCell2 == null) { + return; + } + if (!isPrivate && !canCreatePublic) { + typeInfoCell.setText(LocaleController.getString("ChangePublicLimitReached", R.string.ChangePublicLimitReached)); + typeInfoCell.setTag(Theme.key_windowBackgroundWhiteRedText4); + typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); + linkContainer.setVisibility(View.GONE); + sectionCell2.setVisibility(View.GONE); + adminedInfoCell.setVisibility(View.VISIBLE); + if (loadingAdminedChannels) { + loadingAdminedCell.setVisibility(View.VISIBLE); + adminnedChannelsLayout.setVisibility(View.GONE); + typeInfoCell.setBackgroundDrawable(Theme.getThemedDrawable(typeInfoCell.getContext(), R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + adminedInfoCell.setBackgroundDrawable(null); + } else { + adminedInfoCell.setBackgroundDrawable(Theme.getThemedDrawable(adminedInfoCell.getContext(), R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + typeInfoCell.setBackgroundDrawable(Theme.getThemedDrawable(typeInfoCell.getContext(), R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); + loadingAdminedCell.setVisibility(View.GONE); + adminnedChannelsLayout.setVisibility(View.VISIBLE); + } + } else { + typeInfoCell.setTag(Theme.key_windowBackgroundWhiteGrayText4); + typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText4)); + sectionCell2.setVisibility(View.VISIBLE); + adminedInfoCell.setVisibility(View.GONE); + typeInfoCell.setBackgroundDrawable(Theme.getThemedDrawable(typeInfoCell.getContext(), R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + adminnedChannelsLayout.setVisibility(View.GONE); + linkContainer.setVisibility(View.VISIBLE); + loadingAdminedCell.setVisibility(View.GONE); + if (currentChat.megagroup) { + typeInfoCell.setText(isPrivate ? LocaleController.getString("MegaPrivateLinkHelp", R.string.MegaPrivateLinkHelp) : LocaleController.getString("MegaUsernameHelp", R.string.MegaUsernameHelp)); + headerCell.setText(isPrivate ? LocaleController.getString("ChannelInviteLinkTitle", R.string.ChannelInviteLinkTitle) : LocaleController.getString("ChannelLinkTitle", R.string.ChannelLinkTitle)); + } else { + typeInfoCell.setText(isPrivate ? LocaleController.getString("ChannelPrivateLinkHelp", R.string.ChannelPrivateLinkHelp) : LocaleController.getString("ChannelUsernameHelp", R.string.ChannelUsernameHelp)); + headerCell.setText(isPrivate ? LocaleController.getString("ChannelInviteLinkTitle", R.string.ChannelInviteLinkTitle) : LocaleController.getString("ChannelLinkTitle", R.string.ChannelLinkTitle)); + } + publicContainer.setVisibility(isPrivate ? View.GONE : View.VISIBLE); + privateContainer.setVisibility(isPrivate ? View.VISIBLE : View.GONE); + linkContainer.setPadding(0, 0, 0, isPrivate ? 0 : AndroidUtilities.dp(7)); + privateContainer.setText(invite != null ? invite.link : LocaleController.getString("Loading", R.string.Loading), false); + checkTextView.setVisibility(!isPrivate && checkTextView.length() != 0 ? View.VISIBLE : View.GONE); + } + radioButtonCell1.setChecked(!isPrivate, true); + radioButtonCell2.setChecked(isPrivate, true); + usernameTextView.clearFocus(); + AndroidUtilities.hideKeyboard(nameTextView); + } + + private boolean checkUserName(final String name) { + if (name != null && name.length() > 0) { + checkTextView.setVisibility(View.VISIBLE); + } else { + checkTextView.setVisibility(View.GONE); + } + if (checkRunnable != null) { + AndroidUtilities.cancelRunOnUIThread(checkRunnable); + checkRunnable = null; + lastCheckName = null; + if (checkReqId != 0) { + ConnectionsManager.getInstance().cancelRequest(checkReqId, true); + } + } + lastNameAvailable = false; + if (name != null) { + if (name.startsWith("_") || name.endsWith("_")) { + checkTextView.setText(LocaleController.getString("LinkInvalid", R.string.LinkInvalid)); + checkTextView.setTag(Theme.key_windowBackgroundWhiteRedText4); + checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); + return false; + } + for (int a = 0; a < name.length(); a++) { + char ch = name.charAt(a); + if (a == 0 && ch >= '0' && ch <= '9') { + if (currentChat.megagroup) { + checkTextView.setText(LocaleController.getString("LinkInvalidStartNumberMega", R.string.LinkInvalidStartNumberMega)); + checkTextView.setTag(Theme.key_windowBackgroundWhiteRedText4); + checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); + } else { + checkTextView.setText(LocaleController.getString("LinkInvalidStartNumber", R.string.LinkInvalidStartNumber)); + checkTextView.setTag(Theme.key_windowBackgroundWhiteRedText4); + checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); + } + return false; + } + if (!(ch >= '0' && ch <= '9' || ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch == '_')) { + checkTextView.setText(LocaleController.getString("LinkInvalid", R.string.LinkInvalid)); + checkTextView.setTag(Theme.key_windowBackgroundWhiteRedText4); + checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); + return false; + } + } + } + if (name == null || name.length() < 5) { + if (currentChat.megagroup) { + checkTextView.setText(LocaleController.getString("LinkInvalidShortMega", R.string.LinkInvalidShortMega)); + checkTextView.setTag(Theme.key_windowBackgroundWhiteRedText4); + checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); + } else { + checkTextView.setText(LocaleController.getString("LinkInvalidShort", R.string.LinkInvalidShort)); + checkTextView.setTag(Theme.key_windowBackgroundWhiteRedText4); + checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); + } + return false; + } + if (name.length() > 32) { + checkTextView.setText(LocaleController.getString("LinkInvalidLong", R.string.LinkInvalidLong)); + checkTextView.setTag(Theme.key_windowBackgroundWhiteRedText4); + checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); + return false; + } + + checkTextView.setText(LocaleController.getString("LinkChecking", R.string.LinkChecking)); + checkTextView.setTag(Theme.key_windowBackgroundWhiteGrayText8); + checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText8)); + lastCheckName = name; + checkRunnable = new Runnable() { + @Override + public void run() { + TLRPC.TL_channels_checkUsername req = new TLRPC.TL_channels_checkUsername(); + req.username = name; + req.channel = MessagesController.getInputChannel(chatId); + checkReqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + checkReqId = 0; + if (lastCheckName != null && lastCheckName.equals(name)) { + if (error == null && response instanceof TLRPC.TL_boolTrue) { + checkTextView.setText(LocaleController.formatString("LinkAvailable", R.string.LinkAvailable, name)); + checkTextView.setTag(Theme.key_windowBackgroundWhiteGreenText); + checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGreenText)); + lastNameAvailable = true; + } else { + if (error != null && error.text.equals("CHANNELS_ADMIN_PUBLIC_TOO_MUCH")) { + canCreatePublic = false; + loadAdminedChannels(); + } else { + checkTextView.setText(LocaleController.getString("LinkInUse", R.string.LinkInUse)); + } + checkTextView.setTag(Theme.key_windowBackgroundWhiteRedText4); + checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); + lastNameAvailable = false; + } + } + } + }); + } + }, ConnectionsManager.RequestFlagFailOnServerErrors); + } + }; + AndroidUtilities.runOnUIThread(checkRunnable, 300); + return true; + } + + private void generateLink() { + if (loadingInvite || invite != null) { + return; + } + loadingInvite = true; + TLRPC.TL_channels_exportInvite req = new TLRPC.TL_channels_exportInvite(); + req.channel = MessagesController.getInputChannel(chatId); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (error == null) { + invite = (TLRPC.ExportedChatInvite) response; + } + loadingInvite = false; + if (privateContainer != null) { + privateContainer.setText(invite != null ? invite.link : LocaleController.getString("Loading", R.string.Loading), false); + } + } + }); + } + }); + } + + @Override + public ThemeDescription[] getThemeDescriptions() { + ThemeDescription.ThemeDescriptionDelegate сellDelegate = new ThemeDescription.ThemeDescriptionDelegate() { + @Override + public void didSetColor(int color) { + if (avatarImage != null) { + avatarDrawable.setInfo(5, nameTextView.length() > 0 ? nameTextView.getText().toString() : null, null, false); + avatarImage.invalidate(); + } + } + }; + ThemeDescription.ThemeDescriptionDelegate сellDelegate2 = new ThemeDescription.ThemeDescriptionDelegate() { + @Override + public void didSetColor(int color) { + if (adminnedChannelsLayout != null) { + int count = adminnedChannelsLayout.getChildCount(); + for (int a = 0; a < count; a++) { + View child = adminnedChannelsLayout.getChildAt(a); + if (child instanceof AdminedChannelCell) { + ((AdminedChannelCell) child).update(); + } + } + } + } + }; + return new ThemeDescription[]{ + new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray), + + new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector), + + new ThemeDescription(nameTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(nameTextView, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText), + new ThemeDescription(nameTextView, ThemeDescription.FLAG_BACKGROUNDFILTER, null, null, null, null, Theme.key_windowBackgroundWhiteInputField), + new ThemeDescription(nameTextView, ThemeDescription.FLAG_BACKGROUNDFILTER | ThemeDescription.FLAG_DRAWABLESELECTEDSTATE, null, null, null, null, Theme.key_windowBackgroundWhiteInputFieldActivated), + new ThemeDescription(descriptionTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(descriptionTextView, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText), + + new ThemeDescription(linearLayout2, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(linearLayout3, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(container1, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(container2, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(container3, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(container4, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), + + new ThemeDescription(null, 0, null, null, new Drawable[]{Theme.avatar_photoDrawable, Theme.avatar_broadcastDrawable}, сellDelegate, Theme.key_avatar_text), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundBlue), + + new ThemeDescription(lineView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_divider), + new ThemeDescription(lineView2, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_divider), + new ThemeDescription(lineView3, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_divider), + + new ThemeDescription(sectionCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), + new ThemeDescription(sectionCell2, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), + + new ThemeDescription(textCheckCell, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), + new ThemeDescription(textCheckCell, 0, new Class[]{TextCheckCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(textCheckCell, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchThumb), + new ThemeDescription(textCheckCell, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchTrack), + new ThemeDescription(textCheckCell, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchThumbChecked), + new ThemeDescription(textCheckCell, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchTrackChecked), + + new ThemeDescription(infoCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), + new ThemeDescription(infoCell, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4), + + new ThemeDescription(textCell, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), + new ThemeDescription(textCell, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{TextSettingsCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteRedText5), + + new ThemeDescription(infoCell2, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), + new ThemeDescription(infoCell2, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4), + + new ThemeDescription(usernameTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(usernameTextView, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText), + + new ThemeDescription(linearLayoutTypeContainer, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(linkContainer, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(headerCell, 0, new Class[]{HeaderCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader), + new ThemeDescription(editText, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(editText, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText), + new ThemeDescription(checkTextView, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, null, null, null, null, Theme.key_windowBackgroundWhiteRedText4), + new ThemeDescription(checkTextView, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, null, null, null, null, Theme.key_windowBackgroundWhiteGrayText8), + new ThemeDescription(checkTextView, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, null, null, null, null, Theme.key_windowBackgroundWhiteGreenText), + + new ThemeDescription(typeInfoCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), + new ThemeDescription(typeInfoCell, ThemeDescription.FLAG_CHECKTAG, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4), + new ThemeDescription(typeInfoCell, ThemeDescription.FLAG_CHECKTAG, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteRedText4), + + new ThemeDescription(adminedInfoCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), + new ThemeDescription(adminnedChannelsLayout, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(privateContainer, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), + new ThemeDescription(privateContainer, 0, new Class[]{TextBlockCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(loadingAdminedCell, 0, new Class[]{LoadingCell.class}, new String[]{"progressBar"}, null, null, null, Theme.key_progressCircle), + new ThemeDescription(radioButtonCell1, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), + new ThemeDescription(radioButtonCell1, ThemeDescription.FLAG_CHECKBOX, new Class[]{RadioButtonCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackground), + new ThemeDescription(radioButtonCell1, ThemeDescription.FLAG_CHECKBOXCHECK, new Class[]{RadioButtonCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackgroundChecked), + new ThemeDescription(radioButtonCell1, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{RadioButtonCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(radioButtonCell1, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{RadioButtonCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText2), + new ThemeDescription(radioButtonCell2, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), + new ThemeDescription(radioButtonCell2, ThemeDescription.FLAG_CHECKBOX, new Class[]{RadioButtonCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackground), + new ThemeDescription(radioButtonCell2, ThemeDescription.FLAG_CHECKBOXCHECK, new Class[]{RadioButtonCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackgroundChecked), + new ThemeDescription(radioButtonCell2, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{RadioButtonCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(radioButtonCell2, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{RadioButtonCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText2), + + new ThemeDescription(adminnedChannelsLayout, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{AdminedChannelCell.class}, new String[]{"nameTextView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(adminnedChannelsLayout, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{AdminedChannelCell.class}, new String[]{"statusTextView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText), + new ThemeDescription(adminnedChannelsLayout, ThemeDescription.FLAG_LINKCOLOR, new Class[]{AdminedChannelCell.class}, new String[]{"statusTextView"}, null, null, null, Theme.key_windowBackgroundWhiteLinkText), + new ThemeDescription(adminnedChannelsLayout, ThemeDescription.FLAG_IMAGECOLOR, new Class[]{AdminedChannelCell.class}, new String[]{"deleteButton"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText), + new ThemeDescription(null, 0, null, null, new Drawable[]{Theme.avatar_photoDrawable, Theme.avatar_broadcastDrawable}, сellDelegate2, Theme.key_avatar_text), + new ThemeDescription(null, 0, null, null, null, сellDelegate2, Theme.key_avatar_backgroundRed), + new ThemeDescription(null, 0, null, null, null, сellDelegate2, Theme.key_avatar_backgroundOrange), + new ThemeDescription(null, 0, null, null, null, сellDelegate2, Theme.key_avatar_backgroundViolet), + new ThemeDescription(null, 0, null, null, null, сellDelegate2, Theme.key_avatar_backgroundGreen), + new ThemeDescription(null, 0, null, null, null, сellDelegate2, Theme.key_avatar_backgroundCyan), + new ThemeDescription(null, 0, null, null, null, сellDelegate2, Theme.key_avatar_backgroundBlue), + new ThemeDescription(null, 0, null, null, null, сellDelegate2, Theme.key_avatar_backgroundPink), + }; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditTypeActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditTypeActivity.java deleted file mode 100644 index 2d5d221e9..000000000 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditTypeActivity.java +++ /dev/null @@ -1,725 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.x. - * It is licensed under GNU GPL v. 2 or later. - * You should have received a copy of the license in this archive (see LICENSE). - * - * Copyright Nikolai Kudashov, 2013-2017. - */ - -package org.telegram.ui; - -import android.content.Context; -import android.content.DialogInterface; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.Vibrator; -import android.text.Editable; -import android.text.InputType; -import android.text.TextWatcher; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.ScrollView; -import android.widget.TextView; -import android.widget.Toast; - -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.MessagesStorage; -import org.telegram.messenger.NotificationCenter; -import org.telegram.messenger.R; -import org.telegram.tgnet.ConnectionsManager; -import org.telegram.tgnet.RequestDelegate; -import org.telegram.tgnet.TLObject; -import org.telegram.tgnet.TLRPC; -import org.telegram.ui.ActionBar.ActionBar; -import org.telegram.ui.ActionBar.ActionBarMenu; -import org.telegram.ui.ActionBar.AlertDialog; -import org.telegram.ui.ActionBar.BaseFragment; -import org.telegram.ui.ActionBar.Theme; -import org.telegram.ui.ActionBar.ThemeDescription; -import org.telegram.ui.Cells.AdminedChannelCell; -import org.telegram.ui.Cells.HeaderCell; -import org.telegram.ui.Cells.LoadingCell; -import org.telegram.ui.Cells.RadioButtonCell; -import org.telegram.ui.Cells.ShadowSectionCell; -import org.telegram.ui.Cells.TextBlockCell; -import org.telegram.ui.Cells.TextInfoPrivacyCell; -import org.telegram.ui.Components.LayoutHelper; - -import java.util.ArrayList; -import java.util.concurrent.Semaphore; - -public class ChannelEditTypeActivity extends BaseFragment implements NotificationCenter.NotificationCenterDelegate { - - private LinearLayout linearLayout; - private LinearLayout linearLayout2; - private LinearLayout adminnedChannelsLayout; - private LinearLayout linkContainer; - private LinearLayout publicContainer; - private TextBlockCell privateContainer; - private RadioButtonCell radioButtonCell1; - private RadioButtonCell radioButtonCell2; - private ShadowSectionCell sectionCell; - private TextInfoPrivacyCell typeInfoCell; - private EditText editText; - private TextView checkTextView; - private HeaderCell headerCell; - private EditText nameTextView; - private boolean isPrivate; - private boolean loadingInvite; - private TLRPC.ExportedChatInvite invite; - - private boolean canCreatePublic = true; - private boolean loadingAdminedChannels; - private TextInfoPrivacyCell adminedInfoCell; - private ArrayList adminedChannelCells = new ArrayList<>(); - private LoadingCell loadingAdminedCell; - - private int checkReqId; - private String lastCheckName; - private Runnable checkRunnable; - private boolean lastNameAvailable; - private TLRPC.Chat currentChat; - private int chatId; - - private boolean donePressed; - - private final static int done_button = 1; - - public ChannelEditTypeActivity(Bundle args) { - super(args); - chatId = args.getInt("chat_id", 0); - } - - @SuppressWarnings("unchecked") - @Override - public boolean onFragmentCreate() { - currentChat = MessagesController.getInstance().getChat(chatId); - if (currentChat == null) { - final Semaphore semaphore = new Semaphore(0); - MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { - @Override - public void run() { - currentChat = MessagesStorage.getInstance().getChat(chatId); - semaphore.release(); - } - }); - try { - semaphore.acquire(); - } catch (Exception e) { - FileLog.e(e); - } - if (currentChat != null) { - MessagesController.getInstance().putChat(currentChat, true); - } else { - return false; - } - } - isPrivate = currentChat.username == null || currentChat.username.length() == 0; - if (isPrivate) { - TLRPC.TL_channels_checkUsername req = new TLRPC.TL_channels_checkUsername(); - req.username = "1"; - req.channel = new TLRPC.TL_inputChannelEmpty(); - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(TLObject response, final TLRPC.TL_error error) { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - canCreatePublic = error == null || !error.text.equals("CHANNELS_ADMIN_PUBLIC_TOO_MUCH"); - if (!canCreatePublic) { - loadAdminedChannels(); - } - } - }); - } - }); - } - NotificationCenter.getInstance().addObserver(this, NotificationCenter.chatInfoDidLoaded); - return super.onFragmentCreate(); - } - - @Override - public void onFragmentDestroy() { - super.onFragmentDestroy(); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.chatInfoDidLoaded); - AndroidUtilities.removeAdjustResize(getParentActivity(), classGuid); - } - - @Override - public void onResume() { - super.onResume(); - AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); - } - - @Override - public View createView(Context context) { - actionBar.setBackButtonImage(R.drawable.ic_ab_back); - actionBar.setAllowOverlayTitle(true); - - actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { - @Override - public void onItemClick(int id) { - if (id == -1) { - finishFragment(); - } else if (id == done_button) { - if (donePressed) { - return; - } - - if (!isPrivate && ((currentChat.username == null && nameTextView.length() != 0) || (currentChat.username != null && !currentChat.username.equalsIgnoreCase(nameTextView.getText().toString())))) { - if (nameTextView.length() != 0 && !lastNameAvailable) { - Vibrator v = (Vibrator) getParentActivity().getSystemService(Context.VIBRATOR_SERVICE); - if (v != null) { - v.vibrate(200); - } - AndroidUtilities.shakeView(checkTextView, 2, 0); - return; - } - } - donePressed = true; - - String oldUserName = currentChat.username != null ? currentChat.username : ""; - String newUserName = isPrivate ? "" : nameTextView.getText().toString(); - if (!oldUserName.equals(newUserName)) { - MessagesController.getInstance().updateChannelUserName(chatId, newUserName); - } - finishFragment(); - } - } - }); - - ActionBarMenu menu = actionBar.createMenu(); - menu.addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); - - fragmentView = new ScrollView(context); - fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); - ScrollView scrollView = (ScrollView) fragmentView; - scrollView.setFillViewport(true); - linearLayout = new LinearLayout(context); - scrollView.addView(linearLayout, new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - - linearLayout.setOrientation(LinearLayout.VERTICAL); - - if (currentChat.megagroup) { - actionBar.setTitle(LocaleController.getString("GroupType", R.string.GroupType)); - } else { - actionBar.setTitle(LocaleController.getString("ChannelType", R.string.ChannelType)); - } - - linearLayout2 = new LinearLayout(context); - linearLayout2.setOrientation(LinearLayout.VERTICAL); - linearLayout2.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - linearLayout.addView(linearLayout2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - radioButtonCell1 = new RadioButtonCell(context); - radioButtonCell1.setBackgroundDrawable(Theme.getSelectorDrawable(false)); - if (currentChat.megagroup) { - radioButtonCell1.setTextAndValue(LocaleController.getString("MegaPublic", R.string.MegaPublic), LocaleController.getString("MegaPublicInfo", R.string.MegaPublicInfo), !isPrivate); - } else { - radioButtonCell1.setTextAndValue(LocaleController.getString("ChannelPublic", R.string.ChannelPublic), LocaleController.getString("ChannelPublicInfo", R.string.ChannelPublicInfo), !isPrivate); - } - linearLayout2.addView(radioButtonCell1, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - radioButtonCell1.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (!isPrivate) { - return; - } - isPrivate = false; - updatePrivatePublic(); - } - }); - - radioButtonCell2 = new RadioButtonCell(context); - radioButtonCell2.setBackgroundDrawable(Theme.getSelectorDrawable(false)); - if (currentChat.megagroup) { - radioButtonCell2.setTextAndValue(LocaleController.getString("MegaPrivate", R.string.MegaPrivate), LocaleController.getString("MegaPrivateInfo", R.string.MegaPrivateInfo), isPrivate); - } else { - radioButtonCell2.setTextAndValue(LocaleController.getString("ChannelPrivate", R.string.ChannelPrivate), LocaleController.getString("ChannelPrivateInfo", R.string.ChannelPrivateInfo), isPrivate); - } - linearLayout2.addView(radioButtonCell2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - radioButtonCell2.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (isPrivate) { - return; - } - isPrivate = true; - updatePrivatePublic(); - } - }); - - sectionCell = new ShadowSectionCell(context); - linearLayout.addView(sectionCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - linkContainer = new LinearLayout(context); - linkContainer.setOrientation(LinearLayout.VERTICAL); - linkContainer.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - linearLayout.addView(linkContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - headerCell = new HeaderCell(context); - linkContainer.addView(headerCell); - - publicContainer = new LinearLayout(context); - publicContainer.setOrientation(LinearLayout.HORIZONTAL); - linkContainer.addView(publicContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 36, 17, 7, 17, 0)); - - editText = new EditText(context); - editText.setText(MessagesController.getInstance().linkPrefix + "/"); - editText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); - editText.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); - editText.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); - editText.setMaxLines(1); - editText.setLines(1); - editText.setEnabled(false); - editText.setBackgroundDrawable(null); - editText.setPadding(0, 0, 0, 0); - editText.setSingleLine(true); - editText.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); - editText.setImeOptions(EditorInfo.IME_ACTION_DONE); - publicContainer.addView(editText, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, 36)); - - nameTextView = new EditText(context); - nameTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); - if (!isPrivate) { - nameTextView.setText(currentChat.username); - } - nameTextView.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); - nameTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); - nameTextView.setMaxLines(1); - nameTextView.setLines(1); - nameTextView.setBackgroundDrawable(null); - nameTextView.setPadding(0, 0, 0, 0); - nameTextView.setSingleLine(true); - nameTextView.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); - nameTextView.setImeOptions(EditorInfo.IME_ACTION_DONE); - nameTextView.setHint(LocaleController.getString("ChannelUsernamePlaceholder", R.string.ChannelUsernamePlaceholder)); - AndroidUtilities.clearCursorDrawable(nameTextView); - publicContainer.addView(nameTextView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 36)); - nameTextView.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - checkUserName(nameTextView.getText().toString()); - } - - @Override - public void afterTextChanged(Editable editable) { - - } - }); - - privateContainer = new TextBlockCell(context); - privateContainer.setBackgroundDrawable(Theme.getSelectorDrawable(false)); - linkContainer.addView(privateContainer); - privateContainer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (invite == null) { - return; - } - try { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); - clipboard.setPrimaryClip(clip); - Toast.makeText(getParentActivity(), LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - FileLog.e(e); - } - } - }); - - checkTextView = new TextView(context); - checkTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); - checkTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); - checkTextView.setVisibility(View.GONE); - linkContainer.addView(checkTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 17, 3, 17, 7)); - - typeInfoCell = new TextInfoPrivacyCell(context); - typeInfoCell.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); - linearLayout.addView(typeInfoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - loadingAdminedCell = new LoadingCell(context); - linearLayout.addView(loadingAdminedCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - adminnedChannelsLayout = new LinearLayout(context); - adminnedChannelsLayout.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - adminnedChannelsLayout.setOrientation(LinearLayout.VERTICAL); - linearLayout.addView(adminnedChannelsLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - adminedInfoCell = new TextInfoPrivacyCell(context); - adminedInfoCell.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); - linearLayout.addView(adminedInfoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - updatePrivatePublic(); - - return fragmentView; - } - - @Override - public void didReceivedNotification(int id, Object... args) { - if (id == NotificationCenter.chatInfoDidLoaded) { - TLRPC.ChatFull chatFull = (TLRPC.ChatFull) args[0]; - if (chatFull.id == chatId) { - invite = chatFull.exported_invite; - updatePrivatePublic(); - } - } - } - - public void setInfo(TLRPC.ChatFull chatFull) { - if (chatFull != null) { - if (chatFull.exported_invite instanceof TLRPC.TL_chatInviteExported) { - invite = chatFull.exported_invite; - } else { - generateLink(); - } - } - } - - private void loadAdminedChannels() { - if (loadingAdminedChannels) { - return; - } - loadingAdminedChannels = true; - updatePrivatePublic(); - TLRPC.TL_channels_getAdminedPublicChannels req = new TLRPC.TL_channels_getAdminedPublicChannels(); - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(final TLObject response, final TLRPC.TL_error error) { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - loadingAdminedChannels = false; - if (response != null) { - if (getParentActivity() == null) { - return; - } - for (int a = 0; a < adminedChannelCells.size(); a++) { - linearLayout.removeView(adminedChannelCells.get(a)); - } - adminedChannelCells.clear(); - TLRPC.TL_messages_chats res = (TLRPC.TL_messages_chats) response; - - for (int a = 0; a < res.chats.size(); a++) { - AdminedChannelCell adminedChannelCell = new AdminedChannelCell(getParentActivity(), new View.OnClickListener() { - @Override - public void onClick(View view) { - AdminedChannelCell cell = (AdminedChannelCell) view.getParent(); - final TLRPC.Chat channel = cell.getCurrentChannel(); - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); - if (channel.megagroup) { - builder.setMessage(AndroidUtilities.replaceTags(LocaleController.formatString("RevokeLinkAlert", R.string.RevokeLinkAlert, MessagesController.getInstance().linkPrefix + "/" + channel.username, channel.title))); - } else { - builder.setMessage(AndroidUtilities.replaceTags(LocaleController.formatString("RevokeLinkAlertChannel", R.string.RevokeLinkAlertChannel, MessagesController.getInstance().linkPrefix + "/" + channel.username, channel.title))); - } - builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); - builder.setPositiveButton(LocaleController.getString("RevokeButton", R.string.RevokeButton), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - TLRPC.TL_channels_updateUsername req = new TLRPC.TL_channels_updateUsername(); - req.channel = MessagesController.getInputChannel(channel); - req.username = ""; - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(TLObject response, TLRPC.TL_error error) { - if (response instanceof TLRPC.TL_boolTrue) { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - canCreatePublic = true; - if (nameTextView.length() > 0) { - checkUserName(nameTextView.getText().toString()); - } - updatePrivatePublic(); - } - }); - } - } - }, ConnectionsManager.RequestFlagInvokeAfter); - } - }); - showDialog(builder.create()); - } - }); - adminedChannelCell.setChannel(res.chats.get(a), a == res.chats.size() - 1); - adminedChannelCells.add(adminedChannelCell); - linearLayout.addView(adminedChannelCell, linearLayout.getChildCount() - 1, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 72)); - } - updatePrivatePublic(); - } - } - }); - } - }); - } - - private void updatePrivatePublic() { - if (sectionCell == null) { - return; - } - if (!isPrivate && !canCreatePublic) { - typeInfoCell.setText(LocaleController.getString("ChangePublicLimitReached", R.string.ChangePublicLimitReached)); - typeInfoCell.setTag(Theme.key_windowBackgroundWhiteRedText4); - typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); - linkContainer.setVisibility(View.GONE); - sectionCell.setVisibility(View.GONE); - if (loadingAdminedChannels) { - loadingAdminedCell.setVisibility(View.VISIBLE); - adminnedChannelsLayout.setVisibility(View.GONE); - typeInfoCell.setBackgroundDrawable(Theme.getThemedDrawable(typeInfoCell.getContext(), R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); - adminedInfoCell.setVisibility(View.GONE); - } else { - typeInfoCell.setBackgroundDrawable(Theme.getThemedDrawable(typeInfoCell.getContext(), R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); - loadingAdminedCell.setVisibility(View.GONE); - adminnedChannelsLayout.setVisibility(View.VISIBLE); - adminedInfoCell.setVisibility(View.VISIBLE); - } - } else { - typeInfoCell.setTag(Theme.key_windowBackgroundWhiteGrayText4); - typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText4)); - sectionCell.setVisibility(View.VISIBLE); - adminedInfoCell.setVisibility(View.GONE); - typeInfoCell.setBackgroundDrawable(Theme.getThemedDrawable(typeInfoCell.getContext(), R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); - adminnedChannelsLayout.setVisibility(View.GONE); - linkContainer.setVisibility(View.VISIBLE); - loadingAdminedCell.setVisibility(View.GONE); - if (currentChat.megagroup) { - typeInfoCell.setText(isPrivate ? LocaleController.getString("MegaPrivateLinkHelp", R.string.MegaPrivateLinkHelp) : LocaleController.getString("MegaUsernameHelp", R.string.MegaUsernameHelp)); - headerCell.setText(isPrivate ? LocaleController.getString("ChannelInviteLinkTitle", R.string.ChannelInviteLinkTitle) : LocaleController.getString("ChannelLinkTitle", R.string.ChannelLinkTitle)); - } else { - typeInfoCell.setText(isPrivate ? LocaleController.getString("ChannelPrivateLinkHelp", R.string.ChannelPrivateLinkHelp) : LocaleController.getString("ChannelUsernameHelp", R.string.ChannelUsernameHelp)); - headerCell.setText(isPrivate ? LocaleController.getString("ChannelInviteLinkTitle", R.string.ChannelInviteLinkTitle) : LocaleController.getString("ChannelLinkTitle", R.string.ChannelLinkTitle)); - } - publicContainer.setVisibility(isPrivate ? View.GONE : View.VISIBLE); - privateContainer.setVisibility(isPrivate ? View.VISIBLE : View.GONE); - linkContainer.setPadding(0, 0, 0, isPrivate ? 0 : AndroidUtilities.dp(7)); - privateContainer.setText(invite != null ? invite.link : LocaleController.getString("Loading", R.string.Loading), false); - checkTextView.setVisibility(!isPrivate && checkTextView.length() != 0 ? View.VISIBLE : View.GONE); - } - radioButtonCell1.setChecked(!isPrivate, true); - radioButtonCell2.setChecked(isPrivate, true); - nameTextView.clearFocus(); - AndroidUtilities.hideKeyboard(nameTextView); - } - - private boolean checkUserName(final String name) { - if (name != null && name.length() > 0) { - checkTextView.setVisibility(View.VISIBLE); - } else { - checkTextView.setVisibility(View.GONE); - } - if (checkRunnable != null) { - AndroidUtilities.cancelRunOnUIThread(checkRunnable); - checkRunnable = null; - lastCheckName = null; - if (checkReqId != 0) { - ConnectionsManager.getInstance().cancelRequest(checkReqId, true); - } - } - lastNameAvailable = false; - if (name != null) { - if (name.startsWith("_") || name.endsWith("_")) { - checkTextView.setText(LocaleController.getString("LinkInvalid", R.string.LinkInvalid)); - typeInfoCell.setTag(Theme.key_windowBackgroundWhiteRedText4); - typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); - return false; - } - for (int a = 0; a < name.length(); a++) { - char ch = name.charAt(a); - if (a == 0 && ch >= '0' && ch <= '9') { - if (currentChat.megagroup) { - checkTextView.setText(LocaleController.getString("LinkInvalidStartNumberMega", R.string.LinkInvalidStartNumberMega)); - typeInfoCell.setTag(Theme.key_windowBackgroundWhiteRedText4); - typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); - } else { - checkTextView.setText(LocaleController.getString("LinkInvalidStartNumber", R.string.LinkInvalidStartNumber)); - typeInfoCell.setTag(Theme.key_windowBackgroundWhiteRedText4); - typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); - } - return false; - } - if (!(ch >= '0' && ch <= '9' || ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch == '_')) { - checkTextView.setText(LocaleController.getString("LinkInvalid", R.string.LinkInvalid)); - typeInfoCell.setTag(Theme.key_windowBackgroundWhiteRedText4); - typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); - return false; - } - } - } - if (name == null || name.length() < 5) { - if (currentChat.megagroup) { - checkTextView.setText(LocaleController.getString("LinkInvalidShortMega", R.string.LinkInvalidShortMega)); - typeInfoCell.setTag(Theme.key_windowBackgroundWhiteRedText4); - typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); - } else { - checkTextView.setText(LocaleController.getString("LinkInvalidShort", R.string.LinkInvalidShort)); - typeInfoCell.setTag(Theme.key_windowBackgroundWhiteRedText4); - typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); - } - return false; - } - if (name.length() > 32) { - checkTextView.setText(LocaleController.getString("LinkInvalidLong", R.string.LinkInvalidLong)); - typeInfoCell.setTag(Theme.key_windowBackgroundWhiteRedText4); - typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); - return false; - } - - - checkTextView.setText(LocaleController.getString("LinkChecking", R.string.LinkChecking)); - checkTextView.setTag(Theme.key_windowBackgroundWhiteGrayText8); - checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText8)); - lastCheckName = name; - checkRunnable = new Runnable() { - @Override - public void run() { - TLRPC.TL_channels_checkUsername req = new TLRPC.TL_channels_checkUsername(); - req.username = name; - req.channel = MessagesController.getInputChannel(chatId); - checkReqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(final TLObject response, final TLRPC.TL_error error) { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - checkReqId = 0; - if (lastCheckName != null && lastCheckName.equals(name)) { - if (error == null && response instanceof TLRPC.TL_boolTrue) { - checkTextView.setText(LocaleController.formatString("LinkAvailable", R.string.LinkAvailable, name)); - checkTextView.setTag(Theme.key_windowBackgroundWhiteGreenText); - checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGreenText)); - lastNameAvailable = true; - } else { - if (error != null && error.text.equals("CHANNELS_ADMIN_PUBLIC_TOO_MUCH")) { - canCreatePublic = false; - loadAdminedChannels(); - } else { - checkTextView.setText(LocaleController.getString("LinkInUse", R.string.LinkInUse)); - } - typeInfoCell.setTag(Theme.key_windowBackgroundWhiteRedText4); - typeInfoCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText4)); - lastNameAvailable = false; - } - } - } - }); - } - }, ConnectionsManager.RequestFlagFailOnServerErrors); - } - }; - AndroidUtilities.runOnUIThread(checkRunnable, 300); - return true; - } - - private void generateLink() { - if (loadingInvite || invite != null) { - return; - } - loadingInvite = true; - TLRPC.TL_channels_exportInvite req = new TLRPC.TL_channels_exportInvite(); - req.channel = MessagesController.getInputChannel(chatId); - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(final TLObject response, final TLRPC.TL_error error) { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - if (error == null) { - invite = (TLRPC.ExportedChatInvite) response; - } - loadingInvite = false; - privateContainer.setText(invite != null ? invite.link : LocaleController.getString("Loading", R.string.Loading), false); - } - }); - } - }); - } - - @Override - public ThemeDescription[] getThemeDescriptions() { - ThemeDescription.ThemeDescriptionDelegate сellDelegate = new ThemeDescription.ThemeDescriptionDelegate() { - @Override - public void didSetColor(int color) { - if (adminnedChannelsLayout != null) { - int count = adminnedChannelsLayout.getChildCount(); - for (int a = 0; a < count; a++) { - View child = adminnedChannelsLayout.getChildAt(a); - if (child instanceof AdminedChannelCell) { - ((AdminedChannelCell) child).update(); - } - } - } - } - }; - - return new ThemeDescription[]{ - new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray), - - new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault), - new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon), - new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle), - new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector), - - new ThemeDescription(nameTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(nameTextView, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText), - - new ThemeDescription(linearLayout2, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), - new ThemeDescription(linkContainer, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), - new ThemeDescription(sectionCell, ThemeDescription.FLAG_BACKGROUNDFILTER, null, null, null, null, Theme.key_windowBackgroundGrayShadow), - new ThemeDescription(headerCell, 0, new Class[]{HeaderCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader), - new ThemeDescription(editText, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(editText, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText), - new ThemeDescription(checkTextView, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, null, null, null, null, Theme.key_windowBackgroundWhiteRedText4), - new ThemeDescription(checkTextView, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, null, null, null, null, Theme.key_windowBackgroundWhiteGrayText8), - new ThemeDescription(checkTextView, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, null, null, null, null, Theme.key_windowBackgroundWhiteGreenText), - - new ThemeDescription(typeInfoCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), - new ThemeDescription(typeInfoCell, ThemeDescription.FLAG_CHECKTAG, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4), - new ThemeDescription(typeInfoCell, ThemeDescription.FLAG_CHECKTAG, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteRedText4), - - new ThemeDescription(adminedInfoCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), - new ThemeDescription(adminnedChannelsLayout, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite), - new ThemeDescription(privateContainer, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), - new ThemeDescription(privateContainer, 0, new Class[]{TextBlockCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(loadingAdminedCell, 0, new Class[]{LoadingCell.class}, new String[]{"progressBar"}, null, null, null, Theme.key_progressCircle), - new ThemeDescription(radioButtonCell1, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), - new ThemeDescription(radioButtonCell1, ThemeDescription.FLAG_CHECKBOX, new Class[]{RadioButtonCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackground), - new ThemeDescription(radioButtonCell1, ThemeDescription.FLAG_CHECKBOXCHECK, new Class[]{RadioButtonCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackgroundChecked), - new ThemeDescription(radioButtonCell1, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{RadioButtonCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(radioButtonCell1, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{RadioButtonCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText2), - new ThemeDescription(radioButtonCell2, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), - new ThemeDescription(radioButtonCell2, ThemeDescription.FLAG_CHECKBOX, new Class[]{RadioButtonCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackground), - new ThemeDescription(radioButtonCell2, ThemeDescription.FLAG_CHECKBOXCHECK, new Class[]{RadioButtonCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackgroundChecked), - new ThemeDescription(radioButtonCell2, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{RadioButtonCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(radioButtonCell2, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{RadioButtonCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText2), - - new ThemeDescription(adminnedChannelsLayout, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{AdminedChannelCell.class}, new String[]{"nameTextView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(adminnedChannelsLayout, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{AdminedChannelCell.class}, new String[]{"statusTextView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText), - new ThemeDescription(adminnedChannelsLayout, ThemeDescription.FLAG_LINKCOLOR, new Class[]{AdminedChannelCell.class}, new String[]{"statusTextView"}, null, null, null, Theme.key_windowBackgroundWhiteLinkText), - new ThemeDescription(adminnedChannelsLayout, ThemeDescription.FLAG_IMAGECOLOR, new Class[]{AdminedChannelCell.class}, new String[]{"deleteButton"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText), - new ThemeDescription(null, 0, null, null, new Drawable[]{Theme.avatar_photoDrawable, Theme.avatar_broadcastDrawable}, сellDelegate, Theme.key_avatar_text), - new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundRed), - new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundOrange), - new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundViolet), - new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundGreen), - new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundCyan), - new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundBlue), - new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundPink), - }; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelRightsEditActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelRightsEditActivity.java new file mode 100644 index 000000000..5ee0741d4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelRightsEditActivity.java @@ -0,0 +1,757 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui; + +import android.app.DatePickerDialog; +import android.app.TimePickerDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.DatePicker; +import android.widget.FrameLayout; +import android.widget.TimePicker; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MessagesController; +import org.telegram.messenger.R; +import org.telegram.messenger.support.widget.LinearLayoutManager; +import org.telegram.messenger.support.widget.RecyclerView; +import org.telegram.tgnet.TLRPC; +import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.ActionBarMenu; +import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.ActionBar.ThemeDescription; +import org.telegram.ui.Cells.HeaderCell; +import org.telegram.ui.Cells.ShadowSectionCell; +import org.telegram.ui.Cells.TextCheckCell2; +import org.telegram.ui.Cells.TextInfoPrivacyCell; +import org.telegram.ui.Cells.TextSettingsCell; +import org.telegram.ui.Cells.UserCell; +import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.RecyclerListView; + +import java.util.Calendar; + +public class ChannelRightsEditActivity extends BaseFragment { + + private ListAdapter listViewAdapter; + private RecyclerListView listView; + + private int chatId; + private TLRPC.User currentUser; + private int currentType; + private boolean isMegagroup; + + private boolean canEdit; + private boolean isDemocracy; + + private TLRPC.TL_channelAdminRights adminRights; + private TLRPC.TL_channelAdminRights myAdminRights; + private TLRPC.TL_channelBannedRights bannedRights; + + private int rowCount; + private int changeInfoRow; + private int postMessagesRow; + private int editMesagesRow; + private int deleteMessagesRow; + private int addAdminsRow; + private int banUsersRow; + private int addUsersRow; + private int pinMessagesRow; + private int rightsShadowRow; + private int removeAdminRow; + private int removeAdminShadowRow; + private int cantEditInfoRow; + + private int viewMessagesRow; + private int sendMessagesRow; + private int sendMediaRow; + private int sendStickersRow; + private int embedLinksRow; + private int untilDateRow; + + private ChannelRightsEditActivityDelegate delegate; + + public interface ChannelRightsEditActivityDelegate { + void didSetRights(int rights, TLRPC.TL_channelAdminRights rightsAdmin, TLRPC.TL_channelBannedRights rightsBanned); + } + + private final static int done_button = 1; + + public ChannelRightsEditActivity(int userId, int channelId, TLRPC.TL_channelAdminRights rightsAdmin, TLRPC.TL_channelBannedRights rightsBanned, int type, boolean edit) { + super(); + chatId = channelId; + currentUser = MessagesController.getInstance().getUser(userId); + currentType = type; + canEdit = edit; + boolean initialIsSet; + TLRPC.Chat chat = MessagesController.getInstance().getChat(chatId); + if (chat != null) { + isMegagroup = chat.megagroup; + myAdminRights = chat.admin_rights; + } + if (myAdminRights == null) { + myAdminRights = new TLRPC.TL_channelAdminRights(); + myAdminRights.change_info = myAdminRights.post_messages = myAdminRights.edit_messages = + myAdminRights.delete_messages = myAdminRights.ban_users = myAdminRights.invite_users = + myAdminRights.invite_link = myAdminRights.pin_messages = myAdminRights.add_admins = true; + } + if (type == 0) { + adminRights = new TLRPC.TL_channelAdminRights(); + if (rightsAdmin == null) { + adminRights.change_info = myAdminRights.change_info; + adminRights.post_messages = myAdminRights.post_messages; + adminRights.edit_messages = myAdminRights.edit_messages; + adminRights.delete_messages = myAdminRights.delete_messages; + adminRights.ban_users = myAdminRights.ban_users; + adminRights.invite_users = myAdminRights.invite_users; + adminRights.invite_link = myAdminRights.invite_link; + adminRights.pin_messages = myAdminRights.pin_messages; + initialIsSet = false; + } else { + adminRights.change_info = rightsAdmin.change_info; + adminRights.post_messages = rightsAdmin.post_messages; + adminRights.edit_messages = rightsAdmin.edit_messages; + adminRights.delete_messages = rightsAdmin.delete_messages; + adminRights.ban_users = rightsAdmin.ban_users; + adminRights.invite_users = rightsAdmin.invite_users; + adminRights.invite_link = rightsAdmin.invite_link; + adminRights.pin_messages = rightsAdmin.pin_messages; + adminRights.add_admins = rightsAdmin.add_admins; + + initialIsSet = adminRights.change_info || adminRights.post_messages || adminRights.edit_messages || + adminRights.delete_messages || adminRights.ban_users || adminRights.invite_users || + adminRights.invite_link || adminRights.pin_messages || adminRights.add_admins; + } + } else { + bannedRights = new TLRPC.TL_channelBannedRights(); + if (rightsBanned == null) { + bannedRights.view_messages = bannedRights.send_media = bannedRights.send_messages = + bannedRights.embed_links = bannedRights.send_stickers = bannedRights.send_gifs = + bannedRights.send_games = bannedRights.send_inline = true; + } else { + bannedRights.view_messages = rightsBanned.view_messages; + bannedRights.send_messages = rightsBanned.send_messages; + bannedRights.send_media = rightsBanned.send_media; + bannedRights.send_stickers = rightsBanned.send_stickers; + bannedRights.send_gifs = rightsBanned.send_gifs; + bannedRights.send_games = rightsBanned.send_games; + bannedRights.send_inline = rightsBanned.send_inline; + bannedRights.embed_links = rightsBanned.embed_links; + bannedRights.until_date = rightsBanned.until_date; + } + initialIsSet = rightsBanned == null || !rightsBanned.view_messages; + } + rowCount += 3; + if (type == 0) { + if (isMegagroup) { + changeInfoRow = rowCount++; + deleteMessagesRow = rowCount++; + banUsersRow = rowCount++; + addUsersRow = rowCount++; + pinMessagesRow = rowCount++; + addAdminsRow = rowCount++; + isDemocracy = chat.democracy; + } else { + changeInfoRow = rowCount++; + postMessagesRow = rowCount++; + editMesagesRow = rowCount++; + deleteMessagesRow = rowCount++; + addUsersRow = rowCount++; + addAdminsRow = rowCount++; + } + } else if (type == 1) { + viewMessagesRow = rowCount++; + sendMessagesRow = rowCount++; + sendMediaRow = rowCount++; + sendStickersRow = rowCount++; + embedLinksRow = rowCount++; + untilDateRow = rowCount++; + } + + if (canEdit && initialIsSet) { + rightsShadowRow = rowCount++; + removeAdminRow = rowCount++; + removeAdminShadowRow = rowCount++; + cantEditInfoRow = -1; + } else { + removeAdminRow = -1; + removeAdminShadowRow = -1; + if (type == 0 && !canEdit) { + rightsShadowRow = -1; + cantEditInfoRow = rowCount++; + } else { + rightsShadowRow = rowCount++; + } + } + } + + @Override + public View createView(Context context) { + actionBar.setBackButtonImage(R.drawable.ic_ab_back); + actionBar.setAllowOverlayTitle(true); + if (currentType == 0) { + actionBar.setTitle(LocaleController.getString("EditAdmin", R.string.EditAdmin)); + } else { + actionBar.setTitle(LocaleController.getString("UserRestrictions", R.string.UserRestrictions)); + } + + actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { + @Override + public void onItemClick(int id) { + if (id == -1) { + finishFragment(); + } else if (id == done_button) { + if (currentType == 0) { + if (isMegagroup) { + adminRights.post_messages = adminRights.edit_messages = false; + } else { + adminRights.pin_messages = adminRights.ban_users = false; + } + MessagesController.setUserAdminRole(chatId, currentUser, adminRights, isMegagroup, getFragmentForAlert(1)); + if (delegate != null) { + delegate.didSetRights( + adminRights.change_info || adminRights.post_messages || adminRights.edit_messages || + adminRights.delete_messages || adminRights.ban_users || adminRights.invite_users || + adminRights.invite_link || adminRights.pin_messages || adminRights.add_admins ? 1 : 0, adminRights, bannedRights); + } + } else if (currentType == 1) { + MessagesController.setUserBannedRole(chatId, currentUser, bannedRights, isMegagroup, getFragmentForAlert(1)); + int rights; + if (bannedRights.view_messages) { + rights = 0; + } else if (bannedRights.send_messages || bannedRights.send_stickers || bannedRights.embed_links || bannedRights.send_media || + bannedRights.send_gifs || bannedRights.send_games || bannedRights.send_inline) { + rights = 1; + } else { + bannedRights.until_date = 0; + rights = 2; + } + if (delegate != null) { + delegate.didSetRights(rights, adminRights, bannedRights); + } + } + finishFragment(); + } + } + }); + + if (canEdit) { + ActionBarMenu menu = actionBar.createMenu(); + menu.addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); + } + + fragmentView = new FrameLayout(context); + fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); + FrameLayout frameLayout = (FrameLayout) fragmentView; + + listView = new RecyclerListView(context); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) { + @Override + public boolean supportsPredictiveItemAnimations() { + return false; + } + }; + listView.setItemAnimator(null); + listView.setLayoutAnimation(null); + listView.setLayoutManager(linearLayoutManager); + 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)); + + listView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + if (!canEdit) { + return; + } + if (position == 0) { + Bundle args = new Bundle(); + args.putInt("user_id", currentUser.id); + presentFragment(new ProfileActivity(args)); + } else if (position == removeAdminRow) { + if (currentType == 0) { + MessagesController.setUserAdminRole(chatId, currentUser, new TLRPC.TL_channelAdminRights(), isMegagroup, getFragmentForAlert(0)); + finishFragment(); + } else if (currentType == 1) { + bannedRights = new TLRPC.TL_channelBannedRights(); + bannedRights.view_messages = true; + bannedRights.send_media = true; + bannedRights.send_messages = true; + bannedRights.send_stickers = true; + bannedRights.send_gifs = true; + bannedRights.send_games = true; + bannedRights.send_inline = true; + bannedRights.embed_links = true; + bannedRights.until_date = 0; + MessagesController.setUserBannedRole(chatId, currentUser, bannedRights, isMegagroup, getFragmentForAlert(0)); + finishFragment(); + } + if (delegate != null) { + delegate.didSetRights(0, adminRights, bannedRights); + } + } else if (position == untilDateRow) { + if (getParentActivity() == null) { + return; + } + Calendar calendar = Calendar.getInstance(); + int year = calendar.get(Calendar.YEAR); + int monthOfYear = calendar.get(Calendar.MONTH); + int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + try { + DatePickerDialog dialog = new DatePickerDialog(getParentActivity(), new DatePickerDialog.OnDateSetListener() { + @Override + public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) { + Calendar calendar = Calendar.getInstance(); + calendar.clear(); + calendar.set(year, month, dayOfMonth); + final int time = (int) (calendar.getTime().getTime() / 1000); + try { + TimePickerDialog dialog = new TimePickerDialog(getParentActivity(), new TimePickerDialog.OnTimeSetListener() { + @Override + public void onTimeSet(TimePicker view, int hourOfDay, int minute) { + bannedRights.until_date = time + hourOfDay * 3600 + minute * 60; + listViewAdapter.notifyItemChanged(untilDateRow); + } + }, 0, 0, true); + dialog.setButton(DialogInterface.BUTTON_POSITIVE, LocaleController.getString("Set", R.string.Set), dialog); + dialog.setButton(DialogInterface.BUTTON_NEGATIVE, LocaleController.getString("Cancel", R.string.Cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }); + showDialog(dialog); + } catch (Exception e) { + FileLog.e(e); + } + } + }, year, monthOfYear, dayOfMonth); + + final DatePicker datePicker = dialog.getDatePicker(); + + Calendar date = Calendar.getInstance(); + date.setTimeInMillis(System.currentTimeMillis()); + date.set(Calendar.HOUR_OF_DAY, date.getMinimum(Calendar.HOUR_OF_DAY)); + date.set(Calendar.MINUTE, date.getMinimum(Calendar.MINUTE)); + date.set(Calendar.SECOND, date.getMinimum(Calendar.SECOND)); + date.set(Calendar.MILLISECOND, date.getMinimum(Calendar.MILLISECOND)); + datePicker.setMinDate(date.getTimeInMillis()); + + date.setTimeInMillis(System.currentTimeMillis() + 31536000000L); + date.set(Calendar.HOUR_OF_DAY, date.getMaximum(Calendar.HOUR_OF_DAY)); + date.set(Calendar.MINUTE, date.getMaximum(Calendar.MINUTE)); + date.set(Calendar.SECOND, date.getMaximum(Calendar.SECOND)); + date.set(Calendar.MILLISECOND, date.getMaximum(Calendar.MILLISECOND)); + datePicker.setMaxDate(date.getTimeInMillis()); + + dialog.setButton(DialogInterface.BUTTON_POSITIVE, LocaleController.getString("Set", R.string.Set), dialog); + dialog.setButton(DialogInterface.BUTTON_NEUTRAL, LocaleController.getString("UserRestrictionsUntilForever", R.string.UserRestrictionsUntilForever), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + bannedRights.until_date = 0; + listViewAdapter.notifyItemChanged(untilDateRow); + } + }); + dialog.setButton(DialogInterface.BUTTON_NEGATIVE, LocaleController.getString("Cancel", R.string.Cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }); + if (Build.VERSION.SDK_INT >= 21) { + dialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + int count = datePicker.getChildCount(); + for (int a = 0; a < count; a++) { + View child = datePicker.getChildAt(a); + ViewGroup.LayoutParams layoutParams = child.getLayoutParams(); + layoutParams.width = LayoutHelper.MATCH_PARENT; + child.setLayoutParams(layoutParams); + } + } + }); + } + showDialog(dialog); + } catch (Exception e) { + FileLog.e(e); + } + } else if (view instanceof TextCheckCell2) { + TextCheckCell2 checkCell = (TextCheckCell2) view; + if (!checkCell.isEnabled()) { + return; + } + checkCell.setChecked(!checkCell.isChecked()); + if (position == changeInfoRow) { + adminRights.change_info = !adminRights.change_info; + } else if (position == postMessagesRow) { + adminRights.post_messages = !adminRights.post_messages; + } else if (position == editMesagesRow) { + adminRights.edit_messages = !adminRights.edit_messages; + } else if (position == deleteMessagesRow) { + adminRights.delete_messages = !adminRights.delete_messages; + } else if (position == addAdminsRow) { + adminRights.add_admins = !adminRights.add_admins; + } else if (position == banUsersRow) { + adminRights.ban_users = !adminRights.ban_users; + } else if (position == addUsersRow) { + adminRights.invite_users = adminRights.invite_link = !adminRights.invite_users; + } else if (position == pinMessagesRow) { + adminRights.pin_messages = !adminRights.pin_messages; + } else if (bannedRights != null) { + boolean disabled = !checkCell.isChecked(); + if (position == viewMessagesRow) { + bannedRights.view_messages = !bannedRights.view_messages; + } else if (position == sendMessagesRow) { + bannedRights.send_messages = !bannedRights.send_messages; + } else if (position == sendMediaRow) { + bannedRights.send_media = !bannedRights.send_media; + }else if (position == sendStickersRow) { + bannedRights.send_stickers = bannedRights.send_games = bannedRights.send_gifs = bannedRights.send_inline = !bannedRights.send_stickers; + } else if (position == embedLinksRow) { + bannedRights.embed_links = !bannedRights.embed_links; + } + if (disabled) { + if (bannedRights.view_messages && !bannedRights.send_messages) { + bannedRights.send_messages = true; + RecyclerListView.ViewHolder holder = listView.findViewHolderForAdapterPosition(sendMessagesRow); + if (holder != null) { + ((TextCheckCell2) holder.itemView).setChecked(false); + } + } + if ((bannedRights.view_messages || bannedRights.send_messages) && !bannedRights.send_media) { + bannedRights.send_media = true; + RecyclerListView.ViewHolder holder = listView.findViewHolderForAdapterPosition(sendMediaRow); + if (holder != null) { + ((TextCheckCell2) holder.itemView).setChecked(false); + } + } + if ((bannedRights.view_messages || bannedRights.send_messages || bannedRights.send_media) && !bannedRights.send_stickers) { + bannedRights.send_stickers = bannedRights.send_games = bannedRights.send_gifs = bannedRights.send_inline = true; + RecyclerListView.ViewHolder holder = listView.findViewHolderForAdapterPosition(sendStickersRow); + if (holder != null) { + ((TextCheckCell2) holder.itemView).setChecked(false); + } + } + if ((bannedRights.view_messages || bannedRights.send_messages || bannedRights.send_media) && !bannedRights.embed_links) { + bannedRights.embed_links = true; + RecyclerListView.ViewHolder holder = listView.findViewHolderForAdapterPosition(embedLinksRow); + if (holder != null) { + ((TextCheckCell2) holder.itemView).setChecked(false); + } + } + } else { + if ((!bannedRights.send_messages || !bannedRights.embed_links || !bannedRights.send_inline || !bannedRights.send_media) && bannedRights.view_messages) { + bannedRights.view_messages = false; + RecyclerListView.ViewHolder holder = listView.findViewHolderForAdapterPosition(viewMessagesRow); + if (holder != null) { + ((TextCheckCell2) holder.itemView).setChecked(true); + } + } + if ((!bannedRights.embed_links || !bannedRights.send_inline || !bannedRights.send_media) && bannedRights.send_messages) { + bannedRights.send_messages = false; + RecyclerListView.ViewHolder holder = listView.findViewHolderForAdapterPosition(sendMessagesRow); + if (holder != null) { + ((TextCheckCell2) holder.itemView).setChecked(true); + } + } + if ((!bannedRights.send_inline || !bannedRights.embed_links) && bannedRights.send_media) { + bannedRights.send_media = false; + RecyclerListView.ViewHolder holder = listView.findViewHolderForAdapterPosition(sendMediaRow); + if (holder != null) { + ((TextCheckCell2) holder.itemView).setChecked(true); + } + } + } + } + } + } + }); + return fragmentView; + } + + @Override + public void onResume() { + super.onResume(); + if (listViewAdapter != null) { + listViewAdapter.notifyDataSetChanged(); + } + } + + public void setDelegate(ChannelRightsEditActivityDelegate channelRightsEditActivityDelegate) { + delegate = channelRightsEditActivityDelegate; + } + + private class ListAdapter extends RecyclerListView.SelectionAdapter { + + private Context mContext; + + public ListAdapter(Context context) { + mContext = context; + } + + @Override + public boolean isEnabled(RecyclerView.ViewHolder holder) { + if (!canEdit) { + return false; + } + int type = holder.getItemViewType(); + if (currentType == 0 && type == 4) { + int position = holder.getAdapterPosition(); + if (position == changeInfoRow) { + return myAdminRights.change_info; + } else if (position == postMessagesRow) { + return myAdminRights.post_messages; + } else if (position == editMesagesRow) { + return myAdminRights.edit_messages; + } else if (position == deleteMessagesRow) { + return myAdminRights.delete_messages; + } else if (position == addAdminsRow) { + return myAdminRights.add_admins; + } else if (position == banUsersRow) { + return myAdminRights.ban_users; + } else if (position == addUsersRow) { + return myAdminRights.invite_users; + } else if (position == pinMessagesRow) { + return myAdminRights.pin_messages; + } + } + return type != 3 && type != 1 && type != 5; + } + + @Override + public int getItemCount() { + return rowCount; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view; + switch (viewType) { + case 0: + view = new UserCell(mContext, 1, 0, false); + view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + break; + case 1: + view = new TextInfoPrivacyCell(mContext); + view.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + break; + case 2: + view = new TextSettingsCell(mContext); + view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + break; + case 3: + view = new HeaderCell(mContext); + view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + break; + case 4: + view = new TextCheckCell2(mContext); + view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + break; + case 5: + default: + view = new ShadowSectionCell(mContext); + break; + } + return new RecyclerListView.Holder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + switch (holder.getItemViewType()) { + case 0: + UserCell userCell = (UserCell) holder.itemView; + userCell.setData(currentUser, null, null, 0); + break; + case 1: + TextInfoPrivacyCell privacyCell = (TextInfoPrivacyCell) holder.itemView; + if (position == cantEditInfoRow) { + privacyCell.setText(LocaleController.getString("EditAdminCantEdit", R.string.EditAdminCantEdit)); + } + break; + case 2: + TextSettingsCell actionCell = (TextSettingsCell) holder.itemView; + if (position == removeAdminRow) { + actionCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText3)); + actionCell.setTag(Theme.key_windowBackgroundWhiteRedText3); + if (currentType == 0) { + actionCell.setText(LocaleController.getString("EditAdminRemoveAdmin", R.string.EditAdminRemoveAdmin), false); + } else if (currentType == 1) { + actionCell.setText(LocaleController.getString("UserRestrictionsBlock", R.string.UserRestrictionsBlock), false); + } + } else if (position == untilDateRow) { + actionCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + actionCell.setTag(Theme.key_windowBackgroundWhiteBlackText); + String value; + if (bannedRights.until_date == 0 || Math.abs(bannedRights.until_date - System.currentTimeMillis() / 1000) > 10 * 365 * 24 * 60 * 60) { + value = LocaleController.getString("UserRestrictionsUntilForever", R.string.UserRestrictionsUntilForever); + } else { + value = LocaleController.formatDateForBan(bannedRights.until_date); + } + actionCell.setTextAndValue(LocaleController.getString("UserRestrictionsUntil", R.string.UserRestrictionsUntil), value, false); + } + break; + case 3: + HeaderCell headerCell = (HeaderCell) holder.itemView; + if (currentType == 0) { + headerCell.setText(LocaleController.getString("EditAdminWhatCanDo", R.string.EditAdminWhatCanDo)); + } else if (currentType == 1) { + headerCell.setText(LocaleController.getString("UserRestrictionsCanDo", R.string.UserRestrictionsCanDo)); + } + break; + case 4: + TextCheckCell2 checkCell = (TextCheckCell2) holder.itemView; + if (position == changeInfoRow) { + if (isMegagroup) { + checkCell.setTextAndCheck(LocaleController.getString("EditAdminChangeGroupInfo", R.string.EditAdminChangeGroupInfo), adminRights.change_info, true); + } else { + checkCell.setTextAndCheck(LocaleController.getString("EditAdminChangeChannelInfo", R.string.EditAdminChangeChannelInfo), adminRights.change_info, true); + } + } else if (position == postMessagesRow) { + checkCell.setTextAndCheck(LocaleController.getString("EditAdminPostMessages", R.string.EditAdminPostMessages), adminRights.post_messages, true); + } else if (position == editMesagesRow) { + checkCell.setTextAndCheck(LocaleController.getString("EditAdminEditMessages", R.string.EditAdminEditMessages), adminRights.edit_messages, true); + } else if (position == deleteMessagesRow) { + if (isMegagroup) { + checkCell.setTextAndCheck(LocaleController.getString("EditAdminGroupDeleteMessages", R.string.EditAdminGroupDeleteMessages), adminRights.delete_messages, true); + } else { + checkCell.setTextAndCheck(LocaleController.getString("EditAdminDeleteMessages", R.string.EditAdminDeleteMessages), adminRights.delete_messages, true); + } + } else if (position == addAdminsRow) { + checkCell.setTextAndCheck(LocaleController.getString("EditAdminAddAdmins", R.string.EditAdminAddAdmins), adminRights.add_admins, false); + } else if (position == banUsersRow) { + checkCell.setTextAndCheck(LocaleController.getString("EditAdminBanUsers", R.string.EditAdminBanUsers), adminRights.ban_users, true); + } else if (position == addUsersRow) { + if (!isDemocracy) { + checkCell.setTextAndCheck(LocaleController.getString("EditAdminAddUsers", R.string.EditAdminAddUsers), adminRights.invite_users, true); + } else { + checkCell.setTextAndCheck(LocaleController.getString("EditAdminAddUsersViaLink", R.string.EditAdminAddUsersViaLink), adminRights.invite_users, true); + } + } else if (position == pinMessagesRow) { + checkCell.setTextAndCheck(LocaleController.getString("EditAdminPinMessages", R.string.EditAdminPinMessages), adminRights.pin_messages, true); + } else if (position == viewMessagesRow) { + checkCell.setTextAndCheck(LocaleController.getString("UserRestrictionsRead", R.string.UserRestrictionsRead), !bannedRights.view_messages, true); + } else if (position == sendMessagesRow) { + checkCell.setTextAndCheck(LocaleController.getString("UserRestrictionsSend", R.string.UserRestrictionsSend), !bannedRights.send_messages, true); + } else if (position == sendMediaRow) { + checkCell.setTextAndCheck(LocaleController.getString("UserRestrictionsSendMedia", R.string.UserRestrictionsSendMedia), !bannedRights.send_media, true); + } else if (position == sendStickersRow) { + checkCell.setTextAndCheck(LocaleController.getString("UserRestrictionsSendStickers", R.string.UserRestrictionsSendStickers), !bannedRights.send_stickers, true); + } else if (position == embedLinksRow) { + checkCell.setTextAndCheck(LocaleController.getString("UserRestrictionsEmbedLinks", R.string.UserRestrictionsEmbedLinks), !bannedRights.embed_links, true); + } + if (position == sendMediaRow || position == sendStickersRow || position == embedLinksRow) { + checkCell.setEnabled(!bannedRights.send_messages && !bannedRights.view_messages); + } else if (position == sendMessagesRow) { + checkCell.setEnabled(!bannedRights.view_messages); + } + break; + case 5: + ShadowSectionCell shadowCell = (ShadowSectionCell) holder.itemView; + if (position == rightsShadowRow) { + shadowCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, removeAdminRow == -1 ? R.drawable.greydivider_bottom : R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); + } else if (position == removeAdminShadowRow) { + shadowCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + } else { + shadowCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); + } + break; + } + } + + @Override + public int getItemViewType(int position) { + if (position == 0) { + return 0; + } else if (position == 1 || position == rightsShadowRow || position == removeAdminShadowRow) { + return 5; + } else if (position == 2) { + return 3; + } else if (position == changeInfoRow || position == postMessagesRow || position == editMesagesRow || position == deleteMessagesRow || position == addAdminsRow || + position == banUsersRow || position == addUsersRow || position == pinMessagesRow || + position == viewMessagesRow || position == sendMessagesRow || position == sendMediaRow || position == sendStickersRow || position == embedLinksRow) { + return 4; + } else if (position == cantEditInfoRow) { + return 1; + } else { + return 2; + } + } + } + + @Override + public ThemeDescription[] getThemeDescriptions() { + ThemeDescription.ThemeDescriptionDelegate сellDelegate = new ThemeDescription.ThemeDescriptionDelegate() { + @Override + public void didSetColor(int color) { + int count = listView.getChildCount(); + for (int a = 0; a < count; a++) { + View child = listView.getChildAt(a); + if (child instanceof UserCell) { + ((UserCell) child).update(0); + } + } + } + }; + + return new ThemeDescription[]{ + new ThemeDescription(listView, ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{UserCell.class, TextSettingsCell.class, TextCheckCell2.class, HeaderCell.class}, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray), + + new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault), + new ThemeDescription(listView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_actionBarDefault), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle), + new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector), + + new ThemeDescription(listView, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector), + + new ThemeDescription(listView, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider), + + new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), + new ThemeDescription(listView, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4), + + new ThemeDescription(listView, ThemeDescription.FLAG_CHECKTAG, new Class[]{TextSettingsCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteRedText3), + new ThemeDescription(listView, ThemeDescription.FLAG_CHECKTAG, new Class[]{TextSettingsCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(listView, 0, new Class[]{TextSettingsCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteValueText), + new ThemeDescription(listView, 0, new Class[]{TextSettingsCell.class}, new String[]{"valueImageView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayIcon), + + new ThemeDescription(listView, 0, new Class[]{TextCheckCell2.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(listView, 0, new Class[]{TextCheckCell2.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText2), + new ThemeDescription(listView, 0, new Class[]{TextCheckCell2.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchThumb), + new ThemeDescription(listView, 0, new Class[]{TextCheckCell2.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchTrack), + new ThemeDescription(listView, 0, new Class[]{TextCheckCell2.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchThumbChecked), + new ThemeDescription(listView, 0, new Class[]{TextCheckCell2.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchTrackChecked), + + new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow), + + new ThemeDescription(listView, 0, new Class[]{HeaderCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader), + + new ThemeDescription(listView, 0, new Class[]{UserCell.class}, new String[]{"nameTextView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(listView, 0, new Class[]{UserCell.class}, new String[]{"statusColor"}, null, null, сellDelegate, Theme.key_windowBackgroundWhiteGrayText), + new ThemeDescription(listView, 0, new Class[]{UserCell.class}, new String[]{"statusOnlineColor"}, null, null, сellDelegate, Theme.key_windowBackgroundWhiteBlueText), + new ThemeDescription(listView, 0, new Class[]{UserCell.class}, null, new Drawable[]{Theme.avatar_photoDrawable, Theme.avatar_broadcastDrawable}, null, Theme.key_avatar_text), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundRed), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundOrange), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundViolet), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundGreen), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundCyan), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundBlue), + new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundPink), + }; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java index 32bf10aa7..c6e827870 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java @@ -10,20 +10,33 @@ package org.telegram.ui; import android.content.Context; import android.content.DialogInterface; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.util.TypedValue; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; -import org.telegram.PhoneFormat.PhoneFormat; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ChatObject; +import org.telegram.messenger.ContactsController; import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessagesController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; import org.telegram.messenger.UserConfig; +import org.telegram.messenger.UserObject; import org.telegram.messenger.Utilities; import org.telegram.messenger.support.widget.LinearLayoutManager; import org.telegram.messenger.support.widget.RecyclerView; @@ -32,18 +45,21 @@ import org.telegram.tgnet.RequestDelegate; import org.telegram.tgnet.TLObject; 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.Theme; 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.ManageChatTextCell; import org.telegram.ui.Cells.RadioCell; import org.telegram.ui.Cells.ShadowSectionCell; -import org.telegram.ui.Cells.TextCell; import org.telegram.ui.Cells.TextInfoPrivacyCell; import org.telegram.ui.Cells.TextSettingsCell; -import org.telegram.ui.Cells.UserCell; -import org.telegram.ui.Components.AlertsCreator; +import org.telegram.ui.Cells.ManageChatUserCell; import org.telegram.ui.Components.EmptyTextProgressView; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.RecyclerListView; @@ -51,44 +67,166 @@ import org.telegram.ui.Components.RecyclerListView; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; +import java.util.Timer; +import java.util.TimerTask; public class ChannelUsersActivity extends BaseFragment implements NotificationCenter.NotificationCenterDelegate { private ListAdapter listViewAdapter; private EmptyTextProgressView emptyView; private RecyclerListView listView; + private SearchAdapter searchListViewAdapter; + private ActionBarMenuItem searchItem; + + private TLRPC.Chat currentChat; private ArrayList participants = new ArrayList<>(); + private ArrayList participants2 = new ArrayList<>(); + private HashMap participantsMap = new HashMap<>(); private int chatId; private int type; private boolean loadingUsers; private boolean firstLoaded; - private boolean isAdmin; - private boolean isModerator; - private boolean isPublic; - private boolean isMegagroup; + + private int changeAddHeaderRow; + private int changeAddRadio1Row; + private int changeAddRadio2Row; + private int changeAddSectionRow; + private int addNewRow; + private int addNew2Row; + private int addNewSectionRow; + private int restricted1SectionRow; private int participantsStartRow; + private int participantsEndRow; + private int participantsDividerRow; + private int restricted2SectionRow; + private int participants2StartRow; + private int participants2EndRow; + private int participantsInfoRow; + private int blockedEmptyRow; + private int rowCount; + private int selectType; + + private boolean firstEndReached; + + private boolean needOpenSearch; + + private boolean searchWas; + private boolean searching; + + private final static int search_button = 0; public ChannelUsersActivity(Bundle args) { super(args); chatId = arguments.getInt("chat_id"); type = arguments.getInt("type"); - TLRPC.Chat chat = MessagesController.getInstance().getChat(chatId); - if (chat != null) { - if (chat.creator) { - isAdmin = true; - isPublic = (chat.flags & TLRPC.CHAT_FLAG_IS_PUBLIC) != 0; - } else if (chat.editor) { - isModerator = true; - } - isMegagroup = chat.megagroup; + needOpenSearch = arguments.getBoolean("open_search"); + selectType = arguments.getInt("selectType"); + currentChat = MessagesController.getInstance().getChat(chatId); + } + + private void updateRows() { + currentChat = MessagesController.getInstance().getChat(chatId); + if (currentChat == null) { + return; } + changeAddHeaderRow = -1; + changeAddRadio1Row = -1; + changeAddRadio2Row = -1; + changeAddSectionRow = -1; + addNewRow = -1; + addNew2Row = -1; + addNewSectionRow = -1; + restricted1SectionRow = -1; + participantsStartRow = -1; + participantsDividerRow = -1; + participantsEndRow = -1; + restricted2SectionRow = -1; + participants2StartRow = -1; + participants2EndRow = -1; + participantsInfoRow = -1; + blockedEmptyRow = -1; + + rowCount = 0; if (type == 0) { - participantsStartRow = 0; + if (ChatObject.canBlockUsers(currentChat)) { + addNewRow = rowCount++; + if (!participants.isEmpty() || !participants2.isEmpty()) { + addNewSectionRow = rowCount++; + } + } else { + addNewRow = -1; + addNewSectionRow = -1; + } + if (!participants.isEmpty()) { + restricted1SectionRow = rowCount++; + participantsStartRow = rowCount; + rowCount += participants.size(); + participantsEndRow = rowCount; + } + if (!participants2.isEmpty()) { + if (restricted1SectionRow != -1) { + participantsDividerRow = rowCount++; + } + restricted2SectionRow = rowCount++; + participants2StartRow = rowCount; + rowCount += participants2.size(); + participants2EndRow = rowCount; + } + if (participantsStartRow != -1 || participants2StartRow != -1) { + if (searchItem != null) { + searchItem.setVisibility(View.VISIBLE); + } + participantsInfoRow = rowCount++; + } else { + if (searchItem != null) { + searchItem.setVisibility(View.INVISIBLE); + } + blockedEmptyRow = rowCount++; + } } else if (type == 1) { - participantsStartRow = isAdmin && isMegagroup ? 4 : 0; + if (currentChat.creator && currentChat.megagroup) { + changeAddHeaderRow = rowCount++; + changeAddRadio1Row = rowCount++; + changeAddRadio2Row = rowCount++; + changeAddSectionRow = rowCount++; + } + if (ChatObject.canAddAdmins(currentChat)) { + addNewRow = rowCount++; + addNewSectionRow = rowCount++; + } else { + addNewRow = -1; + addNewSectionRow = -1; + } + if (!participants.isEmpty()) { + participantsStartRow = rowCount; + rowCount += participants.size(); + participantsEndRow = rowCount; + } else { + participantsStartRow = -1; + participantsEndRow = -1; + } + participantsInfoRow = rowCount++; } else if (type == 2) { - participantsStartRow = isAdmin ? (isPublic ? 2 : 3) : 0; + if (selectType == 0 && !currentChat.megagroup && ChatObject.canAddUsers(currentChat)) { + addNewRow = rowCount++; + if ((currentChat.flags & TLRPC.CHAT_FLAG_IS_PUBLIC) == 0 && ChatObject.canAddViaLink(currentChat)) { + addNew2Row = rowCount++; + } + addNewSectionRow = rowCount++; + } + if (!participants.isEmpty()) { + participantsStartRow = rowCount; + rowCount += participants.size(); + participantsEndRow = rowCount; + } else { + participantsStartRow = -1; + participantsEndRow = -1; + } + if (rowCount != 0) { + participantsInfoRow = rowCount++; + } } } @@ -108,14 +246,25 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe @Override public View createView(Context context) { + searching = false; + searchWas = false; + actionBar.setBackButtonImage(R.drawable.ic_ab_back); actionBar.setAllowOverlayTitle(true); if (type == 0) { - actionBar.setTitle(LocaleController.getString("ChannelBlockedUsers", R.string.ChannelBlockedUsers)); + actionBar.setTitle(LocaleController.getString("ChannelBlacklist", R.string.ChannelBlacklist)); } else if (type == 1) { actionBar.setTitle(LocaleController.getString("ChannelAdministrators", R.string.ChannelAdministrators)); } else if (type == 2) { - actionBar.setTitle(LocaleController.getString("ChannelMembers", R.string.ChannelMembers)); + if (selectType == 0) { + actionBar.setTitle(LocaleController.getString("ChannelMembers", R.string.ChannelMembers)); + } else { + if (selectType == 1) { + actionBar.setTitle(LocaleController.getString("ChannelAddAdmin", R.string.ChannelAddAdmin)); + } else if (selectType == 2) { + actionBar.setTitle(LocaleController.getString("ChannelBlockUser", R.string.ChannelBlockUser)); + } + } } actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { @Override @@ -125,18 +274,56 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe } } }); + if (selectType != 0 || type == 2 || type == 0) { + searchListViewAdapter = new SearchAdapter(context); + ActionBarMenu menu = actionBar.createMenu(); + searchItem = menu.addItem(search_button, R.drawable.ic_ab_search).setIsSearchField(true).setActionBarMenuItemSearchListener(new ActionBarMenuItem.ActionBarMenuItemSearchListener() { + @Override + public void onSearchExpand() { + searching = true; + emptyView.setShowAtCenter(true); + } + + @Override + public void onSearchCollapse() { + searchListViewAdapter.searchDialogs(null); + searching = false; + searchWas = false; + listView.setAdapter(listViewAdapter); + listViewAdapter.notifyDataSetChanged(); + listView.setFastScrollVisible(true); + listView.setVerticalScrollBarEnabled(false); + emptyView.setShowAtCenter(false); + } + + @Override + public void onTextChanged(EditText editText) { + if (searchListViewAdapter == null) { + return; + } + String text = editText.getText().toString(); + if (text.length() != 0) { + searchWas = true; + if (listView != null) { + listView.setAdapter(searchListViewAdapter); + searchListViewAdapter.notifyDataSetChanged(); + listView.setFastScrollVisible(false); + listView.setVerticalScrollBarEnabled(true); + } + } + searchListViewAdapter.searchDialogs(text); + } + }); + searchItem.getSearchField().setHint(LocaleController.getString("Search", R.string.Search)); + } fragmentView = new FrameLayout(context); fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); FrameLayout frameLayout = (FrameLayout) fragmentView; emptyView = new EmptyTextProgressView(context); - if (type == 0) { - if (isMegagroup) { - emptyView.setText(LocaleController.getString("NoBlockedGroup", R.string.NoBlockedGroup)); - } else { - emptyView.setText(LocaleController.getString("NoBlocked", R.string.NoBlocked)); - } + if (type == 0 || type == 2) { + emptyView.setText(LocaleController.getString("NoResult", R.string.NoResult)); } frameLayout.addView(emptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); @@ -150,159 +337,183 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe listView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @Override public void onItemClick(View view, int position) { - if (type == 2) { - if (isAdmin) { - if (position == 0) { - Bundle args = new Bundle(); - args.putBoolean("onlyUsers", true); - args.putBoolean("destroyAfterSelect", true); - args.putBoolean("returnAsResult", true); - args.putBoolean("needForwardCount", false); - args.putString("selectAlertString", LocaleController.getString("ChannelAddTo", R.string.ChannelAddTo)); - ContactsActivity fragment = new ContactsActivity(args); - fragment.setDelegate(new ContactsActivity.ContactsActivityDelegate() { - @Override - public void didSelectContact(TLRPC.User user, String param) { - MessagesController.getInstance().addUserToChat(chatId, user, null, param != null ? Utilities.parseInt(param) : 0, null, ChannelUsersActivity.this); - } - }); - presentFragment(fragment); - } else if (!isPublic && position == 1) { - presentFragment(new GroupInviteActivity(chatId)); + if (position == addNewRow) { + if (type == 0) { + Bundle bundle = new Bundle(); + bundle.putInt("chat_id", chatId); + bundle.putInt("type", 2); + bundle.putInt("selectType", 2); + presentFragment(new ChannelUsersActivity(bundle)); + } else if (type == 1) { + Bundle bundle = new Bundle(); + bundle.putInt("chat_id", chatId); + bundle.putInt("type", 2); + bundle.putInt("selectType", 1); + presentFragment(new ChannelUsersActivity(bundle)); + } else if (type == 2) { + Bundle args = new Bundle(); + args.putBoolean("onlyUsers", true); + args.putBoolean("destroyAfterSelect", true); + args.putBoolean("returnAsResult", true); + args.putBoolean("needForwardCount", false); + args.putString("selectAlertString", LocaleController.getString("ChannelAddTo", R.string.ChannelAddTo)); + ContactsActivity fragment = new ContactsActivity(args); + fragment.setDelegate(new ContactsActivity.ContactsActivityDelegate() { + @Override + public void didSelectContact(TLRPC.User user, String param, ContactsActivity activity) { + MessagesController.getInstance().addUserToChat(chatId, user, null, param != null ? Utilities.parseInt(param) : 0, null, ChannelUsersActivity.this); + } + }); + presentFragment(fragment); + } + } else if (position == addNew2Row) { + presentFragment(new GroupInviteActivity(chatId)); + } else if (position == changeAddRadio1Row || position == changeAddRadio2Row) { + TLRPC.Chat chat = MessagesController.getInstance().getChat(chatId); + if (chat == null) { + return; + } + boolean changed = false; + if (position == 1 && !chat.democracy) { + chat.democracy = true; + changed = true; + } else if (position == 2 && chat.democracy) { + chat.democracy = false; + changed = true; + } + if (changed) { + MessagesController.getInstance().toogleChannelInvites(chatId, chat.democracy); + int count = listView.getChildCount(); + for (int a = 0; a < count; a++) { + View child = listView.getChildAt(a); + if (child instanceof RadioCell) { + int num = (Integer) child.getTag(); + ((RadioCell) child).setChecked(num == 0 && chat.democracy || num == 1 && !chat.democracy, true); + } } } - - } else if (type == 1) { - if (isAdmin) { - if (isMegagroup && (position == 1 || position == 2)) { - TLRPC.Chat chat = MessagesController.getInstance().getChat(chatId); - if (chat == null) { - return; + } else { + TLRPC.TL_channelBannedRights banned_rights = null; + TLRPC.TL_channelAdminRights admin_rights = null; + final TLRPC.ChannelParticipant participant; + int user_id = 0; + int promoted_by = 0; + boolean canEditAdmin = false; + if (listView.getAdapter() == listViewAdapter) { + participant = listViewAdapter.getItem(position); + if (participant != null) { + user_id = participant.user_id; + banned_rights = participant.banned_rights; + admin_rights = participant.admin_rights; + canEditAdmin = !(participant instanceof TLRPC.TL_channelParticipantAdmin || participant instanceof TLRPC.TL_channelParticipantCreator) || participant.can_edit; + if (participant instanceof TLRPC.TL_channelParticipantCreator) { + admin_rights = new TLRPC.TL_channelAdminRights(); + admin_rights.change_info = admin_rights.post_messages = admin_rights.edit_messages = + admin_rights.delete_messages = admin_rights.ban_users = admin_rights.invite_users = + admin_rights.invite_link = admin_rights.pin_messages = admin_rights.add_admins = true; } - boolean changed = false; - if (position == 1 && !chat.democracy) { - chat.democracy = true; - changed = true; - } else if (position == 2 && chat.democracy) { - chat.democracy = false; - changed = true; - } - if (changed) { - MessagesController.getInstance().toogleChannelInvites(chatId, chat.democracy); - int count = listView.getChildCount(); - for (int a = 0; a < count; a++) { - View child = listView.getChildAt(a); - if (child instanceof RadioCell) { - int num = (Integer) child.getTag(); - ((RadioCell) child).setChecked(num == 0 && chat.democracy || num == 1 && !chat.democracy, true); + } + } else { + TLObject object = searchListViewAdapter.getItem(position); + if (object instanceof TLRPC.User) { + TLRPC.User user = (TLRPC.User) object; + MessagesController.getInstance().putUser(user, false); + participant = participantsMap.get(user_id = user.id); + } else if (object instanceof TLRPC.ChannelParticipant) { + participant = (TLRPC.ChannelParticipant) object; + } else { + participant = null; + } + if (participant != null) { + user_id = participant.user_id; + canEditAdmin = !(participant instanceof TLRPC.TL_channelParticipantAdmin || participant instanceof TLRPC.TL_channelParticipantCreator) || participant.can_edit; + banned_rights = participant.banned_rights; + admin_rights = participant.admin_rights; + } else { + canEditAdmin = true; + } + } + if (user_id != 0) { + if (selectType != 0) { + if (currentChat.megagroup || selectType == 1) { + ChannelRightsEditActivity fragment = new ChannelRightsEditActivity(user_id, chatId, admin_rights, banned_rights, selectType == 1 ? 0 : 1, canEditAdmin); + fragment.setDelegate(new ChannelRightsEditActivity.ChannelRightsEditActivityDelegate() { + @Override + public void didSetRights(int rights, TLRPC.TL_channelAdminRights rightsAdmin, TLRPC.TL_channelBannedRights rightsBanned) { + if (participant != null) { + participant.admin_rights = rightsAdmin; + participant.banned_rights = rightsBanned; + } + removeSelfFromStack(); } - } + }); + presentFragment(fragment); + } else { + TLRPC.User user = MessagesController.getInstance().getUser(user_id); + MessagesController.getInstance().deleteUserFromChat(chatId, user, null); + finishFragment(); } - return; - } - if (position == participantsStartRow + participants.size()) { - Bundle args = new Bundle(); - args.putBoolean("onlyUsers", true); - args.putBoolean("destroyAfterSelect", true); - args.putBoolean("returnAsResult", true); - args.putBoolean("needForwardCount", false); - args.putBoolean("addingToChannel", !isMegagroup); - /*if (isMegagroup) { - args.putBoolean("allowBots", false); - }*/ - args.putString("selectAlertString", LocaleController.getString("ChannelAddUserAdminAlert", R.string.ChannelAddUserAdminAlert)); - ContactsActivity fragment = new ContactsActivity(args); - fragment.setDelegate(new ContactsActivity.ContactsActivityDelegate() { - @Override - public void didSelectContact(TLRPC.User user, String param) { - setUserChannelRole(user, new TLRPC.TL_channelRoleEditor()); + } else { + boolean canEdit = false; + if (type == 1) { + canEdit = currentChat.creator || canEditAdmin; + } else if (type == 0) { + canEdit = ChatObject.canBlockUsers(currentChat); + } + if (type != 1 && !currentChat.megagroup || type == 2 && selectType == 0) { + Bundle args = new Bundle(); + args.putInt("user_id", user_id); + presentFragment(new ProfileActivity(args)); + } else { + if (banned_rights == null) { + banned_rights = new TLRPC.TL_channelBannedRights(); + banned_rights.view_messages = true; + banned_rights.send_stickers = true; + banned_rights.send_media = true; + banned_rights.embed_links = true; + banned_rights.send_messages = true; + banned_rights.send_games = true; + banned_rights.send_inline = true; + banned_rights.send_gifs = true; } - }); - presentFragment(fragment); - return; + ChannelRightsEditActivity fragment = new ChannelRightsEditActivity(user_id, chatId, admin_rights, banned_rights, type == 1 ? 0 : 1, canEdit); + fragment.setDelegate(new ChannelRightsEditActivity.ChannelRightsEditActivityDelegate() { + @Override + public void didSetRights(int rights, TLRPC.TL_channelAdminRights rightsAdmin, TLRPC.TL_channelBannedRights rightsBanned) { + if (participant != null) { + participant.admin_rights = rightsAdmin; + participant.banned_rights = rightsBanned; + } + } + }); + presentFragment(fragment); + } } } } - TLRPC.ChannelParticipant participant = null; - if (position >= participantsStartRow && position < participants.size() + participantsStartRow) { - participant = participants.get(position - participantsStartRow); - } - if (participant != null) { - Bundle args = new Bundle(); - args.putInt("user_id", participant.user_id); - presentFragment(new ProfileActivity(args)); - } } }); - if (isAdmin || isModerator && type == 2 || isMegagroup && type == 0) { - listView.setOnItemLongClickListener(new RecyclerListView.OnItemLongClickListener() { + listView.setOnItemLongClickListener(new RecyclerListView.OnItemLongClickListener() { + @Override + public boolean onItemClick(View view, int position) { + return !(getParentActivity() == null || listView.getAdapter() != listViewAdapter) && createMenuForParticipant(listViewAdapter.getItem(position), false); + + } + }); + if (searchItem != null) { + listView.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public boolean onItemClick(View view, int position) { - if (getParentActivity() == null) { - return false; - } - TLRPC.ChannelParticipant participant = null; - if (position >= participantsStartRow && position < participants.size() + participantsStartRow) { - participant = participants.get(position - participantsStartRow); - } - if (participant != null) { - if (participant.user_id == UserConfig.getClientUserId()) { - return false; - } - final TLRPC.ChannelParticipant finalParticipant = participant; - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - CharSequence[] items = null; - if (type == 0) { - items = new CharSequence[]{LocaleController.getString("Unblock", R.string.Unblock)}; - } else if (type == 1) { - items = new CharSequence[]{LocaleController.getString("ChannelRemoveUserAdmin", R.string.ChannelRemoveUserAdmin)}; - } else if (type == 2) { - items = new CharSequence[]{LocaleController.getString("ChannelRemoveUser", R.string.ChannelRemoveUser)}; - } - builder.setItems(items, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - if (i == 0) { - if (type == 0) { - participants.remove(finalParticipant); - listViewAdapter.notifyDataSetChanged(); - TLRPC.TL_channels_kickFromChannel req = new TLRPC.TL_channels_kickFromChannel(); - req.kicked = false; - req.user_id = MessagesController.getInputUser(finalParticipant.user_id); - req.channel = MessagesController.getInputChannel(chatId); - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(TLObject response, TLRPC.TL_error error) { - if (response != null) { - final TLRPC.Updates updates = (TLRPC.Updates) response; - MessagesController.getInstance().processUpdates(updates, false); - if (!updates.chats.isEmpty()) { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - TLRPC.Chat chat = updates.chats.get(0); - MessagesController.getInstance().loadFullChat(chat.id, 0, true); - } - }, 1000); - } - } - } - }); - } else if (type == 1) { - setUserChannelRole(MessagesController.getInstance().getUser(finalParticipant.user_id), new TLRPC.TL_channelRoleEmpty()); - } else if (type == 2) { - MessagesController.getInstance().deleteUserFromChat(chatId, MessagesController.getInstance().getUser(finalParticipant.user_id), null); - } - } - } - }); - showDialog(builder.create()); - return true; - } else { - return false; + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + if (newState == RecyclerView.SCROLL_STATE_DRAGGING && searching && searchWas) { + AndroidUtilities.hideKeyboard(getParentActivity().getCurrentFocus()); } } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + } }); } @@ -311,38 +522,176 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe } else { emptyView.showTextView(); } + updateRows(); return fragmentView; } - public void setUserChannelRole(TLRPC.User user, TLRPC.ChannelParticipantRole role) { - if (user == null || role == null) { - return; + private boolean createMenuForParticipant(final TLRPC.ChannelParticipant participant, boolean resultOnly) { + if (participant == null || selectType != 0) { + return false; } - final TLRPC.TL_channels_editAdmin req = new TLRPC.TL_channels_editAdmin(); - req.channel = MessagesController.getInputChannel(chatId); - req.user_id = MessagesController.getInputUser(user); - req.role = role; - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(TLObject response, final TLRPC.TL_error error) { - if (error == null) { - MessagesController.getInstance().processUpdates((TLRPC.Updates) response, false); - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - MessagesController.getInstance().loadFullChat(chatId, 0, true); - } - }, 1000); + if (participant.user_id == UserConfig.getClientUserId()) { + return false; + } + if (type == 2) { + final TLRPC.ChannelParticipant channelParticipant; + + final TLRPC.User user = MessagesController.getInstance().getUser(participant.user_id); + boolean allowSetAdmin = participant instanceof TLRPC.TL_channelParticipant || participant instanceof TLRPC.TL_channelParticipantBanned; + boolean canEditAdmin = !(participant instanceof TLRPC.TL_channelParticipantAdmin || participant instanceof TLRPC.TL_channelParticipantCreator) || participant.can_edit; + + final ArrayList items; + final ArrayList actions; + if (!resultOnly) { + items = new ArrayList<>(); + actions = new ArrayList<>(); + } else { + items = null; + actions = null; + } + + if (allowSetAdmin && ChatObject.canAddAdmins(currentChat)) { + if (resultOnly) { + return true; + } + items.add(LocaleController.getString("SetAsAdmin", R.string.SetAsAdmin)); + actions.add(0); + } + if (ChatObject.canBlockUsers(currentChat) && canEditAdmin) { + if (resultOnly) { + return true; + } + if (currentChat.megagroup) { + items.add(LocaleController.getString("KickFromSupergroup", R.string.KickFromSupergroup)); + actions.add(1); + items.add(LocaleController.getString("KickFromGroup", R.string.KickFromGroup)); + actions.add(2); } else { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - AlertsCreator.processError(error, ChannelUsersActivity.this, req, !isMegagroup); - } - }); + items.add(LocaleController.getString("ChannelRemoveUser", R.string.ChannelRemoveUser)); + actions.add(2); } } - }); + if (actions == null || actions.isEmpty()) { + return false; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setItems(items.toArray(new CharSequence[actions.size()]), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, final int i) { + if (actions.get(i) == 2) { + MessagesController.getInstance().deleteUserFromChat(chatId, user, null); + for (int a = 0; a < participants.size(); a++) { + TLRPC.ChannelParticipant p = participants.get(a); + if (p.user_id == participant.user_id) { + participants.remove(a); + updateRows(); + listViewAdapter.notifyDataSetChanged(); + break; + } + } + } else { + ChannelRightsEditActivity fragment = new ChannelRightsEditActivity(user.id, chatId, participant.admin_rights, participant.banned_rights, actions.get(i), true); + fragment.setDelegate(new ChannelRightsEditActivity.ChannelRightsEditActivityDelegate() { + @Override + public void didSetRights(int rights, TLRPC.TL_channelAdminRights rightsAdmin, TLRPC.TL_channelBannedRights rightsBanned) { + if (actions.get(i) == 0) { + for (int a = 0; a < participants.size(); a++) { + TLRPC.ChannelParticipant p = participants.get(a); + if (p.user_id == participant.user_id) { + TLRPC.ChannelParticipant newPart; + if (rights == 1) { + newPart = new TLRPC.TL_channelParticipantAdmin(); + } else { + newPart = new TLRPC.TL_channelParticipant(); + } + newPart.admin_rights = rightsAdmin; + newPart.banned_rights = rightsBanned; + newPart.inviter_id = UserConfig.getClientUserId(); + newPart.user_id = participant.user_id; + newPart.date = participant.date; + participants.set(a, newPart); + break; + } + } + } else if (actions.get(i) == 1) { + if (rights == 0) { + for (int a = 0; a < participants.size(); a++) { + TLRPC.ChannelParticipant p = participants.get(a); + if (p.user_id == participant.user_id) { + participants.remove(a); + updateRows(); + listViewAdapter.notifyDataSetChanged(); + break; + } + } + } + } + } + }); + presentFragment(fragment); + } + } + }); + showDialog(builder.create()); + } else { + CharSequence[] items = null; + if (type == 0 && ChatObject.canBlockUsers(currentChat)) { + if (resultOnly) { + return true; + } + items = new CharSequence[]{LocaleController.getString("Unban", R.string.Unban)}; + } else if (type == 1 && ChatObject.canAddAdmins(currentChat) && participant.can_edit) { + if (resultOnly) { + return true; + } + items = new CharSequence[]{LocaleController.getString("ChannelRemoveUserAdmin", R.string.ChannelRemoveUserAdmin)}; + } + if (items == null) { + return false; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setItems(items, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + if (i == 0) { + if (type == 0) { + participants.remove(participant); + updateRows(); + listViewAdapter.notifyDataSetChanged(); + TLRPC.TL_channels_editBanned req = new TLRPC.TL_channels_editBanned(); + req.user_id = MessagesController.getInputUser(participant.user_id); + req.channel = MessagesController.getInputChannel(chatId); + req.banned_rights = new TLRPC.TL_channelBannedRights(); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (response != null) { + final TLRPC.Updates updates = (TLRPC.Updates) response; + MessagesController.getInstance().processUpdates(updates, false); + if (!updates.chats.isEmpty()) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + TLRPC.Chat chat = updates.chats.get(0); + MessagesController.getInstance().loadFullChat(chat.id, 0, true); + } + }, 1000); + } + } + } + }); + } else if (type == 1) { + MessagesController.setUserAdminRole(chatId, MessagesController.getInstance().getUser(participant.user_id), new TLRPC.TL_channelAdminRights(), currentChat.megagroup, ChannelUsersActivity.this); + } else if (type == 2) { + MessagesController.getInstance().deleteUserFromChat(chatId, MessagesController.getInstance().getUser(participant.user_id), null); + } + } + } + }); + showDialog(builder.create()); + } + return true; } @Override @@ -353,6 +702,7 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { + firstEndReached = false; getChannelParticipants(0, 200); } }); @@ -363,7 +713,7 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe private int getChannelAdminParticipantType(TLRPC.ChannelParticipant participant) { if (participant instanceof TLRPC.TL_channelParticipantCreator || participant instanceof TLRPC.TL_channelParticipantSelf) { return 0; - } else if (participant instanceof TLRPC.TL_channelParticipantEditor) { + } else if (participant instanceof TLRPC.TL_channelParticipantAdmin) { return 1; } else { return 2; @@ -384,12 +734,17 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe TLRPC.TL_channels_getParticipants req = new TLRPC.TL_channels_getParticipants(); req.channel = MessagesController.getInputChannel(chatId); if (type == 0) { - req.filter = new TLRPC.TL_channelParticipantsKicked(); + if (firstEndReached) { + req.filter = new TLRPC.TL_channelParticipantsKicked(); + } else { + req.filter = new TLRPC.TL_channelParticipantsBanned(); + } } else if (type == 1) { req.filter = new TLRPC.TL_channelParticipantsAdmins(); } else if (type == 2) { req.filter = new TLRPC.TL_channelParticipantsRecent(); } + req.filter.q = ""; req.offset = offset; req.limit = count; int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @@ -398,13 +753,47 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { + boolean changeFirst = !firstLoaded; + loadingUsers = false; + firstLoaded = true; + if (emptyView != null) { + emptyView.showTextView(); + } if (error == null) { TLRPC.TL_channels_channelParticipants res = (TLRPC.TL_channels_channelParticipants) response; MessagesController.getInstance().putUsers(res.users, false); - participants = res.participants; + int selfId = UserConfig.getClientUserId(); + if (selectType != 0) { + for (int a = 0; a < res.participants.size(); a++) { + if (res.participants.get(a).user_id == selfId) { + res.participants.remove(a); + break; + } + } + } + if (type == 0) { + if (firstEndReached) { + participants2 = res.participants; + } else { + participantsMap.clear(); + participants = res.participants; + if (changeFirst) { + firstLoaded = false; + } + firstEndReached = true; + getChannelParticipants(0, 200); + } + } else { + participantsMap.clear(); + participants = res.participants; + } + for (int a = 0; a < res.participants.size(); a++) { + TLRPC.ChannelParticipant participant = res.participants.get(a); + participantsMap.put(participant.user_id, participant); + } try { if (type == 0 || type == 2) { - Collections.sort(participants, new Comparator() { + Collections.sort(res.participants, new Comparator() { @Override public int compare(TLRPC.ChannelParticipant lhs, TLRPC.ChannelParticipant rhs) { TLRPC.User user1 = MessagesController.getInstance().getUser(rhs.user_id); @@ -466,11 +855,7 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe FileLog.e(e); } } - loadingUsers = false; - firstLoaded = true; - if (emptyView != null) { - emptyView.showTextView(); - } + updateRows(); if (listViewAdapter != null) { listViewAdapter.notifyDataSetChanged(); } @@ -489,6 +874,417 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe } } + @Override + protected void onTransitionAnimationEnd(boolean isOpen, boolean backward) { + if (isOpen && !backward && needOpenSearch) { + searchItem.openSearch(true); + } + } + + private class SearchAdapter extends RecyclerListView.SelectionAdapter { + + private Context mContext; + private ArrayList searchResult = new ArrayList<>(); + private ArrayList searchResultNames = new ArrayList<>(); + private SearchAdapterHelper searchAdapterHelper; + private Timer searchTimer; + + private int groupStartRow; + private int group2StartRow; + private int contactsStartRow; + private int globalStartRow; + private int totalCount; + + public SearchAdapter(Context context) { + mContext = context; + searchAdapterHelper = new SearchAdapterHelper(); + searchAdapterHelper.setDelegate(new SearchAdapterHelper.SearchAdapterHelperDelegate() { + @Override + public void onDataSetChanged() { + notifyDataSetChanged(); + } + + @Override + public void onSetHashtags(ArrayList arrayList, HashMap hashMap) { + + } + }); + } + + public void searchDialogs(final String query) { + try { + if (searchTimer != null) { + searchTimer.cancel(); + } + } catch (Exception e) { + FileLog.e(e); + } + if (query == null) { + searchResult.clear(); + searchResultNames.clear(); + searchAdapterHelper.queryServerSearch(null, type != 0, false, true, true, chatId, type == 0); + 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); + } + } + + private void processSearch(final String query) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + searchAdapterHelper.queryServerSearch(query, selectType != 0, false, true, true, chatId, type == 0); + if (selectType == 1) { + final ArrayList contactsCopy = new ArrayList<>(); + contactsCopy.addAll(ContactsController.getInstance().contacts); + Utilities.searchQueue.postRunnable(new Runnable() { + @Override + public void run() { + String search1 = query.trim().toLowerCase(); + if (search1.length() == 0) { + updateSearchResults(new ArrayList(), new ArrayList()); + return; + } + String search2 = LocaleController.getInstance().getTranslitString(search1); + if (search1.equals(search2) || search2.length() == 0) { + search2 = null; + } + String search[] = new String[1 + (search2 != null ? 1 : 0)]; + search[0] = search1; + if (search2 != null) { + search[1] = search2; + } + + ArrayList resultArray = new ArrayList<>(); + ArrayList resultArrayNames = new ArrayList<>(); + + for (int a = 0; a < contactsCopy.size(); a++) { + TLRPC.TL_contact contact = contactsCopy.get(a); + TLRPC.User user = MessagesController.getInstance().getUser(contact.user_id); + if (user.id == UserConfig.getClientUserId()) { + continue; + } + + String name = ContactsController.formatName(user.first_name, user.last_name).toLowerCase(); + String tName = LocaleController.getInstance().getTranslitString(name); + if (name.equals(tName)) { + tName = null; + } + + int found = 0; + for (String q : search) { + if (name.startsWith(q) || name.contains(" " + q) || tName != null && (tName.startsWith(q) || tName.contains(" " + q))) { + found = 1; + } else if (user.username != null && user.username.startsWith(q)) { + found = 2; + } + + if (found != 0) { + if (found == 1) { + resultArrayNames.add(AndroidUtilities.generateSearchName(user.first_name, user.last_name, q)); + } else { + resultArrayNames.add(AndroidUtilities.generateSearchName("@" + user.username, null, "@" + q)); + } + resultArray.add(user); + break; + } + } + } + + updateSearchResults(resultArray, resultArrayNames); + } + }); + } + } + }); + } + + private void updateSearchResults(final ArrayList users, final ArrayList names) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + searchResult = users; + searchResultNames = names; + notifyDataSetChanged(); + } + }); + } + + @Override + public boolean isEnabled(RecyclerView.ViewHolder holder) { + return holder.getItemViewType() != 1; + } + + @Override + public int getItemCount() { + int contactsCount = searchResult.size(); + int globalCount = searchAdapterHelper.getGlobalSearch().size(); + int groupsCount = searchAdapterHelper.getGroupSearch().size(); + int groupsCount2 = searchAdapterHelper.getGroupSearch2().size(); + int count = 0; + if (contactsCount != 0) { + count += contactsCount + 1; + } + if (globalCount != 0) { + count += globalCount + 1; + } + if (groupsCount != 0) { + count += groupsCount + 1; + } + if (groupsCount2 != 0) { + count += groupsCount2 + 1; + } + return count; + } + + @Override + public void notifyDataSetChanged() { + totalCount = 0; + int count = searchAdapterHelper.getGroupSearch().size(); + if (count != 0) { + groupStartRow = 0; + totalCount += count + 1; + } else { + groupStartRow = -1; + } + count = searchAdapterHelper.getGroupSearch2().size(); + if (count != 0) { + group2StartRow = totalCount; + totalCount += count + 1; + } else { + group2StartRow = -1; + } + count = searchResult.size(); + if (count != 0) { + contactsStartRow = totalCount; + totalCount += count + 1; + } else { + contactsStartRow = -1; + } + count = searchAdapterHelper.getGlobalSearch().size(); + if (count != 0) { + globalStartRow = totalCount; + totalCount += count + 1; + } else { + globalStartRow = -1; + } + super.notifyDataSetChanged(); + } + + public TLObject getItem(int i) { + int count = searchAdapterHelper.getGroupSearch().size(); + if (count != 0) { + if (count + 1 > i) { + if (i == 0) { + return null; + } else { + return searchAdapterHelper.getGroupSearch().get(i - 1); + } + } else { + i -= count + 1; + } + } + count = searchAdapterHelper.getGroupSearch2().size(); + if (count != 0) { + if (count + 1 > i) { + if (i == 0) { + return null; + } else { + return searchAdapterHelper.getGroupSearch2().get(i - 1); + } + } else { + i -= count + 1; + } + } + count = searchResult.size(); + if (count != 0) { + if (count + 1 > i) { + if (i == 0) { + return null; + } else { + return searchResult.get(i - 1); + } + } else { + i -= count + 1; + } + } + count = searchAdapterHelper.getGlobalSearch().size(); + if (count != 0) { + if (count + 1 > i) { + if (i == 0) { + return null; + } else { + return searchAdapterHelper.getGlobalSearch().get(i - 1); + } + } + } + return null; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view; + switch (viewType) { + case 0: + view = new ManageChatUserCell(mContext, 2, selectType == 0); + view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + ((ManageChatUserCell) view).setDelegate(new ManageChatUserCell.ManageChatUserCellDelegate() { + @Override + public boolean onOptionsButtonCheck(ManageChatUserCell cell, boolean click) { + TLObject object = getItem((Integer) cell.getTag()); + if (object instanceof TLRPC.ChannelParticipant) { + TLRPC.ChannelParticipant participant = (TLRPC.ChannelParticipant) getItem((Integer) cell.getTag()); + return createMenuForParticipant(participant, !click); + } else { + return false; + } + } + }); + break; + case 1: + default: + view = new GraySectionCell(mContext); + break; + } + return new RecyclerListView.Holder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + switch (holder.getItemViewType()) { + case 0: { + TLObject object = getItem(position); + TLRPC.User user; + if (object instanceof TLRPC.User) { + user = (TLRPC.User) object; + } else { + user = MessagesController.getInstance().getUser(((TLRPC.ChannelParticipant) object).user_id); + } + + String un = user.username; + CharSequence username = null; + CharSequence name = null; + + int count = searchAdapterHelper.getGroupSearch().size(); + boolean ok = false; + String nameSearch = null; + if (count != 0) { + if (count + 1 > position) { + nameSearch = searchAdapterHelper.getLastFoundChannel(); + ok = true; + } else { + position -= count + 1; + } + } + if (!ok) { + count = searchAdapterHelper.getGroupSearch2().size(); + if (count != 0) { + if (count + 1 > position) { + nameSearch = searchAdapterHelper.getLastFoundChannel2(); + } else { + position -= count + 1; + } + } + } + if (!ok) { + count = searchResult.size(); + if (count != 0) { + if (count + 1 > position) { + ok = true; + name = searchResultNames.get(position - 1); + if (name != null && un != null && un.length() > 0) { + if (name.toString().startsWith("@" + un)) { + username = name; + name = null; + } + } + } else { + position -= count + 1; + } + } + } + if (!ok) { + count = searchAdapterHelper.getGlobalSearch().size(); + if (count != 0) { + if (count + 1 > position) { + String foundUserName = searchAdapterHelper.getLastFoundUsername(); + if (foundUserName.startsWith("@")) { + foundUserName = foundUserName.substring(1); + } + try { + username = new SpannableStringBuilder(un); + ((SpannableStringBuilder) username).setSpan(new ForegroundColorSpan(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText4)), 0, foundUserName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } catch (Exception e) { + username = un; + FileLog.e(e); + } + } + } + } + + if (nameSearch != null) { + String u = UserObject.getUserName(user); + name = new SpannableStringBuilder(u); + int idx = u.toLowerCase().indexOf(nameSearch); + if (idx != -1) { + ((SpannableStringBuilder) name).setSpan(new ForegroundColorSpan(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText4)), idx, idx + nameSearch.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + ManageChatUserCell userCell = (ManageChatUserCell) holder.itemView; + userCell.setTag(position); + userCell.setData(user, name, username); + + break; + } + case 1: { + GraySectionCell sectionCell = (GraySectionCell) holder.itemView; + if (position == groupStartRow) { + if (type == 0) { + sectionCell.setText(LocaleController.getString("ChannelRestrictedUsers", R.string.ChannelRestrictedUsers).toUpperCase()); + } else { + sectionCell.setText(LocaleController.getString("ChannelMembers", R.string.ChannelMembers).toUpperCase()); + } + } else if (position == group2StartRow) { + sectionCell.setText(LocaleController.getString("ChannelBlockedUsers", R.string.ChannelBlockedUsers).toUpperCase()); + } else if (position == globalStartRow) { + sectionCell.setText(LocaleController.getString("GlobalSearch", R.string.GlobalSearch).toUpperCase()); + } else if (position == contactsStartRow) { + sectionCell.setText(LocaleController.getString("Contacts", R.string.Contacts).toUpperCase()); + } + break; + } + } + } + + @Override + public void onViewRecycled(RecyclerView.ViewHolder holder) { + if (holder.itemView instanceof ManageChatUserCell) { + ((ManageChatUserCell) holder.itemView).recycle(); + } + } + + @Override + public int getItemViewType(int i) { + if (i == globalStartRow || i == groupStartRow || i == contactsStartRow || i == group2StartRow) { + return 1; + } + return 0; + } + } + private class ListAdapter extends RecyclerListView.SelectionAdapter { private Context mContext; @@ -499,43 +1295,16 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe @Override public boolean isEnabled(RecyclerView.ViewHolder holder) { - int postion = holder.getAdapterPosition(); - if (type == 2) { - if (isAdmin) { - if (!isPublic) { - if (postion == 0 || postion == 1) { - return true; - } else if (postion == 2) { - return false; - } - } else { - if (postion == 0) { - return true; - } else if (postion == 1) { - return false; - } - } - } - } else if (type == 1) { - if (postion == participantsStartRow + participants.size()) { - return isAdmin; - } else if (postion == participantsStartRow + participants.size() + 1) { - return false; - } else if (isMegagroup && isAdmin && postion < 4) { - return postion == 1 || postion == 2; - } - } - return postion != participants.size() + participantsStartRow && participants.get(postion - participantsStartRow).user_id != UserConfig.getClientUserId(); + int type = holder.getItemViewType(); + return type == 0 || type == 2 || type == 6; } @Override public int getItemCount() { - if (participants.isEmpty() && type == 0 || loadingUsers && !firstLoaded) { + if (loadingUsers && !firstLoaded) { return 0; - } else if (type == 1) { - return participants.size() + (isAdmin ? 2 : 1) + (isAdmin && isMegagroup ? 4 : 0); } - return participants.size() + participantsStartRow + 1; + return rowCount; } @Override @@ -543,22 +1312,66 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe View view; switch (viewType) { case 0: - view = new UserCell(mContext, 1, 0, false); + view = new ManageChatUserCell(mContext, type == 0 ? 8 : 1, selectType == 0); view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + ((ManageChatUserCell) view).setDelegate(new ManageChatUserCell.ManageChatUserCellDelegate() { + @Override + public boolean onOptionsButtonCheck(ManageChatUserCell cell, boolean click) { + TLRPC.ChannelParticipant participant = listViewAdapter.getItem((Integer) cell.getTag()); + return createMenuForParticipant(participant, !click); + } + }); break; case 1: view = new TextInfoPrivacyCell(mContext); break; case 2: - view = new TextSettingsCell(mContext); + view = new ManageChatTextCell(mContext); view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); break; case 3: view = new ShadowSectionCell(mContext); break; case 4: - view = new TextCell(mContext); - view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + view = new FrameLayout(mContext) { + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec) - AndroidUtilities.dp(56), MeasureSpec.EXACTLY)); + } + }; + FrameLayout frameLayout = (FrameLayout) view; + frameLayout.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + + LinearLayout linearLayout = new LinearLayout(mContext); + linearLayout.setOrientation(LinearLayout.VERTICAL); + frameLayout.addView(linearLayout, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER, 20, 0, 20, 0)); + + ImageView imageView = new ImageView(mContext); + imageView.setImageResource(R.drawable.group_ban_empty); + imageView.setScaleType(ImageView.ScaleType.CENTER); + imageView.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_emptyListPlaceholder), PorterDuff.Mode.MULTIPLY)); + linearLayout.addView(imageView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL)); + + TextView textView = new TextView(mContext); + textView.setText(LocaleController.getString("NoBlockedUsers", R.string.NoBlockedUsers)); + textView.setTextColor(Theme.getColor(Theme.key_emptyListPlaceholder)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setGravity(Gravity.CENTER_HORIZONTAL); + textView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + linearLayout.addView(textView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL, 0, 10, 0, 0)); + + textView = new TextView(mContext); + if (currentChat.megagroup) { + textView.setText(LocaleController.getString("NoBlockedGroup", R.string.NoBlockedGroup)); + } else { + textView.setText(LocaleController.getString("NoBlockedChannel", R.string.NoBlockedChannel)); + } + textView.setTextColor(Theme.getColor(Theme.key_emptyListPlaceholder)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + textView.setGravity(Gravity.CENTER_HORIZONTAL); + linearLayout.addView(textView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL, 0, 10, 0, 0)); + + view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); break; case 5: view = new HeaderCell(mContext); @@ -577,83 +1390,107 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (holder.getItemViewType()) { case 0: - UserCell userCell = (UserCell) holder.itemView; - TLRPC.ChannelParticipant participant = participants.get(position - participantsStartRow); + ManageChatUserCell userCell = (ManageChatUserCell) holder.itemView; + userCell.setTag(position); + TLRPC.ChannelParticipant participant = getItem(position); TLRPC.User user = MessagesController.getInstance().getUser(participant.user_id); if (user != null) { if (type == 0) { - userCell.setData(user, null, user.phone != null && user.phone.length() != 0 ? PhoneFormat.getInstance().format("+" + user.phone) : LocaleController.getString("NumberUnknown", R.string.NumberUnknown), 0); + String role = null; + if (participant instanceof TLRPC.TL_channelParticipantBanned) { + TLRPC.User user1 = MessagesController.getInstance().getUser(participant.kicked_by); + if (user1 != null) { + role = LocaleController.formatString("UserRestrictionsBy", R.string.UserRestrictionsBy, ContactsController.formatName(user1.first_name, user1.last_name)); + } + } + userCell.setData(user, null, role); } else if (type == 1) { String role = null; if (participant instanceof TLRPC.TL_channelParticipantCreator || participant instanceof TLRPC.TL_channelParticipantSelf) { role = LocaleController.getString("ChannelCreator", R.string.ChannelCreator); - } else if (participant instanceof TLRPC.TL_channelParticipantModerator) { - role = LocaleController.getString("ChannelModerator", R.string.ChannelModerator); - } else if (participant instanceof TLRPC.TL_channelParticipantEditor) { - role = LocaleController.getString("ChannelEditor", R.string.ChannelEditor); + } else if (participant instanceof TLRPC.TL_channelParticipantAdmin) { + TLRPC.User user1 = MessagesController.getInstance().getUser(participant.promoted_by); + if (user1 != null) { + role = LocaleController.formatString("EditAdminPromotedBy", R.string.EditAdminPromotedBy, ContactsController.formatName(user1.first_name, user1.last_name)); + } } - userCell.setData(user, null, role, 0); + userCell.setData(user, null, role); } else if (type == 2) { - userCell.setData(user, null, null, 0); + userCell.setData(user, null, null); } } break; case 1: TextInfoPrivacyCell privacyCell = (TextInfoPrivacyCell) holder.itemView; - if (type == 0) { - privacyCell.setText(String.format("%1$s\n\n%2$s", LocaleController.getString("NoBlockedGroup", R.string.NoBlockedGroup), LocaleController.getString("UnblockText", R.string.UnblockText))); - privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); - } else if (type == 1) { - if (isAdmin) { - if (isMegagroup) { - privacyCell.setText(LocaleController.getString("MegaAdminsInfo", R.string.MegaAdminsInfo)); + if (position == participantsInfoRow) { + if (type == 0) { + if (ChatObject.canBlockUsers(currentChat)) { + if (currentChat.megagroup) { + privacyCell.setText(String.format("%1$s\n\n%2$s", LocaleController.getString("NoBlockedGroup", R.string.NoBlockedGroup), LocaleController.getString("UnbanText", R.string.UnbanText))); + } else { + privacyCell.setText(String.format("%1$s\n\n%2$s", LocaleController.getString("NoBlockedChannel", R.string.NoBlockedChannel), LocaleController.getString("UnbanText", R.string.UnbanText))); + } } else { - privacyCell.setText(LocaleController.getString("ChannelAdminsInfo", R.string.ChannelAdminsInfo)); + if (currentChat.megagroup) { + privacyCell.setText(LocaleController.getString("NoBlockedGroup", R.string.NoBlockedGroup)); + } else { + privacyCell.setText(LocaleController.getString("NoBlockedChannel", R.string.NoBlockedChannel)); + } } 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)); - } - } else if (type == 2) { - if ((!isPublic && position == 2 || position == 1) && isAdmin) { - if (isMegagroup) { + } else if (type == 1) { + if (addNewRow != -1) { + if (currentChat.megagroup) { + privacyCell.setText(LocaleController.getString("MegaAdminsInfo", R.string.MegaAdminsInfo)); + } else { + privacyCell.setText(LocaleController.getString("ChannelAdminsInfo", R.string.ChannelAdminsInfo)); + } + 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)); + } + } else if (type == 2) { + if (currentChat.megagroup || selectType != 0) { privacyCell.setText(""); } else { privacyCell.setText(LocaleController.getString("ChannelMembersInfo", R.string.ChannelMembersInfo)); } - privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); - } else { - privacyCell.setText(""); privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); } } break; case 2: - TextSettingsCell actionCell = (TextSettingsCell) holder.itemView; - if (type == 2) { - if (position == 0) { - actionCell.setText(LocaleController.getString("AddMember", R.string.AddMember), true); - } else if (position == 1) { - actionCell.setText(LocaleController.getString("ChannelInviteViaLink", R.string.ChannelInviteViaLink), false); + ManageChatTextCell actionCell = (ManageChatTextCell) holder.itemView; + if (position == addNewRow) { + if (type == 0) { + actionCell.setText(LocaleController.getString("ChannelBlockUser", R.string.ChannelBlockUser), null, R.drawable.group_ban_new, false); + } else if (type == 1) { + actionCell.setText(LocaleController.getString("ChannelAddAdmin", R.string.ChannelAddAdmin), null, R.drawable.group_admin_new, false); + } else if (type == 2) { + actionCell.setText(LocaleController.getString("AddMember", R.string.AddMember), null, R.drawable.menu_invite, true); } - } else if (type == 1) { - actionCell.setTextAndIcon(LocaleController.getString("ChannelAddAdmin", R.string.ChannelAddAdmin), R.drawable.managers, false); + } else if (position == addNew2Row) { + actionCell.setText(LocaleController.getString("ChannelInviteViaLink", R.string.ChannelInviteViaLink), null, R.drawable.msg_panel_link, false); } break; - case 4: - ((TextCell) holder.itemView).setTextAndIcon(LocaleController.getString("ChannelAddAdmin", R.string.ChannelAddAdmin), R.drawable.managers); - break; case 5: - ((HeaderCell) holder.itemView).setText(LocaleController.getString("WhoCanAddMembers", R.string.WhoCanAddMembers)); + HeaderCell headerCell = (HeaderCell) holder.itemView; + if (position == restricted1SectionRow) { + headerCell.setText(LocaleController.getString("ChannelRestrictedUsers", R.string.ChannelRestrictedUsers)); + } else if (position == restricted2SectionRow) { + headerCell.setText(LocaleController.getString("ChannelBlockedUsers", R.string.ChannelBlockedUsers)); + } else if (position == changeAddHeaderRow) { + headerCell.setText(LocaleController.getString("WhoCanAddMembers", R.string.WhoCanAddMembers)); + } break; case 6: RadioCell radioCell = (RadioCell) holder.itemView; TLRPC.Chat chat = MessagesController.getInstance().getChat(chatId); - if (position == 1) { + if (position == changeAddRadio1Row) { radioCell.setTag(0); radioCell.setText(LocaleController.getString("WhoCanAddMembersAllMembers", R.string.WhoCanAddMembersAllMembers), chat != null && chat.democracy, true); - } else if (position == 2) { + } else if (position == changeAddRadio2Row) { radioCell.setTag(1); radioCell.setText(LocaleController.getString("WhoCanAddMembersAdmins", R.string.WhoCanAddMembersAdmins), chat != null && !chat.democracy, false); } @@ -662,46 +1499,40 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe } @Override - public int getItemViewType(int i) { - if (type == 1) { - if (isAdmin) { - if (isMegagroup) { - if (i == 0) { - return 5; - } else if (i == 1 || i == 2) { - return 6; - } else if (i == 3) { - return 3; - } - } - if (i == participantsStartRow + participants.size()) { - return 4; - } else if (i == participantsStartRow + participants.size() + 1) { - return 1; - } - } - } else if (type == 2) { - if (isAdmin) { - if (!isPublic) { - if (i == 0 || i == 1) { - return 2; - } else if (i == 2) { - return 1; - } - } else { - if (i == 0) { - return 2; - } else if (i == 1) { - return 1; - } - } - } + public void onViewRecycled(RecyclerView.ViewHolder holder) { + if (holder.itemView instanceof ManageChatUserCell) { + ((ManageChatUserCell) holder.itemView).recycle(); } - if (i == participants.size() + participantsStartRow) { + } + + @Override + public int getItemViewType(int position) { + if (position == addNewRow || position == addNew2Row) { + return 2; + } else if (position >= participantsStartRow && position < participantsEndRow || position >= participants2StartRow && position < participants2EndRow) { + return 0; + } else if (position == addNewSectionRow || position == changeAddSectionRow || position == participantsDividerRow) { + return 3; + } else if (position == participantsInfoRow) { return 1; + } else if (position == changeAddHeaderRow || position == restricted1SectionRow || position == restricted2SectionRow) { + return 5; + } else if (position == changeAddRadio1Row || position == changeAddRadio2Row) { + return 6; + } else if (position == blockedEmptyRow) { + return 4; } return 0; } + + public TLRPC.ChannelParticipant getItem(int position) { + if (participantsStartRow != -1 && position >= participantsStartRow && position < participantsEndRow) { + return participants.get(position - participantsStartRow); + } else if (participants2StartRow != -1 && position >= participants2StartRow && position < participants2EndRow) { + return participants2.get(position - participants2StartRow); + } + return null; + } } @Override @@ -712,15 +1543,15 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe int count = listView.getChildCount(); for (int a = 0; a < count; a++) { View child = listView.getChildAt(a); - if (child instanceof UserCell) { - ((UserCell) child).update(0); + if (child instanceof ManageChatUserCell) { + ((ManageChatUserCell) child).update(0); } } } }; return new ThemeDescription[]{ - new ThemeDescription(listView, ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{UserCell.class, TextSettingsCell.class, TextCell.class, RadioCell.class}, null, null, null, Theme.key_windowBackgroundWhite), + new ThemeDescription(listView, ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{ManageChatUserCell.class, TextSettingsCell.class, ManageChatTextCell.class, RadioCell.class}, null, null, null, Theme.key_windowBackgroundWhite), new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray), new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault), @@ -748,10 +1579,10 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe new ThemeDescription(listView, ThemeDescription.FLAG_CHECKBOX, new Class[]{RadioCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackground), new ThemeDescription(listView, ThemeDescription.FLAG_CHECKBOXCHECK, new Class[]{RadioCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackgroundChecked), - new ThemeDescription(listView, 0, new Class[]{UserCell.class}, new String[]{"nameTextView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(listView, 0, new Class[]{UserCell.class}, new String[]{"statusColor"}, null, null, сellDelegate, Theme.key_windowBackgroundWhiteGrayText), - new ThemeDescription(listView, 0, new Class[]{UserCell.class}, new String[]{"statusOnlineColor"}, null, null, сellDelegate, Theme.key_windowBackgroundWhiteBlueText), - new ThemeDescription(listView, 0, new Class[]{UserCell.class}, null, new Drawable[]{Theme.avatar_photoDrawable, Theme.avatar_broadcastDrawable}, null, Theme.key_avatar_text), + new ThemeDescription(listView, 0, new Class[]{ManageChatUserCell.class}, new String[]{"nameTextView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(listView, 0, new Class[]{ManageChatUserCell.class}, new String[]{"statusColor"}, null, null, сellDelegate, Theme.key_windowBackgroundWhiteGrayText), + new ThemeDescription(listView, 0, new Class[]{ManageChatUserCell.class}, new String[]{"statusOnlineColor"}, null, null, сellDelegate, Theme.key_windowBackgroundWhiteBlueText), + new ThemeDescription(listView, 0, new Class[]{ManageChatUserCell.class}, null, new Drawable[]{Theme.avatar_photoDrawable, Theme.avatar_broadcastDrawable}, null, Theme.key_avatar_text), new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundRed), new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundOrange), new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundViolet), @@ -760,8 +1591,8 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundBlue), new ThemeDescription(null, 0, null, null, null, сellDelegate, Theme.key_avatar_backgroundPink), - new ThemeDescription(listView, 0, new Class[]{TextCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), - new ThemeDescription(listView, 0, new Class[]{TextCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayIcon), + new ThemeDescription(listView, 0, new Class[]{ManageChatTextCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(listView, 0, new Class[]{ManageChatTextCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayIcon), }; } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index 0c0a273f6..3bf57375e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -26,9 +26,12 @@ import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Outline; import android.graphics.Paint; +import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.media.ExifInterface; @@ -52,8 +55,10 @@ import android.util.TypedValue; import android.view.Gravity; import android.view.Menu; import android.view.MotionEvent; +import android.view.TextureView; import android.view.View; import android.view.ViewGroup; +import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.animation.DecelerateInterpolator; @@ -81,6 +86,7 @@ import org.telegram.messenger.UserObject; import org.telegram.messenger.Utilities; import org.telegram.messenger.VideoEditedInfo; import org.telegram.messenger.browser.Browser; +import org.telegram.messenger.exoplayer2.ui.AspectRatioFrameLayout; import org.telegram.messenger.query.BotQuery; import org.telegram.messenger.query.DraftQuery; import org.telegram.messenger.query.MessagesSearchQuery; @@ -89,6 +95,7 @@ import org.telegram.messenger.query.SearchQuery; import org.telegram.messenger.query.StickersQuery; import org.telegram.messenger.support.widget.GridLayoutManager; import org.telegram.messenger.support.widget.LinearLayoutManager; +import org.telegram.messenger.support.widget.LinearSmoothScrollerMiddle; import org.telegram.messenger.support.widget.RecyclerView; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLoader; @@ -133,6 +140,7 @@ import org.telegram.ui.Components.ChatAttachAlert; import org.telegram.ui.Components.ChatAvatarContainer; import org.telegram.ui.Components.ChatBigEmptyView; import org.telegram.ui.Components.CombinedDrawable; +import org.telegram.ui.Components.CorrectlyMeasuringTextView; import org.telegram.ui.Components.EmbedBottomSheet; import org.telegram.ui.Components.EmojiView; import org.telegram.ui.Components.ExtendedGridLayoutManager; @@ -140,6 +148,7 @@ import org.telegram.ui.Components.FragmentContextView; import org.telegram.ui.Components.InstantCameraView; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.NumberTextView; +import org.telegram.ui.Components.PipRoundVideoView; import org.telegram.ui.Components.RadialProgressView; import org.telegram.ui.Components.RecyclerListView; import org.telegram.ui.Components.ShareAlert; @@ -192,10 +201,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private TextView bottomOverlayChatText; private FrameLayout bottomOverlayChat; private FrameLayout emptyViewContainer; + private SizeNotifierFrameLayout contentView; private ChatBigEmptyView bigEmptyView; private ArrayList actionModeViews = new ArrayList<>(); private ChatAvatarContainer avatarContainer; - private View actionBarOverlay; private TextView bottomOverlayText; private NumberTextView selectedMessagesCountTextView; private FrameLayout actionModeTitleContainer; @@ -234,6 +243,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private TextView emptyView; private ImageView pagedownButtonImage; private TextView gifHintTextView; + private TextView mediaBanTooltip; + private TextView voiceHintTextView; + private Runnable voiceHintHideRunnable; + private AnimatorSet voiceHintAnimation; private View emojiButtonRed; private FrameLayout pinnedMessageView; private View pinnedLineView; @@ -254,10 +267,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private SimpleTextView searchCountText; private ChatActionCell floatingDateView; private InstantCameraView instantCameraView; + private View overlayView; private boolean currentFloatingDateOnScreen; private boolean currentFloatingTopIsNotMessage; private AnimatorSet floatingDateAnimation; private boolean scrollingFloatingDate; + private boolean checkTextureViewPosition; private ArrayList animatingMessageObjects = new ArrayList<>(); @@ -376,6 +391,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private float startX = 0; private float startY = 0; + private FrameLayout roundVideoContainer; + private AspectRatioFrameLayout aspectRatioFrameLayout; + private TextureView videoTextureView; + private Path aspectPath; + private Paint aspectPaint; + private Runnable readRunnable = new Runnable() { @Override public void run() { @@ -453,7 +474,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void setPhotoChecked(int index) { + public void setPhotoChecked(int index, VideoEditedInfo videoEditedInfo) { } @@ -680,14 +701,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.getInstance().addObserver(this, NotificationCenter.encryptedChatUpdated); NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagesReadEncrypted); NotificationCenter.getInstance().addObserver(this, NotificationCenter.removeAllMessagesFromDialog); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioProgressDidChanged); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidReset); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioPlayStateChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidReset); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingPlayStateChanged); NotificationCenter.getInstance().addObserver(this, NotificationCenter.screenshotTook); NotificationCenter.getInstance().addObserver(this, NotificationCenter.blockedUsersDidLoaded); NotificationCenter.getInstance().addObserver(this, NotificationCenter.FileNewChunkAvailable); NotificationCenter.getInstance().addObserver(this, NotificationCenter.didCreatedNewDeleteTask); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidStarted); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidStarted); NotificationCenter.getInstance().addObserver(this, NotificationCenter.updateMessageMedia); NotificationCenter.getInstance().addObserver(this, NotificationCenter.replaceMessagesObjects); NotificationCenter.getInstance().addObserver(this, NotificationCenter.notificationsSettingsUpdated); @@ -705,6 +726,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.getInstance().addObserver(this, NotificationCenter.newDraftReceived); NotificationCenter.getInstance().addObserver(this, NotificationCenter.userInfoDidLoaded); NotificationCenter.getInstance().addObserver(this, NotificationCenter.didSetNewWallpapper); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.channelRightsUpdated); super.onFragmentCreate(); @@ -802,7 +824,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didReceivedNewMessages); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.closeChats); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagesRead); - NotificationCenter.getInstance().removeObserver (this, NotificationCenter.messagesDeleted); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagesDeleted); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messageReceivedByServer); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messageReceivedByAck); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messageSendError); @@ -811,13 +833,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagesReadEncrypted); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.removeAllMessagesFromDialog); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.contactsDidLoaded); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioProgressDidChanged); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidReset); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidReset); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.screenshotTook); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.blockedUsersDidLoaded); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.FileNewChunkAvailable); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didCreatedNewDeleteTask); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidStarted); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidStarted); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.updateMessageMedia); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.replaceMessagesObjects); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.notificationsSettingsUpdated); @@ -828,7 +850,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.getInstance().removeObserver(this, NotificationCenter.botInfoDidLoaded); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.botKeyboardDidLoaded); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.chatSearchResultsAvailable); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioPlayStateChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingPlayStateChanged); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didUpdatedMessagesViews); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.chatInfoCantLoad); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didLoadedPinnedMessage); @@ -836,6 +858,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.getInstance().removeObserver(this, NotificationCenter.newDraftReceived); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.userInfoDidLoaded); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didSetNewWallpapper); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.channelRightsUpdated); if (AndroidUtilities.isTablet()) { NotificationCenter.getInstance().postNotificationName(NotificationCenter.openedChatChanged, dialog_id, true); @@ -861,10 +884,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatAttachAlert.onDestroy(); } AndroidUtilities.unlockOrientation(getParentActivity()); - /*MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); - if (messageObject != null && !messageObject.isMusic()) { - MediaController.getInstance().stopAudio(); - }*/ if (ChatObject.isChannel(currentChat)) { MessagesController.getInstance().startShortPoll(currentChat.id, true); } @@ -872,7 +891,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public View createView(Context context) { - if (chatMessageCellsCache.isEmpty()) { for (int a = 0; a < 8; a++) { chatMessageCellsCache.add(new ChatMessageCell(context)); @@ -886,6 +904,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not hasOwnBackground = true; if (chatAttachAlert != null) { + try { + if (chatAttachAlert.isShowing()) { + chatAttachAlert.dismiss(); + } + } catch (Exception ignore) { + + } chatAttachAlert.onDestroy(); chatAttachAlert = null; } @@ -1049,11 +1074,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not avatarContainer = new ChatAvatarContainer(context, this, currentEncryptedChat != null); actionBar.addView(avatarContainer, 0, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 56, 0, 40, 0)); - actionBarOverlay = new View(context); - actionBarOverlay.setBackgroundColor(0x7f000000); - actionBarOverlay.setVisibility(View.GONE); - actionBar.addView(actionBarOverlay, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); - if (currentChat != null) { if (!ChatObject.isChannel(currentChat)) { int count = currentChat.participants_count; @@ -1260,9 +1280,39 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int inputFieldHeight = 0; + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject != null && messageObject.isRoundVideo() && messageObject.eventId == 0 && messageObject.getDialogId() == dialog_id) { + MediaController.getInstance().setTextureView(createTextureView(false), aspectRatioFrameLayout, roundVideoContainer, true); + } + } + @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - boolean result = super.drawChild(canvas, child, drawingTime); + boolean result; + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + boolean isRoundVideo = messageObject != null && messageObject.eventId == 0 && messageObject.isRoundVideo(); + if (isRoundVideo && child == roundVideoContainer) { + if (messageObject.type == 5) { + if (Theme.chat_roundVideoShadow != null && aspectRatioFrameLayout.isDrawingReady()) { + int x = (int) child.getX() - AndroidUtilities.dp(3); + int y = (int) child.getY() - AndroidUtilities.dp(2); + Theme.chat_roundVideoShadow.setAlpha(255); + Theme.chat_roundVideoShadow.setBounds(x, y, x + AndroidUtilities.roundMessageSize + AndroidUtilities.dp(6), y + AndroidUtilities.roundMessageSize + AndroidUtilities.dp(6)); + Theme.chat_roundVideoShadow.draw(canvas); + } + result = super.drawChild(canvas, child, drawingTime); + } else { + result = false; + } + } else { + result = super.drawChild(canvas, child, drawingTime); + if (isRoundVideo && child == chatListView && messageObject.type != 5 && roundVideoContainer != null) { + super.drawChild(canvas, roundVideoContainer, drawingTime); + } + } if (child == actionBar && parentLayout != null) { parentLayout.drawHeaderShadow(canvas, actionBar.getVisibility() == VISIBLE ? actionBar.getMeasuredHeight() : 0); } @@ -1276,8 +1326,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int allHeight; int widthSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int heightSize = allHeight = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(widthSize, heightSize); heightSize -= getPaddingTop(); @@ -1292,6 +1343,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (keyboardSize <= AndroidUtilities.dp(20) && !AndroidUtilities.isInMultiwindow) { heightSize -= chatActivityEnterView.getEmojiPadding(); + allHeight -= chatActivityEnterView.getEmojiPadding(); } int childCount = getChildCount(); @@ -1308,6 +1360,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); int contentHeightSpec = MeasureSpec.makeMeasureSpec(Math.max(AndroidUtilities.dp(10), heightSize - inputFieldHeight + AndroidUtilities.dp(2 + (chatActivityEnterView.isTopViewVisible() ? 48 : 0))), MeasureSpec.EXACTLY); child.measure(contentWidthSpec, contentHeightSpec); + } else if (child == instantCameraView || child == overlayView) { + int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + int contentHeightSpec = MeasureSpec.makeMeasureSpec(allHeight - inputFieldHeight + AndroidUtilities.dp(3), MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); } else if (child == emptyViewContainer) { int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); int contentHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); @@ -1324,40 +1380,43 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } else if (child == mentionContainer) { FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mentionContainer.getLayoutParams(); - int height; - mentionListViewIgnoreLayout = true; - - if (mentionsAdapter.isBotContext() && mentionsAdapter.isMediaLayout()) { - int size = mentionGridLayoutManager.getRowsCount(widthSize); - int maxHeight = size * 102; - if (mentionsAdapter.isBotContext()) { - if (mentionsAdapter.getBotContextSwitch() != null) { - maxHeight += 34; - } - } - height = heightSize - chatActivityEnterView.getMeasuredHeight() + (maxHeight != 0 ? AndroidUtilities.dp(2) : 0); - mentionListView.setPadding(0, Math.max(0, height - AndroidUtilities.dp(Math.min(maxHeight, 68 * 1.8f))), 0, 0); + if (mentionsAdapter.isBannedInline()) { + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST)); } else { - int size = mentionsAdapter.getItemCount(); - int maxHeight = 0; - if (mentionsAdapter.isBotContext()) { - if (mentionsAdapter.getBotContextSwitch() != null) { - maxHeight += 36; - size -= 1; + int height; + mentionListViewIgnoreLayout = true; + if (mentionsAdapter.isBotContext() && mentionsAdapter.isMediaLayout()) { + int size = mentionGridLayoutManager.getRowsCount(widthSize); + int maxHeight = size * 102; + if (mentionsAdapter.isBotContext()) { + if (mentionsAdapter.getBotContextSwitch() != null) { + maxHeight += 34; + } } - maxHeight += size * 68; + height = heightSize - chatActivityEnterView.getMeasuredHeight() + (maxHeight != 0 ? AndroidUtilities.dp(2) : 0); + mentionListView.setPadding(0, Math.max(0, height - AndroidUtilities.dp(Math.min(maxHeight, 68 * 1.8f))), 0, 0); } else { - maxHeight += size * 36; + int size = mentionsAdapter.getItemCount(); + int maxHeight = 0; + if (mentionsAdapter.isBotContext()) { + if (mentionsAdapter.getBotContextSwitch() != null) { + maxHeight += 36; + size -= 1; + } + maxHeight += size * 68; + } else { + maxHeight += size * 36; + } + height = heightSize - chatActivityEnterView.getMeasuredHeight() + (maxHeight != 0 ? AndroidUtilities.dp(2) : 0); + mentionListView.setPadding(0, Math.max(0, height - AndroidUtilities.dp(Math.min(maxHeight, 68 * 1.8f))), 0, 0); } - height = heightSize - chatActivityEnterView.getMeasuredHeight() + (maxHeight != 0 ? AndroidUtilities.dp(2) : 0); - mentionListView.setPadding(0, Math.max(0, height - AndroidUtilities.dp(Math.min(maxHeight, 68 * 1.8f))), 0, 0); + + layoutParams.height = height; + layoutParams.topMargin = 0; + + mentionListViewIgnoreLayout = false; + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY)); } - - layoutParams.height = height; - layoutParams.topMargin = 0; - - mentionListViewIgnoreLayout = false; - child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY)); } else { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); } @@ -1433,7 +1492,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else { childTop = chatActivityEnterView.getBottom(); } - } else if (child == gifHintTextView) { + } else if (child == gifHintTextView || child == voiceHintTextView || child == mediaBanTooltip) { childTop -= inputFieldHeight; } else if (child == chatListView || child == progressView) { if (chatActivityEnterView.isTopViewVisible()) { @@ -1441,6 +1500,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } else if (child == actionBar) { childTop -= getPaddingTop(); + } else if (child == roundVideoContainer) { + childTop = actionBar.getMeasuredHeight(); + } else if (child == instantCameraView || child == overlayView) { + childTop = 0; } child.layout(childLeft, childTop, childLeft + width, childTop + height); } @@ -1450,7 +1513,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }; - SizeNotifierFrameLayout contentView = (SizeNotifierFrameLayout) fragmentView; + contentView = (SizeNotifierFrameLayout) fragmentView; contentView.setBackgroundImage(Theme.getCachedWallpaper()); @@ -1584,6 +1647,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not public boolean supportsPredictiveItemAnimations() { return false; } + + @Override + public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { + LinearSmoothScrollerMiddle linearSmoothScroller = new LinearSmoothScrollerMiddle(recyclerView.getContext()); + linearSmoothScroller.setTargetPosition(position); + startSmoothScroll(linearSmoothScroller); + } }; chatLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); chatLayoutManager.setStackFromEnd(true); @@ -1600,8 +1670,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { scrollingFloatingDate = true; + checkTextureViewPosition = true; } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { scrollingFloatingDate = false; + checkTextureViewPosition = false; hideFloatingDateView(true); } } @@ -1740,7 +1812,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } final ChatMessageCell cell = (ChatMessageCell) view; final MessageObject messageObject = cell.getMessageObject(); - if (messageObject == null || messageObject.isSending() || !messageObject.isSecretPhoto() || !cell.getPhotoImage().isInsideImage(x, y - top)) { + if (messageObject == null || messageObject.isSending() || messageObject.isRoundVideo() || !messageObject.isSecretPhoto() || !cell.getPhotoImage().isInsideImage(x, y - top)) { break; } File file = FileLoader.getPathToMessage(messageObject.messageOwner); @@ -1825,7 +1897,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pinnedMessageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - scrollToMessageId(info.pinned_msg_id, 0, true, 0); + scrollToMessageId(info.pinned_msg_id, 0, true, 0, false); } }); @@ -1858,7 +1930,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (getParentActivity() == null) { return; } - if (currentChat.creator || currentChat.editor) { + if (currentChat.creator || currentChat.admin_rights != null && currentChat.admin_rights.pin_messages) { AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); builder.setMessage(LocaleController.getString("UnpinMessageAlert", R.string.UnpinMessageAlert)); builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { @@ -2009,10 +2081,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pagedownButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { + checkTextureViewPosition = true; if (createUnreadMessageAfterId != 0) { - scrollToMessageId(createUnreadMessageAfterId, 0, false, returnToLoadIndex); + scrollToMessageId(createUnreadMessageAfterId, 0, false, returnToLoadIndex, false); } else if (returnToMessageId > 0) { - scrollToMessageId(returnToMessageId, 0, true, returnToLoadIndex); + scrollToMessageId(returnToMessageId, 0, true, returnToLoadIndex, false); } else { scrollToLastMessage(true); } @@ -2344,6 +2417,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not mentionListView.setOnItemClickListener(mentionsOnItemClickListener = new RecyclerListView.OnItemClickListener() { @Override public void onItemClick(View view, int position) { + if (mentionsAdapter.isBannedInline()) { + return; + } Object object = mentionsAdapter.getItem(position); int start = mentionsAdapter.getResultStartPosition(); int len = mentionsAdapter.getResultLength(); @@ -2474,9 +2550,29 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pagedownButtonCounter.setPadding(AndroidUtilities.dp(8), 0, AndroidUtilities.dp(8), AndroidUtilities.dp(1)); pagedownButton.addView(pagedownButtonCounter, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 23, Gravity.TOP | Gravity.CENTER_HORIZONTAL)); - if (BuildVars.DEBUG_PRIVATE_VERSION) { - instantCameraView = new InstantCameraView(context, this, actionBarOverlay); - contentView.addView(instantCameraView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 48)); + if (!AndroidUtilities.isTablet() || AndroidUtilities.isSmallTablet()) { + contentView.addView(fragmentContextView = new FragmentContextView(context, this), LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 39, Gravity.TOP | Gravity.LEFT, 0, -36, 0, 0)); + } + + contentView.addView(actionBar); + + overlayView = new View(context); + overlayView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + checkRecordLocked(); + } + overlayView.getParent().requestDisallowInterceptTouchEvent(true); + return true; + } + }); + contentView.addView(overlayView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP)); + overlayView.setVisibility(View.GONE); + + if (Build.VERSION.SDK_INT >= 16) { + instantCameraView = new InstantCameraView(context, this); + contentView.addView(instantCameraView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP)); } chatActivityEnterView = new ChatActivityEnterView(getParentActivity(), contentView, this, true); @@ -2495,10 +2591,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } + @Override + public void onSwitchRecordMode(boolean video) { + showVoiceHint(false, video); + } + + @Override + public void onPreAudioVideoRecord() { + showVoiceHint(true, false); + } + @Override public void onTextChanged(final CharSequence text, boolean bigChange) { MediaController.getInstance().setInputFieldHasText(text != null && text.length() != 0 || chatActivityEnterView.isEditingMessage()); - if (stickersAdapter != null && !chatActivityEnterView.isEditingMessage()) { + if (stickersAdapter != null && !chatActivityEnterView.isEditingMessage() && ChatObject.canSendStickers(currentChat)) { stickersAdapter.loadStikersForEmoji(text); } if (mentionsAdapter != null) { @@ -2508,7 +2614,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not AndroidUtilities.cancelRunOnUIThread(waitingForCharaterEnterRunnable); waitingForCharaterEnterRunnable = null; } - if (chatActivityEnterView.isMessageWebPageSearchEnabled() && (!chatActivityEnterView.isEditingMessage() || !chatActivityEnterView.isEditingCaption())) { + if (ChatObject.canSendEmbed(currentChat) && chatActivityEnterView.isMessageWebPageSearchEnabled() && (!chatActivityEnterView.isEditingMessage() || !chatActivityEnterView.isEditingCaption())) { if (bigChange) { searchLinks(text, true); } else { @@ -2575,6 +2681,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } actionBar.hideActionMode(); updatePinnedMessageView(true); + updateBottomOverlay(); updateVisibleRows(); } } @@ -2623,13 +2730,30 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (instantCameraView != null) { if (state == 0) { instantCameraView.showCamera(); - } else if (state == 1) { - instantCameraView.send(); + } else if (state == 1 || state == 3 || state == 4) { + instantCameraView.send(state); } else if (state == 2) { instantCameraView.cancel(); } } } + + @Override + public void needChangeVideoPreviewState(int state, float seekProgress) { + if (instantCameraView != null) { + instantCameraView.changeVideoPreviewState(state, seekProgress); + } + } + + @Override + public void needStartRecordAudio(int state) { + overlayView.setVisibility(state == 0 ? View.GONE : View.VISIBLE); + } + + @Override + public void needShowMediaBanHint() { + showMediaBannedHint(); + } }); FrameLayout replyLayout = new FrameLayout(context) { @@ -2680,8 +2804,15 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } }; - replyLayout.setClickable(true); chatActivityEnterView.addTopView(replyLayout, 48); + replyLayout.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (replyingMessageObject != null) { + scrollToMessageId(replyingMessageObject.getId(), 0, true, 0, false); + } + } + }); replyLineView = new View(context); replyLineView.setBackgroundColor(Theme.getColor(Theme.key_chat_replyPanelLine)); @@ -2844,7 +2975,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not layoutParams.width = LayoutHelper.MATCH_PARENT; child.setLayoutParams(layoutParams); } - FileLog.e(""); } }); } @@ -2875,13 +3005,15 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not bottomOverlay.setFocusable(true); bottomOverlay.setFocusableInTouchMode(true); bottomOverlay.setClickable(true); - bottomOverlay.setPadding(0, AndroidUtilities.dp(3), 0, 0); + bottomOverlay.setPadding(0, AndroidUtilities.dp(2), 0, 0); contentView.addView(bottomOverlay, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 51, Gravity.BOTTOM)); bottomOverlayText = new TextView(context); - bottomOverlayText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + bottomOverlayText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + bottomOverlayText.setGravity(Gravity.CENTER); + bottomOverlayText.setLineSpacing(AndroidUtilities.dp(2), 1); bottomOverlayText.setTextColor(Theme.getColor(Theme.key_chat_secretChatStatusText)); - bottomOverlay.addView(bottomOverlayText, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER)); + bottomOverlay.addView(bottomOverlayText, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER, 14, 0, 14, 0)); bottomOverlayChat = new FrameLayout(context) { @Override @@ -2934,7 +3066,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else { if (ChatObject.isChannel(currentChat) && !(currentChat instanceof TLRPC.TL_channelForbidden)) { if (ChatObject.isNotInChat(currentChat)) { - MessagesController.getInstance().addUserToChat(currentChat.id, UserConfig.getCurrentUser(), null, 0, null, null); + MessagesController.getInstance().addUserToChat(currentChat.id, UserConfig.getCurrentUser(), null, 0, null, ChatActivity.this); } else { toggleMute(true); } @@ -2975,10 +3107,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatActivityEnterView.setButtons(userBlocked ? null : botButtons); - if (!AndroidUtilities.isTablet() || AndroidUtilities.isSmallTablet()) { - contentView.addView(fragmentContextView = new FragmentContextView(context, this), LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 39, Gravity.TOP | Gravity.LEFT, 0, -36, 0, 0)); - } - updateContactStatus(); updateBottomOverlay(); updateSecretStatus(); @@ -2994,11 +3122,98 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } fixLayoutInternal(); - contentView.addView(actionBar); - return fragmentView; } + private TextureView createTextureView(boolean add) { + if (parentLayout == null) { + return null; + } + if (roundVideoContainer == null) { + if (Build.VERSION.SDK_INT >= 21) { + roundVideoContainer = new FrameLayout(getParentActivity()) { + @Override + public void setTranslationY(float translationY) { + super.setTranslationY(translationY); + contentView.invalidate(); + } + }; + roundVideoContainer.setOutlineProvider(new ViewOutlineProvider() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void getOutline(View view, Outline outline) { + outline.setOval(0, 0, AndroidUtilities.roundMessageSize, AndroidUtilities.roundMessageSize); + } + }); + roundVideoContainer.setClipToOutline(true); + } else { + roundVideoContainer = new FrameLayout(getParentActivity()) { + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + aspectPath.reset(); + aspectPath.addCircle(w / 2, h / 2, w / 2, Path.Direction.CW); + aspectPath.toggleInverseFillType(); + } + + @Override + public void setTranslationY(float translationY) { + super.setTranslationY(translationY); + contentView.invalidate(); + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (visibility == VISIBLE) { + setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + canvas.drawPath(aspectPath, aspectPaint); + } + }; + aspectPath = new Path(); + aspectPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + aspectPaint.setColor(0xff000000); + aspectPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + } + roundVideoContainer.setWillNotDraw(false); + roundVideoContainer.setVisibility(View.INVISIBLE); + + aspectRatioFrameLayout = new AspectRatioFrameLayout(getParentActivity()); + aspectRatioFrameLayout.setBackgroundColor(0); + if (add) { + roundVideoContainer.addView(aspectRatioFrameLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + } + + videoTextureView = new TextureView(getParentActivity()); + videoTextureView.setOpaque(false); + aspectRatioFrameLayout.addView(videoTextureView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + } + if (roundVideoContainer.getParent() == null) { + contentView.addView(roundVideoContainer, 1, new FrameLayout.LayoutParams(AndroidUtilities.roundMessageSize, AndroidUtilities.roundMessageSize)); + } + roundVideoContainer.setVisibility(View.INVISIBLE); + aspectRatioFrameLayout.setDrawingReady(false); + return videoTextureView; + } + + private void destroyTextureView() { + if (roundVideoContainer == null || roundVideoContainer.getParent() == null) { + return; + } + contentView.removeView(roundVideoContainer); + aspectRatioFrameLayout.setDrawingReady(false); + roundVideoContainer.setVisibility(View.INVISIBLE); + if (Build.VERSION.SDK_INT < 21) { + roundVideoContainer.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + private void sendBotInlineResult(TLRPC.BotInlineResult result) { int uid = mentionsAdapter.getContextBotId(); HashMap params = new HashMap<>(); @@ -3065,7 +3280,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not for (int a = messages.size() - 1; a >= 0; a--) { MessageObject message = messages.get(a); if (message.messageOwner.date >= date && message.getId() != 0) { - scrollToMessageId(message.getId(), 0, false, message.getDialogId() == mergeDialogId ? 1 : 0); + scrollToMessageId(message.getId(), 0, false, message.getDialogId() == mergeDialogId ? 1 : 0, false); break; } } @@ -3111,10 +3326,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatAttachAlert.setDelegate(new ChatAttachAlert.ChatAttachViewDelegate() { @Override public void didPressedButton(int button) { - if (getParentActivity() == null) { + if (getParentActivity() == null || chatAttachAlert == null) { return; } - if (button == 7) { + if (button == 7 || button == 4 && !chatAttachAlert.getSelectedPhotos().isEmpty()) { chatAttachAlert.dismiss(); HashMap selectedPhotos = chatAttachAlert.getSelectedPhotos(); if (!selectedPhotos.isEmpty()) { @@ -3123,6 +3338,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not ArrayList> masks = new ArrayList<>(); for (HashMap.Entry entry : selectedPhotos.entrySet()) { MediaController.PhotoEntry photoEntry = entry.getValue(); + if (photoEntry.isVideo) { + if (photoEntry.editedInfo != null) { + SendMessagesHelper.prepareSendingVideo(photoEntry.path, photoEntry.editedInfo.estimatedSize, photoEntry.editedInfo.estimatedDuration, photoEntry.editedInfo.resultWidth, photoEntry.editedInfo.resultHeight, photoEntry.editedInfo, dialog_id, replyingMessageObject, photoEntry.caption); + } else { + SendMessagesHelper.prepareSendingVideo(photoEntry.path, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, photoEntry.caption); + } + continue; + } if (photoEntry.imagePath != null) { photos.add(photoEntry.imagePath); captions.add(photoEntry.caption != null ? photoEntry.caption.toString() : null); @@ -3137,7 +3360,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not photoEntry.caption = null; photoEntry.stickers.clear(); } - SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions, masks, null); + SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions, masks, null, button == 4); showReplyPanel(false, null, null, null, false); DraftQuery.cleanDraft(dialog_id, true); } @@ -3184,10 +3407,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } public boolean playFirstUnreadVoiceMessage() { + if (chatActivityEnterView != null && chatActivityEnterView.isRecordingAudioVideo()) { + return true; + } for (int a = messages.size() - 1; a >= 0; a--) { MessageObject messageObject = messages.get(a); - if (messageObject.isVoice() && messageObject.isContentUnread() && !messageObject.isOut() && messageObject.messageOwner.to_id.channel_id == 0) { - MediaController.getInstance().setVoiceMessagesPlaylist(MediaController.getInstance().playAudio(messageObject) ? createVoiceMessagesPlaylist(messageObject, true) : null, true); + if ((messageObject.isVoice() || messageObject.isRoundVideo()) && messageObject.isContentUnread() && !messageObject.isOut() && messageObject.messageOwner.to_id.channel_id == 0) { + MediaController.getInstance().setVoiceMessagesPlaylist(MediaController.getInstance().playMessage(messageObject) ? createVoiceMessagesPlaylist(messageObject, true) : null, true); //TODO return true; } } @@ -3295,6 +3521,176 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not showDialog(builder.create()); } + private void hideVoiceHint() { + voiceHintAnimation = new AnimatorSet(); + voiceHintAnimation.playTogether( + ObjectAnimator.ofFloat(voiceHintTextView, "alpha", 0.0f) + ); + voiceHintAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animation.equals(voiceHintAnimation)) { + voiceHintAnimation = null; + voiceHintHideRunnable = null; + if (voiceHintTextView != null) { + voiceHintTextView.setVisibility(View.GONE); + } + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (animation.equals(voiceHintAnimation)) { + voiceHintHideRunnable = null; + voiceHintHideRunnable = null; + } + } + }); + voiceHintAnimation.setDuration(300); + voiceHintAnimation.start(); + } + + private void showVoiceHint(boolean hide, boolean video) { + if (getParentActivity() == null || fragmentView == null || hide && voiceHintTextView == null) { + return; + } + if (voiceHintTextView == null) { + SizeNotifierFrameLayout frameLayout = (SizeNotifierFrameLayout) fragmentView; + int index = frameLayout.indexOfChild(chatActivityEnterView); + if (index == -1) { + return; + } + voiceHintTextView = new TextView(getParentActivity()); + voiceHintTextView.setBackgroundDrawable(Theme.createRoundRectDrawable(AndroidUtilities.dp(3), Theme.getColor(Theme.key_chat_gifSaveHintBackground))); + voiceHintTextView.setTextColor(Theme.getColor(Theme.key_chat_gifSaveHintText)); + voiceHintTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + voiceHintTextView.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(7), AndroidUtilities.dp(8), AndroidUtilities.dp(7)); + voiceHintTextView.setGravity(Gravity.CENTER_VERTICAL); + voiceHintTextView.setAlpha(0.0f); + frameLayout.addView(voiceHintTextView, index + 1, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM, 5, 0, 5, 3)); + } + if (hide) { + if (voiceHintAnimation != null) { + voiceHintAnimation.cancel(); + voiceHintAnimation = null; + } + AndroidUtilities.cancelRunOnUIThread(voiceHintHideRunnable); + voiceHintHideRunnable = null; + hideVoiceHint(); + return; + } + + voiceHintTextView.setText(video ? LocaleController.getString("HoldToVideo", R.string.HoldToVideo) : LocaleController.getString("HoldToAudio", R.string.HoldToAudio)); + + if (voiceHintHideRunnable != null) { + if (voiceHintAnimation != null) { + voiceHintAnimation.cancel(); + voiceHintAnimation = null; + } else { + AndroidUtilities.cancelRunOnUIThread(voiceHintHideRunnable); + AndroidUtilities.runOnUIThread(voiceHintHideRunnable = new Runnable() { + @Override + public void run() { + hideVoiceHint(); + } + }, 2000); + return; + } + } else if (voiceHintAnimation != null) { + return; + } + + voiceHintTextView.setVisibility(View.VISIBLE); + voiceHintAnimation = new AnimatorSet(); + voiceHintAnimation.playTogether( + ObjectAnimator.ofFloat(voiceHintTextView, "alpha", 1.0f) + ); + voiceHintAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animation.equals(voiceHintAnimation)) { + voiceHintAnimation = null; + AndroidUtilities.runOnUIThread(voiceHintHideRunnable = new Runnable() { + @Override + public void run() { + hideVoiceHint(); + } + }, 2000); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (animation.equals(voiceHintAnimation)) { + voiceHintAnimation = null; + } + } + }); + voiceHintAnimation.setDuration(300); + voiceHintAnimation.start(); + } + + private void showMediaBannedHint() { + if (getParentActivity() == null || currentChat == null || currentChat.banned_rights == null || fragmentView == null || mediaBanTooltip != null && mediaBanTooltip.getVisibility() == View.VISIBLE) { + return; + } + SizeNotifierFrameLayout frameLayout = (SizeNotifierFrameLayout) fragmentView; + int index = frameLayout.indexOfChild(chatActivityEnterView); + if (index == -1) { + return; + } + + if (mediaBanTooltip == null) { + mediaBanTooltip = new CorrectlyMeasuringTextView(getParentActivity()); + mediaBanTooltip.setBackgroundDrawable(Theme.createRoundRectDrawable(AndroidUtilities.dp(3), Theme.getColor(Theme.key_chat_gifSaveHintBackground))); + mediaBanTooltip.setTextColor(Theme.getColor(Theme.key_chat_gifSaveHintText)); + mediaBanTooltip.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(7), AndroidUtilities.dp(8), AndroidUtilities.dp(7)); + mediaBanTooltip.setGravity(Gravity.CENTER_VERTICAL); + mediaBanTooltip.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + frameLayout.addView(mediaBanTooltip, index + 1, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM, 30, 0, 5, 3)); + } + + if (AndroidUtilities.isBannedForever(currentChat.banned_rights.until_date)) { + mediaBanTooltip.setText(LocaleController.getString("AttachMediaRestrictedForever", R.string.AttachMediaRestrictedForever)); + } else { + mediaBanTooltip.setText(LocaleController.formatString("AttachMediaRestricted", R.string.AttachMediaRestricted, LocaleController.formatDateForBan(currentChat.banned_rights.until_date))); + } + mediaBanTooltip.setVisibility(View.VISIBLE); + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(mediaBanTooltip, "alpha", 0.0f, 1.0f) + ); + AnimatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (mediaBanTooltip == null) { + return; + } + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(mediaBanTooltip, "alpha", 0.0f) + ); + AnimatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mediaBanTooltip != null) { + mediaBanTooltip.setVisibility(View.GONE); + } + } + }); + AnimatorSet.setDuration(300); + AnimatorSet.start(); + } + }, 5000); + } + }); + AnimatorSet.setDuration(300); + AnimatorSet.start(); + } + private void showGifHint() { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); if (preferences.getBoolean("gifhint", false)) { @@ -3325,10 +3721,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not gifHintTextView.setBackgroundDrawable(Theme.createRoundRectDrawable(AndroidUtilities.dp(3), Theme.getColor(Theme.key_chat_gifSaveHintBackground))); gifHintTextView.setTextColor(Theme.getColor(Theme.key_chat_gifSaveHintText)); gifHintTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); - gifHintTextView.setPadding(AndroidUtilities.dp(10), 0, AndroidUtilities.dp(10), 0); + gifHintTextView.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(7), AndroidUtilities.dp(8), AndroidUtilities.dp(7)); gifHintTextView.setText(LocaleController.getString("TapHereGifs", R.string.TapHereGifs)); gifHintTextView.setGravity(Gravity.CENTER_VERTICAL); - frameLayout.addView(gifHintTextView, index + 1, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 32, Gravity.LEFT | Gravity.BOTTOM, 5, 0, 0, 3)); + frameLayout.addView(gifHintTextView, index + 1, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.BOTTOM, 5, 0, 5, 3)); AnimatorSet AnimatorSet = new AnimatorSet(); AnimatorSet.playTogether( @@ -3471,6 +3867,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } + @Override + protected void onRemoveFromParent() { + MediaController.getInstance().setTextureView(videoTextureView, null, null, false); + } + protected void setIgnoreAttachOnPause(boolean value) { ignoreAttachOnPause = value; } @@ -3577,8 +3978,16 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not PhotoAlbumPickerActivity fragment = new PhotoAlbumPickerActivity(false, currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 46, true, ChatActivity.this); fragment.setDelegate(new PhotoAlbumPickerActivity.PhotoAlbumPickerActivityDelegate() { @Override - public void didSelectPhotos(ArrayList photos, ArrayList captions, ArrayList> masks, ArrayList webPhotos) { - SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions, masks, null); + public void didSelectPhotos(ArrayList photos, ArrayList captions, ArrayList videos, ArrayList> masks, ArrayList webPhotos) { + for (int a = 0; a < videos.size(); a++) { + MediaController.PhotoEntry video = videos.get(a); + if (video.editedInfo != null) { + SendMessagesHelper.prepareSendingVideo(video.path, video.editedInfo.estimatedSize, video.editedInfo.estimatedDuration, video.editedInfo.resultWidth, video.editedInfo.resultHeight, video.editedInfo, dialog_id, replyingMessageObject, video.caption); + } else { + SendMessagesHelper.prepareSendingVideo(video.path, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, video.caption); + } + } + SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions, masks, null, false); SendMessagesHelper.prepareSendingPhotosSearch(webPhotos, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false); DraftQuery.cleanDraft(dialog_id, true); @@ -3888,7 +4297,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } if (!fromMyName) { - SendMessagesHelper.getInstance().sendMessage(arrayList, dialog_id); + AlertsCreator.showSendMediaAlert(SendMessagesHelper.getInstance().sendMessage(arrayList, dialog_id), this); } else { for (MessageObject object : arrayList) { SendMessagesHelper.getInstance().processForwardFromMyName(object, dialog_id); @@ -4058,6 +4467,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedContact", messageObjectsToForward.size())); } else if (type == 2) { replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedAudio", messageObjectsToForward.size())); + } else if (type == 5) { + replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedRound", messageObjectsToForward.size())); } else if (type == 14) { replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedMusic", messageObjectsToForward.size())); } else if (type == 13) { @@ -4118,6 +4529,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not replyImageView.setVisibility(View.INVISIBLE); layoutParams1.leftMargin = layoutParams2.leftMargin = AndroidUtilities.dp(52); } else { + if (messageObjectToReply.isRoundVideo()) { + replyImageView.setRoundRadius(AndroidUtilities.dp(17)); + } else { + replyImageView.setRoundRadius(0); + } replyImageLocation = photoSize.location; replyImageView.setImage(replyImageLocation, "50_50", (Drawable) null); replyImageView.setVisibility(View.VISIBLE); @@ -4219,6 +4635,47 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } + private void updateTextureViewPosition() { + boolean foundTextureViewMessage = false; + int count = chatListView.getChildCount(); + int additionalTop = chatActivityEnterView.isTopViewVisible() ? AndroidUtilities.dp(48) : 0; + for (int a = 0; a < count; a++) { + View view = chatListView.getChildAt(a); + if (view instanceof ChatMessageCell) { + ChatMessageCell messageCell = (ChatMessageCell) view; + MessageObject messageObject = messageCell.getMessageObject(); + if (roundVideoContainer != null && messageObject.isRoundVideo() && MediaController.getInstance().isPlayingMessage(messageObject)) { + ImageReceiver imageReceiver = messageCell.getPhotoImage(); + roundVideoContainer.setTranslationX(imageReceiver.getImageX()); + roundVideoContainer.setTranslationY(fragmentView.getPaddingTop() + messageCell.getTop() + imageReceiver.getImageY() - additionalTop); + fragmentView.invalidate(); + roundVideoContainer.invalidate(); + foundTextureViewMessage = true; + break; + } + } + } + if (roundVideoContainer != null) { + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject.eventId == 0) { + if (!foundTextureViewMessage) { + roundVideoContainer.setTranslationY(-AndroidUtilities.roundMessageSize - 100); + fragmentView.invalidate(); + if (messageObject != null && messageObject.isRoundVideo()) { + if (checkTextureViewPosition || PipRoundVideoView.getInstance() != null) { + MediaController.getInstance().setCurrentRoundVisible(false); + } else { + scrollToMessageId(messageObject.getId(), 0, false, 0, true); + } + } + } else { + MediaController.getInstance().setCurrentRoundVisible(true); + scrollToMessageId(messageObject.getId(), 0, false, 0, true); + } + } + } + } + private void updateMessagesVisisblePart() { if (chatListView == null) { return; @@ -4231,6 +4688,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not View minDateChild = null; View minChild = null; View minMessageChild = null; + boolean foundTextureViewMessage = false; for (int a = 0; a < count; a++) { View view = chatListView.getChildAt(a); if (view instanceof ChatMessageCell) { @@ -4243,6 +4701,16 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not viewBottom = viewTop + height; } messageCell.setVisiblePart(viewTop, viewBottom - viewTop); + + MessageObject messageObject = messageCell.getMessageObject(); + if (roundVideoContainer != null && messageObject.isRoundVideo() && MediaController.getInstance().isPlayingMessage(messageObject)) { + ImageReceiver imageReceiver = messageCell.getPhotoImage(); + roundVideoContainer.setTranslationX(imageReceiver.getImageX()); + roundVideoContainer.setTranslationY(fragmentView.getPaddingTop() + top + imageReceiver.getImageY() - additionalTop); + fragmentView.invalidate(); + roundVideoContainer.invalidate(); + foundTextureViewMessage = true; + } } if (view.getBottom() <= chatListView.getPaddingTop()) { continue; @@ -4265,6 +4733,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } } + if (roundVideoContainer != null) { + if (!foundTextureViewMessage) { + roundVideoContainer.setTranslationY(-AndroidUtilities.roundMessageSize - 100); + fragmentView.invalidate(); + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject != null && messageObject.isRoundVideo() && messageObject.eventId == 0 &&checkTextureViewPosition) { + MediaController.getInstance().setCurrentRoundVisible(false); + } + } else { + MediaController.getInstance().setCurrentRoundVisible(true); + } + } if (minMessageChild != null) { MessageObject messageObject; if (minMessageChild instanceof ChatMessageCell) { @@ -4345,7 +4825,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } - private void scrollToMessageId(int id, int fromMessageId, boolean select, int loadIndex) { + public void scrollToMessageId(int id, int fromMessageId, boolean select, int loadIndex, boolean smooth) { MessageObject object = messagesDict[loadIndex].get(id); boolean query = false; if (object != null) { @@ -4357,10 +4837,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not highlightMessageId = Integer.MAX_VALUE; } final int yOffset = Math.max(0, (chatListView.getHeight() - object.getApproximateHeight()) / 2); - if (messages.get(messages.size() - 1) == object) { - chatLayoutManager.scrollToPositionWithOffset(0, -chatListView.getPaddingTop() - AndroidUtilities.dp(7) + yOffset); + if (smooth) { + if (messages.get(messages.size() - 1) == object) { + chatListView.smoothScrollToPosition(0); + } else { + chatListView.smoothScrollToPosition(chatAdapter.messagesStartRow + messages.size() - messages.indexOf(object) - 1); + } } else { - chatLayoutManager.scrollToPositionWithOffset(chatAdapter.messagesStartRow + messages.size() - messages.indexOf(object) - 1, -AndroidUtilities.dp(7) + yOffset); + if (messages.get(messages.size() - 1) == object) { + chatLayoutManager.scrollToPositionWithOffset(0, -chatListView.getPaddingTop() - AndroidUtilities.dp(7) + yOffset); + } else { + chatLayoutManager.scrollToPositionWithOffset(chatAdapter.messagesStartRow + messages.size() - messages.indexOf(object) - 1, -AndroidUtilities.dp(7) + yOffset); + } } updateVisibleRows(); boolean found = false; @@ -4472,36 +4960,46 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (bottomOverlay == null) { return; } - if (currentEncryptedChat == null || bigEmptyView == null) { - bottomOverlay.setVisibility(View.INVISIBLE); - return; - } boolean hideKeyboard = false; - if (currentEncryptedChat instanceof TLRPC.TL_encryptedChatRequested) { - bottomOverlayText.setText(LocaleController.getString("EncryptionProcessing", R.string.EncryptionProcessing)); + if (ChatObject.isChannel(currentChat) && currentChat.banned_rights != null && currentChat.banned_rights.send_messages) { + if (AndroidUtilities.isBannedForever(currentChat.banned_rights.until_date)) { + bottomOverlayText.setText(LocaleController.getString("SendMessageRestrictedForever", R.string.SendMessageRestrictedForever)); + } else { + bottomOverlayText.setText(LocaleController.formatString("SendMessageRestricted", R.string.SendMessageRestricted, LocaleController.formatDateForBan(currentChat.banned_rights.until_date))); + } bottomOverlay.setVisibility(View.VISIBLE); hideKeyboard = true; - } else if (currentEncryptedChat instanceof TLRPC.TL_encryptedChatWaiting) { - bottomOverlayText.setText(AndroidUtilities.replaceTags(LocaleController.formatString("AwaitingEncryption", R.string.AwaitingEncryption, "" + currentUser.first_name + ""))); - bottomOverlay.setVisibility(View.VISIBLE); - hideKeyboard = true; - } else if (currentEncryptedChat instanceof TLRPC.TL_encryptedChatDiscarded) { - bottomOverlayText.setText(LocaleController.getString("EncryptionRejected", R.string.EncryptionRejected)); - bottomOverlay.setVisibility(View.VISIBLE); - chatActivityEnterView.setFieldText(""); - DraftQuery.cleanDraft(dialog_id, false); - hideKeyboard = true; - } else if (currentEncryptedChat instanceof TLRPC.TL_encryptedChat) { - bottomOverlay.setVisibility(View.INVISIBLE); + } else { + if (currentEncryptedChat == null || bigEmptyView == null) { + bottomOverlay.setVisibility(View.INVISIBLE); + return; + } + if (currentEncryptedChat instanceof TLRPC.TL_encryptedChatRequested) { + bottomOverlayText.setText(LocaleController.getString("EncryptionProcessing", R.string.EncryptionProcessing)); + bottomOverlay.setVisibility(View.VISIBLE); + hideKeyboard = true; + } else if (currentEncryptedChat instanceof TLRPC.TL_encryptedChatWaiting) { + bottomOverlayText.setText(AndroidUtilities.replaceTags(LocaleController.formatString("AwaitingEncryption", R.string.AwaitingEncryption, "" + currentUser.first_name + ""))); + bottomOverlay.setVisibility(View.VISIBLE); + hideKeyboard = true; + } else if (currentEncryptedChat instanceof TLRPC.TL_encryptedChatDiscarded) { + bottomOverlayText.setText(LocaleController.getString("EncryptionRejected", R.string.EncryptionRejected)); + bottomOverlay.setVisibility(View.VISIBLE); + chatActivityEnterView.setFieldText(""); + DraftQuery.cleanDraft(dialog_id, false); + hideKeyboard = true; + } else if (currentEncryptedChat instanceof TLRPC.TL_encryptedChat) { + bottomOverlay.setVisibility(View.INVISIBLE); + } + checkRaiseSensors(); + checkActionBarMenu(); } - checkRaiseSensors(); if (hideKeyboard) { chatActivityEnterView.hidePopup(false); if (getParentActivity() != null) { AndroidUtilities.hideKeyboard(getParentActivity().getCurrentFocus()); } } - checkActionBarMenu(); } @Override @@ -4613,7 +5111,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return 7; } } - } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaPhoto || messageObject.getDocument() != null || messageObject.isMusic() || messageObject.isVideo()) { + } else if ((!messageObject.isRoundVideo() || messageObject.isRoundVideo() && BuildVars.DEBUG_VERSION) && (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaPhoto || messageObject.getDocument() != null || messageObject.isMusic() || messageObject.isVideo())) { boolean canSave = false; if (messageObject.messageOwner.attachPath != null && messageObject.messageOwner.attachPath.length() != 0) { File f = new File(messageObject.messageOwner.attachPath); @@ -4678,7 +5176,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return 7; } } - } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaPhoto || messageObject.getDocument() != null || messageObject.isMusic() || messageObject.isVideo()) { + } else if (!messageObject.isRoundVideo() && (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaPhoto || messageObject.getDocument() != null || messageObject.isMusic() || messageObject.isVideo())) { boolean canSave = false; if (messageObject.messageOwner.attachPath != null && messageObject.messageOwner.attachPath.length() != 0) { File f = new File(messageObject.messageOwner.attachPath); @@ -4747,7 +5245,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not final ActionBarMenuItem replyItem = actionBar.createActionMode().getItem(reply); if (replyItem != null) { boolean allowChatActions = true; - if (currentEncryptedChat != null && AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) < 46 || isBroadcast || currentChat != null && (ChatObject.isNotInChat(currentChat) || ChatObject.isChannel(currentChat) && !currentChat.creator && !currentChat.editor && !currentChat.megagroup)) { + if (currentEncryptedChat != null && AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) < 46 || + isBroadcast || + currentChat != null && (ChatObject.isNotInChat(currentChat) || ChatObject.isChannel(currentChat) && !ChatObject.canPost(currentChat) && !currentChat.megagroup || !ChatObject.canSendMessages(currentChat))) { allowChatActions = false; } final int newVisibility = allowChatActions && selectedMessagesIds[0].size() + selectedMessagesIds[1].size() == 1 ? View.VISIBLE : View.GONE; @@ -4938,6 +5438,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not videoEditedInfo.resultWidth = resultWidth; videoEditedInfo.resultHeight = resultHeight; videoEditedInfo.originalPath = videoPath; + videoEditedInfo.muted = videoEditedInfo.bitrate == -1; SendMessagesHelper.prepareSendingVideo(videoPath, estimatedSize, estimatedDuration, resultWidth, resultHeight, videoEditedInfo, dialog_id, replyingMessageObject, caption); showReplyPanel(false, null, null, null, false); DraftQuery.cleanDraft(dialog_id, true); @@ -5458,7 +5959,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (load_type == 1) { - if (messArr.size() != count && !isCache) { + if (messArr.size() != count && (!isCache || currentEncryptedChat != null)) { forwardEndReached[loadIndex] = true; if (loadIndex != 1) { first_unread_id = 0; @@ -5843,8 +6344,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (a == 0) { if (obj.messageOwner.id < 0) { placeToPaste = 0; - if (BuildVars.DEBUG_PRIVATE_VERSION && obj.isVideoVoice()) { - animatingMessageObjects.add(obj); //TODO + if (obj.type == 5) { + animatingMessageObjects.add(obj); } } else { if (!messages.isEmpty()) { @@ -6425,6 +6926,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not initStickers(); if (chatActivityEnterView != null) { chatActivityEnterView.setAllowStickersAndGifs(currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 23, currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 46); + chatActivityEnterView.checkRoundVideo(); } if (mentionsAdapter != null) { mentionsAdapter.setNeedBotContext(!chatActivityEnterView.isEditingMessage() && (currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 46)); @@ -6446,52 +6948,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } updateVisibleRows(); } - } else if (id == NotificationCenter.audioDidReset || id == NotificationCenter.audioPlayStateChanged) { - if (chatListView != null) { - int count = chatListView.getChildCount(); - for (int a = 0; a < count; a++) { - View view = chatListView.getChildAt(a); - if (view instanceof ChatMessageCell) { - ChatMessageCell cell = (ChatMessageCell) view; - MessageObject messageObject = cell.getMessageObject(); - if (messageObject != null && (messageObject.isVoice() || messageObject.isMusic())) { - cell.updateButtonState(false); - } - } - } - count = mentionListView.getChildCount(); - for (int a = 0; a < count; a++) { - View view = mentionListView.getChildAt(a); - if (view instanceof ContextLinkCell) { - ContextLinkCell cell = (ContextLinkCell) view; - MessageObject messageObject = cell.getMessageObject(); - if (messageObject != null && (messageObject.isVoice() || messageObject.isMusic())) { - cell.updateButtonState(false); - } - } - } - } - } else if (id == NotificationCenter.audioProgressDidChanged) { - Integer mid = (Integer) args[0]; - if (chatListView != null) { - int count = chatListView.getChildCount(); - for (int a = 0; a < count; a++) { - View view = chatListView.getChildAt(a); - if (view instanceof ChatMessageCell) { - ChatMessageCell cell = (ChatMessageCell) view; - if (cell.getMessageObject() != null && cell.getMessageObject().getId() == mid) { - MessageObject playing = cell.getMessageObject(); - MessageObject player = MediaController.getInstance().getPlayingMessageObject(); - if (player != null) { - playing.audioProgress = player.audioProgress; - playing.audioProgressSec = player.audioProgressSec; - cell.updateAudioProgress(); - } - break; - } - } - } - } } else if (id == NotificationCenter.removeAllMessagesFromDialog) { long did = (Long) args[0]; if (dialog_id == did) { @@ -6591,9 +7047,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (changed) { updateVisibleRows(); } - } else if (id == NotificationCenter.audioDidStarted) { + } else if (id == NotificationCenter.messagePlayingDidStarted) { MessageObject messageObject = (MessageObject) args[0]; + if (messageObject.eventId != 0) { + return; + } sendSecretMessageRead(messageObject); + + if (messageObject.isRoundVideo()) { + MediaController.getInstance().setTextureView(createTextureView(true), aspectRatioFrameLayout, roundVideoContainer, true); + updateTextureViewPosition(); + } + if (chatListView != null) { int count = chatListView.getChildCount(); for (int a = 0; a < count; a++) { @@ -6601,8 +7066,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (view instanceof ChatMessageCell) { ChatMessageCell cell = (ChatMessageCell) view; MessageObject messageObject1 = cell.getMessageObject(); - if (messageObject1 != null && (messageObject1.isVoice() || messageObject1.isMusic())) { - cell.updateButtonState(false); + if (messageObject1 != null) { + if (messageObject1.isVoice() || messageObject1.isMusic()) { + cell.updateButtonState(false); + } else if (messageObject1.isRoundVideo()) { + cell.checkRoundVideoPlayback(false); + } } } } @@ -6618,6 +7087,61 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } } + } else if (id == NotificationCenter.messagePlayingDidReset || id == NotificationCenter.messagePlayingPlayStateChanged) { + if (id == NotificationCenter.messagePlayingDidReset) { + destroyTextureView(); + } + if (chatListView != null) { + int count = chatListView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = chatListView.getChildAt(a); + if (view instanceof ChatMessageCell) { + ChatMessageCell cell = (ChatMessageCell) view; + MessageObject messageObject = cell.getMessageObject(); + if (messageObject != null) { + if (messageObject.isVoice() || messageObject.isMusic()) { + cell.updateButtonState(false); + } else if (messageObject.isRoundVideo()) { + if (!MediaController.getInstance().isPlayingMessage(messageObject)) { + cell.checkRoundVideoPlayback(true); + } + } + } + } + } + count = mentionListView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = mentionListView.getChildAt(a); + if (view instanceof ContextLinkCell) { + ContextLinkCell cell = (ContextLinkCell) view; + MessageObject messageObject = cell.getMessageObject(); + if (messageObject != null && (messageObject.isVoice() || messageObject.isMusic())) { + cell.updateButtonState(false); + } + } + } + } + } else if (id == NotificationCenter.messagePlayingProgressDidChanged) { + Integer mid = (Integer) args[0]; + if (chatListView != null) { + int count = chatListView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = chatListView.getChildAt(a); + if (view instanceof ChatMessageCell) { + ChatMessageCell cell = (ChatMessageCell) view; + MessageObject playing = cell.getMessageObject(); + if (playing != null && playing.getId() == mid) { + MessageObject player = MediaController.getInstance().getPlayingMessageObject(); + if (player != null) { + playing.audioProgress = player.audioProgress; + playing.audioProgressSec = player.audioProgressSec; + cell.updatePlayingMessageProgress(); + } + break; + } + } + } + } } else if (id == NotificationCenter.updateMessageMedia) { MessageObject messageObject = (MessageObject) args[0]; MessageObject existMessageObject = messagesDict[0].get(messageObject.getId()); @@ -6823,7 +7347,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int messageId = (Integer) args[1]; long did = (Long) args[3]; if (messageId != 0) { - scrollToMessageId(messageId, 0, true, did == dialog_id ? 0 : 1); + scrollToMessageId(messageId, 0, true, did == dialog_id ? 0 : 1, false); } updateSearchButtons((Integer) args[2], (Integer) args[4], (Integer) args[5]); } @@ -6881,11 +7405,19 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } chatListView.invalidateViews(); } + } else if (id == NotificationCenter.channelRightsUpdated) { + TLRPC.Chat chat = (TLRPC.Chat) args[0]; + if (currentChat != null && chat.id == currentChat.id && chatActivityEnterView != null) { + currentChat = chat; + chatActivityEnterView.checkChannelRights(); + checkRaiseSensors(); + updateSecretStatus(); + } } } public boolean processSwitchButton(TLRPC.TL_keyboardButtonSwitchInline button) { - if (inlineReturn == 0 || button.same_peer) { + if (inlineReturn == 0 || button.same_peer || parentLayout == null) { return false; } String query = "@" + currentUser.username + " " + button.query; @@ -6963,6 +7495,21 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (Build.VERSION.SDK_INT >= 21) { createChatAttachView(); } + + if (chatActivityEnterView.hasRecordVideo()) { + boolean isChannel = false; + if (currentChat != null) { + isChannel = ChatObject.isChannel(currentChat) && !currentChat.megagroup; + } + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + String key = isChannel ? "needShowRoundHintChannel" : "needShowRoundHint"; + if (preferences.getBoolean(key, true)) { + if (Utilities.random.nextFloat() < 0.2f) { + showVoiceHint(false, chatActivityEnterView.isInVideoMode()); + preferences.edit().putBoolean(key, false).commit(); + } + } + } } } @@ -7059,12 +7606,25 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not searchContainer.setVisibility(View.INVISIBLE); if (currentChat != null && (ChatObject.isNotInChat(currentChat) || !ChatObject.canWriteToChat(currentChat)) || currentUser != null && (UserObject.isDeleted(currentUser) || userBlocked)) { - bottomOverlayChat.setVisibility(View.VISIBLE); + if (chatActivityEnterView.isEditingMessage()) { + chatActivityEnterView.setVisibility(View.VISIBLE); + bottomOverlayChat.setVisibility(View.INVISIBLE); + chatActivityEnterView.setFieldFocused(); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + chatActivityEnterView.openKeyboard(); + } + }, 100); + } else { + bottomOverlayChat.setVisibility(View.VISIBLE); + chatActivityEnterView.setFieldFocused(false); + chatActivityEnterView.setVisibility(View.INVISIBLE); + chatActivityEnterView.closeKeyboard(); + } if (muteItem != null) { muteItem.setVisibility(View.GONE); } - chatActivityEnterView.setFieldFocused(false); - chatActivityEnterView.setVisibility(View.INVISIBLE); attachItem.setVisibility(View.GONE); headerItem.setVisibility(View.VISIBLE); } else { @@ -7258,6 +7818,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pinnedMessageImageView.setVisibility(View.INVISIBLE); layoutParams1.leftMargin = layoutParams2.leftMargin = AndroidUtilities.dp(18); } else { + if (pinnedMessageObject.isRoundVideo()) { + pinnedMessageImageView.setRoundRadius(AndroidUtilities.dp(16)); + } else { + pinnedMessageImageView.setRoundRadius(0); + } pinnedImageLocation = photoSize.location; pinnedMessageImageView.setImage(pinnedImageLocation, "50_50", (Drawable) null); pinnedMessageImageView.setVisibility(View.VISIBLE); @@ -7293,6 +7858,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private void updateSpamView() { if (reportSpamView == null) { + FileLog.d("no spam view found"); return; } SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE); @@ -7307,6 +7873,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (!show) { if (reportSpamView.getTag() == null) { + FileLog.d("hide spam button"); reportSpamView.setTag(1); if (reportSpamViewAnimator != null) { @@ -7335,6 +7902,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } else { if (reportSpamView.getTag() != null) { + FileLog.d("show spam button"); reportSpamView.setTag(null); reportSpamView.setVisibility(View.VISIBLE); if (reportSpamViewAnimator != null) { @@ -7438,7 +8006,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } private void checkRaiseSensors() { - if (!ApplicationLoader.mainInterfacePaused && (bottomOverlayChat == null || bottomOverlayChat.getVisibility() != View.VISIBLE) && (bottomOverlay == null || bottomOverlay.getVisibility() != View.VISIBLE) && (searchContainer == null || searchContainer.getVisibility() != View.VISIBLE)) { + if (ChatObject.isChannel(currentChat) && currentChat.banned_rights != null && currentChat.banned_rights.send_media) { + MediaController.getInstance().setAllowStartRecord(false); + } else if (!ApplicationLoader.mainInterfacePaused && (bottomOverlayChat == null || bottomOverlayChat.getVisibility() != View.VISIBLE) && (bottomOverlay == null || bottomOverlay.getVisibility() != View.VISIBLE) && (searchContainer == null || searchContainer.getVisibility() != View.VISIBLE)) { MediaController.getInstance().setAllowStartRecord(true); } else { MediaController.getInstance().setAllowStartRecord(false); @@ -7542,19 +8112,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not super.onPause(); AndroidUtilities.cancelRunOnUIThread(readRunnable); MediaController.getInstance().stopRaiseToEarSensors(this); - if (chatAttachAlert != null) { - if (!ignoreAttachOnPause){ - chatAttachAlert.onPause(); - } else { - ignoreAttachOnPause = false; - } - } paused = true; wasPaused = true; NotificationsController.getInstance().setOpenedDialogId(0); CharSequence draftMessage = null; boolean searchWebpage = true; - if (chatActivityEnterView != null) { + if (!ignoreAttachOnPause && chatActivityEnterView != null) { chatActivityEnterView.onPause(); if (!chatActivityEnterView.isEditingMessage()) { CharSequence text = AndroidUtilities.getTrimmedString(chatActivityEnterView.getFieldText()); @@ -7565,6 +8128,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not searchWebpage = chatActivityEnterView.isMessageWebPageSearchEnabled(); chatActivityEnterView.setFieldFocused(false); } + if (chatAttachAlert != null) { + if (!ignoreAttachOnPause){ + chatAttachAlert.onPause(); + } else { + ignoreAttachOnPause = false; + } + } CharSequence[] message = new CharSequence[] {draftMessage}; ArrayList entities = MessagesQuery.getEntities(message); DraftQuery.saveDraft(dialog_id, message[0], entities, replyingMessageObject != null ? replyingMessageObject.messageOwner : null, !searchWebpage); @@ -7585,7 +8155,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not messageId = ((ChatActionCell) holder.itemView).getMessageObject().getId(); } if (messageId != 0) { - offset = holder.itemView.getMeasuredHeight() - (holder.itemView.getBottom() - chatListView.getMeasuredHeight()); + offset = holder.itemView.getMeasuredHeight() - (holder.itemView.getBottom() - chatListView.getMeasuredHeight()) + AndroidUtilities.dp2(1); + FileLog.d("save offset = " + offset + " for mid " + messageId); } } } @@ -7744,6 +8315,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } private void createDeleteMessagesAlert(final MessageObject finalSelectedObject) { + createDeleteMessagesAlert(finalSelectedObject, 1); + } + + private void createDeleteMessagesAlert(final MessageObject finalSelectedObject, int loadParticipant) { + if (getParentActivity() == null) { + return; + } AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); builder.setMessage(LocaleController.formatString("AreYouSureDeleteMessages", R.string.AreYouSureDeleteMessages, LocaleController.formatPluralString("messages", finalSelectedObject != null ? 1 : selectedMessagesIds[0].size() + selectedMessagesIds[1].size()))); builder.setTitle(LocaleController.getString("Message", R.string.Message)); @@ -7753,12 +8331,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not TLRPC.User user = null; if (currentChat != null && currentChat.megagroup) { boolean hasOutgoing = false; + boolean canBan = ChatObject.canBlockUsers(currentChat); int currentDate = ConnectionsManager.getInstance().getCurrentTime(); if (finalSelectedObject != null) { if (finalSelectedObject.messageOwner.action == null || finalSelectedObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty) { user = MessagesController.getInstance().getUser(finalSelectedObject.messageOwner.from_id); } - hasOutgoing = finalSelectedObject.getDialogId() == mergeDialogId && (finalSelectedObject.messageOwner.action == null || finalSelectedObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty) && finalSelectedObject.isOut() && (currentDate - finalSelectedObject.messageOwner.date) <= 2 * 24 * 60 * 60; + hasOutgoing = !finalSelectedObject.isSendError() && finalSelectedObject.getDialogId() == mergeDialogId && (finalSelectedObject.messageOwner.action == null || finalSelectedObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty) && finalSelectedObject.isOut() && (currentDate - finalSelectedObject.messageOwner.date) <= 2 * 24 * 60 * 60; } else { int from_id = -1; for (int a = 1; a >= 0; a--) { @@ -7807,9 +8386,71 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not user = MessagesController.getInstance().getUser(from_id); } } - if (user != null && user.id != UserConfig.getClientUserId()) { + if (user != null && user.id != UserConfig.getClientUserId() && loadParticipant != 2) { + if (loadParticipant == 1 && !currentChat.creator) { + final AlertDialog progressDialog[] = new AlertDialog[] {new AlertDialog(getParentActivity(), 1)}; + + TLRPC.TL_channels_getParticipant req = new TLRPC.TL_channels_getParticipant(); + req.channel = MessagesController.getInputChannel(currentChat); + req.user_id = MessagesController.getInputUser(user); + int requestId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + try { + progressDialog[0].dismiss(); + } catch (Throwable ignore) { + //ignore + } + progressDialog[0] = null; + int loadType = 2; + if (response != null) { + TLRPC.TL_channels_channelParticipant participant = (TLRPC.TL_channels_channelParticipant) response; + if (!(participant.participant instanceof TLRPC.TL_channelParticipantAdmin || participant.participant instanceof TLRPC.TL_channelParticipantCreator)) { + loadType = 0; + } + } + createDeleteMessagesAlert(finalSelectedObject, loadType); + } + }); + } + }); + if (requestId != 0) { + final int reqId = requestId; + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (progressDialog[0] == null) { + return; + } + progressDialog[0].setMessage(LocaleController.getString("Loading", R.string.Loading)); + progressDialog[0].setCanceledOnTouchOutside(false); + progressDialog[0].setCancelable(false); + progressDialog[0].setButton(DialogInterface.BUTTON_NEGATIVE, LocaleController.getString("Cancel", R.string.Cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ConnectionsManager.getInstance().cancelRequest(reqId, true); + try { + dialog.dismiss(); + } catch (Exception e) { + FileLog.e(e); + } + } + }); + showDialog(progressDialog[0]); + } + }, 1000); + } + return; + } FrameLayout frameLayout = new FrameLayout(getParentActivity()); + int num = 0; for (int a = 0; a < 3; a++) { + if (!canBan && a == 0) { + continue; + } CheckBoxCell cell = new CheckBoxCell(getParentActivity(), true); cell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); cell.setTag(a); @@ -7821,16 +8462,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not cell.setText(LocaleController.formatString("DeleteAllFrom", R.string.DeleteAllFrom, ContactsController.formatName(user.first_name, user.last_name)), "", false, false); } cell.setPadding(LocaleController.isRTL ? AndroidUtilities.dp(16) : AndroidUtilities.dp(8), 0, LocaleController.isRTL ? AndroidUtilities.dp(8) : AndroidUtilities.dp(16), 0); - frameLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.TOP | Gravity.LEFT, 0, 48 * a, 0, 0)); + frameLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.TOP | Gravity.LEFT, 0, 48 * num, 0, 0)); cell.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + if (!v.isEnabled()) { + return; + } CheckBoxCell cell = (CheckBoxCell) v; Integer num = (Integer) cell.getTag(); checks[num] = !checks[num]; cell.setChecked(checks[num], true); } }); + num++; } builder.setView(frameLayout); } else if (hasOutgoing) { @@ -7861,7 +8506,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int currentDate = ConnectionsManager.getInstance().getCurrentTime(); if (currentUser != null && currentUser.id != UserConfig.getClientUserId() || currentChat != null) { if (finalSelectedObject != null) { - hasOutgoing = (finalSelectedObject.messageOwner.action == null || finalSelectedObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty) && finalSelectedObject.isOut() && (currentDate - finalSelectedObject.messageOwner.date) <= 2 * 24 * 60 * 60; + hasOutgoing = !finalSelectedObject.isSendError() && (finalSelectedObject.messageOwner.action == null || finalSelectedObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty) && (finalSelectedObject.isOut() || currentChat != null && (currentChat.creator || currentChat.admin && currentChat.admins_enabled)) && (currentDate - finalSelectedObject.messageOwner.date) <= 2 * 24 * 60 * 60; } else { boolean exit = false; for (int a = 1; a >= 0; a--) { @@ -7871,13 +8516,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (msg.messageOwner.action != null) { continue; } - if (msg.isOut()) { - if ((currentDate - msg.messageOwner.date) <= 2 * 24 * 60 * 60) { + if (msg.isOut() || currentChat != null && (currentChat.creator || currentChat.admin && currentChat.admins_enabled)) { + if (!hasOutgoing && (currentDate - msg.messageOwner.date) <= 2 * 24 * 60 * 60) { hasOutgoing = true; } } else { - hasOutgoing = false; exit = true; + hasOutgoing = false; break; } } @@ -7991,7 +8636,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not final int type = getMessageType(message); if (single) { if (message.messageOwner.action instanceof TLRPC.TL_messageActionPinMessage) { - scrollToMessageId(message.messageOwner.reply_to_msg_id, message.messageOwner.id, true, 0); + scrollToMessageId(message.messageOwner.reply_to_msg_id, message.messageOwner.id, true, 0, false); return; } } @@ -8007,10 +8652,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not updatePinnedMessageView(true); boolean allowChatActions = true; - boolean allowPin = message.getDialogId() != mergeDialogId && message.getId() > 0 && ChatObject.isChannel(currentChat) && currentChat.megagroup && (currentChat.creator || currentChat.editor) && (message.messageOwner.action == null || message.messageOwner.action instanceof TLRPC.TL_messageActionEmpty); - boolean allowUnpin = message.getDialogId() != mergeDialogId && info != null && info.pinned_msg_id == message.getId() && (currentChat.creator || currentChat.editor); + boolean allowPin = message.getDialogId() != mergeDialogId && message.getId() > 0 && ChatObject.isChannel(currentChat) && currentChat.megagroup && (currentChat.creator || currentChat.admin_rights != null && currentChat.admin_rights.pin_messages) && (message.messageOwner.action == null || message.messageOwner.action instanceof TLRPC.TL_messageActionEmpty); + boolean allowUnpin = message.getDialogId() != mergeDialogId && info != null && info.pinned_msg_id == message.getId() && (currentChat.creator || currentChat.admin_rights != null && currentChat.admin_rights.pin_messages); boolean allowEdit = message.canEditMessage(currentChat) && !chatActivityEnterView.hasAudioToSend() && message.getDialogId() != mergeDialogId; - if (currentEncryptedChat != null && AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) < 46 || type == 1 && message.getDialogId() == mergeDialogId || currentEncryptedChat == null && message.getId() < 0 || isBroadcast || currentChat != null && (ChatObject.isNotInChat(currentChat) || ChatObject.isChannel(currentChat) && !currentChat.creator && !currentChat.editor && !currentChat.megagroup)) { + if (currentEncryptedChat != null && AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) < 46 || + type == 1 && message.getDialogId() == mergeDialogId || + currentEncryptedChat == null && message.getId() < 0 || + isBroadcast || + currentChat != null && (ChatObject.isNotInChat(currentChat) || ChatObject.isChannel(currentChat) && !ChatObject.canPost(currentChat) && !currentChat.megagroup || !ChatObject.canSendMessages(currentChat))) { allowChatActions = false; } @@ -8056,6 +8705,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not TLRPC.TL_messageActionPhoneCall call = (TLRPC.TL_messageActionPhoneCall) message.messageOwner.action; items.add((call.reason instanceof TLRPC.TL_phoneCallDiscardReasonMissed || call.reason instanceof TLRPC.TL_phoneCallDiscardReasonBusy) && !message.isOutOwner() ? LocaleController.getString("CallBack", R.string.CallBack) : LocaleController.getString("CallAgain", R.string.CallAgain)); options.add(18); + if(VoIPHelper.canRateCall(call)){ + items.add(LocaleController.getString("CallMessageReportProblem", R.string.CallMessageReportProblem)); + options.add(19); + } } if (single && selectedObject.getId() > 0 && allowChatActions) { items.add(LocaleController.getString("Reply", R.string.Reply)); @@ -8447,7 +9100,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not break; } case 9: { - showDialog(new StickersAlert(getParentActivity(), this, selectedObject.getInputStickerSet(), null, bottomOverlayChat.getVisibility() != View.VISIBLE ? chatActivityEnterView : null)); + showDialog(new StickersAlert(getParentActivity(), this, selectedObject.getInputStickerSet(), null, bottomOverlayChat.getVisibility() != View.VISIBLE && ChatObject.canSendStickers(currentChat) ? chatActivityEnterView : null)); break; } case 10: { @@ -8496,6 +9149,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatListView.setClickable(false); chatListView.setLongClickable(false); chatActivityEnterView.setEditingMessageObject(selectedObject, !selectedObject.isMediaEmpty()); + updateBottomOverlay(); if (chatActivityEnterView.isEditingCaption()) { mentionsAdapter.setAllowNewMentions(false); } @@ -8618,6 +9272,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not VoIPHelper.startCall(currentUser, getParentActivity(), MessagesController.getInstance().getUserFull(currentUser.id)); break; } + case 19:{ + VoIPHelper.showRateAlert(getParentActivity(), (TLRPC.TL_messageActionPhoneCall)selectedObject.messageOwner.action); + break; + } } selectedObject = null; } @@ -8689,9 +9347,36 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } + public boolean checkRecordLocked() { + if (chatActivityEnterView != null && chatActivityEnterView.isRecordLocked()) { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + if (chatActivityEnterView.isInVideoMode()) { + builder.setTitle(LocaleController.getString("DiscardVideoMessageTitle", R.string.DiscardVideoMessageTitle)); + builder.setMessage(LocaleController.getString("DiscardVideoMessageDescription", R.string.DiscardVideoMessageDescription)); + } else { + builder.setTitle(LocaleController.getString("DiscardVoiceMessageTitle", R.string.DiscardVoiceMessageTitle)); + builder.setMessage(LocaleController.getString("DiscardVoiceMessageDescription", R.string.DiscardVoiceMessageDescription)); + } + builder.setPositiveButton(LocaleController.getString("DiscardVoiceMessageAction", R.string.DiscardVoiceMessageAction), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (chatActivityEnterView != null) { + chatActivityEnterView.cancelRecordingAudioVideo(); + } + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + showDialog(builder.create()); + return true; + } + return false; + } + @Override public boolean onBackPressed() { - if (actionBar != null && actionBar.isActionModeShowed()) { + if (checkRecordLocked()) { + return false; + } else if (actionBar != null && actionBar.isActionModeShowed()) { for (int a = 1; a >= 0; a--) { selectedMessagesIds[a].clear(); selectedMessagesCanCopyIds[a].clear(); @@ -8796,7 +9481,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not boolean started = false; for (int a = messages.size() - 1; a >= 0; a--) { MessageObject messageObject = messages.get(a); - if ((currentEncryptedChat == null && messageObject.getId() > messageId || currentEncryptedChat != null && messageObject.getId() < messageId) && messageObject.isVoice() && (!playingUnreadMedia || messageObject.isContentUnread() && !messageObject.isOut())) { + if ((currentEncryptedChat == null && messageObject.getId() > messageId || currentEncryptedChat != null && messageObject.getId() < messageId) && (messageObject.isVoice() || messageObject.isRoundVideo()) && (!playingUnreadMedia || messageObject.isContentUnread() && !messageObject.isOut())) { messageObjects.add(messageObject); } } @@ -8946,7 +9631,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void setPhotoChecked(int index) { + public void setPhotoChecked(int index, VideoEditedInfo videoEditedInfo) { } @Override @@ -9121,9 +9806,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public boolean needPlayAudio(MessageObject messageObject) { - if (messageObject.isVoice()) { - boolean result = MediaController.getInstance().playAudio(messageObject); + public boolean needPlayMessage(MessageObject messageObject) { + if (messageObject.isVoice() || messageObject.isRoundVideo()) { + boolean result = MediaController.getInstance().playMessage(messageObject); MediaController.getInstance().setVoiceMessagesPlaylist(result ? createVoiceMessagesPlaylist(messageObject, false) : null, false); return result; } else if (messageObject.isMusic()) { @@ -9263,8 +9948,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not 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(lowerUrl2) || lowerUrl2.contains(lowerUrl))) { - ArticleViewer.getInstance().setParentActivity(getParentActivity()); + if ((lowerUrl.contains("telegra.ph") || lowerUrl.contains("t.me/iv")) && (lowerUrl.contains(lowerUrl2) || lowerUrl2.contains(lowerUrl))) { + ArticleViewer.getInstance().setParentActivity(getParentActivity(), ChatActivity.this); ArticleViewer.getInstance().open(messageObject); return; } @@ -9285,7 +9970,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void didPressedReplyMessage(ChatMessageCell cell, int id) { MessageObject messageObject = cell.getMessageObject(); - scrollToMessageId(id, messageObject.getId(), true, messageObject.getDialogId() == mergeDialogId ? 1 : 0); + scrollToMessageId(id, messageObject.getId(), true, messageObject.getDialogId() == mergeDialogId ? 1 : 0, false); } @Override @@ -9309,7 +9994,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } if (message.type == 13) { - showDialog(new StickersAlert(getParentActivity(), ChatActivity.this, message.getInputStickerSet(), null, bottomOverlayChat.getVisibility() != View.VISIBLE ? chatActivityEnterView : null)); + showDialog(new StickersAlert(getParentActivity(), ChatActivity.this, message.getInputStickerSet(), null, bottomOverlayChat.getVisibility() != View.VISIBLE && ChatObject.canSendStickers(currentChat) ? chatActivityEnterView : null)); } else if (Build.VERSION.SDK_INT >= 16 && message.isVideo() || message.type == 1 || message.type == 0 && !message.isWebpageDocument() || message.isGif()) { if (message.isVideo()) { sendSecretMessageRead(message); @@ -9392,11 +10077,15 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void didPressedInstantButton(ChatMessageCell cell) { + public void didPressedInstantButton(ChatMessageCell cell, int type) { MessageObject messageObject = cell.getMessageObject(); - if (messageObject.messageOwner.media != null && messageObject.messageOwner.media.webpage != null && messageObject.messageOwner.media.webpage.cached_page != null) { - ArticleViewer.getInstance().setParentActivity(getParentActivity()); - ArticleViewer.getInstance().open(messageObject); + if (type == 0) { + if (messageObject.messageOwner.media != null && messageObject.messageOwner.media.webpage != null && messageObject.messageOwner.media.webpage.cached_page != null) { + ArticleViewer.getInstance().setParentActivity(getParentActivity(), ChatActivity.this); + ArticleViewer.getInstance().open(messageObject); + } + } else { + Browser.openUrl(getParentActivity(), messageObject.messageOwner.media.webpage.url); } } }); @@ -9446,7 +10135,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void didPressedReplyMessage(ChatActionCell cell, int id) { MessageObject messageObject = cell.getMessageObject(); - scrollToMessageId(id, messageObject.getId(), true, messageObject.getDialogId() == mergeDialogId ? 1 : 0); + scrollToMessageId(id, messageObject.getId(), true, messageObject.getDialogId() == mergeDialogId ? 1 : 0, false); } @Override @@ -9527,14 +10216,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not messageCell.setHighlightedText(null); } int index; - if (BuildVars.DEBUG_PRIVATE_VERSION && (index = animatingMessageObjects.indexOf(message)) != -1) { + if ((index = animatingMessageObjects.indexOf(message)) != -1) { animatingMessageObjects.remove(index); - final ChatMessageCell animatingView = messageCell; messageCell.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { - animatingView.getViewTreeObserver().removeOnPreDrawListener(this); //TODO - ImageReceiver imageReceiver = animatingView.getPhotoImage(); + PipRoundVideoView pipRoundVideoView = PipRoundVideoView.getInstance(); + if (pipRoundVideoView != null) { + pipRoundVideoView.showTemporary(true); + } + + messageCell.getViewTreeObserver().removeOnPreDrawListener(this); //TODO + ImageReceiver imageReceiver = messageCell.getPhotoImage(); int w = imageReceiver.getImageWidth(); org.telegram.ui.Components.Rect rect = instantCameraView.getCameraRect(); float scale = w / rect.width; @@ -9548,12 +10241,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not cameraContainer.setPivotY(0.0f); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimator.ofFloat(actionBarOverlay, "alpha", 0.0f), ObjectAnimator.ofFloat(instantCameraView, "alpha", 0.0f), ObjectAnimator.ofFloat(cameraContainer, "scaleX", scale), ObjectAnimator.ofFloat(cameraContainer, "scaleY", scale), ObjectAnimator.ofFloat(cameraContainer, "translationX", position[0] - rect.x), - ObjectAnimator.ofFloat(cameraContainer, "translationY", position[1] - rect.y)); + ObjectAnimator.ofFloat(cameraContainer, "translationY", position[1] - rect.y), + ObjectAnimator.ofFloat(instantCameraView.getSwitchButtonView(), "alpha", 0.0f), + ObjectAnimator.ofInt(instantCameraView.getPaint(), "alpha", 0), + ObjectAnimator.ofFloat(instantCameraView.getMuteImageView(), "alpha", 0.0f)); animatorSet.setDuration(180); animatorSet.setInterpolator(new DecelerateInterpolator()); animatorSet.addListener(new AnimatorListenerAdapter() { @@ -9571,7 +10266,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void onAnimationEnd(Animator animation) { instantCameraView.hideCamera(true); - instantCameraView.setVisibility(View.GONE); + instantCameraView.setVisibility(View.INVISIBLE); } }); animatorSet.start(); @@ -9829,9 +10524,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInMenuSelectedDrawable}, null, Theme.key_chat_inMenuSelected), new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgMediaMenuDrawable}, null, Theme.key_chat_mediaMenu), new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutInstantDrawable, Theme.chat_msgOutCallDrawable}, null, Theme.key_chat_outInstant), - new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutInstantSelectedDrawable, Theme.chat_msgOutCallSelectedDrawable}, null, Theme.key_chat_outInstantSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgOutCallSelectedDrawable}, null, Theme.key_chat_outInstantSelected), new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInInstantDrawable, Theme.chat_msgInCallDrawable}, null, Theme.key_chat_inInstant), - new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInInstantSelectedDrawable, Theme.chat_msgInCallSelectedDrawable}, null, Theme.key_chat_inInstantSelected), + new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgInCallSelectedDrawable}, null, Theme.key_chat_inInstantSelected), new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgCallUpRedDrawable, Theme.chat_msgCallDownRedDrawable}, null, Theme.key_calls_callReceivedRedIcon), new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, null, new Drawable[]{Theme.chat_msgCallUpGreenDrawable, Theme.chat_msgCallDownGreenDrawable}, null, Theme.key_calls_callReceivedGreenIcon), new ThemeDescription(chatListView, 0, new Class[]{ChatMessageCell.class}, Theme.chat_msgErrorPaint, null, null, Theme.key_chat_sentError), @@ -9972,6 +10667,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not new ThemeDescription(chatActivityEnterView, 0, null, null, new Drawable[]{Theme.chat_composeShadowDrawable}, null, Theme.key_chat_messagePanelShadow), new ThemeDescription(chatActivityEnterView, ThemeDescription.FLAG_BACKGROUND, new Class[]{ChatActivityEnterView.class}, new String[]{"audioVideoButtonContainer"}, null, null, null, Theme.key_chat_messagePanelBackground), new ThemeDescription(chatActivityEnterView, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{ChatActivityEnterView.class}, new String[]{"messageEditText"}, null, null, null, Theme.key_chat_messagePanelText), + new ThemeDescription(chatActivityEnterView, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{ChatActivityEnterView.class}, new String[]{"recordSendText"}, null, null, null, Theme.key_chat_fieldOverlayText), new ThemeDescription(chatActivityEnterView, ThemeDescription.FLAG_HINTTEXTCOLOR, new Class[]{ChatActivityEnterView.class}, new String[]{"messageEditText"}, null, null, null, Theme.key_chat_messagePanelHint), new ThemeDescription(chatActivityEnterView, 0, new Class[]{ChatActivityEnterView.class}, new String[]{"sendButton"}, null, null, null, Theme.key_chat_messagePanelSend), new ThemeDescription(chatActivityEnterView, 0, new Class[]{ChatActivityEnterView.class}, new String[]{"emojiButton"}, null, null, null, Theme.key_chat_messagePanelIcons), @@ -9984,6 +10680,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not new ThemeDescription(chatActivityEnterView, ThemeDescription.FLAG_BACKGROUND, new Class[]{ChatActivityEnterView.class}, new String[]{"recordedAudioPanel"}, null, null, null, Theme.key_chat_messagePanelBackground), new ThemeDescription(chatActivityEnterView, 0, new Class[]{ChatActivityEnterView.class}, new String[]{"micDrawable"}, null, null, null, Theme.key_chat_messagePanelVoicePressed), new ThemeDescription(chatActivityEnterView, 0, new Class[]{ChatActivityEnterView.class}, new String[]{"cameraDrawable"}, null, null, null, Theme.key_chat_messagePanelVoicePressed), + new ThemeDescription(chatActivityEnterView, 0, new Class[]{ChatActivityEnterView.class}, new String[]{"sendDrawable"}, null, null, null, Theme.key_chat_messagePanelVoicePressed), + new ThemeDescription(chatActivityEnterView, 0, new Class[]{ChatActivityEnterView.class}, new String[]{"lockDrawable"}, null, null, null, Theme.key_chat_messagePanelVoiceLock), + new ThemeDescription(chatActivityEnterView, 0, new Class[]{ChatActivityEnterView.class}, new String[]{"lockTopDrawable"}, null, null, null, Theme.key_chat_messagePanelVoiceLock), + new ThemeDescription(chatActivityEnterView, 0, new Class[]{ChatActivityEnterView.class}, new String[]{"lockArrowDrawable"}, null, null, null, Theme.key_chat_messagePanelVoiceLock), + new ThemeDescription(chatActivityEnterView, 0, new Class[]{ChatActivityEnterView.class}, new String[]{"lockBackgroundDrawable"}, null, null, null, Theme.key_chat_messagePanelVoiceLockBackground), + new ThemeDescription(chatActivityEnterView, 0, new Class[]{ChatActivityEnterView.class}, new String[]{"lockShadowDrawable"}, null, null, null, Theme.key_chat_messagePanelVoiceLockShadow), new ThemeDescription(chatActivityEnterView, ThemeDescription.FLAG_IMAGECOLOR, new Class[]{ChatActivityEnterView.class}, new String[]{"recordDeleteImageView"}, null, null, null, Theme.key_chat_messagePanelVoiceDelete), new ThemeDescription(chatActivityEnterView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ChatActivityEnterView.class}, new String[]{"recordedAudioBackground"}, null, null, null, Theme.key_chat_recordedVoiceBackground), new ThemeDescription(chatActivityEnterView, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{ChatActivityEnterView.class}, new String[]{"recordTimeText"}, null, null, null, Theme.key_chat_recordTime), diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/AdminLogFilterAlert.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/AdminLogFilterAlert.java new file mode 100644 index 000000000..103b00fbe --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AdminLogFilterAlert.java @@ -0,0 +1,510 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Components; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MessagesController; +import org.telegram.messenger.R; +import org.telegram.messenger.support.widget.LinearLayoutManager; +import org.telegram.messenger.support.widget.RecyclerView; +import org.telegram.tgnet.TLRPC; +import org.telegram.ui.ActionBar.BottomSheet; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Cells.CheckBoxCell; +import org.telegram.ui.Cells.CheckBoxUserCell; +import org.telegram.ui.Cells.ShadowSectionCell; +import org.telegram.ui.StickerPreviewViewer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.regex.Pattern; + +public class AdminLogFilterAlert extends BottomSheet { + + public interface AdminLogFilterAlertDelegate { + void didSelectRights(TLRPC.TL_channelAdminLogEventsFilter filter, HashMap admins); + } + + private Pattern urlPattern; + private RecyclerListView listView; + private ListAdapter adapter; + private FrameLayout pickerBottomLayout; + private Drawable shadowDrawable; + private BottomSheet.BottomSheetCell saveButton; + + private AdminLogFilterAlertDelegate delegate; + + private int scrollOffsetY; + private int reqId; + private boolean ignoreLayout; + + private TLRPC.TL_channelAdminLogEventsFilter currentFilter; + private ArrayList currentAdmins; + private HashMap selectedAdmins; + private boolean isMegagroup; + + private int restrictionsRow; + private int adminsRow; + private int membersRow; + private int infoRow; + private int deleteRow; + private int editRow; + private int pinnedRow; + private int leavingRow; + private int allAdminsRow; + + public AdminLogFilterAlert(Context context, TLRPC.TL_channelAdminLogEventsFilter filter, HashMap admins, boolean megagroup) { + super(context, false); + if (filter != null) { + currentFilter = new TLRPC.TL_channelAdminLogEventsFilter(); + currentFilter.join = filter.join; + currentFilter.leave = filter.leave; + currentFilter.invite = filter.invite; + currentFilter.ban = filter.ban; + currentFilter.unban = filter.unban; + currentFilter.kick = filter.kick; + currentFilter.unkick = filter.unkick; + currentFilter.promote = filter.promote; + currentFilter.demote = filter.demote; + currentFilter.info = filter.info; + currentFilter.settings = filter.settings; + currentFilter.pinned = filter.pinned; + currentFilter.edit = filter.edit; + currentFilter.delete = filter.delete; + } + if (admins != null) { + selectedAdmins = new HashMap<>(admins); + } + isMegagroup = megagroup; + + int rowCount = 1; + if (isMegagroup) { + restrictionsRow = rowCount++; + } else { + restrictionsRow = -1; + } + adminsRow = rowCount++; + membersRow = rowCount++; + infoRow = rowCount++; + deleteRow = rowCount++; + editRow = rowCount++; + if (isMegagroup) { + pinnedRow = rowCount++; + } else { + pinnedRow = -1; + } + leavingRow = rowCount; + rowCount += 2; + allAdminsRow = rowCount; + + shadowDrawable = context.getResources().getDrawable(R.drawable.sheet_shadow).mutate(); + shadowDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogBackground), PorterDuff.Mode.MULTIPLY)); + + containerView = new FrameLayout(context) { + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN && scrollOffsetY != 0 && ev.getY() < scrollOffsetY) { + dismiss(); + return true; + } + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent e) { + return !isDismissed() && super.onTouchEvent(e); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int height = MeasureSpec.getSize(heightMeasureSpec); + if (Build.VERSION.SDK_INT >= 21) { + height -= AndroidUtilities.statusBarHeight; + } + int measuredWidth = getMeasuredWidth(); + int contentSize = AndroidUtilities.dp(48) + (isMegagroup ? 9 : 7) * AndroidUtilities.dp(48) + backgroundPaddingTop; + if (currentAdmins != null) { + contentSize += (currentAdmins.size() + 1) * AndroidUtilities.dp(48) + AndroidUtilities.dp(20); + } + + int padding = contentSize < (height / 5 * 3.2f) ? 0 : (height / 5 * 2); + if (padding != 0 && contentSize < height) { + padding -= (height - contentSize); + } + if (padding == 0) { + padding = backgroundPaddingTop; + } + if (listView.getPaddingTop() != padding) { + ignoreLayout = true; + listView.setPadding(0, padding, 0, 0); + ignoreLayout = false; + } + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateLayout(); + } + + @Override + public void requestLayout() { + if (ignoreLayout) { + return; + } + super.requestLayout(); + } + + @Override + protected void onDraw(Canvas canvas) { + shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); + shadowDrawable.draw(canvas); + } + }; + containerView.setWillNotDraw(false); + containerView.setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); + + listView = new RecyclerListView(context) { + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + boolean result = StickerPreviewViewer.getInstance().onInterceptTouchEvent(event, listView, 0, null); + return super.onInterceptTouchEvent(event) || result; + } + + @Override + public void requestLayout() { + if (ignoreLayout) { + return; + } + super.requestLayout(); + } + }; + listView.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)); + listView.setAdapter(adapter = new ListAdapter(context)); + listView.setVerticalScrollBarEnabled(false); + listView.setClipToPadding(false); + listView.setEnabled(true); + listView.setGlowColor(Theme.getColor(Theme.key_dialogScrollGlow)); + listView.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + updateLayout(); + } + }); + listView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + if (view instanceof CheckBoxCell) { + CheckBoxCell cell = (CheckBoxCell) view; + boolean isChecked = cell.isChecked(); + cell.setChecked(!isChecked, true); + if (position == 0) { + if (isChecked) { + currentFilter = new TLRPC.TL_channelAdminLogEventsFilter(); + currentFilter.join = currentFilter.leave = currentFilter.invite = currentFilter.ban = + currentFilter.unban = currentFilter.kick = currentFilter.unkick = currentFilter.promote = + currentFilter.demote = currentFilter.info = currentFilter.settings = currentFilter.pinned = + currentFilter.edit = currentFilter.delete = false; + } else { + currentFilter = null; + } + int count = listView.getChildCount(); + for (int a = 0; a < count; a++) { + View child = listView.getChildAt(a); + RecyclerView.ViewHolder holder = listView.findContainingViewHolder(child); + int pos = holder.getAdapterPosition(); + if (holder.getItemViewType() == 0 && pos > 0 && pos < allAdminsRow - 1) { + ((CheckBoxCell) child).setChecked(!isChecked, true); + } + } + } else if (position == allAdminsRow) { + if (isChecked) { + selectedAdmins = new HashMap<>(); + } else { + selectedAdmins = null; + } + int count = listView.getChildCount(); + for (int a = 0; a < count; a++) { + View child = listView.getChildAt(a); + RecyclerView.ViewHolder holder = listView.findContainingViewHolder(child); + int pos = holder.getAdapterPosition(); + if (holder.getItemViewType() == 2) { + CheckBoxUserCell userCell = (CheckBoxUserCell) child; + userCell.setChecked(!isChecked, true); + } + } + } else { + if (currentFilter == null) { + currentFilter = new TLRPC.TL_channelAdminLogEventsFilter(); + currentFilter.join = currentFilter.leave = currentFilter.invite = currentFilter.ban = + currentFilter.unban = currentFilter.kick = currentFilter.unkick = currentFilter.promote = + currentFilter.demote = currentFilter.info = currentFilter.settings = currentFilter.pinned = + currentFilter.edit = currentFilter.delete = true; + RecyclerView.ViewHolder holder = listView.findViewHolderForAdapterPosition(0); + if (holder != null) { + ((CheckBoxCell) holder.itemView).setChecked(false, true); + } + } + if (position == restrictionsRow) { + currentFilter.kick = currentFilter.ban = currentFilter.unkick = currentFilter.unban = !currentFilter.kick; + } else if (position == adminsRow) { + currentFilter.promote = currentFilter.demote = !currentFilter.demote; + } else if (position == membersRow) { + currentFilter.invite = currentFilter.join = !currentFilter.join; + } else if (position == infoRow) { + currentFilter.info = currentFilter.settings = !currentFilter.info; + } else if (position == deleteRow) { + currentFilter.delete = !currentFilter.delete; + } else if (position == editRow) { + currentFilter.edit = !currentFilter.edit; + } else if (position == pinnedRow) { + currentFilter.pinned = !currentFilter.pinned; + } else if (position == leavingRow) { + currentFilter.leave = !currentFilter.leave; + } + } + if (currentFilter != null && !currentFilter.join && !currentFilter.leave && + !currentFilter.leave && !currentFilter.invite && !currentFilter.ban && + !currentFilter.unban && !currentFilter.kick && !currentFilter.unkick && + !currentFilter.promote && !currentFilter.demote && !currentFilter.info && + !currentFilter.settings && !currentFilter.pinned && !currentFilter.edit && + !currentFilter.delete) { + saveButton.setEnabled(false); + saveButton.setAlpha(0.5f); + } else { + saveButton.setEnabled(true); + saveButton.setAlpha(1.0f); + } + } else if (view instanceof CheckBoxUserCell) { + CheckBoxUserCell checkBoxUserCell = (CheckBoxUserCell) view; + if (selectedAdmins == null) { + selectedAdmins = new HashMap<>(); + RecyclerView.ViewHolder holder = listView.findViewHolderForAdapterPosition(allAdminsRow); + if (holder != null) { + ((CheckBoxCell) holder.itemView).setChecked(false, true); + } + for (int a = 0; a < currentAdmins.size(); a++) { + TLRPC.User user = MessagesController.getInstance().getUser(currentAdmins.get(a).user_id); + selectedAdmins.put(user.id, user); + } + } + boolean isChecked = checkBoxUserCell.isChecked(); + TLRPC.User user = checkBoxUserCell.getCurrentUser(); + if (isChecked) { + selectedAdmins.remove(user.id); + } else { + selectedAdmins.put(user.id, user); + } + checkBoxUserCell.setChecked(!isChecked, true); + } + } + }); + containerView.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 0, 0, 48)); + + View shadow = new View(context); + shadow.setBackgroundResource(R.drawable.header_shadow_reverse); + containerView.addView(shadow, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.BOTTOM | Gravity.LEFT, 0, 0, 0, 48)); + + saveButton = new BottomSheet.BottomSheetCell(context, 1); + saveButton.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + saveButton.setTextAndIcon(LocaleController.getString("Save", R.string.Save).toUpperCase(), 0); + saveButton.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2)); + saveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + delegate.didSelectRights(currentFilter, selectedAdmins); + dismiss(); + } + }); + containerView.addView(saveButton, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.BOTTOM)); + + adapter.notifyDataSetChanged(); + } + + public void setCurrentAdmins(ArrayList admins) { + currentAdmins = admins; + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + } + + @Override + protected boolean canDismissWithSwipe() { + return false; + } + + public void setAdminLogFilterAlertDelegate(AdminLogFilterAlertDelegate adminLogFilterAlertDelegate) { + delegate = adminLogFilterAlertDelegate; + } + + @SuppressLint("NewApi") + 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() - AndroidUtilities.dp(8); + int newOffset = top > 0 && holder != null && holder.getAdapterPosition() == 0 ? top : 0; + if (scrollOffsetY != newOffset) { + listView.setTopGlowOffset(scrollOffsetY = newOffset); + containerView.invalidate(); + } + } + + private class ListAdapter extends RecyclerListView.SelectionAdapter { + + private Context context; + + public ListAdapter(Context context) { + this.context = context; + } + + @Override + public int getItemCount() { + return (isMegagroup ? 9 : 7) + (currentAdmins != null ? 2 + currentAdmins.size() : 0); + } + + @Override + public int getItemViewType(int position) { + if (position < allAdminsRow - 1 || position == allAdminsRow) { + return 0; + } else if (position == allAdminsRow - 1) { + return 1; + } else { + return 2; + } + } + + @Override + public boolean isEnabled(RecyclerView.ViewHolder holder) { + return holder.getItemViewType() != 1; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = null; + switch (viewType) { + case 0: + view = new CheckBoxCell(context, true); + view.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + break; + case 1: + ShadowSectionCell shadowSectionCell = new ShadowSectionCell(context); + shadowSectionCell.setSize(18); + view = new FrameLayout(context); + ((FrameLayout) view).addView(shadowSectionCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + view.setBackgroundColor(Theme.getColor(Theme.key_dialogBackgroundGray)); + break; + case 2: + view = new CheckBoxUserCell(context, true); + break; + } + + return new RecyclerListView.Holder(view); + } + + @Override + public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { + int position = holder.getAdapterPosition(); + switch (holder.getItemViewType()) { + case 0: { + CheckBoxCell cell = (CheckBoxCell) holder.itemView; + if (position == 0) { + cell.setChecked(currentFilter == null, false); + } else if (position == restrictionsRow) { + cell.setChecked(currentFilter == null || currentFilter.kick && currentFilter.ban && currentFilter.unkick && currentFilter.unban, false); + } else if (position == adminsRow) { + cell.setChecked(currentFilter == null || currentFilter.promote && currentFilter.demote, false); + } else if (position == membersRow) { + cell.setChecked(currentFilter == null || currentFilter.invite && currentFilter.join, false); + } else if (position == infoRow) { + cell.setChecked(currentFilter == null || currentFilter.info, false); + } else if (position == deleteRow) { + cell.setChecked(currentFilter == null || currentFilter.delete, false); + } else if (position == editRow) { + cell.setChecked(currentFilter == null || currentFilter.edit, false); + } else if (position == pinnedRow) { + cell.setChecked(currentFilter == null || currentFilter.pinned, false); + } else if (position == leavingRow) { + cell.setChecked(currentFilter == null || currentFilter.leave, false); + } else if (position == allAdminsRow) { + cell.setChecked(selectedAdmins == null, false); + } + break; + } + case 2: { + CheckBoxUserCell userCell = (CheckBoxUserCell) holder.itemView; + int userId = currentAdmins.get(position - allAdminsRow - 1).user_id; + userCell.setChecked(selectedAdmins == null || selectedAdmins.containsKey(userId), false); + break; + } + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + switch (holder.getItemViewType()) { + case 0: { + CheckBoxCell cell = (CheckBoxCell) holder.itemView; + if (position == 0) { + cell.setText(LocaleController.getString("EventLogFilterAll", R.string.EventLogFilterAll), "", currentFilter == null, true); + } else if (position == restrictionsRow) { + cell.setText(LocaleController.getString("EventLogFilterNewRestrictions", R.string.EventLogFilterNewRestrictions), "", currentFilter == null || currentFilter.kick && currentFilter.ban && currentFilter.unkick && currentFilter.unban, true); + } else if (position == adminsRow) { + cell.setText(LocaleController.getString("EventLogFilterNewAdmins", R.string.EventLogFilterNewAdmins), "", currentFilter == null || currentFilter.promote && currentFilter.demote, true); + } else if (position == membersRow) { + cell.setText(LocaleController.getString("EventLogFilterNewMembers", R.string.EventLogFilterNewMembers), "", currentFilter == null || currentFilter.invite && currentFilter.join, true); + } else if (position == infoRow) { + if (isMegagroup) { + cell.setText(LocaleController.getString("EventLogFilterGroupInfo", R.string.EventLogFilterGroupInfo), "", currentFilter == null || currentFilter.info, true); + } else { + cell.setText(LocaleController.getString("EventLogFilterChannelInfo", R.string.EventLogFilterChannelInfo), "", currentFilter == null || currentFilter.info, true); + } + } else if (position == deleteRow) { + cell.setText(LocaleController.getString("EventLogFilterDeletedMessages", R.string.EventLogFilterDeletedMessages), "", currentFilter == null || currentFilter.delete, true); + } else if (position == editRow) { + cell.setText(LocaleController.getString("EventLogFilterEditedMessages", R.string.EventLogFilterEditedMessages), "", currentFilter == null || currentFilter.edit, true); + } else if (position == pinnedRow) { + cell.setText(LocaleController.getString("EventLogFilterPinnedMessages", R.string.EventLogFilterPinnedMessages), "", currentFilter == null || currentFilter.pinned, true); + } else if (position == leavingRow) { + cell.setText(LocaleController.getString("EventLogFilterLeavingMembers", R.string.EventLogFilterLeavingMembers), "", currentFilter == null || currentFilter.leave, false); + } else if (position == allAdminsRow) { + cell.setText(LocaleController.getString("EventLogAllAdmins", R.string.EventLogAllAdmins), "", selectedAdmins == null, true); + } + break; + } + case 2: { + CheckBoxUserCell userCell = (CheckBoxUserCell) holder.itemView; + int userId = currentAdmins.get(position - allAdminsRow - 1).user_id; + userCell.setUser(MessagesController.getInstance().getUser(userId), selectedAdmins == null || selectedAdmins.containsKey(userId), position != getItemCount() - 1); + break; + } + } + } + } +} 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 36e40de57..bc1219239 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java @@ -49,7 +49,8 @@ public class AlertsCreator { request instanceof TLRPC.TL_channels_editAdmin || request instanceof TLRPC.TL_channels_inviteToChannel || request instanceof TLRPC.TL_messages_addChatUser || - request instanceof TLRPC.TL_messages_startBot) { + request instanceof TLRPC.TL_messages_startBot || + request instanceof TLRPC.TL_channels_editBanned) { if (fragment != null) { AlertsCreator.showAddUserAlert(error.text, fragment, (Boolean) args[0]); } else { @@ -352,6 +353,21 @@ public class AlertsCreator { fragment.showDialog(builder.create(), true, null); } + public static void showSendMediaAlert(int result, final BaseFragment fragment) { + if (result == 0) { + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(fragment.getParentActivity()); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + if (result == 1) { + builder.setMessage(LocaleController.getString("ErrorSendRestrictedStickers", R.string.ErrorSendRestrictedStickers)); + } else if (result == 2) { + builder.setMessage(LocaleController.getString("ErrorSendRestrictedMedia", R.string.ErrorSendRestrictedMedia)); + } + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); + fragment.showDialog(builder.create(), true, null); + } + public static void showAddUserAlert(String error, final BaseFragment fragment, boolean isChannel) { if (error == null || fragment == null || fragment.getParentActivity() == null) { return; @@ -421,8 +437,18 @@ public class AlertsCreator { case "YOU_BLOCKED_USER": builder.setMessage(LocaleController.getString("YouBlockedUser", R.string.YouBlockedUser)); break; + case "CHAT_ADMIN_BAN_REQUIRED": + case "USER_KICKED": + builder.setMessage(LocaleController.getString("AddAdminErrorBlacklisted", R.string.AddAdminErrorBlacklisted)); + break; + case "CHAT_ADMIN_INVITE_REQUIRED": + builder.setMessage(LocaleController.getString("AddAdminErrorNotAMember", R.string.AddAdminErrorNotAMember)); + break; + case "USER_ADMIN_INVALID": + builder.setMessage(LocaleController.getString("AddBannedErrorAdmin", R.string.AddBannedErrorAdmin)); + break; default: - builder.setMessage(error); + builder.setMessage(LocaleController.getString("ErrorOccurred", R.string.ErrorOccurred) + "\n" + error); break; } builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); 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 848bd9538..1f7a49138 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java @@ -47,6 +47,8 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private Bitmap backgroundBitmap; private boolean destroyWhenDone; private boolean decoderCreated; + private boolean decodeSingleFrame; + private boolean singleFrameDecoded; private File path; private boolean recycleWithSecond; @@ -105,6 +107,7 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { } return; } + singleFrameDecoded = true; loadFrameTask = null; nextRenderingBitmap = backgroundBitmap; nextRenderingShader = backgroundShader; @@ -186,6 +189,13 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { } } + public void setAllowDecodeSingleFrame(boolean value) { + decodeSingleFrame = value; + if (decodeSingleFrame) { + scheduleNextGetFrame(); + } + } + public void recycle() { if (secondParentView != null) { recycleWithSecond = true; @@ -244,7 +254,7 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { } private void scheduleNextGetFrame() { - if (loadFrameTask != null || nativePtr == 0 && decoderCreated || destroyWhenDone || !isRunning) { + if (loadFrameTask != null || nativePtr == 0 && decoderCreated || destroyWhenDone || !isRunning && (!decodeSingleFrame || decodeSingleFrame && singleFrameDecoded)) { return; } long ms = 0; @@ -298,6 +308,12 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { lastFrameTime = now; } } + } else if (!isRunning && decodeSingleFrame && Math.abs(now - lastFrameTime) >= invalidateAfter && nextRenderingBitmap != null) { + renderingBitmap = nextRenderingBitmap; + renderingShader = nextRenderingShader; + nextRenderingBitmap = null; + nextRenderingShader = null; + lastFrameTime = now; } if (renderingBitmap != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/AvatarUpdater.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/AvatarUpdater.java index 2028c804a..91ec63848 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/AvatarUpdater.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AvatarUpdater.java @@ -39,6 +39,7 @@ import org.telegram.ui.PhotoViewer; import java.io.File; import java.util.ArrayList; +import java.util.Objects; public class AvatarUpdater implements NotificationCenter.NotificationCenterDelegate, PhotoCropActivity.PhotoEditActivityDelegate { @@ -66,7 +67,14 @@ public class AvatarUpdater implements NotificationCenter.NotificationCenterDeleg } public void openCamera() { + 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 takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); File image = AndroidUtilities.generatePicturePath(); if (image != null) { @@ -95,7 +103,7 @@ public class AvatarUpdater implements NotificationCenter.NotificationCenterDeleg PhotoAlbumPickerActivity fragment = new PhotoAlbumPickerActivity(true, false, false, null); fragment.setDelegate(new PhotoAlbumPickerActivity.PhotoAlbumPickerActivityDelegate() { @Override - public void didSelectPhotos(ArrayList photos, ArrayList captions, ArrayList> masks, ArrayList webPhotos) { + public void didSelectPhotos(ArrayList photos, ArrayList captions, ArrayList videos, ArrayList> masks, ArrayList webPhotos) { if (!photos.isEmpty()) { Bitmap bitmap = ImageLoader.loadBitmap(photos.get(0), null, 800, 800, true); processBitmap(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 dbedd5974..ec4319254 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java @@ -23,6 +23,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.os.Build; @@ -55,7 +56,6 @@ import android.widget.TextView; import android.widget.Toast; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.BuildVars; import org.telegram.messenger.ChatObject; import org.telegram.messenger.Emoji; import org.telegram.messenger.LocaleController; @@ -67,6 +67,8 @@ import org.telegram.messenger.SendMessagesHelper; import org.telegram.messenger.FileLog; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; +import org.telegram.messenger.VideoEditedInfo; +import org.telegram.messenger.camera.CameraController; import org.telegram.messenger.query.DraftQuery; import org.telegram.messenger.query.MessagesQuery; import org.telegram.messenger.query.StickersQuery; @@ -98,6 +100,11 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe void onMessageEditEnd(boolean loading); void didPressedAttachButton(); void needStartRecordVideo(int state); + void needChangeVideoPreviewState(int state, float seekProgress); + void onSwitchRecordMode(boolean video); + void onPreAudioVideoRecord(); + void needStartRecordAudio(int state); + void needShowMediaBanHint(); } private class SeekBarWaveformView extends View { @@ -170,6 +177,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe private ImageView videoSendButton; private FrameLayout recordPanel; private FrameLayout recordedAudioPanel; + private VideoTimelineView videoTimelineView; private ImageView recordDeleteImageView; private SeekBarWaveformView recordedAudioSeekBar; private View recordedAudioBackground; @@ -178,6 +186,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe private LinearLayout slideText; private ImageView recordCancelImage; private TextView recordCancelText; + private TextView recordSendText; private LinearLayout recordTimeContainer; private RecordDot recordDot; private SizeNotifierFrameLayout sizeNotifierLayout; @@ -204,7 +213,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe private int editingMessageReqId; private boolean editingCaption; - private boolean hasRecordVideo = BuildVars.DEBUG_PRIVATE_VERSION; + private boolean hasRecordVideo; private int currentPopupContentType = -1; @@ -233,6 +242,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe private boolean sendByEnter; private long lastTypingTimeSend; private String lastTimeString; + private long lastTypingSendTime; private float startedDraggingX = -1; private float distCanMove = AndroidUtilities.dp(80); private boolean recordingAudioVideo; @@ -257,6 +267,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe private TLRPC.TL_document audioToSend; private String audioToSendPath; private MessageObject audioToSendMessageObject; + private VideoEditedInfo videoToSendMessageObject; private boolean topViewShowed; private boolean needShowTopView; @@ -282,13 +293,20 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe private Paint redDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private boolean recordAudioVideoRunnableStarted; + private boolean calledRecordRunnable; private Runnable recordAudioVideoRunnable = new Runnable() { @Override public void run() { if (delegate == null || parentActivity == null) { return; } + delegate.onPreAudioVideoRecord(); + calledRecordRunnable = true; recordAudioVideoRunnableStarted = false; + recordCircle.setLockTranslation(10000); + recordSendText.setAlpha(0.0f); + slideText.setAlpha(1.0f); + slideText.setTranslationY(0); if (videoSendButton != null && videoSendButton.getTag() != null) { if (Build.VERSION.SDK_INT >= 23) { boolean hasAudio = parentActivity.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED; @@ -331,6 +349,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe return; } } + delegate.needStartRecordAudio(1); startedDraggingX = -1; MediaController.getInstance().startRecording(dialog_id, replyingMessageObject); updateRecordIntefrace(); @@ -384,23 +403,50 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe private Paint paintRecord = new Paint(Paint.ANTI_ALIAS_FLAG); private Drawable micDrawable; private Drawable cameraDrawable; + private Drawable sendDrawable; + private Drawable lockDrawable; + private Drawable lockTopDrawable; + private Drawable lockArrowDrawable; + private Drawable lockBackgroundDrawable; + private Drawable lockShadowDrawable; + private RectF rect = new RectF(); private class RecordCircle extends View { + private float scale; private float amplitude; private float animateToAmplitude; private float animateAmplitudeDiff; private long lastUpdateTime; + private float lockAnimatedTranslation; + private float startTranslation; + private boolean sendButtonVisible; + private boolean pressed; public RecordCircle(Context context) { super(context); paint.setColor(Theme.getColor(Theme.key_chat_messagePanelVoiceBackground)); paintRecord.setColor(Theme.getColor(Theme.key_chat_messagePanelVoiceShadow)); + + lockDrawable = getResources().getDrawable(R.drawable.lock_middle); + lockDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_messagePanelVoiceLock), PorterDuff.Mode.MULTIPLY)); + lockTopDrawable = getResources().getDrawable(R.drawable.lock_top); + lockTopDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_messagePanelVoiceLock), PorterDuff.Mode.MULTIPLY)); + lockArrowDrawable = getResources().getDrawable(R.drawable.lock_arrow); + lockArrowDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_messagePanelVoiceLock), PorterDuff.Mode.MULTIPLY)); + lockBackgroundDrawable = getResources().getDrawable(R.drawable.lock_round); + lockBackgroundDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_messagePanelVoiceLockBackground), PorterDuff.Mode.MULTIPLY)); + lockShadowDrawable = getResources().getDrawable(R.drawable.lock_round_shadow); + lockShadowDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_messagePanelVoiceLockShadow), PorterDuff.Mode.MULTIPLY)); + micDrawable = getResources().getDrawable(R.drawable.mic).mutate(); micDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_messagePanelVoicePressed), PorterDuff.Mode.MULTIPLY)); cameraDrawable = getResources().getDrawable(R.drawable.ic_msg_panel_video).mutate(); cameraDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_messagePanelVoicePressed), PorterDuff.Mode.MULTIPLY)); + + sendDrawable = getResources().getDrawable(R.drawable.ic_send).mutate(); + sendDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_messagePanelVoicePressed), PorterDuff.Mode.MULTIPLY)); } public void setAmplitude(double value) { @@ -419,10 +465,88 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe invalidate(); } + public void setLockAnimatedTranslation(float value) { + lockAnimatedTranslation = value; + invalidate(); + } + + public float getLockAnimatedTranslation() { + return lockAnimatedTranslation; + } + + public boolean isSendButtonVisible() { + return sendButtonVisible; + } + + public void setSendButtonInvisible() { + sendButtonVisible = false; + invalidate(); + } + + public int setLockTranslation(float value) { + if (value == 10000) { + sendButtonVisible = false; + lockAnimatedTranslation = -1; + startTranslation = -1; + invalidate(); + return 0; + } else { + if (sendButtonVisible) { + return 2; + } + if (lockAnimatedTranslation == -1) { + startTranslation = value; + } + lockAnimatedTranslation = value; + invalidate(); + if (startTranslation - lockAnimatedTranslation >= AndroidUtilities.dp(57)) { + sendButtonVisible = true; + return 2; + } + } + return 1; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (sendButtonVisible) { + int x = (int) event.getX(); + int y = (int) event.getY(); + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (pressed = lockBackgroundDrawable.getBounds().contains(x, y)) { + return true; + } + } else if (pressed) { + if (event.getAction() == MotionEvent.ACTION_UP) { + if (lockBackgroundDrawable.getBounds().contains(x, y)) { + if (videoSendButton != null && videoSendButton.getTag() != null) { + delegate.needStartRecordVideo(3); + } else { + MediaController.getInstance().stopRecording(2); + delegate.needStartRecordAudio(0); + } + } + } + return true; + } + } + return false; + } + @Override protected void onDraw(Canvas canvas) { int cx = getMeasuredWidth() / 2; - int cy = getMeasuredHeight() / 2; + int cy = AndroidUtilities.dp(170); + float yAdd = 0; + + if (lockAnimatedTranslation != 10000) { + yAdd = Math.max(0, (int) (startTranslation - lockAnimatedTranslation)); + if (yAdd > AndroidUtilities.dp(57)) { + yAdd = AndroidUtilities.dp(57); + } + } + cy -= yAdd; + float sc; float alpha; if (scale <= 0.5f) { @@ -450,13 +574,69 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } lastUpdateTime = System.currentTimeMillis(); if (amplitude != 0) { - canvas.drawCircle(getMeasuredWidth() / 2.0f, getMeasuredHeight() / 2.0f, (AndroidUtilities.dp(42) + AndroidUtilities.dp(20) * amplitude) * scale, paintRecord); + canvas.drawCircle(getMeasuredWidth() / 2.0f, cy, (AndroidUtilities.dp(42) + AndroidUtilities.dp(20) * amplitude) * scale, paintRecord); + } + canvas.drawCircle(getMeasuredWidth() / 2.0f, cy, AndroidUtilities.dp(42) * sc, paint); + Drawable drawable; + if (isSendButtonVisible()) { + drawable = sendDrawable; + } else { + drawable = videoSendButton != null && videoSendButton.getTag() != null ? cameraDrawable : micDrawable; } - canvas.drawCircle(getMeasuredWidth() / 2.0f, getMeasuredHeight() / 2.0f, AndroidUtilities.dp(42) * sc, paint); - Drawable drawable = videoSendButton != null && videoSendButton.getTag() != null ? cameraDrawable : micDrawable; drawable.setBounds(cx - drawable.getIntrinsicWidth() / 2, cy - drawable.getIntrinsicHeight() / 2, cx + drawable.getIntrinsicWidth() / 2, cy + drawable.getIntrinsicHeight() / 2); drawable.setAlpha((int) (255 * alpha)); drawable.draw(canvas); + + float moveProgress = 1.0f - yAdd / AndroidUtilities.dp(57); + float moveProgress2 = Math.max(0.0f, 1.0f - yAdd / AndroidUtilities.dp(57) * 2); + int lockSize; + int lockY; + int lockTopY; + int lockMiddleY; + int lockArrowY; + int intAlpha = (int) (alpha * 255); + if (isSendButtonVisible()) { + lockSize = AndroidUtilities.dp(31); + lockY = AndroidUtilities.dp(57) + (int) (AndroidUtilities.dp(30) * (1.0f - sc) - yAdd + AndroidUtilities.dp(20) * moveProgress); + lockTopY = lockY + AndroidUtilities.dp(5); + lockMiddleY = lockY + AndroidUtilities.dp(11); + lockArrowY = lockY + AndroidUtilities.dp(25); + + intAlpha *= yAdd / AndroidUtilities.dp(57); + lockBackgroundDrawable.setAlpha(255); + lockShadowDrawable.setAlpha(255); + lockTopDrawable.setAlpha(intAlpha); + lockDrawable.setAlpha(intAlpha); + lockArrowDrawable.setAlpha((int) (intAlpha * moveProgress2)); + } else { + lockSize = AndroidUtilities.dp(31) + (int) (AndroidUtilities.dp(29) * moveProgress); + lockY = AndroidUtilities.dp(57) + (int) (AndroidUtilities.dp(30) * (1.0f - sc)) - (int) yAdd; + lockTopY = lockY + AndroidUtilities.dp(5) + (int) (AndroidUtilities.dp(4) * moveProgress); + lockMiddleY = lockY + AndroidUtilities.dp(11) + (int) (AndroidUtilities.dp(10) * moveProgress); + lockArrowY = lockY + AndroidUtilities.dp(25) + (int) (AndroidUtilities.dp(16) * moveProgress); + + lockBackgroundDrawable.setAlpha(intAlpha); + lockShadowDrawable.setAlpha(intAlpha); + lockTopDrawable.setAlpha(intAlpha); + lockDrawable.setAlpha(intAlpha); + lockArrowDrawable.setAlpha((int) (intAlpha * moveProgress2)); + } + + lockBackgroundDrawable.setBounds(cx - AndroidUtilities.dp(15), lockY, cx + AndroidUtilities.dp(15), lockY + lockSize); + lockBackgroundDrawable.draw(canvas); + lockShadowDrawable.setBounds(cx - AndroidUtilities.dp(16), lockY - AndroidUtilities.dp(1), cx + AndroidUtilities.dp(16), lockY + lockSize + AndroidUtilities.dp(1)); + lockShadowDrawable.draw(canvas); + lockTopDrawable.setBounds(cx - AndroidUtilities.dp(6), lockTopY, cx + AndroidUtilities.dp(6), lockTopY + AndroidUtilities.dp(14)); + lockTopDrawable.draw(canvas); + lockDrawable.setBounds(cx - AndroidUtilities.dp(7), lockMiddleY, cx + AndroidUtilities.dp(7), lockMiddleY + AndroidUtilities.dp(12)); + lockDrawable.draw(canvas); + lockArrowDrawable.setBounds(cx - AndroidUtilities.dp(7.5f), lockArrowY, cx + AndroidUtilities.dp(7.5f), lockArrowY + AndroidUtilities.dp(9)); + lockArrowDrawable.draw(canvas); + if (isSendButtonVisible()) { + redDotPaint.setAlpha(255); + rect.set(cx - AndroidUtilities.dp2(6.5f), lockY + AndroidUtilities.dp(9), cx + AndroidUtilities.dp(6.5f), lockY + AndroidUtilities.dp(9 + 13)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(1), AndroidUtilities.dp(1), redDotPaint); + } } } @@ -477,8 +657,8 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidSent); NotificationCenter.getInstance().addObserver(this, NotificationCenter.emojiDidLoaded); NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioRouteChanged); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidReset); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioProgressDidChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidReset); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingProgressDidChanged); NotificationCenter.getInstance().addObserver(this, NotificationCenter.featuredStickersDidLoaded); parentActivity = context; parentFragment = fragment; @@ -799,9 +979,13 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe recordDeleteImageView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - MessageObject playing = MediaController.getInstance().getPlayingMessageObject(); - if (playing != null && playing == audioToSendMessageObject) { - MediaController.getInstance().cleanupPlayer(true, true); + if (videoToSendMessageObject != null) { + delegate.needStartRecordVideo(2); + } else { + MessageObject playing = MediaController.getInstance().getPlayingMessageObject(); + if (playing != null && playing == audioToSendMessageObject) { + MediaController.getInstance().cleanupPlayer(true, true); + } } if (audioToSendPath != null) { new File(audioToSendPath).delete(); @@ -811,9 +995,43 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } }); + videoTimelineView = new VideoTimelineView(context); + videoTimelineView.setColor(0xff4badf7); + videoTimelineView.setRoundFrames(true); + videoTimelineView.setDelegate(new VideoTimelineView.VideoTimelineViewDelegate() { + @Override + public void onLeftProgressChanged(float progress) { + if (videoToSendMessageObject == null) { + return; + } + videoToSendMessageObject.startTime = (long) (progress * videoToSendMessageObject.estimatedDuration); + delegate.needChangeVideoPreviewState(2, progress); + } + + @Override + public void onRifhtProgressChanged(float progress) { + if (videoToSendMessageObject == null) { + return; + } + videoToSendMessageObject.endTime = (long) (progress * videoToSendMessageObject.estimatedDuration); + delegate.needChangeVideoPreviewState(2, progress); + } + + @Override + public void didStartDragging() { + delegate.needChangeVideoPreviewState(1, 0); + } + + @Override + public void didStopDragging() { + delegate.needChangeVideoPreviewState(0, 0); + } + }); + recordedAudioPanel.addView(videoTimelineView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 32, Gravity.CENTER_VERTICAL | Gravity.LEFT, 40, 0, 0, 0)); + recordedAudioBackground = new View(context); recordedAudioBackground.setBackgroundDrawable(Theme.createRoundRectDrawable(AndroidUtilities.dp(16), Theme.getColor(Theme.key_chat_recordedVoiceBackground))); - recordedAudioPanel.addView(recordedAudioBackground, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 32, Gravity.CENTER_VERTICAL | Gravity.LEFT, 48, 0, 0, 0)); + recordedAudioPanel.addView(recordedAudioBackground, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 36, Gravity.CENTER_VERTICAL | Gravity.LEFT, 48, 0, 0, 0)); recordedAudioSeekBar = new SeekBarWaveformView(context); recordedAudioPanel.addView(recordedAudioSeekBar, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 32, Gravity.CENTER_VERTICAL | Gravity.LEFT, 48 + 44, 0, 52, 0)); @@ -831,12 +1049,12 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe if (audioToSend == null) { return; } - if (MediaController.getInstance().isPlayingAudio(audioToSendMessageObject) && !MediaController.getInstance().isAudioPaused()) { - MediaController.getInstance().pauseAudio(audioToSendMessageObject); + if (MediaController.getInstance().isPlayingMessage(audioToSendMessageObject) && !MediaController.getInstance().isMessagePaused()) { + MediaController.getInstance().pauseMessage(audioToSendMessageObject); recordedAudioPlayButton.setImageDrawable(playDrawable); } else { recordedAudioPlayButton.setImageDrawable(pauseDrawable); - MediaController.getInstance().playAudio(audioToSendMessageObject); + MediaController.getInstance().playMessage(audioToSendMessageObject); } } }); @@ -850,6 +1068,12 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe recordPanel.setVisibility(GONE); recordPanel.setBackgroundColor(Theme.getColor(Theme.key_chat_messagePanelBackground)); frameLayout.addView(recordPanel, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.BOTTOM)); + recordPanel.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return true; + } + }); slideText = new LinearLayout(context); slideText.setOrientation(LinearLayout.HORIZONTAL); @@ -866,6 +1090,29 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe recordCancelText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); slideText.addView(recordCancelText, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_VERTICAL, 6, 0, 0, 0)); + recordSendText = new TextView(context); + recordSendText.setText(LocaleController.getString("Cancel", R.string.Cancel).toUpperCase()); + recordSendText.setTextColor(Theme.getColor(Theme.key_chat_fieldOverlayText)); + recordSendText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + recordSendText.setGravity(Gravity.CENTER); + recordSendText.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + recordSendText.setAlpha(0.0f); + recordSendText.setPadding(AndroidUtilities.dp(36), 0, 0, 0); + recordSendText.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (hasRecordVideo && videoSendButton.getTag() != null) { + delegate.needStartRecordVideo(2); + } else { + delegate.needStartRecordAudio(0); + MediaController.getInstance().stopRecording(0); + } + recordingAudioVideo = false; + updateRecordIntefrace(); + } + }); + recordPanel.addView(recordSendText, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 0, 0, 0)); + recordTimeContainer = new LinearLayout(context); recordTimeContainer.setOrientation(LinearLayout.HORIZONTAL); recordTimeContainer.setPadding(AndroidUtilities.dp(13), 0, 0, 0); @@ -892,21 +1139,50 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe @Override public boolean onTouch(View view, MotionEvent motionEvent) { if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + if (recordCircle.isSendButtonVisible()) { + if (!hasRecordVideo || calledRecordRunnable) { + startedDraggingX = -1; + if (hasRecordVideo && videoSendButton.getTag() != null) { + delegate.needStartRecordVideo(1); + } else { + delegate.needStartRecordAudio(0); + MediaController.getInstance().stopRecording(1); + } + recordingAudioVideo = false; + updateRecordIntefrace(); + } + return false; + } + if (parentFragment != null) { + TLRPC.Chat chat = parentFragment.getCurrentChat(); + if (ChatObject.isChannel(chat)) { + if (chat.banned_rights != null && chat.banned_rights.send_media) { + delegate.needShowMediaBanHint(); + return false; + } + } + } if (hasRecordVideo) { + calledRecordRunnable = false; recordAudioVideoRunnableStarted = true; AndroidUtilities.runOnUIThread(recordAudioVideoRunnable, 150); } else { recordAudioVideoRunnable.run(); } } else if (motionEvent.getAction() == MotionEvent.ACTION_UP || motionEvent.getAction() == MotionEvent.ACTION_CANCEL) { + if (recordCircle.isSendButtonVisible() || recordedAudioPanel.getVisibility() == VISIBLE) { + return false; + } if (recordAudioVideoRunnableStarted) { AndroidUtilities.cancelRunOnUIThread(recordAudioVideoRunnable); + delegate.onSwitchRecordMode(videoSendButton.getTag() == null); setRecordVideoButtonVisible(videoSendButton.getTag() == null, true); - } else { + } else if (!hasRecordVideo || calledRecordRunnable) { startedDraggingX = -1; if (hasRecordVideo && videoSendButton.getTag() != null) { delegate.needStartRecordVideo(1); } else { + delegate.needStartRecordAudio(0); MediaController.getInstance().stopRecording(1); } recordingAudioVideo = false; @@ -914,10 +1190,27 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } } else if (motionEvent.getAction() == MotionEvent.ACTION_MOVE && recordingAudioVideo) { float x = motionEvent.getX(); + float y = motionEvent.getY(); + if (recordCircle.isSendButtonVisible()) { + return false; + } + if (recordCircle.setLockTranslation(y) == 2) { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(recordCircle, "lockAnimatedTranslation", recordCircle.startTranslation), + ObjectAnimator.ofFloat(slideText, "alpha", 0.0f), + ObjectAnimator.ofFloat(slideText, "translationY", AndroidUtilities.dp(20)), + ObjectAnimator.ofFloat(recordSendText, "alpha", 1.0f), + ObjectAnimator.ofFloat(recordSendText, "translationY", -AndroidUtilities.dp(20), 0)); + animatorSet.setInterpolator(new DecelerateInterpolator()); + animatorSet.setDuration(150); + animatorSet.start(); + return false; + } if (x < -distCanMove) { if (hasRecordVideo && videoSendButton.getTag() != null) { delegate.needStartRecordVideo(2); } else { + delegate.needStartRecordAudio(0); MediaController.getInstance().stopRecording(0); } recordingAudioVideo = false; @@ -928,7 +1221,6 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) slideText.getLayoutParams(); if (startedDraggingX != -1) { float dist = (x - startedDraggingX); - recordCircle.setTranslationX(dist); params.leftMargin = AndroidUtilities.dp(30) + (int) dist; slideText.setLayoutParams(params); float alpha = 1.0f + dist / distCanMove; @@ -952,7 +1244,6 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } if (params.leftMargin > AndroidUtilities.dp(30)) { params.leftMargin = AndroidUtilities.dp(30); - recordCircle.setTranslationX(0); slideText.setLayoutParams(params); slideText.setAlpha(1); startedDraggingX = -1; @@ -970,7 +1261,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe audioSendButton.setPadding(0, 0, AndroidUtilities.dp(4), 0); audioVideoButtonContainer.addView(audioSendButton, LayoutHelper.createFrame(48, 48)); - if (hasRecordVideo) { + if (isChat) { videoSendButton = new ImageView(context); videoSendButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE); videoSendButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_messagePanelIcons), PorterDuff.Mode.MULTIPLY)); @@ -981,7 +1272,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe recordCircle = new RecordCircle(context); recordCircle.setVisibility(GONE); - sizeNotifierLayout.addView(recordCircle, LayoutHelper.createFrame(124, 124, Gravity.BOTTOM | Gravity.RIGHT, 0, 0, -36, -38)); + sizeNotifierLayout.addView(recordCircle, LayoutHelper.createFrame(124, 194, Gravity.BOTTOM | Gravity.RIGHT, 0, 0, -36, 0)); cancelBotButton = new ImageView(context); cancelBotButton.setVisibility(INVISIBLE); @@ -1052,6 +1343,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe setRecordVideoButtonVisible(false, false); checkSendButton(false); + checkChannelRights(); } @Override @@ -1082,7 +1374,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } private void setRecordVideoButtonVisible(boolean visible, boolean animated) { - if (!hasRecordVideo) { + if (videoSendButton == null) { return; } videoSendButton.setTag(visible ? 1 : null); @@ -1091,6 +1383,13 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe audioVideoButtonAnimation = null; } if (animated) { + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + boolean isChannel = false; + if ((int) dialog_id < 0) { + TLRPC.Chat chat = MessagesController.getInstance().getChat(-(int) dialog_id); + isChannel = ChatObject.isChannel(chat) && !chat.megagroup; + } + preferences.edit().putBoolean(isChannel ? "currentModeVideoChannel" : "currentModeVideo", visible).commit(); audioVideoButtonAnimation = new AnimatorSet(); audioVideoButtonAnimation.playTogether( ObjectAnimator.ofFloat(videoSendButton, "scaleX", visible ? 1.0f : 0.1f), @@ -1120,6 +1419,25 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } } + public boolean isRecordingAudioVideo() { + return recordingAudioVideo; + } + + public boolean isRecordLocked() { + return recordingAudioVideo && recordCircle.isSendButtonVisible(); + } + + public void cancelRecordingAudioVideo() { + if (hasRecordVideo && videoSendButton.getTag() != null) { + delegate.needStartRecordVideo(2); + } else { + delegate.needStartRecordAudio(0); + MediaController.getInstance().stopRecording(0); + } + recordingAudioVideo = false; + updateRecordIntefrace(); + } + public void showContextProgress(boolean show) { if (progressDrawable == null) { return; @@ -1395,8 +1713,8 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidSent); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.emojiDidLoaded); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioRouteChanged); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidReset); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioProgressDidChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidReset); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingProgressDidChanged); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.featuredStickersDidLoaded); if (emojiView != null) { emojiView.onDestroy(); @@ -1414,6 +1732,19 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } } + public void checkChannelRights() { + if (parentFragment == null) { + return; + } + TLRPC.Chat chat = parentFragment.getCurrentChat(); + if (ChatObject.isChannel(chat)) { + audioVideoButtonContainer.setAlpha(chat.banned_rights == null || !chat.banned_rights.send_media ? 1.0f : 0.5f); + if (emojiView != null) { + emojiView.setStickersBanned(chat.banned_rights != null && chat.banned_rights.send_stickers, chat.id); + } + } + } + public void onPause() { isPaused = true; closeKeyboard(); @@ -1435,10 +1766,12 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe public void setDialogId(long id) { dialog_id = id; + int lower_id = (int) dialog_id; + int high_id = (int) (dialog_id >> 32); if ((int) dialog_id < 0) { TLRPC.Chat currentChat = MessagesController.getInstance().getChat(-(int) dialog_id); silent = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE).getBoolean("silent_" + dialog_id, false); - canWriteToChannel = ChatObject.isChannel(currentChat) && (currentChat.creator || currentChat.editor) && !currentChat.megagroup; + canWriteToChannel = ChatObject.isChannel(currentChat) && (currentChat.creator || currentChat.admin_rights != null && currentChat.admin_rights.post_messages) && !currentChat.megagroup; if (notifyButton != null) { notifyButton.setVisibility(canWriteToChannel ? VISIBLE : GONE); notifyButton.setImageResource(silent ? R.drawable.notify_members_off : R.drawable.notify_members_on); @@ -1448,9 +1781,55 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe updateFieldRight(attachLayout.getVisibility() == VISIBLE ? 1 : 0); } } + checkRoundVideo(); updateFieldHint(); } + public void checkRoundVideo() { + if (hasRecordVideo) { + return; + } + if (attachLayout == null || Build.VERSION.SDK_INT < 18) { + hasRecordVideo = false; + setRecordVideoButtonVisible(false, false); + return; + } + int lower_id = (int) dialog_id; + int high_id = (int) (dialog_id >> 32); + if (lower_id == 0 && high_id != 0) { + TLRPC.EncryptedChat encryptedChat = MessagesController.getInstance().getEncryptedChat(high_id); + if (AndroidUtilities.getPeerLayerVersion(encryptedChat.layer) >= 66) { + hasRecordVideo = true; + } + } else { + hasRecordVideo = true; + } + boolean isChannel = false; + if ((int) dialog_id < 0) { + TLRPC.Chat chat = MessagesController.getInstance().getChat(-(int) dialog_id); + isChannel = ChatObject.isChannel(chat) && !chat.megagroup; + if (isChannel && !chat.creator && (chat.admin_rights == null || !chat.admin_rights.post_messages)) { + hasRecordVideo = false; + } + } + if (hasRecordVideo) { + CameraController.getInstance().initCamera(); + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + boolean currentModeVideo = preferences.getBoolean(isChannel ? "currentModeVideoChannel" : "currentModeVideo", isChannel); + setRecordVideoButtonVisible(currentModeVideo, false); + } else { + setRecordVideoButtonVisible(false, false); + } + } + + public boolean isInVideoMode() { + return videoSendButton.getTag() != null; + } + + public boolean hasRecordVideo() { + return hasRecordVideo; + } + private void updateFieldHint() { boolean isChannel = false; if ((int) dialog_id < 0) { @@ -1459,16 +1838,16 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } if (isChannel) { if (editingMessageObject != null) { - messageEditText.setHint(editingCaption ? LocaleController.getString("Caption", R.string.Caption) : LocaleController.getString("TypeMessage", R.string.TypeMessage)); + messageEditText.setHintText(editingCaption ? LocaleController.getString("Caption", R.string.Caption) : LocaleController.getString("TypeMessage", R.string.TypeMessage)); } else { if (silent) { - messageEditText.setHint(LocaleController.getString("ChannelSilentBroadcast", R.string.ChannelSilentBroadcast)); + messageEditText.setHintText(LocaleController.getString("ChannelSilentBroadcast", R.string.ChannelSilentBroadcast)); } else { - messageEditText.setHint(LocaleController.getString("ChannelBroadcast", R.string.ChannelBroadcast)); + messageEditText.setHintText(LocaleController.getString("ChannelBroadcast", R.string.ChannelBroadcast)); } } } else { - messageEditText.setHint(LocaleController.getString("TypeMessage", R.string.TypeMessage)); + messageEditText.setHintText(LocaleController.getString("TypeMessage", R.string.TypeMessage)); } } @@ -1501,6 +1880,8 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe audioToSendPath = null; audioToSend = null; audioToSendMessageObject = null; + videoToSendMessageObject = null; + videoTimelineView.destroy(); AnimatorSet AnimatorSet = new AnimatorSet(); AnimatorSet.playTogether( ObjectAnimator.ofFloat(recordedAudioPanel, "alpha", 0.0f) @@ -1534,7 +1915,12 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe return; } } - if (audioToSend != null) { + if (videoToSendMessageObject != null) { + delegate.needStartRecordVideo(4); + hideRecordedAudioPanel(); + checkSendButton(true); + return; + } else if (audioToSend != null) { MessageObject playing = MediaController.getInstance().getPlayingMessageObject(); if (playing != null && playing == audioToSendMessageObject) { MediaController.getInstance().cleanupPlayer(true, true); @@ -1599,7 +1985,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe animated = false; } CharSequence message = AndroidUtilities.getTrimmedString(messageEditText.getText()); - if (message.length() > 0 || forceShowSendButton || audioToSend != null) { + if (message.length() > 0 || forceShowSendButton || audioToSend != null || videoToSendMessageObject != null) { final String caption = messageEditText.getCaption(); boolean showBotButton = caption != null && sendButton.getVisibility() == VISIBLE; boolean showSendButton = caption == null && cancelBotButton.getVisibility() == VISIBLE; @@ -1879,13 +2265,13 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe recordTimeText.setText("00:00"); recordDot.resetAlpha(); lastTimeString = null; + lastTypingSendTime = -1; FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) slideText.getLayoutParams(); params.leftMargin = AndroidUtilities.dp(30); slideText.setLayoutParams(params); slideText.setAlpha(1); recordPanel.setX(AndroidUtilities.displaySize.x); - recordCircle.setTranslationX(0); if (runningAnimationAudio != null) { runningAnimationAudio.cancel(); } @@ -1938,6 +2324,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe slideText.setAlpha(1); recordPanel.setVisibility(GONE); recordCircle.setVisibility(GONE); + recordCircle.setSendButtonInvisible(); runningAnimationAudio = null; } } @@ -1947,8 +2334,16 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } } - public void setDelegate(ChatActivityEnterViewDelegate delegate) { - this.delegate = delegate; + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (recordingAudioVideo) { + getParent().requestDisallowInterceptTouchEvent(true); + } + return super.onInterceptTouchEvent(ev); + } + + public void setDelegate(ChatActivityEnterViewDelegate chatActivityEnterViewDelegate) { + delegate = chatActivityEnterViewDelegate; } public void setCommand(MessageObject messageObject, String command, boolean longPress, boolean username) { @@ -1984,7 +2379,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } public void setEditingMessageObject(MessageObject messageObject, boolean caption) { - if (audioToSend != null || editingMessageObject == messageObject) { + if (audioToSend != null || videoToSendMessageObject != null || editingMessageObject == messageObject) { return; } if (editingMessageReqId != 0) { @@ -2533,6 +2928,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe }); emojiView.setVisibility(GONE); sizeNotifierLayout.addView(emojiView); + checkChannelRights(); } @Override @@ -2687,7 +3083,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } public boolean hasAudioToSend() { - return audioToSendMessageObject != null; + return audioToSendMessageObject != null || videoToSendMessageObject != null; } public void openKeyboard() { @@ -2713,6 +3109,12 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } } + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + videoTimelineView.clearFrames(); + } + @Override public void onSizeChanged(int height, boolean isWidthGreater) { if (height > AndroidUtilities.dp(50) && keyboardVisible && !AndroidUtilities.isInMultiwindow) { @@ -2792,12 +3194,13 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } } else if (id == NotificationCenter.recordProgressChanged) { long t = (Long) args[0]; - Long time = t / 1000; + long time = t / 1000; int ms = (int) (t % 1000L) / 10; String str = String.format("%02d:%02d.%02d", time / 60, time % 60, ms); if (lastTimeString == null || !lastTimeString.equals(str)) { - if (time % 5 == 0) { - MessagesController.getInstance().sendTyping(dialog_id, 1, 0); + if (lastTypingSendTime != time && time % 5 == 0) { + lastTypingSendTime = time; + MessagesController.getInstance().sendTyping(dialog_id, videoSendButton != null && videoSendButton.getTag() != null ? 7 : 1, 0); } if (recordTimeText != null) { recordTimeText.setText(str); @@ -2806,6 +3209,10 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe if (recordCircle != null) { recordCircle.setAmplitude((Double) args[1]); } + if (videoSendButton != null && videoSendButton.getTag() != null && t >= 59500) { + startedDraggingX = -1; + delegate.needStartRecordVideo(3); + } } else if (id == NotificationCenter.closeChats) { if (messageEditText != null && messageEditText.isFocused()) { AndroidUtilities.hideKeyboard(messageEditText); @@ -2816,60 +3223,106 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe recordingAudioVideo = false; updateRecordIntefrace(); } + if (id == NotificationCenter.recordStopped) { + Integer reason = (Integer) args[0]; + if (reason == 2) { + videoTimelineView.setVisibility(VISIBLE); + recordedAudioBackground.setVisibility(GONE); + recordedAudioTimeTextView.setVisibility(GONE); + recordedAudioPlayButton.setVisibility(GONE); + recordedAudioSeekBar.setVisibility(GONE); + recordedAudioPanel.setAlpha(1.0f); + recordedAudioPanel.setVisibility(VISIBLE); + } else if (reason == 1) { + /*videoTimelineView.setVisibility(GONE); + recordedAudioBackground.setVisibility(VISIBLE); + recordedAudioTimeTextView.setVisibility(VISIBLE); + recordedAudioPlayButton.setVisibility(VISIBLE); + recordedAudioSeekBar.setVisibility(VISIBLE); + recordedAudioPanel.setAlpha(1.0f); + recordedAudioPanel.setVisibility(VISIBLE);*/ + } + } } else if (id == NotificationCenter.recordStarted) { if (!recordingAudioVideo) { recordingAudioVideo = true; updateRecordIntefrace(); } } else if (id == NotificationCenter.audioDidSent) { - audioToSend = (TLRPC.TL_document) args[0]; - audioToSendPath = (String) args[1]; - if (audioToSend != null) { - if (recordedAudioPanel == null) { - return; - } + Object audio = args[0]; + if (audio instanceof VideoEditedInfo) { + videoToSendMessageObject = (VideoEditedInfo) audio; - TLRPC.TL_message message = new TLRPC.TL_message(); - message.out = true; - message.id = 0; - message.to_id = new TLRPC.TL_peerUser(); - message.to_id.user_id = message.from_id = UserConfig.getClientUserId(); - message.date = (int) (System.currentTimeMillis() / 1000); - message.message = "-1"; - message.attachPath = audioToSendPath; - message.media = new TLRPC.TL_messageMediaDocument(); - message.media.document = audioToSend; - message.flags |= TLRPC.MESSAGE_FLAG_HAS_MEDIA | TLRPC.MESSAGE_FLAG_HAS_FROM_ID; - audioToSendMessageObject = new MessageObject(message, null, false); + audioToSendPath = (String) args[1]; + videoTimelineView.setVideoPath(audioToSendPath); + videoTimelineView.setVisibility(VISIBLE); + videoTimelineView.setMinProgressDiff(1000.0f / videoToSendMessageObject.estimatedDuration); + recordedAudioBackground.setVisibility(GONE); + recordedAudioTimeTextView.setVisibility(GONE); + recordedAudioPlayButton.setVisibility(GONE); + recordedAudioSeekBar.setVisibility(GONE); recordedAudioPanel.setAlpha(1.0f); recordedAudioPanel.setVisibility(VISIBLE); - int duration = 0; - for (int a = 0; a < audioToSend.attributes.size(); a++) { - TLRPC.DocumentAttribute attribute = audioToSend.attributes.get(a); - if (attribute instanceof TLRPC.TL_documentAttributeAudio) { - duration = attribute.duration; - break; - } - } - - for (int a = 0; a < audioToSend.attributes.size(); a++) { - TLRPC.DocumentAttribute attribute = audioToSend.attributes.get(a); - if (attribute instanceof TLRPC.TL_documentAttributeAudio) { - if (attribute.waveform == null || attribute.waveform.length == 0) { - attribute.waveform = MediaController.getInstance().getWaveform(audioToSendPath); - } - recordedAudioSeekBar.setWaveform(attribute.waveform); - break; - } - } - recordedAudioTimeTextView.setText(String.format("%d:%02d", duration / 60, duration % 60)); closeKeyboard(); hidePopup(false); checkSendButton(false); } else { - if (delegate != null) { - delegate.onMessageSend(null); + audioToSend = (TLRPC.TL_document) args[0]; + audioToSendPath = (String) args[1]; + if (audioToSend != null) { + if (recordedAudioPanel == null) { + return; + } + + videoTimelineView.setVisibility(GONE); + recordedAudioBackground.setVisibility(VISIBLE); + recordedAudioTimeTextView.setVisibility(VISIBLE); + recordedAudioPlayButton.setVisibility(VISIBLE); + recordedAudioSeekBar.setVisibility(VISIBLE); + + TLRPC.TL_message message = new TLRPC.TL_message(); + message.out = true; + message.id = 0; + message.to_id = new TLRPC.TL_peerUser(); + message.to_id.user_id = message.from_id = UserConfig.getClientUserId(); + message.date = (int) (System.currentTimeMillis() / 1000); + message.message = "-1"; + message.attachPath = audioToSendPath; + message.media = new TLRPC.TL_messageMediaDocument(); + message.media.document = audioToSend; + message.flags |= TLRPC.MESSAGE_FLAG_HAS_MEDIA | TLRPC.MESSAGE_FLAG_HAS_FROM_ID; + audioToSendMessageObject = new MessageObject(message, null, false); + + recordedAudioPanel.setAlpha(1.0f); + recordedAudioPanel.setVisibility(VISIBLE); + int duration = 0; + for (int a = 0; a < audioToSend.attributes.size(); a++) { + TLRPC.DocumentAttribute attribute = audioToSend.attributes.get(a); + if (attribute instanceof TLRPC.TL_documentAttributeAudio) { + duration = attribute.duration; + break; + } + } + + for (int a = 0; a < audioToSend.attributes.size(); a++) { + TLRPC.DocumentAttribute attribute = audioToSend.attributes.get(a); + if (attribute instanceof TLRPC.TL_documentAttributeAudio) { + if (attribute.waveform == null || attribute.waveform.length == 0) { + attribute.waveform = MediaController.getInstance().getWaveform(audioToSendPath); + } + recordedAudioSeekBar.setWaveform(attribute.waveform); + break; + } + } + recordedAudioTimeTextView.setText(String.format("%d:%02d", duration / 60, duration % 60)); + closeKeyboard(); + hidePopup(false); + checkSendButton(false); + } else { + if (delegate != null) { + delegate.onMessageSend(null); + } } } } else if (id == NotificationCenter.audioRouteChanged) { @@ -2877,14 +3330,14 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe boolean frontSpeaker = (Boolean) args[0]; parentActivity.setVolumeControlStream(frontSpeaker ? AudioManager.STREAM_VOICE_CALL : AudioManager.USE_DEFAULT_STREAM_TYPE); } - } else if (id == NotificationCenter.audioDidReset) { - if (audioToSendMessageObject != null && !MediaController.getInstance().isPlayingAudio(audioToSendMessageObject)) { + } else if (id == NotificationCenter.messagePlayingDidReset) { + if (audioToSendMessageObject != null && !MediaController.getInstance().isPlayingMessage(audioToSendMessageObject)) { recordedAudioPlayButton.setImageDrawable(playDrawable); recordedAudioSeekBar.setProgress(0); } - } else if (id == NotificationCenter.audioProgressDidChanged) { + } else if (id == NotificationCenter.messagePlayingProgressDidChanged) { Integer mid = (Integer) args[0]; - if (audioToSendMessageObject != null && MediaController.getInstance().isPlayingAudio(audioToSendMessageObject)) { + if (audioToSendMessageObject != null && MediaController.getInstance().isPlayingMessage(audioToSendMessageObject)) { MessageObject player = MediaController.getInstance().getPlayingMessageObject(); audioToSendMessageObject.audioProgress = player.audioProgress; audioToSendMessageObject.audioProgressSec = player.audioProgressSec; 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 599ec7a12..379c7efca 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java @@ -49,6 +49,7 @@ import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.ChatObject; import org.telegram.messenger.MessagesController; import org.telegram.messenger.ContactsController; import org.telegram.messenger.FileLog; @@ -94,6 +95,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N private PhotoAttachAdapter photoAttachAdapter; private ChatActivity baseFragment; private AttachButton sendPhotosButton; + private AttachButton sendDocumentsButton; private View views[] = new View[20]; private RecyclerListView attachPhotoRecyclerView; private View lineView; @@ -103,12 +105,17 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N private LinearLayoutManager layoutManager; private Drawable shadowDrawable; private ViewGroup attachView; + private ArrayList attachButtons = new ArrayList<>(); private ListAdapter adapter; private TextView hintTextView; private ArrayList innerAnimators = new ArrayList<>(); private boolean requestingPermissions; private Paint ciclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private CorrectlyMeasuringTextView mediaBanTooltip; + + private boolean mediaEnabled = true; + private CameraView cameraView; private FrameLayout cameraIcon; private TextView recordTime; @@ -542,11 +549,14 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N progressView.layout(0, t, width, t + progressView.getMeasuredHeight()); lineView.layout(0, AndroidUtilities.dp(96), width, AndroidUtilities.dp(96) + lineView.getMeasuredHeight()); hintTextView.layout(width - hintTextView.getMeasuredWidth() - AndroidUtilities.dp(5), height - hintTextView.getMeasuredHeight() - AndroidUtilities.dp(5), width - AndroidUtilities.dp(5), height - AndroidUtilities.dp(5)); + int x = (width - mediaBanTooltip.getMeasuredWidth()) / 2; + int y = t + (attachPhotoRecyclerView.getMeasuredHeight() - mediaBanTooltip.getMeasuredHeight()) / 2; + mediaBanTooltip.layout(x, y, x + mediaBanTooltip.getMeasuredWidth(), y + mediaBanTooltip.getMeasuredHeight()); int diff = (width - AndroidUtilities.dp(85 * 4 + 20)) / 3; for (int a = 0; a < 8; a++) { - int y = AndroidUtilities.dp(105 + 95 * (a / 4)); - int x = AndroidUtilities.dp(10) + (a % 4) * (AndroidUtilities.dp(85) + diff); + y = AndroidUtilities.dp(105 + 95 * (a / 4)); + x = AndroidUtilities.dp(10) + (a % 4) * (AndroidUtilities.dp(85) + diff); views[a].layout(x, y, x + views[a].getMeasuredWidth(), y + views[a].getMeasuredHeight()); } } @@ -580,10 +590,10 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N if (deviceHasGoodCamera) { position--; } - if (MediaController.allPhotosAlbumEntry == null) { + if (MediaController.allMediaAlbumEntry == null) { return; } - ArrayList arrayList = (ArrayList) MediaController.allPhotosAlbumEntry.photos; + ArrayList arrayList = (ArrayList) MediaController.allMediaAlbumEntry.photos; if (position < 0 || position >= arrayList.size()) { return; } @@ -603,6 +613,15 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } }); + views[11] = mediaBanTooltip = new CorrectlyMeasuringTextView(context); + mediaBanTooltip.setBackgroundDrawable(Theme.createRoundRectDrawable(AndroidUtilities.dp(3), 0xff464646)); + mediaBanTooltip.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(8), AndroidUtilities.dp(8), AndroidUtilities.dp(8)); + mediaBanTooltip.setGravity(Gravity.CENTER_VERTICAL); + mediaBanTooltip.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + mediaBanTooltip.setTextColor(0xffffffff); + mediaBanTooltip.setVisibility(View.INVISIBLE); + attachView.addView(mediaBanTooltip, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 14, 0, 14, 0)); + views[9] = progressView = new EmptyTextProgressView(context); if (Build.VERSION.SDK_INT >= 23 && getContext().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { progressView.setText(LocaleController.getString("PermissionStorage", R.string.PermissionStorage)); @@ -634,6 +653,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N }; for (int a = 0; a < 8; a++) { AttachButton attachButton = new AttachButton(context); + attachButtons.add(attachButton); attachButton.setTextAndIcon(items[a], Theme.chat_attachButtonDrawables[a]); attachView.addView(attachButton, LayoutHelper.createFrame(85, 90, Gravity.LEFT | Gravity.TOP)); attachButton.setTag(a); @@ -641,6 +661,8 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N if (a == 7) { sendPhotosButton = attachButton; sendPhotosButton.imageView.setPadding(0, AndroidUtilities.dp(4), 0, 0); + } else if (a == 4) { + sendDocumentsButton = attachButton; } attachButton.setOnClickListener(new View.OnClickListener() { @Override @@ -782,7 +804,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } @Override public void sendButtonPressed(int index, VideoEditedInfo videoEditedInfo) { - if (cameraFile == null) { + if (cameraFile == null || baseFragment == null) { return; } AndroidUtilities.addMediaToGallery(cameraFile.getAbsolutePath()); @@ -820,7 +842,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N @Override public void shutterReleased() { - if (takingPhoto || cameraView == null || mediaCaptured) { + if (takingPhoto || cameraView == null || mediaCaptured || cameraView.getCameraSession() == null) { return; } mediaCaptured = true; @@ -883,7 +905,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N @Override public void sendButtonPressed(int index, VideoEditedInfo videoEditedInfo) { - if (cameraFile == null) { + if (cameraFile == null || baseFragment == null) { return; } AndroidUtilities.addMediaToGallery(cameraFile.getAbsolutePath()); @@ -895,6 +917,9 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N @Override public boolean scaleToFill() { + if (baseFragment == null || baseFragment.getParentActivity() == null) { + return false; + } int locked = Settings.System.getInt(baseFragment.getParentActivity().getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0); return sameTakePictureOrientation || locked == 1; } @@ -1432,7 +1457,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N @TargetApi(16) public void showCamera() { - if (paused) { + if (paused || !mediaEnabled) { return; } if (cameraView == null) { @@ -1483,6 +1508,11 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N cameraImageView.setScaleType(ImageView.ScaleType.CENTER); cameraImageView.setImageResource(R.drawable.instant_camera); cameraIcon.addView(cameraImageView, LayoutHelper.createFrame(80, 80, Gravity.RIGHT | Gravity.BOTTOM)); + + cameraView.setAlpha(mediaEnabled ? 1.0f : 0.2f); + cameraView.setEnabled(mediaEnabled); + cameraIcon.setAlpha(mediaEnabled ? 1.0f : 0.2f); + cameraIcon.setEnabled(mediaEnabled); } cameraView.setTranslationX(cameraViewLocation[0]); cameraView.setTranslationY(cameraViewLocation[1]); @@ -1604,11 +1634,13 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N sendPhotosButton.imageView.setBackgroundResource(R.drawable.attach_hide_states); sendPhotosButton.imageView.setImageResource(R.drawable.attach_hide2); sendPhotosButton.textView.setText(""); + sendDocumentsButton.textView.setText(LocaleController.getString("ChatDocument", R.string.ChatDocument)); } else { sendPhotosButton.imageView.setPadding(AndroidUtilities.dp(2), 0, 0, 0); sendPhotosButton.imageView.setBackgroundResource(R.drawable.attach_send_states); sendPhotosButton.imageView.setImageResource(R.drawable.attach_send2); sendPhotosButton.textView.setText(LocaleController.formatString("SendItems", R.string.SendItems, String.format("(%d)", count))); + sendDocumentsButton.textView.setText(count == 1 ? LocaleController.getString("SendAsFile", R.string.SendAsFile) : LocaleController.getString("SendAsFiles", R.string.SendAsFiles)); } if (Build.VERSION.SDK_INT >= 23 && getContext().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { @@ -1625,15 +1657,15 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } public void loadGalleryPhotos() { - if (MediaController.allPhotosAlbumEntry == null && Build.VERSION.SDK_INT >= 21) { + if (MediaController.allMediaAlbumEntry == null && Build.VERSION.SDK_INT >= 21) { MediaController.loadGalleryPhotosAlbums(0); } } public void init() { - if (MediaController.allPhotosAlbumEntry != null) { - for (int a = 0; a < Math.min(100, MediaController.allPhotosAlbumEntry.photos.size()); a++) { - MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.photos.get(a); + if (MediaController.allMediaAlbumEntry != null) { + for (int a = 0; a < Math.min(100, MediaController.allMediaAlbumEntry.photos.size()); a++) { + MediaController.PhotoEntry photoEntry = MediaController.allMediaAlbumEntry.photos.get(a); photoEntry.caption = null; photoEntry.imagePath = null; photoEntry.thumbPath = null; @@ -1664,7 +1696,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } private PhotoAttachPhotoCell getCellForIndex(int index) { - if (MediaController.allPhotosAlbumEntry == null) { + if (MediaController.allMediaAlbumEntry == null) { return null; } int count = attachPhotoRecyclerView.getChildCount(); @@ -1673,7 +1705,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N if (view instanceof PhotoAttachPhotoCell) { PhotoAttachPhotoCell cell = (PhotoAttachPhotoCell) view; int num = (Integer) cell.getImageView().getTag(); - if (num < 0 || num >= MediaController.allPhotosAlbumEntry.photos.size()) { + if (num < 0 || num >= MediaController.allMediaAlbumEntry.photos.size()) { continue; } if (num == index) { @@ -1698,7 +1730,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N object.imageReceiver = cell.getImageView().getImageReceiver(); object.thumb = object.imageReceiver.getBitmap(); object.scale = cell.getImageView().getScaleX(); - cell.getCheckBox().setVisibility(View.GONE); + cell.showCheck(false); return object; } return null; @@ -1719,7 +1751,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N PhotoAttachPhotoCell cell = getCellForIndex(index); if (cell != null) { cell.getImageView().setOrientation(0, true); - MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.photos.get(index); + MediaController.PhotoEntry photoEntry = MediaController.allMediaAlbumEntry.photos.get(index); if (photoEntry.thumbPath != null) { cell.getImageView().setImage(photoEntry.thumbPath, null, cell.getContext().getResources().getDrawable(R.drawable.nophotos)); } else if (photoEntry.path != null) { @@ -1744,7 +1776,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N public void willSwitchFromPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { PhotoAttachPhotoCell cell = getCellForIndex(index); if (cell != null) { - cell.getCheckBox().setVisibility(View.VISIBLE); + cell.showCheck(true); } } @@ -1755,29 +1787,29 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N View view = attachPhotoRecyclerView.getChildAt(a); if (view instanceof PhotoAttachPhotoCell) { PhotoAttachPhotoCell cell = (PhotoAttachPhotoCell) view; - if (cell.getCheckBox().getVisibility() != View.VISIBLE) { - cell.getCheckBox().setVisibility(View.VISIBLE); - } + cell.showCheck(true); } } } @Override public boolean isPhotoChecked(int index) { - return !(index < 0 || index >= MediaController.allPhotosAlbumEntry.photos.size()) && photoAttachAdapter.getSelectedPhotos().containsKey(MediaController.allPhotosAlbumEntry.photos.get(index).imageId); + return !(index < 0 || index >= MediaController.allMediaAlbumEntry.photos.size()) && photoAttachAdapter.getSelectedPhotos().containsKey(MediaController.allMediaAlbumEntry.photos.get(index).imageId); } @Override - public void setPhotoChecked(int index) { + public void setPhotoChecked(int index, VideoEditedInfo videoEditedInfo) { boolean add = true; - if (index < 0 || index >= MediaController.allPhotosAlbumEntry.photos.size()) { + if (index < 0 || index >= MediaController.allMediaAlbumEntry.photos.size()) { return; } - MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.photos.get(index); + MediaController.PhotoEntry photoEntry = MediaController.allMediaAlbumEntry.photos.get(index); if (photoAttachAdapter.getSelectedPhotos().containsKey(photoEntry.imageId)) { photoAttachAdapter.getSelectedPhotos().remove(photoEntry.imageId); + photoEntry.editedInfo = null; add = false; } else { + photoEntry.editedInfo = videoEditedInfo; photoAttachAdapter.getSelectedPhotos().put(photoEntry.imageId, photoEntry); } int count = attachPhotoRecyclerView.getChildCount(); @@ -1802,10 +1834,11 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N @Override public void sendButtonPressed(int index, VideoEditedInfo videoEditedInfo) { if (photoAttachAdapter.getSelectedPhotos().isEmpty()) { - if (index < 0 || index >= MediaController.allPhotosAlbumEntry.photos.size()) { + if (index < 0 || index >= MediaController.allMediaAlbumEntry.photos.size()) { return; } - MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.photos.get(index); + MediaController.PhotoEntry photoEntry = MediaController.allMediaAlbumEntry.photos.get(index); + photoEntry.editedInfo = videoEditedInfo; photoAttachAdapter.getSelectedPhotos().put(photoEntry.imageId, photoEntry); } delegate.didPressedButton(7); @@ -1819,7 +1852,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N private void onRevealAnimationEnd(boolean open) { NotificationCenter.getInstance().setAnimationInProgress(false); revealAnimationInProgress = false; - if (open && Build.VERSION.SDK_INT <= 19 && MediaController.allPhotosAlbumEntry == null) { + if (open && Build.VERSION.SDK_INT <= 19 && MediaController.allMediaAlbumEntry == null) { MediaController.loadGalleryPhotosAlbums(0); } if (open) { @@ -1891,9 +1924,13 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N case 0: view = attachView; break; - case 1: - view = new ShadowSectionCell(mContext); + case 1: { + FrameLayout frameLayout = new FrameLayout(mContext); + frameLayout.setBackgroundColor(0xfff0f0f0); + frameLayout.addView(new ShadowSectionCell(mContext), LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + view = frameLayout; break; + } default: FrameLayout frameLayout = new FrameLayout(mContext) { @Override @@ -1974,6 +2011,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N photoEntry.imagePath = null; photoEntry.thumbPath = null; photoEntry.caption = null; + photoEntry.editedInfo = null; photoEntry.stickers.clear(); } selectedPhotos.clear(); @@ -1987,6 +2025,9 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N cell.setDelegate(new PhotoAttachPhotoCell.PhotoAttachPhotoCellDelegate() { @Override public void onCheckClick(PhotoAttachPhotoCell v) { + if (!mediaEnabled) { + return; + } MediaController.PhotoEntry photoEntry = v.getPhotoEntry(); if (selectedPhotos.containsKey(photoEntry.imageId)) { selectedPhotos.remove(photoEntry.imageId); @@ -1994,7 +2035,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N photoEntry.imagePath = null; photoEntry.thumbPath = null; photoEntry.stickers.clear(); - v.setPhotoEntry(photoEntry, (Integer) v.getTag() == MediaController.allPhotosAlbumEntry.photos.size() - 1); + v.setPhotoEntry(photoEntry, (Integer) v.getTag() == MediaController.allMediaAlbumEntry.photos.size() - 1); } else { selectedPhotos.put(photoEntry.imageId, photoEntry); v.setChecked(true, true); @@ -2016,8 +2057,8 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N position--; } PhotoAttachPhotoCell cell = (PhotoAttachPhotoCell) holder.itemView; - MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.photos.get(position); - cell.setPhotoEntry(photoEntry, position == MediaController.allPhotosAlbumEntry.photos.size() - 1); + MediaController.PhotoEntry photoEntry = MediaController.allMediaAlbumEntry.photos.get(position); + cell.setPhotoEntry(photoEntry, position == MediaController.allMediaAlbumEntry.photos.size() - 1); cell.setChecked(selectedPhotos.containsKey(photoEntry.imageId), false); cell.getImageView().setTag(position); cell.setTag(position); @@ -2061,8 +2102,8 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N if (deviceHasGoodCamera) { count++; } - if (MediaController.allPhotosAlbumEntry != null) { - count += MediaController.allPhotosAlbumEntry.photos.size(); + if (MediaController.allMediaAlbumEntry != null) { + count += MediaController.allMediaAlbumEntry.photos.size(); } return count; } @@ -2204,7 +2245,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N NotificationCenter.getInstance().setAnimationInProgress(true); revealAnimationInProgress = true; - int count = Build.VERSION.SDK_INT <= 19 ? 11 : 8; + int count = Build.VERSION.SDK_INT <= 19 ? 12 : 8; for (int a = 0; a < count; a++) { if (Build.VERSION.SDK_INT <= 19) { if (a < 8) { @@ -2276,6 +2317,34 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N @Override protected boolean onCustomOpenAnimation() { + if (baseFragment != null) { + TLRPC.Chat chat = baseFragment.getCurrentChat(); + if (ChatObject.isChannel(chat)) { + mediaEnabled = chat.banned_rights == null || !chat.banned_rights.send_media; + for (int a = 0; a < 5; a++) { + attachButtons.get(a).setAlpha(mediaEnabled ? 1.0f : 0.2f); + attachButtons.get(a).setEnabled(mediaEnabled); + } + attachPhotoRecyclerView.setAlpha(mediaEnabled ? 1.0f : 0.2f); + attachPhotoRecyclerView.setEnabled(mediaEnabled); + if (!mediaEnabled) { + if (AndroidUtilities.isBannedForever(chat.banned_rights.until_date)) { + mediaBanTooltip.setText(LocaleController.formatString("AttachMediaRestrictedForever", R.string.AttachMediaRestrictedForever)); + } else { + mediaBanTooltip.setText(LocaleController.formatString("AttachMediaRestricted", R.string.AttachMediaRestricted, LocaleController.formatDateForBan(chat.banned_rights.until_date))); + } + } + mediaBanTooltip.setVisibility(mediaEnabled ? View.INVISIBLE : View.VISIBLE); + if (cameraView != null) { + cameraView.setAlpha(mediaEnabled ? 1.0f : 0.2f); + cameraView.setEnabled(mediaEnabled); + } + if (cameraIcon != null) { + cameraIcon.setAlpha(mediaEnabled ? 1.0f : 0.2f); + cameraIcon.setEnabled(mediaEnabled); + } + } + } if (useRevealAnimation) { startRevealAnimation(true); return true; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java index f74388e74..90a191953 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java @@ -41,10 +41,7 @@ public class ChatAvatarContainer extends FrameLayout { private ImageView timeItem; private TimerDrawable timerDrawable; private ChatActivity parentFragment; - private TypingDotsDrawable typingDotsDrawable; - private RecordStatusDrawable recordStatusDrawable; - private SendingFileDrawable sendingFileDrawable; - private PlayingGameDrawable playingGameDrawable; + private StatusDrawable[] statusDrawables = new StatusDrawable[5]; private AvatarDrawable avatarDrawable = new AvatarDrawable(); private int onlineCount = -1; @@ -85,40 +82,42 @@ public class ChatAvatarContainer extends FrameLayout { }); } - setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - TLRPC.User user = parentFragment.getCurrentUser(); - TLRPC.Chat chat = parentFragment.getCurrentChat(); - if (user != null) { - Bundle args = new Bundle(); - args.putInt("user_id", user.id); - if (timeItem != null) { - args.putLong("dialog_id", parentFragment.getDialogId()); + if (parentFragment != null) { + setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + TLRPC.User user = parentFragment.getCurrentUser(); + TLRPC.Chat chat = parentFragment.getCurrentChat(); + if (user != null) { + Bundle args = new Bundle(); + args.putInt("user_id", user.id); + if (timeItem != null) { + args.putLong("dialog_id", parentFragment.getDialogId()); + } + ProfileActivity fragment = new ProfileActivity(args); + fragment.setPlayProfileAnimation(true); + parentFragment.presentFragment(fragment); + } else if (chat != null) { + Bundle args = new Bundle(); + args.putInt("chat_id", chat.id); + ProfileActivity fragment = new ProfileActivity(args); + fragment.setChatInfo(parentFragment.getCurrentChatInfo()); + fragment.setPlayProfileAnimation(true); + parentFragment.presentFragment(fragment); } - ProfileActivity fragment = new ProfileActivity(args); - fragment.setPlayProfileAnimation(true); - parentFragment.presentFragment(fragment); - } else if (chat != null) { - Bundle args = new Bundle(); - args.putInt("chat_id", chat.id); - ProfileActivity fragment = new ProfileActivity(args); - fragment.setChatInfo(parentFragment.getCurrentChatInfo()); - fragment.setPlayProfileAnimation(true); - parentFragment.presentFragment(fragment); } - } - }); + }); - TLRPC.Chat chat = parentFragment.getCurrentChat(); - typingDotsDrawable = new TypingDotsDrawable(); - typingDotsDrawable.setIsChat(chat != null); - recordStatusDrawable = new RecordStatusDrawable(); - recordStatusDrawable.setIsChat(chat != null); - sendingFileDrawable = new SendingFileDrawable(); - sendingFileDrawable.setIsChat(chat != null); - playingGameDrawable = new PlayingGameDrawable(); - playingGameDrawable.setIsChat(chat != null); + TLRPC.Chat chat = parentFragment.getCurrentChat(); + statusDrawables[0] = new TypingDotsDrawable(); + statusDrawables[1] = new RecordStatusDrawable(); + statusDrawables[2] = new SendingFileDrawable(); + statusDrawables[3] = new PlayingGameDrawable(); + statusDrawables[4] = new RoundStatusDrawable(); + for (int a = 0; a < statusDrawables.length; a++) { + statusDrawables[a].setIsChat(chat != null); + } + } } @Override @@ -181,6 +180,10 @@ public class ChatAvatarContainer extends FrameLayout { titleTextView.setText(value); } + public void setSubtitle(CharSequence value) { + subtitleTextView.setText(value); + } + public ImageView getTimeItem() { return timeItem; } @@ -197,44 +200,29 @@ public class ChatAvatarContainer extends FrameLayout { if (start) { try { Integer type = MessagesController.getInstance().printingStringsTypes.get(parentFragment.getDialogId()); - if (type == 0) { - subtitleTextView.setLeftDrawable(typingDotsDrawable); - typingDotsDrawable.start(); - recordStatusDrawable.stop(); - sendingFileDrawable.stop(); - playingGameDrawable.stop(); - } else if (type == 1) { - subtitleTextView.setLeftDrawable(recordStatusDrawable); - recordStatusDrawable.start(); - typingDotsDrawable.stop(); - sendingFileDrawable.stop(); - playingGameDrawable.stop(); - } else if (type == 2) { - subtitleTextView.setLeftDrawable(sendingFileDrawable); - sendingFileDrawable.start(); - typingDotsDrawable.stop(); - recordStatusDrawable.stop(); - playingGameDrawable.stop(); - } else if (type == 3) { - subtitleTextView.setLeftDrawable(playingGameDrawable); - playingGameDrawable.start(); - typingDotsDrawable.stop(); - recordStatusDrawable.stop(); - sendingFileDrawable.stop(); + subtitleTextView.setLeftDrawable(statusDrawables[type]); + for (int a = 0; a < statusDrawables.length; a++) { + if (a == type) { + statusDrawables[a].start(); + } else { + statusDrawables[a].stop(); + } } } catch (Exception e) { FileLog.e(e); } } else { subtitleTextView.setLeftDrawable(null); - typingDotsDrawable.stop(); - recordStatusDrawable.stop(); - sendingFileDrawable.stop(); - playingGameDrawable.stop(); + for (int a = 0; a < statusDrawables.length; a++) { + statusDrawables[a].stop(); + } } } public void updateSubtitle() { + if (parentFragment == null) { + return; + } TLRPC.User user = parentFragment.getCurrentUser(); TLRPC.Chat chat = parentFragment.getCurrentChat(); CharSequence printString = MessagesController.getInstance().printingStrings.get(parentFragment.getDialogId()); @@ -249,7 +237,7 @@ public class ChatAvatarContainer extends FrameLayout { if (info != null && info.participants_count != 0) { if (chat.megagroup && info.participants_count <= 200) { if (onlineCount > 1 && info.participants_count != 0) { - subtitleTextView.setText(String.format("%s, %s", LocaleController.formatPluralString("Members", info.participants_count), LocaleController.formatPluralString("Online", onlineCount))); + subtitleTextView.setText(String.format("%s, %s", LocaleController.formatPluralString("Members", info.participants_count), LocaleController.formatPluralString("OnlineCount", onlineCount))); } else { subtitleTextView.setText(LocaleController.formatPluralString("Members", info.participants_count)); } @@ -281,7 +269,7 @@ public class ChatAvatarContainer extends FrameLayout { count = info.participants.participants.size(); } if (onlineCount > 1 && count != 0) { - subtitleTextView.setText(String.format("%s, %s", LocaleController.formatPluralString("Members", count), LocaleController.formatPluralString("Online", onlineCount))); + subtitleTextView.setText(String.format("%s, %s", LocaleController.formatPluralString("Members", count), LocaleController.formatPluralString("OnlineCount", onlineCount))); } else { subtitleTextView.setText(LocaleController.formatPluralString("Members", count)); } @@ -310,7 +298,21 @@ public class ChatAvatarContainer extends FrameLayout { } } + public void setChatAvatar(TLRPC.Chat chat) { + TLRPC.FileLocation newPhoto = null; + if (chat.photo != null) { + newPhoto = chat.photo.photo_small; + } + avatarDrawable.setInfo(chat); + if (avatarImageView != null) { + avatarImageView.setImage(newPhoto, "50_50", avatarDrawable); + } + } + public void checkAndUpdateAvatar() { + if (parentFragment == null) { + return; + } TLRPC.FileLocation newPhoto = null; TLRPC.User user = parentFragment.getCurrentUser(); TLRPC.Chat chat = parentFragment.getCurrentChat(); @@ -331,6 +333,9 @@ public class ChatAvatarContainer extends FrameLayout { } public void updateOnlineCount() { + if (parentFragment == null) { + return; + } onlineCount = 0; TLRPC.ChatFull info = parentFragment.getCurrentChatInfo(); if (info == null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java index e187cae29..77351c0ca 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java @@ -42,6 +42,7 @@ public class CheckBoxSquare extends View { rectF = new RectF(); drawBitmap = Bitmap.createBitmap(AndroidUtilities.dp(18), AndroidUtilities.dp(18), Bitmap.Config.ARGB_4444); drawCanvas = new Canvas(drawBitmap); + isAlert = alert; } @Keep diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/CorrectlyMeasuringTextView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/CorrectlyMeasuringTextView.java index f9f196f8d..32e43722d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/CorrectlyMeasuringTextView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/CorrectlyMeasuringTextView.java @@ -2,34 +2,28 @@ package org.telegram.ui.Components; import android.content.Context; import android.text.Layout; -import android.util.AttributeSet; import android.widget.TextView; -public class CorrectlyMeasuringTextView extends TextView{ +public class CorrectlyMeasuringTextView extends TextView { public CorrectlyMeasuringTextView(Context context) { super(context); } - public CorrectlyMeasuringTextView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public CorrectlyMeasuringTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - public void onMeasure(int wms, int hms) { super.onMeasure(wms, hms); try { Layout l = getLayout(); - if (l.getLineCount() <= 1) return; + if (l.getLineCount() <= 1) { + return; + } int maxw = 0; for (int i = l.getLineCount() - 1; i >= 0; --i) { maxw = Math.max(maxw, Math.round(l.getPaint().measureText(getText(), l.getLineStart(i), l.getLineEnd(i)))); } super.onMeasure(Math.min(maxw + getPaddingLeft() + getPaddingRight(), getMeasuredWidth()) | MeasureSpec.EXACTLY, getMeasuredHeight() | MeasureSpec.EXACTLY); - } catch (Exception x) { + } catch (Exception ignore) { + } } } \ No newline at end of file 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 8983e5d84..5d8f4e25b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmbedBottomSheet.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmbedBottomSheet.java @@ -267,6 +267,7 @@ public class EmbedBottomSheet extends BottomSheet { @Override public void onInitFailed() { webView.setVisibility(View.VISIBLE); + webView.setKeepScreenOn(true); videoView.setVisibility(View.INVISIBLE); videoView.getControlsView().setVisibility(View.INVISIBLE); videoView.getTextureView().setVisibility(View.INVISIBLE); @@ -649,6 +650,7 @@ public class EmbedBottomSheet extends BottomSheet { } else { progressBar.setVisibility(View.VISIBLE); webView.setVisibility(View.VISIBLE); + webView.setKeepScreenOn(true); videoView.setVisibility(View.INVISIBLE); videoView.getControlsView().setVisibility(View.INVISIBLE); videoView.getTextureView().setVisibility(View.INVISIBLE); 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 4d3d8024e..39f39508c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java @@ -8,6 +8,10 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; @@ -46,6 +50,7 @@ import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.Emoji; import org.telegram.messenger.EmojiData; import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MessagesController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.Utilities; import org.telegram.messenger.query.StickersQuery; @@ -576,6 +581,9 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific private TrendingGridAdapter trendingGridAdapter; private RecyclerListView.OnItemClickListener stickersOnItemClickListener; private PagerSlidingTabStrip pagerSlidingTabStrip; + private TextView mediaBanTooltip; + + private int currentChatId; private HashMap installingStickerSets = new HashMap<>(); private HashMap removingStickerSets = new HashMap<>(); @@ -1087,6 +1095,15 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific addView(pager, 0, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP)); + mediaBanTooltip = new CorrectlyMeasuringTextView(context); + mediaBanTooltip.setBackgroundDrawable(Theme.createRoundRectDrawable(AndroidUtilities.dp(3), Theme.getColor(Theme.key_chat_gifSaveHintBackground))); + mediaBanTooltip.setTextColor(Theme.getColor(Theme.key_chat_gifSaveHintText)); + mediaBanTooltip.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(7), AndroidUtilities.dp(8), AndroidUtilities.dp(7)); + mediaBanTooltip.setGravity(Gravity.CENTER_VERTICAL); + mediaBanTooltip.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + mediaBanTooltip.setVisibility(INVISIBLE); + addView(mediaBanTooltip, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.RIGHT | Gravity.TOP, 30, 48 + 5, 5, 0)); + emojiSize = AndroidUtilities.dp(AndroidUtilities.isTablet() ? 40 : 32); pickerView = new EmojiColorPickerView(context); pickerViewPopup = new EmojiPopupWindow(pickerView, popupWidth = AndroidUtilities.dp((AndroidUtilities.isTablet() ? 40 : 32) * 6 + 10 + 4 * 5), popupHeight = AndroidUtilities.dp(AndroidUtilities.isTablet() ? 64 : 56)); @@ -1609,6 +1626,9 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific public void onOpen(boolean forceEmoji) { if (stickersTab != null) { + if (currentPage != 0 && currentChatId != 0) { + currentPage = 0; + } if (currentPage == 0 || forceEmoji) { if (pager.getCurrentItem() == 6) { pager.setCurrentItem(0, !forceEmoji); @@ -1726,6 +1746,71 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific } } + public void setStickersBanned(boolean value, int chatId) { + if (value) { + currentChatId = chatId; + } else { + currentChatId = 0; + } + View view = pagerSlidingTabStrip.getTab(6); + if (view != null) { + view.setAlpha(currentChatId != 0 ? 0.5f : 1.0f); + if (currentChatId != 0 && pager.getCurrentItem() == 6) { + pager.setCurrentItem(0); + } + } + } + + public void showStickerBanHint() { + if (mediaBanTooltip.getVisibility() == VISIBLE) { + return; + } + TLRPC.Chat chat = MessagesController.getInstance().getChat(currentChatId); + if (chat == null || chat.banned_rights == null) { + return; + } + + if (AndroidUtilities.isBannedForever(chat.banned_rights.until_date)) { + mediaBanTooltip.setText(LocaleController.getString("AttachStickersRestrictedForever", R.string.AttachStickersRestrictedForever)); + } else { + mediaBanTooltip.setText(LocaleController.formatString("AttachStickersRestricted", R.string.AttachStickersRestricted, LocaleController.formatDateForBan(chat.banned_rights.until_date))); + } + mediaBanTooltip.setVisibility(View.VISIBLE); + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(mediaBanTooltip, "alpha", 0.0f, 1.0f) + ); + AnimatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (mediaBanTooltip == null) { + return; + } + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(mediaBanTooltip, "alpha", 0.0f) + ); + AnimatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mediaBanTooltip != null) { + mediaBanTooltip.setVisibility(View.INVISIBLE); + } + } + }); + AnimatorSet.setDuration(300); + AnimatorSet.start(); + } + }, 5000); + } + }); + AnimatorSet.setDuration(300); + AnimatorSet.start(); + } + private void updateVisibleTrendingSets() { if (trendingGridAdapter == null || trendingGridAdapter == null) { return; @@ -2194,6 +2279,15 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific viewGroup.removeView(view); } + @Override + public boolean canScrollToTab(int position) { + if (position == 6 && currentChatId != 0) { + showStickerBanHint(); + return false; + } + return true; + } + public int getCount() { return views.size(); } 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 b478e0cf9..f03937476 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.graphics.Canvas; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Typeface; +import android.os.Bundle; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -39,6 +40,7 @@ import org.telegram.messenger.voip.VoIPService; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.AudioPlayerActivity; +import org.telegram.ui.ChatActivity; import org.telegram.ui.VoIPActivity; public class FragmentContextView extends FrameLayout implements NotificationCenter.NotificationCenterDelegate { @@ -64,6 +66,7 @@ public class FragmentContextView extends FrameLayout implements NotificationCent 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)); View shadow = new View(context); @@ -77,10 +80,10 @@ public class FragmentContextView extends FrameLayout implements NotificationCent playButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - if (MediaController.getInstance().isAudioPaused()) { - MediaController.getInstance().playAudio(MediaController.getInstance().getPlayingMessageObject()); + if (MediaController.getInstance().isMessagePaused()) { + MediaController.getInstance().playMessage(MediaController.getInstance().getPlayingMessageObject()); } else { - MediaController.getInstance().pauseAudio(MediaController.getInstance().getPlayingMessageObject()); + MediaController.getInstance().pauseMessage(MediaController.getInstance().getPlayingMessageObject()); } } }); @@ -111,8 +114,38 @@ public class FragmentContextView extends FrameLayout implements NotificationCent public void onClick(View v) { if (currentStyle == 0) { MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); - if (messageObject != null && messageObject.isMusic() && fragment != null) { - fragment.presentFragment(new AudioPlayerActivity()); + if (fragment != null && messageObject != null) { + if (messageObject.isMusic()) { + fragment.presentFragment(new AudioPlayerActivity()); + } else { + long dialog_id = 0; + if (fragment instanceof ChatActivity) { + dialog_id = ((ChatActivity) fragment).getDialogId(); + } + if (messageObject.getDialogId() == dialog_id) { + ((ChatActivity) fragment).scrollToMessageId(messageObject.getId(), 0, false, 0, true); + } else { + dialog_id = messageObject.getDialogId(); + Bundle args = new Bundle(); + int lower_part = (int) dialog_id; + int high_id = (int) (dialog_id >> 32); + if (lower_part != 0) { + if (high_id == 1) { + args.putInt("chat_id", lower_part); + } else { + if (lower_part > 0) { + args.putInt("user_id", lower_part); + } else if (lower_part < 0) { + args.putInt("chat_id", -lower_part); + } + } + } else { + args.putInt("enc_id", high_id); + } + args.putInt("message_id", messageObject.getId()); + fragment.presentFragment(new ChatActivity(args), fragment instanceof ChatActivity); + } + } } } else if (currentStyle == 1) { Intent intent = new Intent(getContext(), VoIPActivity.class); @@ -166,9 +199,9 @@ public class FragmentContextView extends FrameLayout implements NotificationCent protected void onDetachedFromWindow() { super.onDetachedFromWindow(); topPadding = 0; - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidReset); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioPlayStateChanged); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidStarted); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidReset); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingPlayStateChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidStarted); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didStartedCall); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didEndedCall); } @@ -176,9 +209,9 @@ public class FragmentContextView extends FrameLayout implements NotificationCent @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidReset); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioPlayStateChanged); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidStarted); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidReset); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingPlayStateChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidStarted); NotificationCenter.getInstance().addObserver(this, NotificationCenter.didStartedCall); NotificationCenter.getInstance().addObserver(this, NotificationCenter.didEndedCall); boolean callAvailable = VoIPService.getSharedInstance() != null && VoIPService.getSharedInstance().getCallState() != VoIPService.STATE_WAITING_INCOMING; @@ -196,7 +229,7 @@ public class FragmentContextView extends FrameLayout implements NotificationCent @Override public void didReceivedNotification(int id, Object... args) { - if (id == NotificationCenter.audioDidStarted || id == NotificationCenter.audioPlayStateChanged || id == NotificationCenter.audioDidReset || id == NotificationCenter.didEndedCall) { + if (id == NotificationCenter.messagePlayingDidStarted || id == NotificationCenter.messagePlayingPlayStateChanged || id == NotificationCenter.messagePlayingDidReset || id == NotificationCenter.didEndedCall) { checkPlayer(false); } else if (id == NotificationCenter.didStartedCall) { checkCall(false); @@ -274,7 +307,7 @@ public class FragmentContextView extends FrameLayout implements NotificationCent visible = true; setVisibility(VISIBLE); } - if (MediaController.getInstance().isAudioPaused()) { + if (MediaController.getInstance().isMessagePaused()) { playButton.setImageResource(R.drawable.miniplayer_play); } else { playButton.setImageResource(R.drawable.miniplayer_pause); @@ -282,7 +315,7 @@ public class FragmentContextView extends FrameLayout implements NotificationCent if (lastMessageObject != messageObject || prevStyle != 0) { lastMessageObject = messageObject; SpannableStringBuilder stringBuilder; - if (lastMessageObject.isVoice()) { + if (lastMessageObject.isVoice() || lastMessageObject.isRoundVideo()) { stringBuilder = new SpannableStringBuilder(String.format("%s %s", messageObject.getMusicAuthor(), messageObject.getMusicTitle())); titleTextView.setEllipsize(TextUtils.TruncateAt.MIDDLE); } else { 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 e0937523c..498acbada 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/InstantCameraView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/InstantCameraView.java @@ -8,51 +8,117 @@ 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.annotation.TargetApi; +import android.app.Activity; import android.content.Context; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.ImageFormat; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.graphics.SurfaceTexture; import android.graphics.drawable.ColorDrawable; -import android.hardware.Camera; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.media.MediaRecorder; +import android.net.Uri; +import android.opengl.EGL14; +import android.opengl.EGLExt; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.opengl.GLUtils; import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.os.Vibrator; import android.view.Gravity; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.TextureView; import android.view.View; import android.view.ViewOutlineProvider; +import android.view.WindowManager; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; +import android.widget.ImageView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.DispatchQueue; +import org.telegram.messenger.FileLoader; import org.telegram.messenger.FileLog; import org.telegram.messenger.MediaController; import org.telegram.messenger.NotificationCenter; +import org.telegram.messenger.R; +import org.telegram.messenger.UserConfig; import org.telegram.messenger.VideoEditedInfo; import org.telegram.messenger.camera.CameraController; -import org.telegram.messenger.camera.CameraView; +import org.telegram.messenger.camera.CameraInfo; +import org.telegram.messenger.camera.CameraSession; +import org.telegram.messenger.camera.Size; +import org.telegram.messenger.exoplayer2.ExoPlayer; +import org.telegram.messenger.video.MP4Builder; +import org.telegram.messenger.video.Mp4Movie; +import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.TLRPC; +import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ChatActivity; import java.io.File; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.Semaphore; -public class InstantCameraView extends FrameLayout { +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; + +@TargetApi(18) +public class InstantCameraView extends FrameLayout implements NotificationCenter.NotificationCenterDelegate { private FrameLayout cameraContainer; - private CameraView cameraView; private ChatActivity baseFragment; - private View actionBar; + private Paint paint; + private RectF rect; + private ImageView switchCameraButton; + private ImageView muteImageView; + private float progress; + private CameraInfo selectedCamera; + private boolean isFrontface = true; + private volatile boolean cameraReady; + private AnimatorSet muteAnimation; + private TLRPC.InputFile file; + private TLRPC.InputEncryptedFile encryptedFile; + private byte[] key; + private byte[] iv; + private long size; + private VideoEditedInfo videoEditedInfo; + private VideoPlayer videoPlayer; private int[] position = new int[2]; + private int cameraTexture[] = new int[1]; + private int oldCameraTexture[] = new int[1]; + private float cameraTextureAlpha = 1.0f; private AnimatorSet animatorSet; @@ -61,6 +127,9 @@ public class InstantCameraView extends FrameLayout { private File cameraFile; private long recordStartTime; private boolean recording; + private long recordedTime; + private boolean cancelled; + private long duration; private Runnable timerRunnable = new Runnable() { @Override @@ -68,19 +137,136 @@ public class InstantCameraView extends FrameLayout { if (!recording) { return; } - NotificationCenter.getInstance().postNotificationName(NotificationCenter.recordProgressChanged, System.currentTimeMillis() - recordStartTime, 0.0); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.recordProgressChanged, duration = System.currentTimeMillis() - recordStartTime, 0.0); AndroidUtilities.runOnUIThread(timerRunnable, 50); } }; - public InstantCameraView(Context context, ChatActivity parentFragment, View actionBarOverlay) { + private CameraGLThread cameraThread; + private Size previewSize; + private Size pictureSize; + private Size aspectRatio = new Size(16, 9); + private TextureView textureView; + private CameraSession cameraSession; + + private float[] mMVPMatrix = new float[16]; + private float[] mSTMatrix = new float[16]; + private float[] moldSTMatrix = new float[16]; + private static final String VERTEX_SHADER = + "uniform mat4 uMVPMatrix;\n" + + "uniform mat4 uSTMatrix;\n" + + "attribute vec4 aPosition;\n" + + "attribute vec4 aTextureCoord;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " gl_Position = uMVPMatrix * aPosition;\n" + + " vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" + + "}\n"; + + private static final String FRAGMENT_SHADER = + "#extension GL_OES_EGL_image_external : require\n" + + "precision highp float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform float scaleX;\n" + + "uniform float scaleY;\n" + + "uniform float alpha;\n" + + "uniform samplerExternalOES sTexture;\n" + + "void main() {\n" + + " vec2 coord = vec2((vTextureCoord.x - 0.5) * scaleX, (vTextureCoord.y - 0.5) * scaleY);\n" + + " float coef = ceil(clamp(0.2601 - dot(coord, coord), 0.0, 1.0));\n" + + " vec3 color = texture2D(sTexture, vTextureCoord).rgb * coef + (1.0 - step(0.001, coef));\n" + + " gl_FragColor = vec4(color * alpha, alpha);\n" + + "}\n"; + + private static final String FRAGMENT_SCREEN_SHADER = + "#extension GL_OES_EGL_image_external : require\n" + + "precision lowp float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + + "}\n"; + + private FloatBuffer vertexBuffer; + private FloatBuffer textureBuffer; + private float scaleX; + private float scaleY; + + public InstantCameraView(Context context, ChatActivity parentFragment) { super(context); - actionBar = actionBarOverlay; - setBackgroundColor(0x7f000000); + setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN && baseFragment != null) { + if (videoPlayer != null) { + boolean mute = !videoPlayer.isMuted(); + videoPlayer.setMute(mute); + if (muteAnimation != null) { + muteAnimation.cancel(); + } + muteAnimation = new AnimatorSet(); + muteAnimation.playTogether( + ObjectAnimator.ofFloat(muteImageView, "alpha", mute ? 1.0f : 0.0f), + ObjectAnimator.ofFloat(muteImageView, "scaleX", mute ? 1.0f : 0.5f), + ObjectAnimator.ofFloat(muteImageView, "scaleY", mute ? 1.0f : 0.5f)); + muteAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animation.equals(muteAnimation)) { + muteAnimation = null; + } + } + }); + muteAnimation.setDuration(180); + muteAnimation.setInterpolator(new DecelerateInterpolator()); + muteAnimation.start(); + } else { + baseFragment.checkRecordLocked(); + } + } + return true; + } + }); + setWillNotDraw(false); + setBackgroundColor(0xc0000000); baseFragment = parentFragment; + paint = new Paint(Paint.ANTI_ALIAS_FLAG) { + @Override + public void setAlpha(int a) { + super.setAlpha(a); + invalidate(); + } + }; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeWidth(AndroidUtilities.dp(3)); + paint.setColor(0xffffffff); + + rect = new RectF(); if (Build.VERSION.SDK_INT >= 21) { - cameraContainer = new FrameLayout(context); + cameraContainer = new FrameLayout(context) { + @Override + public void setScaleX(float scaleX) { + super.setScaleX(scaleX); + InstantCameraView.this.invalidate(); + } + + @Override + public void setAlpha(float alpha) { + super.setAlpha(alpha); + InstantCameraView.this.invalidate(); + } + }; + cameraContainer.setOutlineProvider(new ViewOutlineProvider() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void getOutline(View view, Outline outline) { + outline.setOval(0, 0, AndroidUtilities.roundMessageSize, AndroidUtilities.roundMessageSize); + } + }); + cameraContainer.setClipToOutline(true); + cameraContainer.setWillNotDraw(false); } else { final Path path = new Path(); final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -88,6 +274,12 @@ public class InstantCameraView extends FrameLayout { paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); cameraContainer = new FrameLayout(context) { + @Override + public void setScaleX(float scaleX) { + super.setScaleX(scaleX); + InstantCameraView.this.invalidate(); + } + @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); @@ -98,135 +290,222 @@ public class InstantCameraView extends FrameLayout { @Override protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - canvas.drawPath(path, paint); + try { + super.dispatchDraw(canvas); + canvas.drawPath(path, paint); + } catch (Exception ignore) { + + } } }; - } - final int size; - if (AndroidUtilities.isTablet()) { - size = AndroidUtilities.dp(100); - } else { - size = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) / 2; - } - FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(size, size, Gravity.CENTER); - layoutParams.bottomMargin = AndroidUtilities.dp(48); - addView(cameraContainer, layoutParams); - if (Build.VERSION.SDK_INT >= 21) { - cameraContainer.setOutlineProvider(new ViewOutlineProvider() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public void getOutline(View view, Outline outline) { - outline.setOval(0, 0, size, size); - } - }); - cameraContainer.setClipToOutline(true); - } else { + cameraContainer.setWillNotDraw(false); cameraContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null); } - setVisibility(GONE); + + addView(cameraContainer, new FrameLayout.LayoutParams(AndroidUtilities.roundMessageSize, AndroidUtilities.roundMessageSize, Gravity.CENTER)); + + switchCameraButton = new ImageView(context); + switchCameraButton.setScaleType(ImageView.ScaleType.CENTER); + addView(switchCameraButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.BOTTOM, 20, 0, 0, 14)); + switchCameraButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (!cameraReady || cameraSession == null || !cameraSession.isInitied() || cameraThread == null) { + return; + } + switchCamera(); + ObjectAnimator animator = ObjectAnimator.ofFloat(switchCameraButton, "scaleX", 0.0f).setDuration(100); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + switchCameraButton.setImageResource(isFrontface ? R.drawable.camera_revert1 : R.drawable.camera_revert2); + ObjectAnimator.ofFloat(switchCameraButton, "scaleX", 1.0f).setDuration(100).start(); + } + }); + animator.start(); + } + }); + + muteImageView = new ImageView(context); + muteImageView.setScaleType(ImageView.ScaleType.CENTER); + muteImageView.setImageResource(R.drawable.video_mute); + muteImageView.setAlpha(0.0f); + addView(muteImageView, LayoutHelper.createFrame(48, 48, Gravity.CENTER)); + ((LayoutParams) muteImageView.getLayoutParams()).topMargin = AndroidUtilities.roundMessageSize / 2 - AndroidUtilities.dp(24); + + setVisibility(INVISIBLE); } - public void checkCamera(boolean request) { - if (baseFragment == null) { - return; + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + getParent().requestDisallowInterceptTouchEvent(true); + return super.onInterceptTouchEvent(ev); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (getVisibility() != VISIBLE) { + cameraContainer.setTranslationY(getMeasuredHeight() / 2); } - boolean old = deviceHasGoodCamera; - if (Build.VERSION.SDK_INT >= 23) { - if (baseFragment.getParentActivity().checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - if (request) { - baseFragment.getParentActivity().requestPermissions(new String[]{Manifest.permission.CAMERA}, 17); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.recordProgressChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.FileDidUpload); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.recordProgressChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.FileDidUpload); + } + + @Override + public void didReceivedNotification(int id, Object... args) { + if (id == NotificationCenter.recordProgressChanged) { + long t = (Long) args[0]; + progress = t / 60000.0f; + recordedTime = t; + invalidate(); + } else if (id == NotificationCenter.FileDidUpload) { + final String location = (String) args[0]; + if (cameraFile != null && cameraFile.getAbsolutePath().equals(location)) { + file = (TLRPC.InputFile) args[1]; + encryptedFile = (TLRPC.InputEncryptedFile) args[2]; + size = (Long) args[5]; + if (encryptedFile != null) { + key = (byte[]) args[3]; + iv = (byte[]) args[4]; } - deviceHasGoodCamera = false; - } else { - CameraController.getInstance().initCamera(); - deviceHasGoodCamera = CameraController.getInstance().isCameraInitied(); } - } else if (Build.VERSION.SDK_INT >= 16) { - CameraController.getInstance().initCamera(); - deviceHasGoodCamera = CameraController.getInstance().isCameraInitied(); } - if (deviceHasGoodCamera && baseFragment != null) { - showCamera(); + } + + public void destroy(boolean async, final Runnable beforeDestroyRunnable) { + if (cameraSession != null) { + cameraSession.destroy(); + CameraController.getInstance().close(cameraSession, !async ? new Semaphore(0) : null, beforeDestroyRunnable); + } + } + + @Override + protected void onDraw(Canvas canvas) { + float x = cameraContainer.getX(); + float y = cameraContainer.getY(); + rect.set(x - AndroidUtilities.dp(8), y - AndroidUtilities.dp(8), x + cameraContainer.getMeasuredWidth() + AndroidUtilities.dp(8), y + cameraContainer.getMeasuredHeight() + AndroidUtilities.dp(8)); + if (progress != 0) { + canvas.drawArc(rect, -90, 360 * progress, false, paint); + } + if (Theme.chat_roundVideoShadow != null) { + int x1 = (int) x - AndroidUtilities.dp(3); + int y1 = (int) y - AndroidUtilities.dp(2); + canvas.save(); + canvas.scale(cameraContainer.getScaleX(), cameraContainer.getScaleY(), x1 + AndroidUtilities.roundMessageSize / 2 + AndroidUtilities.dp(3), y1 + AndroidUtilities.roundMessageSize / 2 + AndroidUtilities.dp(3)); + Theme.chat_roundVideoShadow.setAlpha((int) (cameraContainer.getAlpha() * 255)); + Theme.chat_roundVideoShadow.setBounds(x1, y1, x1 + AndroidUtilities.roundMessageSize + AndroidUtilities.dp(6), y1 + AndroidUtilities.roundMessageSize + AndroidUtilities.dp(6)); + Theme.chat_roundVideoShadow.draw(canvas); + canvas.restore(); } } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); - actionBar.setVisibility(visibility); setAlpha(0.0f); - actionBar.setAlpha(0.0f); + switchCameraButton.setAlpha(0.0f); cameraContainer.setAlpha(0.0f); + muteImageView.setAlpha(0.0f); + muteImageView.setScaleX(1.0f); + muteImageView.setScaleY(1.0f); cameraContainer.setScaleX(0.1f); cameraContainer.setScaleY(0.1f); if (cameraContainer.getMeasuredWidth() != 0) { cameraContainer.setPivotX(cameraContainer.getMeasuredWidth() / 2); cameraContainer.setPivotY(cameraContainer.getMeasuredHeight() / 2); } + try { + if (visibility == VISIBLE) { + ((Activity) getContext()).getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + ((Activity) getContext()).getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } catch (Exception e) { + FileLog.e(e); + } } - @TargetApi(16) public void showCamera() { - if (cameraView != null) { + if (textureView != null) { return; } - setVisibility(VISIBLE); - cameraView = new CameraView(getContext(), true); - cameraView.setMirror(true); - cameraContainer.addView(cameraView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); - cameraView.setDelegate(new CameraView.CameraViewDelegate() { + switchCameraButton.setImageResource(R.drawable.camera_revert1); + isFrontface = true; + selectedCamera = null; + recordedTime = 0; + progress = 0; + cancelled = false; + file = null; + encryptedFile = null; + key = null; + iv = null; + + if (!initCamera()) { + return; + } + MediaController.getInstance().pauseMessage(MediaController.getInstance().getPlayingMessageObject()); + + cameraFile = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), UserConfig.lastLocalId + ".mp4"); + UserConfig.lastLocalId--; + UserConfig.saveConfig(false); + + FileLog.e("show round camera"); + + textureView = new TextureView(getContext()); + textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override - public void onCameraCreated(Camera camera) { - - } - - @Override - public void onCameraInit() { - if (Build.VERSION.SDK_INT >= 23) { - if (baseFragment.getParentActivity().checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { - requestingPermissions = true; - baseFragment.getParentActivity().requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, 21); + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + FileLog.e("camera surface available"); + if (cameraThread == null && surface != null) { + if (cancelled) { return; } + FileLog.e("start create thread"); + cameraThread = new CameraGLThread(surface, width, height); } - try { - Vibrator v = (Vibrator) ApplicationLoader.applicationContext.getSystemService(Context.VIBRATOR_SERVICE); - v.vibrate(50); - } catch (Exception e) { - FileLog.e(e); - } - AndroidUtilities.lockOrientation(baseFragment.getParentActivity()); - cameraFile = AndroidUtilities.generateVideoPath(); - CameraController.getInstance().recordVideo(cameraView.getCameraSession(), cameraFile, new CameraController.VideoTakeCallback() { - @Override - public void onFinishVideoRecording(final Bitmap thumb) { - if (cameraFile == null || baseFragment == null) { - return; - } - AndroidUtilities.addMediaToGallery(cameraFile.getAbsolutePath()); - VideoEditedInfo videoEditedInfo = new VideoEditedInfo(); - videoEditedInfo.bitrate = -1; - videoEditedInfo.originalPath = cameraFile.getAbsolutePath(); - videoEditedInfo.startTime = videoEditedInfo.endTime = -1; - videoEditedInfo.estimatedSize = cameraFile.length(); - //videoEditedInfo.estimatedDuration = cameraFile.length(); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, final int width, final int height) { + + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + if (cameraThread != null) { + cameraThread.shutdown(0); + cameraThread = null; + } + if (cameraSession != null) { + CameraController.getInstance().close(cameraSession, null, null); + } + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { - baseFragment.sendMedia(new MediaController.PhotoEntry(0, 0, 0, cameraFile.getAbsolutePath(), 0, true), null); - } - }, new Runnable() { - @Override - public void run() { - recording = true; - recordStartTime = System.currentTimeMillis(); - AndroidUtilities.runOnUIThread(timerRunnable); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.recordStarted); - startAnimation(true); - } - }, true); } }); + cameraContainer.addView(textureView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + setVisibility(VISIBLE); + + startAnimation(true); } public FrameLayout getCameraContainer() { @@ -237,10 +516,16 @@ public class InstantCameraView extends FrameLayout { if (animatorSet != null) { animatorSet.cancel(); } + PipRoundVideoView pipRoundVideoView = PipRoundVideoView.getInstance(); + if (pipRoundVideoView != null) { + pipRoundVideoView.showTemporary(!open); + } animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimator.ofFloat(actionBar, "alpha", open ? 1.0f : 0.0f), ObjectAnimator.ofFloat(this, "alpha", open ? 1.0f : 0.0f), + ObjectAnimator.ofFloat(switchCameraButton, "alpha", open ? 1.0f : 0.0f), + ObjectAnimator.ofFloat(muteImageView, "alpha", 0.0f), + ObjectAnimator.ofInt(paint, "alpha", open ? 255 : 0), ObjectAnimator.ofFloat(cameraContainer, "alpha", open ? 1.0f : 0.0f), ObjectAnimator.ofFloat(cameraContainer, "scaleX", open ? 1.0f : 0.1f), ObjectAnimator.ofFloat(cameraContainer, "scaleY", open ? 1.0f : 0.1f), @@ -252,7 +537,7 @@ public class InstantCameraView extends FrameLayout { public void onAnimationEnd(Animator animation) { if (animation.equals(animatorSet)) { hideCamera(true); - setVisibility(GONE); + setVisibility(INVISIBLE); } } }); @@ -267,42 +552,1509 @@ public class InstantCameraView extends FrameLayout { return new Rect(position[0], position[1], cameraContainer.getWidth(), cameraContainer.getHeight()); } - public void send() { - if (cameraView == null || cameraFile == null) { + public void changeVideoPreviewState(int state, float progress) { + if (videoPlayer == null) { return; } - recording = false; - AndroidUtilities.cancelRunOnUIThread(timerRunnable); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.recordStopped); - CameraController.getInstance().stopVideoRecording(cameraView.getCameraSession(), false); - //startAnimation(false); + if (state == 0) { + startProgressTimer(); + videoPlayer.play(); + } else if (state == 1) { + stopProgressTimer(); + videoPlayer.pause(); + } else if (state == 2) { + videoPlayer.seekTo((long) (progress * videoPlayer.getDuration())); + } + } + + public void send(int state) { + if (textureView == null) { + return; + } + stopProgressTimer(); + if (videoPlayer != null) { + videoPlayer.releasePlayer(); + videoPlayer = null; + } + if (state == 4) { + if (videoEditedInfo.needConvert()) { + file = null; + encryptedFile = null; + key = null; + iv = null; + double totalDuration = videoEditedInfo.estimatedDuration; + long startTime = videoEditedInfo.startTime >= 0 ? videoEditedInfo.startTime : 0; + long endTime = videoEditedInfo.endTime >= 0 ? videoEditedInfo.endTime : videoEditedInfo.estimatedDuration; + videoEditedInfo.estimatedDuration = endTime - startTime; + videoEditedInfo.estimatedSize = (long) (size * (videoEditedInfo.estimatedDuration / totalDuration)); + videoEditedInfo.bitrate = 400000; + if (videoEditedInfo.startTime > 0) { + videoEditedInfo.startTime *= 1000; + } + if (videoEditedInfo.endTime > 0) { + videoEditedInfo.endTime *= 1000; + } + FileLoader.getInstance().cancelUploadFile(cameraFile.getAbsolutePath(), false); + } else { + videoEditedInfo.estimatedSize = size; + } + videoEditedInfo.file = file; + videoEditedInfo.encryptedFile = encryptedFile; + videoEditedInfo.key = key; + videoEditedInfo.iv = iv; + baseFragment.sendMedia(new MediaController.PhotoEntry(0, 0, 0, cameraFile.getAbsolutePath(), 0, true), videoEditedInfo); + } else { + cancelled = recordedTime < 800; + recording = false; + AndroidUtilities.cancelRunOnUIThread(timerRunnable); + if (cameraThread != null) { + NotificationCenter.getInstance().postNotificationName(NotificationCenter.recordStopped, !cancelled && state == 3 ? 2 : 0); + int send; + if (cancelled) { + send = 0; + } else if (state == 3) { + send = 2; + } else { + send = 1; + } + cameraThread.shutdown(send); + cameraThread = null; + } + if (cancelled) { + startAnimation(false); + } + } } public void cancel() { - if (cameraView == null || cameraFile == null) { + stopProgressTimer(); + if (videoPlayer != null) { + videoPlayer.releasePlayer(); + videoPlayer = null; + } + if (textureView == null) { return; } + cancelled = true; recording = false; AndroidUtilities.cancelRunOnUIThread(timerRunnable); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.recordStopped); - CameraController.getInstance().stopVideoRecording(cameraView.getCameraSession(), true); - cameraFile.delete(); - cameraFile = null; + NotificationCenter.getInstance().postNotificationName(NotificationCenter.recordStopped, 0); + if (cameraThread != null) { + cameraThread.shutdown(0); + cameraThread = null; + } + if (cameraFile != null) { + cameraFile.delete(); + cameraFile = null; + } startAnimation(false); } @Override public void setAlpha(float alpha) { ColorDrawable colorDrawable = (ColorDrawable) getBackground(); - colorDrawable.setAlpha((int) (0x7f * alpha)); + colorDrawable.setAlpha((int) (0xc0 * alpha)); + invalidate(); + } + + public View getSwitchButtonView() { + return switchCameraButton; + } + + public View getMuteImageView() { + return muteImageView; + } + + public Paint getPaint() { + return paint; } public void hideCamera(boolean async) { - if (/*!deviceHasGoodCamera || */cameraView == null) { - return; + destroy(async, null); + cameraContainer.removeView(textureView); + cameraContainer.setTranslationX(0); + cameraContainer.setTranslationY(0); + textureView = null; + } + + private void switchCamera() { + if (cameraSession != null) { + cameraSession.destroy(); + CameraController.getInstance().close(cameraSession, null, null); + cameraSession = null; + } + isFrontface = !isFrontface; + initCamera(); + cameraReady = false; + cameraThread.reinitForNewCamera(); + } + + private boolean initCamera() { + ArrayList cameraInfos = CameraController.getInstance().getCameras(); + if (cameraInfos == null) { + return false; + } + CameraInfo notFrontface = null; + for (int a = 0; a < cameraInfos.size(); a++) { + CameraInfo cameraInfo = cameraInfos.get(a); + if (!cameraInfo.isFrontface()) { + notFrontface = cameraInfo; + } + if (isFrontface && cameraInfo.isFrontface() || !isFrontface && !cameraInfo.isFrontface()) { + selectedCamera = cameraInfo; + break; + } else { + notFrontface = cameraInfo; + } + } + if (selectedCamera == null) { + selectedCamera = notFrontface; + } + if (selectedCamera == null) { + return false; + } + + ArrayList previewSizes = selectedCamera.getPreviewSizes(); + ArrayList pictureSizes = selectedCamera.getPictureSizes(); + previewSize = CameraController.chooseOptimalSize(previewSizes, 480, 270, aspectRatio); + pictureSize = CameraController.chooseOptimalSize(pictureSizes, 480, 270, aspectRatio); + if (previewSize.mWidth != pictureSize.mWidth) { + boolean found = false; + for (int a = previewSizes.size() - 1; a >= 0; a--) { + Size preview = previewSizes.get(a); + for (int b = pictureSizes.size() - 1; b >= 0; b--) { + Size picture = pictureSizes.get(b); + if (preview.mWidth >= pictureSize.mWidth && preview.mHeight >= pictureSize.mHeight && preview.mWidth == picture.mWidth && preview.mHeight == picture.mHeight) { + previewSize = preview; + pictureSize = picture; + found = true; + break; + } + } + if (found) { + break; + } + } + + if (!found) { + for (int a = previewSizes.size() - 1; a >= 0; a--) { + Size preview = previewSizes.get(a); + for (int b = pictureSizes.size() - 1; b >= 0; b--) { + Size picture = pictureSizes.get(b); + if (preview.mWidth >= 240 && preview.mHeight >= 240 && preview.mWidth == picture.mWidth && preview.mHeight == picture.mHeight) { + previewSize = preview; + pictureSize = picture; + found = true; + break; + } + } + if (found) { + break; + } + } + } + } + FileLog.d("preview w = " + previewSize.mWidth + " h = " + previewSize.mHeight); + return true; + } + + private void createCamera(final SurfaceTexture surfaceTexture) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (cameraThread == null) { + return; + } + FileLog.e("create camera session"); + + + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + cameraSession = new CameraSession(selectedCamera, previewSize, pictureSize, ImageFormat.JPEG); + cameraThread.setCurrentSession(cameraSession); + CameraController.getInstance().openRound(cameraSession, surfaceTexture, new Runnable() { + @Override + public void run() { + if (cameraSession != null) { + FileLog.e("camera initied"); + cameraSession.setInitied(); + } + } + }, new Runnable() { + @Override + public void run() { + cameraThread.setCurrentSession(cameraSession); + } + }); + } + }); + } + + private int loadShader(int type, String shaderCode) { + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, shaderCode); + GLES20.glCompileShader(shader); + int[] compileStatus = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); + if (compileStatus[0] == 0) { + FileLog.e(GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + shader = 0; + } + return shader; + } + + private Timer progressTimer; + + private void startProgressTimer() { + if (progressTimer != null) { + try { + progressTimer.cancel(); + progressTimer = null; + } catch (Exception e) { + FileLog.e(e); + } + } + progressTimer = new Timer(); + progressTimer.schedule(new TimerTask() { + @Override + public void run() { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + try { + if (videoPlayer != null && videoEditedInfo != null && videoEditedInfo.endTime > 0 && videoPlayer.getCurrentPosition() >= videoEditedInfo.endTime) { + videoPlayer.seekTo(videoEditedInfo.startTime > 0 ? videoEditedInfo.startTime : 0); + } + } catch (Exception e) { + FileLog.e(e); + } + } + }); + } + }, 0, 17); + } + + private void stopProgressTimer() { + if (progressTimer != null) { + try { + progressTimer.cancel(); + progressTimer = null; + } catch (Exception e) { + FileLog.e(e); + } + } + } + + public class CameraGLThread extends DispatchQueue { + + private final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private final int EGL_OPENGL_ES2_BIT = 4; + private SurfaceTexture surfaceTexture; + private EGL10 egl10; + private EGLDisplay eglDisplay; + private EGLConfig eglConfig; + private EGLContext eglContext; + private EGLSurface eglSurface; + private GL gl; + private boolean initied; + + private CameraSession currentSession; + + private SurfaceTexture cameraSurface; + + private final int DO_RENDER_MESSAGE = 0; + private final int DO_SHUTDOWN_MESSAGE = 1; + private final int DO_REINIT_MESSAGE = 2; + private final int DO_SETSESSION_MESSAGE = 3; + + private int drawProgram; + private int vertexMatrixHandle; + private int textureMatrixHandle; + private int positionHandle; + private int textureHandle; + + private int rotationAngle; + + private boolean recording; + + private Integer cameraId = 0; + + private VideoRecorder videoEncoder; + + public CameraGLThread(SurfaceTexture surface, int surfaceWidth, int surfaceHeight) { + super("CameraGLThread"); + surfaceTexture = surface; + + int width = previewSize.getWidth(); + int height = previewSize.getHeight(); + + int minSide = Math.min(width, height); + float scale = surfaceWidth / (float) minSide; + + width *= scale; + height *= scale; + if (width > height) { + scaleX = 1.0f; + scaleY = width / (float) surfaceHeight; + } else { + scaleX = height / (float) surfaceWidth; + scaleY = 1.0f; + } + } + + private boolean initGL() { + FileLog.e("start init gl"); + egl10 = (EGL10) EGLContext.getEGL(); + + eglDisplay = egl10.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + if (eglDisplay == EGL10.EGL_NO_DISPLAY) { + FileLog.e("eglGetDisplay failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); + finish(); + return false; + } + + int[] version = new int[2]; + if (!egl10.eglInitialize(eglDisplay, version)) { + FileLog.e("eglInitialize failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); + finish(); + return false; + } + + int[] configsCount = new int[1]; + EGLConfig[] configs = new EGLConfig[1]; + int[] configSpec = new int[] { + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_RED_SIZE, 8, + EGL10.EGL_GREEN_SIZE, 8, + EGL10.EGL_BLUE_SIZE, 8, + EGL10.EGL_ALPHA_SIZE, 0, + EGL10.EGL_DEPTH_SIZE, 0, + EGL10.EGL_STENCIL_SIZE, 0, + EGL10.EGL_NONE + }; + if (!egl10.eglChooseConfig(eglDisplay, configSpec, configs, 1, configsCount)) { + FileLog.e("eglChooseConfig failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); + finish(); + return false; + } else if (configsCount[0] > 0) { + eglConfig = configs[0]; + } else { + FileLog.e("eglConfig not initialized"); + finish(); + return false; + } + + int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; + eglContext = egl10.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); + if (eglContext == null) { + FileLog.e("eglCreateContext failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); + finish(); + return false; + } + + if (surfaceTexture instanceof SurfaceTexture) { + eglSurface = egl10.eglCreateWindowSurface(eglDisplay, eglConfig, surfaceTexture, null); + } else { + finish(); + return false; + } + + if (eglSurface == null || eglSurface == EGL10.EGL_NO_SURFACE) { + FileLog.e("createWindowSurface failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); + finish(); + return false; + } + if (!egl10.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { + FileLog.e("eglMakeCurrent failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); + finish(); + return false; + } + gl = eglContext.getGL(); + + float tX = 1.0f / scaleX / 2.0f; + float tY = 1.0f / scaleY / 2.0f; + float[] verticesData = { + -1.0f, -1.0f, 0, + 1.0f, -1.0f, 0, + -1.0f, 1.0f, 0, + 1.0f, 1.0f, 0 + }; + float[] texData = { + 0.5f - tX, 0.5f - tY, + 0.5f + tX, 0.5f - tY, + 0.5f - tX, 0.5f + tY, + 0.5f + tX, 0.5f + tY + }; + + videoEncoder = new VideoRecorder(); + + vertexBuffer = ByteBuffer.allocateDirect(verticesData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + vertexBuffer.put(verticesData).position(0); + + textureBuffer = ByteBuffer.allocateDirect(texData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + textureBuffer.put(texData).position(0); + + android.opengl.Matrix.setIdentityM(mSTMatrix, 0); + + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER); + int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SCREEN_SHADER); + if (vertexShader != 0 && fragmentShader != 0) { + drawProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(drawProgram, vertexShader); + GLES20.glAttachShader(drawProgram, fragmentShader); + GLES20.glLinkProgram(drawProgram); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(drawProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + FileLog.e("failed link shader"); + GLES20.glDeleteProgram(drawProgram); + drawProgram = 0; + } else { + positionHandle = GLES20.glGetAttribLocation(drawProgram, "aPosition"); + textureHandle = GLES20.glGetAttribLocation(drawProgram, "aTextureCoord"); + vertexMatrixHandle = GLES20.glGetUniformLocation(drawProgram, "uMVPMatrix"); + textureMatrixHandle = GLES20.glGetUniformLocation(drawProgram, "uSTMatrix"); + } + } else { + FileLog.e("failed creating shader"); + finish(); + return false; + } + + GLES20.glGenTextures(1, cameraTexture, 0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexture[0]); + 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); + + android.opengl.Matrix.setIdentityM(mMVPMatrix, 0); + + cameraSurface = new SurfaceTexture(cameraTexture[0]); + cameraSurface.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + requestRender(); + } + }); + createCamera(cameraSurface); + FileLog.e("gl initied"); + + return true; + } + + public void reinitForNewCamera() { + Handler handler = getHandler(); + if (handler != null) { + sendMessage(handler.obtainMessage(DO_REINIT_MESSAGE), 0); + } + } + + public void finish() { + if (eglSurface != null) { + egl10.eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + egl10.eglDestroySurface(eglDisplay, eglSurface); + eglSurface = null; + } + if (eglContext != null) { + egl10.eglDestroyContext(eglDisplay, eglContext); + eglContext = null; + } + if (eglDisplay != null) { + egl10.eglTerminate(eglDisplay); + eglDisplay = null; + } + } + + public void setCurrentSession(CameraSession session) { + Handler handler = getHandler(); + if (handler != null) { + sendMessage(handler.obtainMessage(DO_SETSESSION_MESSAGE, session), 0); + } + } + + private void onDraw(Integer cameraId) { + if (!initied) { + return; + } + + if (!eglContext.equals(egl10.eglGetCurrentContext()) || !eglSurface.equals(egl10.eglGetCurrentSurface(EGL10.EGL_DRAW))) { + if (!egl10.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { + FileLog.e("eglMakeCurrent failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); + return; + } + } + cameraSurface.updateTexImage(); + + if (!recording) { + int resolution; + int bitrate; + String model = Build.DEVICE; + if (model == null) { + model = ""; + } + if (model.startsWith("zeroflte") || model.startsWith("zenlte")) { + resolution = 320; + bitrate = 600000; + } else { + resolution = 240; + bitrate = 400000; + } + videoEncoder.startRecording(cameraFile, resolution, bitrate, EGL14.eglGetCurrentContext()); + recording = true; + int orientation = currentSession.getCurrentOrientation(); + if (orientation == 90 || orientation == 270) { + float temp = scaleX; + scaleX = scaleY; + scaleY = temp; + } + } + + videoEncoder.frameAvailable(cameraSurface, cameraId); + + cameraSurface.getTransformMatrix(mSTMatrix); + + GLES20.glUseProgram(drawProgram); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexture[0]); + + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 12, vertexBuffer); + GLES20.glEnableVertexAttribArray(positionHandle); + + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); + GLES20.glEnableVertexAttribArray(textureHandle); + + GLES20.glUniformMatrix4fv(textureMatrixHandle, 1, false, mSTMatrix, 0); + GLES20.glUniformMatrix4fv(vertexMatrixHandle, 1, false, mMVPMatrix, 0); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + GLES20.glDisableVertexAttribArray(positionHandle); + GLES20.glDisableVertexAttribArray(textureHandle); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0); + GLES20.glUseProgram(0); + + egl10.eglSwapBuffers(eglDisplay, eglSurface); + } + + @Override + public void run() { + initied = initGL(); + super.run(); + } + + @Override + public void handleMessage(Message inputMessage) { + int what = inputMessage.what; + + switch (what) { + case DO_RENDER_MESSAGE: + onDraw((Integer) inputMessage.obj); + break; + case DO_SHUTDOWN_MESSAGE: + finish(); + if (recording) { + videoEncoder.stopRecording(inputMessage.arg1); + } + Looper looper = Looper.myLooper(); + if (looper != null) { + looper.quit(); + } + break; + case DO_REINIT_MESSAGE: { + if (!egl10.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { + FileLog.e("eglMakeCurrent failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); + return; + } + + if (cameraSurface != null) { + cameraSurface.getTransformMatrix(moldSTMatrix); + cameraSurface.setOnFrameAvailableListener(null); + cameraSurface.release(); + oldCameraTexture[0] = cameraTexture[0]; + cameraTextureAlpha = 0.0f; + cameraTexture[0] = 0; + } + cameraId++; + cameraReady = false; + + GLES20.glGenTextures(1, cameraTexture, 0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexture[0]); + 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); + + cameraSurface = new SurfaceTexture(cameraTexture[0]); + cameraSurface.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + requestRender(); + } + }); + createCamera(cameraSurface); + break; + } + case DO_SETSESSION_MESSAGE: { + FileLog.d("set gl rednderer session"); + CameraSession newSession = (CameraSession) inputMessage.obj; + if (currentSession == newSession) { + rotationAngle = currentSession.getWorldAngle(); + android.opengl.Matrix.setIdentityM(mMVPMatrix, 0); + if (rotationAngle != 0) { + android.opengl.Matrix.rotateM(mMVPMatrix, 0, rotationAngle, 0, 0, 1); + } + } else { + currentSession = newSession; + } + break; + } + } + } + + public void shutdown(int send) { + Handler handler = getHandler(); + if (handler != null) { + sendMessage(handler.obtainMessage(DO_SHUTDOWN_MESSAGE, send, 0), 0); + } + } + + public void requestRender() { + Handler handler = getHandler(); + if (handler != null) { + sendMessage(handler.obtainMessage(DO_RENDER_MESSAGE, cameraId), 0); + } + } + } + + private static final int MSG_START_RECORDING = 0; + private static final int MSG_STOP_RECORDING = 1; + private static final int MSG_VIDEOFRAME_AVAILABLE = 2; + private static final int MSG_AUDIOFRAME_AVAILABLE = 3; + + private static class EncoderHandler extends Handler { + private WeakReference mWeakEncoder; + + public EncoderHandler(VideoRecorder encoder) { + mWeakEncoder = new WeakReference<>(encoder); + } + + @Override + public void handleMessage(Message inputMessage) { + int what = inputMessage.what; + Object obj = inputMessage.obj; + + VideoRecorder encoder = mWeakEncoder.get(); + if (encoder == null) { + return; + } + + switch (what) { + case MSG_START_RECORDING: { + try { + FileLog.e("start encoder"); + encoder.prepareEncoder(); + } catch (Exception e) { + FileLog.e(e); + encoder.handleStopRecording(0); + Looper.myLooper().quit(); + } + break; + } + case MSG_STOP_RECORDING: { + FileLog.e("stop encoder"); + encoder.handleStopRecording(inputMessage.arg1); + break; + } + case MSG_VIDEOFRAME_AVAILABLE: { + long timestamp = (((long) inputMessage.arg1) << 32) | (((long) inputMessage.arg2) & 0xffffffffL); + Integer cameraId = (Integer) inputMessage.obj; + encoder.handleVideoFrameAvailable(timestamp, cameraId); + break; + } + case MSG_AUDIOFRAME_AVAILABLE: { + encoder.handleAudioFrameAvailable((AudioBufferInfo) inputMessage.obj); + break; + } + } + } + + public void exit() { + Looper.myLooper().quit(); + } + } + + private class AudioBufferInfo { + byte[] buffer = new byte[2048 * 10]; + long[] offset = new long[10]; + int[] read = new int[10]; + int results; + int lastWroteBuffer; + boolean last; + } + + private class VideoRecorder implements Runnable { + + private static final String VIDEO_MIME_TYPE = "video/avc"; + private static final String AUDIO_MIME_TYPE = "audio/mp4a-latm"; + private static final int FRAME_RATE = 30; + private static final int IFRAME_INTERVAL = 1; + + private File videoFile; + private int videoWidth; + private int videoHeight; + private int videoBitrate; + private boolean videoConvertFirstWrite = true; + private boolean blendEnabled; + + private Surface surface; + private android.opengl.EGLDisplay eglDisplay = EGL14.EGL_NO_DISPLAY; + private android.opengl.EGLContext eglContext = EGL14.EGL_NO_CONTEXT; + private android.opengl.EGLContext sharedEglContext; + private android.opengl.EGLConfig eglConfig; + private android.opengl.EGLSurface eglSurface = EGL14.EGL_NO_SURFACE; + + private MediaCodec videoEncoder; + private MediaCodec audioEncoder; + + private MediaCodec.BufferInfo videoBufferInfo; + private MediaCodec.BufferInfo audioBufferInfo; + private MP4Builder mediaMuxer; + private ArrayList buffersToWrite = new ArrayList<>(); + private int videoTrackIndex = -5; + private int audioTrackIndex = -5; + + private long lastCommitedFrameTime; + private long audioStartTime = -1; + + private long currentTimestamp = 0; + private long lastTimestamp = -1; + + private volatile EncoderHandler handler; + + private final Object sync = new Object(); + private boolean ready; + private volatile boolean running; + private volatile int sendWhenDone; + private long skippedTime; + private boolean skippedFirst; + + private int drawProgram; + private int vertexMatrixHandle; + private int textureMatrixHandle; + private int positionHandle; + private int textureHandle; + private int scaleXHandle; + private int scaleYHandle; + private int alphaHandle; + + private Integer lastCameraId = 0; + + private AudioRecord audioRecorder; + + private ArrayBlockingQueue buffers = new ArrayBlockingQueue<>(10); + + private Runnable recorderRunnable = new Runnable() { + + @Override + public void run() { + long audioPresentationTimeNs; + int readResult; + boolean done = false; + while (!done) { + if (!running && audioRecorder.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) { + try { + audioRecorder.stop(); + } catch (Exception e) { + done = true; + } + if (sendWhenDone == 0) { + break; + } + } + AudioBufferInfo buffer; + if (buffers.isEmpty()) { + buffer = new AudioBufferInfo(); + } else { + buffer = buffers.poll(); + } + buffer.lastWroteBuffer = 0; + buffer.results = 10; + for (int a = 0; a < 10; a++) { + audioPresentationTimeNs = System.nanoTime(); + readResult = audioRecorder.read(buffer.buffer, a * 2048, 2048); + if (readResult <= 0) { + buffer.results = a; + if (!running) { + buffer.last = true; + } + break; + } + buffer.offset[a] = audioPresentationTimeNs; + buffer.read[a] = readResult; + } + if (buffer.results >= 0 || buffer.last) { + if (!running && buffer.results < 10) { + done = true; + } + handler.sendMessage(handler.obtainMessage(MSG_AUDIOFRAME_AVAILABLE, buffer)); + } else { + if (!running) { + done = true; + } else { + try { + buffers.put(buffer); + } catch (Exception ignore) { + + } + } + } + } + try { + audioRecorder.release(); + } catch (Exception e) { + FileLog.e(e); + } + handler.sendMessage(handler.obtainMessage(MSG_STOP_RECORDING, sendWhenDone, 0)); + } + }; + + public void startRecording(File outputFile, int size, int bitRate, android.opengl.EGLContext sharedContext) { + videoFile = outputFile; + videoWidth = size; + videoHeight = size; + videoBitrate = bitRate; + sharedEglContext = sharedContext; + + synchronized (sync) { + if (running) { + return; + } + running = true; + new Thread(this, "TextureMovieEncoder").start(); + while (!ready) { + try { + sync.wait(); + } catch (InterruptedException ie) { + // ignore + } + } + } + handler.sendMessage(handler.obtainMessage(MSG_START_RECORDING)); + } + + public void stopRecording(int send) { + handler.sendMessage(handler.obtainMessage(MSG_STOP_RECORDING, send, 0)); + } + + public void frameAvailable(SurfaceTexture st, Integer cameraId) { + synchronized (sync) { + if (!ready) { + return; + } + } + + long timestamp = st.getTimestamp(); + if (timestamp == 0) { + return; + } + + handler.sendMessage(handler.obtainMessage(MSG_VIDEOFRAME_AVAILABLE, (int) (timestamp >> 32), (int) timestamp, cameraId)); + } + + @Override + public void run() { + Looper.prepare(); + synchronized (sync) { + handler = new EncoderHandler(this); + ready = true; + sync.notify(); + } + Looper.loop(); + + synchronized (sync) { + ready = false; + } + } + + private void handleAudioFrameAvailable(AudioBufferInfo input) { + if (audioStartTime == -1) { + audioStartTime = input.offset[0]; + } + buffersToWrite.add(input); + if (buffersToWrite.size() > 1) { + input = buffersToWrite.get(0); + } + try { + drainEncoder(false); + } catch (Exception e) { + FileLog.e(e); + } + try { + boolean isLast = false; + while (input != null) { + int inputBufferIndex = audioEncoder.dequeueInputBuffer(0); + if (inputBufferIndex >= 0) { + ByteBuffer inputBuffer; + if (Build.VERSION.SDK_INT >= 21) { + inputBuffer = audioEncoder.getInputBuffer(inputBufferIndex); + } else { + ByteBuffer[] inputBuffers = audioEncoder.getInputBuffers(); + inputBuffer = inputBuffers[inputBufferIndex]; + inputBuffer.clear(); + } + long startWriteTime = input.offset[input.lastWroteBuffer]; + for (int a = input.lastWroteBuffer; a <= input.results; a++) { + if (a < input.results) { + if (inputBuffer.remaining() < input.read[a]) { + input.lastWroteBuffer = a; + input = null; + break; + } + inputBuffer.put(input.buffer, a * 2048, input.read[a]); + } + if (a >= input.results - 1) { + buffersToWrite.remove(input); + if (running) { + buffers.put(input); + } + if (!buffersToWrite.isEmpty()) { + input = buffersToWrite.get(0); + } else { + isLast = input.last; + input = null; + break; + } + } + } + audioEncoder.queueInputBuffer(inputBufferIndex, 0, inputBuffer.position(), startWriteTime == 0 ? 0 : (startWriteTime - audioStartTime) / 1000, isLast ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0); + } + } + } catch (Throwable e) { + FileLog.e(e); + } + } + + private void handleVideoFrameAvailable(long timestampNanos, Integer cameraId) { + try { + drainEncoder(false); + } catch (Exception e) { + FileLog.e(e); + } + long dt, alphaDt; + if (!lastCameraId.equals(cameraId)) { + lastTimestamp = -1; + lastCameraId = cameraId; + } + if (lastTimestamp == -1) { + lastTimestamp = timestampNanos; + if (currentTimestamp != 0) { + dt = (System.currentTimeMillis() - lastCommitedFrameTime) * 1000000; + alphaDt = 0; + } else { + alphaDt = dt = 0; + } + } else { + alphaDt = dt = (timestampNanos - lastTimestamp); + lastTimestamp = timestampNanos; + } + lastCommitedFrameTime = System.currentTimeMillis(); + if (!skippedFirst) { + skippedTime += dt; + if (skippedTime < 200000000) { + return; + } + skippedFirst = true; + } + currentTimestamp += dt; + + GLES20.glUseProgram(drawProgram); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 12, vertexBuffer); + GLES20.glEnableVertexAttribArray(positionHandle); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); + GLES20.glEnableVertexAttribArray(textureHandle); + GLES20.glUniform1f(scaleXHandle, scaleX); + GLES20.glUniform1f(scaleYHandle, scaleY); + GLES20.glUniformMatrix4fv(vertexMatrixHandle, 1, false, mMVPMatrix, 0); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + if (oldCameraTexture[0] != 0) { + if (!blendEnabled) { + GLES20.glEnable(GLES20.GL_BLEND); + blendEnabled = true; + } + GLES20.glUniformMatrix4fv(textureMatrixHandle, 1, false, moldSTMatrix, 0); + GLES20.glUniform1f(alphaHandle, 1.0f); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oldCameraTexture[0]); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } + GLES20.glUniformMatrix4fv(textureMatrixHandle, 1, false, mSTMatrix, 0); + GLES20.glUniform1f(alphaHandle, cameraTextureAlpha); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexture[0]); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + GLES20.glDisableVertexAttribArray(positionHandle); + GLES20.glDisableVertexAttribArray(textureHandle); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0); + GLES20.glUseProgram(0); + + FileLog.e("frame time = " + currentTimestamp); + EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, currentTimestamp); + EGL14.eglSwapBuffers(eglDisplay, eglSurface); + + if (oldCameraTexture[0] != 0 && cameraTextureAlpha < 1.0f) { + cameraTextureAlpha += alphaDt / 200000000.0f; + if (cameraTextureAlpha > 1) { + GLES20.glDisable(GLES20.GL_BLEND); + blendEnabled = false; + cameraTextureAlpha = 1; + GLES20.glDeleteTextures(1, oldCameraTexture, 0); + oldCameraTexture[0] = 0; + if (!cameraReady) { + cameraReady = true; + } + } + } else if (!cameraReady) { + cameraReady = true; + } + } + + private void handleStopRecording(final int send) { + if (running) { + sendWhenDone = send; + running = false; + return; + } + try { + drainEncoder(true); + } catch (Exception e) { + FileLog.e(e); + } + if (videoEncoder != null) { + try { + videoEncoder.stop(); + videoEncoder.release(); + videoEncoder = null; + } catch (Exception e) { + FileLog.e(e); + } + } + if (audioEncoder != null) { + try { + audioEncoder.stop(); + audioEncoder.release(); + audioEncoder = null; + } catch (Exception e) { + FileLog.e(e); + } + } + if (mediaMuxer != null) { + try { + mediaMuxer.finishMovie(); + } catch (Exception e) { + FileLog.e(e); + } + } + if (send != 0) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + videoEditedInfo = new VideoEditedInfo(); + videoEditedInfo.roundVideo = true; + videoEditedInfo.startTime = -1; + videoEditedInfo.endTime = -1; + videoEditedInfo.file = file; + videoEditedInfo.encryptedFile = encryptedFile; + videoEditedInfo.key = key; + videoEditedInfo.iv = iv; + videoEditedInfo.estimatedSize = size; + videoEditedInfo.resultWidth = videoEditedInfo.originalWidth = 240; + videoEditedInfo.resultHeight = videoEditedInfo.originalHeight = 240; + videoEditedInfo.originalPath = videoFile.getAbsolutePath(); + if (send == 1) { + baseFragment.sendMedia(new MediaController.PhotoEntry(0, 0, 0, videoFile.getAbsolutePath(), 0, true), videoEditedInfo); + } else { + videoPlayer = new VideoPlayer(); + videoPlayer.setDelegate(new VideoPlayer.VideoPlayerDelegate() { + @Override + public void onStateChanged(boolean playWhenReady, int playbackState) { + if (videoPlayer == null) { + return; + } + if (videoPlayer.isPlaying() && playbackState == ExoPlayer.STATE_ENDED) { + videoPlayer.seekTo(videoEditedInfo.startTime > 0 ? videoEditedInfo.startTime : 0); + } + } + + @Override + public void onError(Exception e) { + FileLog.e(e); + } + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + + } + + @Override + public void onRenderedFirstFrame() { + + } + + @Override + public boolean onSurfaceDestroyed(SurfaceTexture surfaceTexture) { + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + + } + }); + videoPlayer.setTextureView(textureView); + videoPlayer.preparePlayer(Uri.fromFile(videoFile), "other"); + videoPlayer.play(); + videoPlayer.setMute(true); + startProgressTimer(); + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(switchCameraButton, "alpha", 0.0f), + ObjectAnimator.ofInt(paint, "alpha", 0), + ObjectAnimator.ofFloat(muteImageView, "alpha", 1.0f)); + animatorSet.setDuration(180); + animatorSet.setInterpolator(new DecelerateInterpolator()); + animatorSet.start(); + videoEditedInfo.estimatedDuration = duration; + NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioDidSent, videoEditedInfo, videoFile.getAbsolutePath()); + } + didWriteData(videoFile, true); + } + }); + } else { + FileLoader.getInstance().cancelUploadFile(videoFile.getAbsolutePath(), false); + videoFile.delete(); + } + EGL14.eglDestroySurface(eglDisplay, eglSurface); + eglSurface = EGL14.EGL_NO_SURFACE; + if (surface != null) { + surface.release(); + surface = null; + } + if (eglDisplay != EGL14.EGL_NO_DISPLAY) { + EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); + EGL14.eglDestroyContext(eglDisplay, eglContext); + EGL14.eglReleaseThread(); + EGL14.eglTerminate(eglDisplay); + } + eglDisplay = EGL14.EGL_NO_DISPLAY; + eglContext = EGL14.EGL_NO_CONTEXT; + eglConfig = null; + handler.exit(); + } + + private void prepareEncoder() { + try { + int recordBufferSize = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); + if (recordBufferSize <= 0) { + recordBufferSize = 3584; + } + int bufferSize = 2048 * 24; + if (bufferSize < recordBufferSize) { + bufferSize = ((recordBufferSize / 2048) + 1) * 2048 * 2; + } + for (int a = 0; a < 3; a++) { + buffers.add(new AudioBufferInfo()); + } + audioRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC, 44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); + audioRecorder.startRecording(); + new Thread(recorderRunnable).start(); + + audioBufferInfo = new MediaCodec.BufferInfo(); + videoBufferInfo = new MediaCodec.BufferInfo(); + + MediaFormat audioFormat = new MediaFormat(); + audioFormat.setString(MediaFormat.KEY_MIME, AUDIO_MIME_TYPE); + audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); + audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100); + audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); + audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 32000); + audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 2048 * 10); + + audioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE); + audioEncoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + audioEncoder.start(); + + videoEncoder = MediaCodec.createEncoderByType(VIDEO_MIME_TYPE); + + MediaFormat format = MediaFormat.createVideoFormat(VIDEO_MIME_TYPE, videoWidth, videoHeight); + + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate); + format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); + + videoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + surface = videoEncoder.createInputSurface(); + videoEncoder.start(); + + Mp4Movie movie = new Mp4Movie(); + movie.setCacheFile(videoFile); + movie.setRotation(0); + movie.setSize(videoWidth, videoHeight); + mediaMuxer = new MP4Builder().createMovie(movie); + + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (cancelled) { + return; + } + try { + Vibrator v = (Vibrator) ApplicationLoader.applicationContext.getSystemService(Context.VIBRATOR_SERVICE); + v.vibrate(50); + } catch (Exception e) { + FileLog.e(e); + } + AndroidUtilities.lockOrientation(baseFragment.getParentActivity()); + recording = true; + recordStartTime = System.currentTimeMillis(); + AndroidUtilities.runOnUIThread(timerRunnable); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.recordStarted); + } + }); + } catch (Exception ioe) { + throw new RuntimeException(ioe); + } + + if (eglDisplay != EGL14.EGL_NO_DISPLAY) { + throw new RuntimeException("EGL already set up"); + } + + eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (eglDisplay == EGL14.EGL_NO_DISPLAY) { + throw new RuntimeException("unable to get EGL14 display"); + } + int[] version = new int[2]; + if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) { + eglDisplay = null; + throw new RuntimeException("unable to initialize EGL14"); + } + + if (eglContext == EGL14.EGL_NO_CONTEXT) { + int renderableType = EGL14.EGL_OPENGL_ES2_BIT; + + int[] attribList = { + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_RENDERABLE_TYPE, renderableType, + 0x3142, 1, + EGL14.EGL_NONE + }; + android.opengl.EGLConfig[] configs = new android.opengl.EGLConfig[1]; + int[] numConfigs = new int[1]; + if (!EGL14.eglChooseConfig(eglDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0)) { + throw new RuntimeException("Unable to find a suitable EGLConfig"); + } + + int[] attrib2_list = { + EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, + EGL14.EGL_NONE + }; + eglContext = EGL14.eglCreateContext(eglDisplay, configs[0], sharedEglContext, attrib2_list, 0); + eglConfig = configs[0]; + } + + int[] values = new int[1]; + EGL14.eglQueryContext(eglDisplay, eglContext, EGL14.EGL_CONTEXT_CLIENT_VERSION, values, 0); + + if (eglSurface != EGL14.EGL_NO_SURFACE) { + throw new IllegalStateException("surface already created"); + } + + int[] surfaceAttribs = { + EGL14.EGL_NONE + }; + eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, surface, surfaceAttribs, 0); + if (eglSurface == null) { + throw new RuntimeException("surface was null"); + } + + if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { + FileLog.e("eglMakeCurrent failed " + GLUtils.getEGLErrorString(EGL14.eglGetError())); + throw new RuntimeException("eglMakeCurrent failed"); + } + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER); + int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER); + if (vertexShader != 0 && fragmentShader != 0) { + drawProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(drawProgram, vertexShader); + GLES20.glAttachShader(drawProgram, fragmentShader); + GLES20.glLinkProgram(drawProgram); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(drawProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + GLES20.glDeleteProgram(drawProgram); + drawProgram = 0; + } else { + positionHandle = GLES20.glGetAttribLocation(drawProgram, "aPosition"); + textureHandle = GLES20.glGetAttribLocation(drawProgram, "aTextureCoord"); + scaleXHandle = GLES20.glGetUniformLocation(drawProgram, "scaleX"); + scaleYHandle = GLES20.glGetUniformLocation(drawProgram, "scaleY"); + alphaHandle = GLES20.glGetUniformLocation(drawProgram, "alpha"); + vertexMatrixHandle = GLES20.glGetUniformLocation(drawProgram, "uMVPMatrix"); + textureMatrixHandle = GLES20.glGetUniformLocation(drawProgram, "uSTMatrix"); + } + } + } + + public Surface getInputSurface() { + return surface; + } + + private void didWriteData(File file, boolean last) { + if (videoConvertFirstWrite) { + FileLoader.getInstance().uploadFile(file.toString(), false, false, 1, ConnectionsManager.FileTypeVideo); + videoConvertFirstWrite = false; + } else { + FileLoader.getInstance().checkUploadNewDataAvailable(file.toString(), false, last ? file.length() : 0); + } + } + + public void drainEncoder(boolean endOfStream) throws Exception { + if (endOfStream) { + videoEncoder.signalEndOfInputStream(); + } + + ByteBuffer[] encoderOutputBuffers = videoEncoder.getOutputBuffers(); + while (true) { + int encoderStatus = videoEncoder.dequeueOutputBuffer(videoBufferInfo, 10000); + if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { + if (!endOfStream) { + break; + } + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + if (Build.VERSION.SDK_INT < 21) { + encoderOutputBuffers = videoEncoder.getOutputBuffers(); + } + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + MediaFormat newFormat = videoEncoder.getOutputFormat(); + if (videoTrackIndex == -5) { + videoTrackIndex = mediaMuxer.addTrack(newFormat, false); + } + } else if (encoderStatus >= 0) { + ByteBuffer encodedData; + if (Build.VERSION.SDK_INT < 21) { + encodedData = encoderOutputBuffers[encoderStatus]; + } else { + encodedData = videoEncoder.getOutputBuffer(encoderStatus); + } + if (encodedData == null) { + throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null"); + } + if (videoBufferInfo.size > 1) { + if ((videoBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) { + if (mediaMuxer.writeSampleData(videoTrackIndex, encodedData, videoBufferInfo, true)) { + didWriteData(videoFile, false); + } + } else if (videoTrackIndex == -5) { + byte[] csd = new byte[videoBufferInfo.size]; + encodedData.limit(videoBufferInfo.offset + videoBufferInfo.size); + encodedData.position(videoBufferInfo.offset); + encodedData.get(csd); + ByteBuffer sps = null; + ByteBuffer pps = null; + for (int a = videoBufferInfo.size - 1; a >= 0; a--) { + if (a > 3) { + if (csd[a] == 1 && csd[a - 1] == 0 && csd[a - 2] == 0 && csd[a - 3] == 0) { + sps = ByteBuffer.allocate(a - 3); + pps = ByteBuffer.allocate(videoBufferInfo.size - (a - 3)); + sps.put(csd, 0, a - 3).position(0); + pps.put(csd, a - 3, videoBufferInfo.size - (a - 3)).position(0); + break; + } + } else { + break; + } + } + + MediaFormat newFormat = MediaFormat.createVideoFormat("video/avc", videoWidth, videoHeight); + if (sps != null && pps != null) { + newFormat.setByteBuffer("csd-0", sps); + newFormat.setByteBuffer("csd-1", pps); + } + videoTrackIndex = mediaMuxer.addTrack(newFormat, false); + } + } + videoEncoder.releaseOutputBuffer(encoderStatus, false); + if ((videoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + break; + } + } + } + + encoderOutputBuffers = audioEncoder.getOutputBuffers(); + boolean encoderOutputAvailable = true; + while (true) { + int encoderStatus = audioEncoder.dequeueOutputBuffer(audioBufferInfo, 0); + if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { + if (!endOfStream || !running && sendWhenDone == 0) { + break; + } + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + if (Build.VERSION.SDK_INT < 21) { + encoderOutputBuffers = audioEncoder.getOutputBuffers(); + } + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + MediaFormat newFormat = audioEncoder.getOutputFormat(); + if (audioTrackIndex == -5) { + audioTrackIndex = mediaMuxer.addTrack(newFormat, true); + } + } else if (encoderStatus >= 0) { + ByteBuffer encodedData; + if (Build.VERSION.SDK_INT < 21) { + encodedData = encoderOutputBuffers[encoderStatus]; + } else { + encodedData = audioEncoder.getOutputBuffer(encoderStatus); + } + if (encodedData == null) { + throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null"); + } + if ((audioBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + audioBufferInfo.size = 0; + } + if (audioBufferInfo.size != 0) { + if (mediaMuxer.writeSampleData(audioTrackIndex, encodedData, audioBufferInfo, false)) { + didWriteData(videoFile, false); + } + } + audioEncoder.releaseOutputBuffer(encoderStatus, false); + if ((audioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + break; + } + } + } + } + + @Override + protected void finalize() throws Throwable { + try { + if (eglDisplay != EGL14.EGL_NO_DISPLAY) { + EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); + EGL14.eglDestroyContext(eglDisplay, eglContext); + EGL14.eglReleaseThread(); + EGL14.eglTerminate(eglDisplay); + eglDisplay = EGL14.EGL_NO_DISPLAY; + eglContext = EGL14.EGL_NO_CONTEXT; + eglConfig = null; + } + } finally { + super.finalize(); + } } - cameraView.destroy(async, null); - cameraContainer.removeView(cameraView); - cameraView = null; } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PagerSlidingTabStrip.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PagerSlidingTabStrip.java index 336f36a7f..0e3ebcf12 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PagerSlidingTabStrip.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PagerSlidingTabStrip.java @@ -30,6 +30,7 @@ public class PagerSlidingTabStrip extends HorizontalScrollView { public interface IconTabProvider { Drawable getPageIconDrawable(int position); void customOnDraw(Canvas canvas, int position); + boolean canScrollToTab(int position); } private LinearLayout.LayoutParams defaultTabLayoutParams; @@ -114,6 +115,13 @@ public class PagerSlidingTabStrip extends HorizontalScrollView { }); } + public View getTab(int position) { + if (position < 0 || position >= tabsContainer.getChildCount()) { + return null; + } + return tabsContainer.getChildAt(position); + } + private void addIconTab(final int position, Drawable drawable) { ImageView tab = new ImageView(getContext()) { @Override @@ -130,6 +138,11 @@ public class PagerSlidingTabStrip extends HorizontalScrollView { tab.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { + if (pager.getAdapter() instanceof IconTabProvider) { + if (!((IconTabProvider) pager.getAdapter()).canScrollToTab(position)) { + return; + } + } pager.setCurrentItem(position); } }); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Painting.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Painting.java index 75be4d73c..5af89ba2e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Painting.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Painting.java @@ -232,17 +232,13 @@ public class Painting { GLES20.glUseProgram(shader.program); GLES20.glUniformMatrix4fv(shader.getUniform("mvpMatrix"), 1, false, FloatBuffer.wrap(projection)); - GLES20.glUniform1i(shader.getUniform("texture"), 0); - GLES20.glUniform1i(shader.getUniform("mask"), 1); + GLES20.glUniform1i(shader.getUniform("mask"), 0); Shader.SetColorUniform(shader.getUniform("color"), color); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTexture()); - - GLES20.glActiveTexture(GLES20.GL_TEXTURE1); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getPaintTexture()); - GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO); + GLES20.glBlendFuncSeparate(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA, GLES20.GL_SRC_ALPHA, GLES20.GL_ONE ); GLES20.glVertexAttribPointer(0, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer); GLES20.glEnableVertexAttribArray(0); @@ -250,8 +246,6 @@ public class Painting { GLES20.glEnableVertexAttribArray(1); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); - - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTexture()); } }); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/RenderView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/RenderView.java index 5071be08c..cc80673e4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/RenderView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/RenderView.java @@ -208,8 +208,7 @@ public class RenderView extends TextureView { private void updateTransform() { Matrix matrix = new Matrix(); - int width = getWidth(); - float scale = painting != null ? width / painting.getSize().width : 1.0f; + float scale = painting != null ? getWidth() / painting.getSize().width : 1.0f; if (scale <= 0) { scale = 1.0f; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/ShaderSet.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/ShaderSet.java index 4a55982b3..7f72a8d02 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/ShaderSet.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/ShaderSet.java @@ -21,8 +21,8 @@ public class ShaderSet { private static final String PAINT_BLIT_FSH = "precision highp float; varying vec2 varTexcoord; uniform sampler2D texture; void main (void) { vec4 tex = texture2D(texture, varTexcoord.st, 0.0); gl_FragColor = texture2D(texture, varTexcoord.st, 0.0); gl_FragColor.rgb *= gl_FragColor.a; }"; private static final String PAINT_BLITWITHMASKLIGHT_FSH = "precision highp float; varying vec2 varTexcoord; uniform sampler2D texture; uniform sampler2D mask; uniform vec4 color; void main (void) { vec4 dst = texture2D(texture, varTexcoord.st, 0.0); vec3 maskColor = texture2D(mask, varTexcoord.st, 0.0).rgb; float srcAlpha = clamp(0.78 * maskColor.r + maskColor.b + maskColor.g, 0.0, 1.0); vec3 borderColor = mix(color.rgb, vec3(1.0, 1.0, 1.0), 0.86); vec3 finalColor = mix(color.rgb, borderColor, maskColor.g); finalColor = mix(finalColor.rgb, vec3(1.0, 1.0, 1.0), maskColor.b); float outAlpha = srcAlpha + dst.a * (1.0 - srcAlpha); gl_FragColor.rgb = (finalColor * srcAlpha + dst.rgb * dst.a * (1.0 - srcAlpha)) / outAlpha; gl_FragColor.a = outAlpha; gl_FragColor.rgb *= gl_FragColor.a; }"; private static final String PAINT_BLITWITHMASK_FSH = "precision highp float; varying vec2 varTexcoord; uniform sampler2D texture; uniform sampler2D mask; uniform vec4 color; void main (void) { vec4 dst = texture2D(texture, varTexcoord.st, 0.0); float srcAlpha = color.a * texture2D(mask, varTexcoord.st, 0.0).a; float outAlpha = srcAlpha + dst.a * (1.0 - srcAlpha); gl_FragColor.rgb = (color.rgb * srcAlpha + dst.rgb * dst.a * (1.0 - srcAlpha)) / outAlpha; gl_FragColor.a = outAlpha; gl_FragColor.rgb *= gl_FragColor.a; }"; - private static final String PAINT_COMPOSITEWITHMASK_FSH = "precision highp float; varying vec2 varTexcoord; uniform sampler2D texture; uniform sampler2D mask; uniform vec4 color; void main (void) { vec4 dst = texture2D(texture, varTexcoord.st, 0.0); float srcAlpha = color.a * texture2D(mask, varTexcoord.st, 0.0).a; float outAlpha = srcAlpha + dst.a * (1.0 - srcAlpha); gl_FragColor.rgb = (color.rgb * srcAlpha + dst.rgb * dst.a * (1.0 - srcAlpha)) / outAlpha; gl_FragColor.a = outAlpha; }"; - private static final String PAINT_COMPOSITEWITHMASKLIGHT_FSH = "precision highp float; varying vec2 varTexcoord; uniform sampler2D texture; uniform sampler2D mask; uniform vec4 color; void main (void) { vec4 dst = texture2D(texture, varTexcoord.st, 0.0); vec3 maskColor = texture2D(mask, varTexcoord.st, 0.0).rgb; float srcAlpha = clamp(0.78 * maskColor.r + maskColor.b + maskColor.g, 0.0, 1.0); vec3 borderColor = mix(color.rgb, vec3(1.0, 1.0, 1.0), 0.86); vec3 finalColor = mix(color.rgb, borderColor, maskColor.g); finalColor = mix(finalColor.rgb, vec3(1.0, 1.0, 1.0), maskColor.b); float outAlpha = srcAlpha + dst.a * (1.0 - srcAlpha); gl_FragColor.rgb = (finalColor * srcAlpha + dst.rgb * dst.a * (1.0 - srcAlpha)) / outAlpha; gl_FragColor.a = outAlpha; }"; + private static final String PAINT_COMPOSITEWITHMASK_FSH = "precision highp float; varying vec2 varTexcoord; uniform sampler2D mask; uniform vec4 color; void main(void) { float alpha = color.a * texture2D(mask, varTexcoord.st, 0.0).a; gl_FragColor.rgb = color.rgb; gl_FragColor.a = alpha; }"; + private static final String PAINT_COMPOSITEWITHMASKLIGHT_FSH = "precision highp float; varying vec2 varTexcoord; uniform sampler2D mask; uniform vec4 color; void main (void) { vec3 maskColor = texture2D(mask, varTexcoord.st, 0.0).rgb; float alpha = clamp(0.78 * maskColor.r + maskColor.b + maskColor.g, 0.0, 1.0); vec3 borderColor = mix(color.rgb, vec3(1.0, 1.0, 1.0), 0.86); vec3 finalColor = mix(color.rgb, borderColor, maskColor.g); finalColor = mix(finalColor.rgb, vec3(1.0, 1.0, 1.0), maskColor.b); gl_FragColor.rgb = finalColor; gl_FragColor.a = alpha; }"; private static final String PAINT_NONPREMULTIPLIEDBLIT_FSH = "precision highp float; varying vec2 varTexcoord; uniform sampler2D texture; void main (void) { gl_FragColor = texture2D(texture, varTexcoord.st, 0.0); }"; private static Map> createMap() { @@ -67,7 +67,7 @@ public class ShaderSet { shader.put(VERTEX, PAINT_BLIT_VSH); shader.put(FRAGMENT, PAINT_COMPOSITEWITHMASK_FSH); shader.put(ATTRIBUTES, new String[]{"inPosition", "inTexcoord"}); - shader.put(UNIFORMS, new String[]{"mvpMatrix", "texture", "mask", "color"}); + shader.put(UNIFORMS, new String[]{"mvpMatrix", "mask", "color"}); result.put("compositeWithMask", Collections.unmodifiableMap(shader)); shader = new HashMap<>(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Texture.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Texture.java index 860849a7c..78262d5bd 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Texture.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Texture.java @@ -48,9 +48,10 @@ public class Texture { GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture); - boolean mipMappable = isPOT(bitmap.getWidth()) && isPOT(bitmap.getHeight()); + boolean mipMappable = false; //isPOT(bitmap.getWidth()) && isPOT(bitmap.getHeight()); 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); + 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, mipMappable ? GLES20.GL_LINEAR_MIPMAP_LINEAR : GLES20.GL_LINEAR); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java index 39ef8ba55..6fc98e530 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java @@ -855,7 +855,7 @@ public class PasscodeView extends FrameLayout { relativeLayout.setPadding(AndroidUtilities.dp(24), 0, AndroidUtilities.dp(24), 0); TextView fingerprintTextView = new TextView(getContext()); - fingerprintTextView.setTextColor(0xff939393); + fingerprintTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); fingerprintTextView.setId(id_fingerprint_textview); fingerprintTextView.setTextAppearance(android.R.style.TextAppearance_Material_Subhead); fingerprintTextView.setText(LocaleController.getString("FingerprintInfo", R.string.FingerprintInfo)); @@ -874,7 +874,7 @@ public class PasscodeView extends FrameLayout { fingerprintStatusTextView.setGravity(Gravity.CENTER_VERTICAL); fingerprintStatusTextView.setText(LocaleController.getString("FingerprintHelp", R.string.FingerprintHelp)); fingerprintStatusTextView.setTextAppearance(android.R.style.TextAppearance_Material_Body1); - fingerprintStatusTextView.setTextColor(0x42000000); + fingerprintStatusTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlack) & 0x42ffffff); relativeLayout.addView(fingerprintStatusTextView); layoutParams = LayoutHelper.createRelative(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT); layoutParams.setMarginStart(AndroidUtilities.dp(16)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PermissionAlert.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PermissionAlert.java new file mode 100644 index 000000000..734bb27cb --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PermissionAlert.java @@ -0,0 +1,18 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Components; + +import org.telegram.ui.ActionBar.AlertDialog; + +public class PermissionAlert { + + public static AlertDialog create() { + return null; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PipRoundVideoView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PipRoundVideoView.java new file mode 100644 index 000000000..7586bdb7d --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PipRoundVideoView.java @@ -0,0 +1,542 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Components; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.os.Build; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.TextureView; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.view.WindowManager; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.Bitmaps; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.MediaController; +import org.telegram.messenger.MessageObject; +import org.telegram.messenger.NotificationCenter; +import org.telegram.messenger.exoplayer2.ui.AspectRatioFrameLayout; +import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.Theme; + +import java.util.ArrayList; + +public class PipRoundVideoView implements NotificationCenter.NotificationCenterDelegate { + + private FrameLayout windowView; + private Activity parentActivity; + private TextureView textureView; + private ImageView imageView; + private AspectRatioFrameLayout aspectRatioFrameLayout; + private Bitmap bitmap; + private int videoWidth; + private int videoHeight; + private AnimatorSet hideShowAnimation; + private Runnable onCloseRunnable; + + private WindowManager.LayoutParams windowLayoutParams; + private WindowManager windowManager; + private SharedPreferences preferences; + private DecelerateInterpolator decelerateInterpolator; + + private RectF rect = new RectF(); + + @SuppressLint("StaticFieldLeak") + private static PipRoundVideoView instance; + + public void show(Activity activity, Runnable closeRunnable) { + if (activity == null) { + return; + } + instance = this; + onCloseRunnable = closeRunnable; + windowView = new FrameLayout(activity) { + + private float startX; + private float startY; + private boolean dragging; + private boolean startDragging; + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + startX = event.getRawX(); + startY = event.getRawY(); + startDragging = true; + } + return true; + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!startDragging && !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); + if (startDragging) { + if (Math.abs(dx) >= AndroidUtilities.getPixelsInCM(0.3f, true) || Math.abs(dy) >= AndroidUtilities.getPixelsInCM(0.3f, false)) { + dragging = true; + startDragging = false; + } + } else if (dragging) { + 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) { + if (startDragging && !dragging) { + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject != null) { + if (MediaController.getInstance().isMessagePaused()) { + MediaController.getInstance().playMessage(messageObject); + } else { + MediaController.getInstance().pauseMessage(messageObject); + } + } + } + dragging = false; + startDragging = false; + animateToBoundsMaybe(); + } + return true; + } + + @Override + protected void onDraw(Canvas canvas) { + if (Theme.chat_roundVideoShadow != null/* && aspectRatioFrameLayout.isDrawingReady()*/) { + Theme.chat_roundVideoShadow.setAlpha((int) (getAlpha() * 255)); + Theme.chat_roundVideoShadow.setBounds(AndroidUtilities.dp(1), AndroidUtilities.dp(2), AndroidUtilities.dp(125), AndroidUtilities.dp(125)); + Theme.chat_roundVideoShadow.draw(canvas); + } + } + }; + windowView.setWillNotDraw(false); + + videoWidth = AndroidUtilities.dp(120 + 6); + videoHeight = AndroidUtilities.dp(120 + 6); + + if (Build.VERSION.SDK_INT >= 21) { + aspectRatioFrameLayout = new AspectRatioFrameLayout(activity) { + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean result = super.drawChild(canvas, child, drawingTime); + if (child == textureView) { + MessageObject currentMessageObject = MediaController.getInstance().getPlayingMessageObject(); + if (currentMessageObject != null) { + rect.set(AndroidUtilities.dpf2(1.5f), AndroidUtilities.dpf2(1.5f), getMeasuredWidth() - AndroidUtilities.dpf2(1.5f), getMeasuredHeight() - AndroidUtilities.dpf2(1.5f)); + canvas.drawArc(rect, -90, 360 * currentMessageObject.audioProgress, false, Theme.chat_radialProgressPaint); + } + } + return result; + } + }; + aspectRatioFrameLayout.setOutlineProvider(new ViewOutlineProvider() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void getOutline(View view, Outline outline) { + outline.setOval(0, 0, AndroidUtilities.dp(120), AndroidUtilities.dp(120)); + } + }); + aspectRatioFrameLayout.setClipToOutline(true); + } else { + final Paint aspectPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + aspectPaint.setColor(0xff000000); + aspectPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + aspectRatioFrameLayout = new AspectRatioFrameLayout(activity) { + + private Path aspectPath = new Path(); + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + aspectPath.reset(); + aspectPath.addCircle(w / 2, h / 2, w / 2, Path.Direction.CW); + aspectPath.toggleInverseFillType(); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + canvas.drawPath(aspectPath, aspectPaint); + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean result; + try { + result = super.drawChild(canvas, child, drawingTime); + } catch (Throwable ignore) { + result = false; + } + if (child == textureView) { + MessageObject currentMessageObject = MediaController.getInstance().getPlayingMessageObject(); + if (currentMessageObject != null) { + rect.set(AndroidUtilities.dpf2(1.5f), AndroidUtilities.dpf2(1.5f), getMeasuredWidth() - AndroidUtilities.dpf2(1.5f), getMeasuredHeight() - AndroidUtilities.dpf2(1.5f)); + canvas.drawArc(rect, -90, 360 * currentMessageObject.audioProgress, false, Theme.chat_radialProgressPaint); + } + } + return result; + } + }; + aspectRatioFrameLayout.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + aspectRatioFrameLayout.setAspectRatio(1.0f, 0); + windowView.addView(aspectRatioFrameLayout, LayoutHelper.createFrame(120, 120, Gravity.LEFT | Gravity.TOP, 3, 3, 0, 0)); + windowView.setAlpha(1.0f); + windowView.setScaleX(0.8f); + windowView.setScaleY(0.8f); + + textureView = new TextureView(activity); + aspectRatioFrameLayout.addView(textureView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + imageView = new ImageView(activity); + aspectRatioFrameLayout.addView(imageView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + imageView.setVisibility(View.INVISIBLE); + + windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE); + + preferences = ApplicationLoader.applicationContext.getSharedPreferences("pipconfig", Context.MODE_PRIVATE); + + int sidex = preferences.getInt("sidex", 1); + int sidey = preferences.getInt("sidey", 0); + float px = preferences.getFloat("px", 0); + float py = preferences.getFloat("py", 0); + + try { + windowLayoutParams = new WindowManager.LayoutParams(); + windowLayoutParams.width = videoWidth; + windowLayoutParams.height = videoHeight; + windowLayoutParams.x = getSideCoord(true, sidex, px, videoWidth); + windowLayoutParams.y = getSideCoord(false, sidey, py, videoHeight); + windowLayoutParams.format = PixelFormat.TRANSLUCENT; + windowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; + windowLayoutParams.type = WindowManager.LayoutParams.LAST_APPLICATION_WINDOW; + windowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + windowManager.addView(windowView, windowLayoutParams); + } catch (Exception e) { + FileLog.e(e); + return; + } + parentActivity = activity; + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + runShowHideAnimation(true); + } + + private static int getSideCoord(boolean isX, int side, float p, int sideSize) { + int total; + if (isX) { + total = AndroidUtilities.displaySize.x - sideSize; + } else { + total = AndroidUtilities.displaySize.y - sideSize - ActionBar.getCurrentActionBarHeight(); + } + int result; + if (side == 0) { + result = AndroidUtilities.dp(10); + } else if (side == 1) { + result = total - AndroidUtilities.dp(10); + } else { + result = Math.round((total - AndroidUtilities.dp(20)) * p) + AndroidUtilities.dp(10); + } + if (!isX) { + result += ActionBar.getCurrentActionBarHeight(); + } + return result; + } + + @Override + public void didReceivedNotification(int id, Object... args) { + if (id == NotificationCenter.messagePlayingProgressDidChanged) { + if (aspectRatioFrameLayout != null) { + aspectRatioFrameLayout.invalidate(); + } + } + } + + public TextureView getTextureView() { + return textureView; + } + + public void close(boolean animated) { + if (animated) { + if (textureView != null && textureView.getParent() != null) { + if (textureView.getWidth() > 0 && textureView.getHeight() > 0) { + bitmap = Bitmaps.createBitmap(textureView.getWidth(), textureView.getHeight(), Bitmap.Config.ARGB_8888); + } + try { + textureView.getBitmap(bitmap); + } catch (Throwable e) { + bitmap = null; + } + imageView.setImageBitmap(bitmap); + try { + aspectRatioFrameLayout.removeView(textureView); + } catch (Exception ignore) { + + } + imageView.setVisibility(View.VISIBLE); + runShowHideAnimation(false); + } + } else { + if (bitmap != null) { + imageView.setImageDrawable(null); + bitmap.recycle(); + bitmap = null; + } + try { + windowManager.removeView(windowView); + } catch (Exception e) { + //don't promt + } + if (instance == this) { + instance = null; + } + parentActivity = null; + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + } + } + + public void onConfigurationChanged() { + int sidex = preferences.getInt("sidex", 1); + int sidey = preferences.getInt("sidey", 0); + float px = preferences.getFloat("px", 0); + float py = preferences.getFloat("py", 0); + windowLayoutParams.x = getSideCoord(true, sidex, px, videoWidth); + windowLayoutParams.y = getSideCoord(false, sidey, py, videoHeight); + windowManager.updateViewLayout(windowView, windowLayoutParams); + } + + public void showTemporary(boolean show) { + if (hideShowAnimation != null) { + hideShowAnimation.cancel(); + } + hideShowAnimation = new AnimatorSet(); + hideShowAnimation.playTogether( + ObjectAnimator.ofFloat(windowView, "alpha", show ? 1.0f : 0.0f), + ObjectAnimator.ofFloat(windowView, "scaleX", show ? 1.0f : 0.8f), + ObjectAnimator.ofFloat(windowView, "scaleY", show ? 1.0f : 0.8f)); + hideShowAnimation.setDuration(150); + if (decelerateInterpolator == null) { + decelerateInterpolator = new DecelerateInterpolator(); + } + hideShowAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animation.equals(hideShowAnimation)) { + hideShowAnimation = null; + } + } + }); + hideShowAnimation.setInterpolator(decelerateInterpolator); + hideShowAnimation.start(); + } + + private void runShowHideAnimation(final boolean show) { + if (hideShowAnimation != null) { + hideShowAnimation.cancel(); + } + hideShowAnimation = new AnimatorSet(); + hideShowAnimation.playTogether( + ObjectAnimator.ofFloat(windowView, "alpha", show ? 1.0f : 0.0f), + ObjectAnimator.ofFloat(windowView, "scaleX", show ? 1.0f : 0.8f), + ObjectAnimator.ofFloat(windowView, "scaleY", show ? 1.0f : 0.8f)); + hideShowAnimation.setDuration(150); + if (decelerateInterpolator == null) { + decelerateInterpolator = new DecelerateInterpolator(); + } + hideShowAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animation.equals(hideShowAnimation)) { + if (!show) { + close(false); + } + hideShowAnimation = null; + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (animation.equals(hideShowAnimation)) { + hideShowAnimation = null; + } + } + }); + hideShowAnimation.setInterpolator(decelerateInterpolator); + hideShowAnimation.start(); + } + + private void animateToBoundsMaybe() { + int startX = getSideCoord(true, 0, 0, videoWidth); + int endX = getSideCoord(true, 1, 0, videoWidth); + int startY = getSideCoord(false, 0, 0, videoHeight); + int endY = getSideCoord(false, 1, 0, videoHeight); + ArrayList animators = null; + SharedPreferences.Editor editor = preferences.edit(); + int maxDiff = AndroidUtilities.dp(20); + boolean slideOut = false; + if (Math.abs(startX - windowLayoutParams.x) <= maxDiff || windowLayoutParams.x < 0 && windowLayoutParams.x > -videoWidth / 4) { + if (animators == null) { + animators = new ArrayList<>(); + } + editor.putInt("sidex", 0); + if (windowView.getAlpha() != 1.0f) { + animators.add(ObjectAnimator.ofFloat(windowView, "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) { + if (animators == null) { + animators = new ArrayList<>(); + } + editor.putInt("sidex", 1); + if (windowView.getAlpha() != 1.0f) { + animators.add(ObjectAnimator.ofFloat(windowView, "alpha", 1.0f)); + } + animators.add(ObjectAnimator.ofInt(this, "x", endX)); + } else if (windowView.getAlpha() != 1.0f) { + if (animators == null) { + animators = new ArrayList<>(); + } + if (windowLayoutParams.x < 0) { + animators.add(ObjectAnimator.ofInt(this, "x", -videoWidth)); + } else { + animators.add(ObjectAnimator.ofInt(this, "x", AndroidUtilities.displaySize.x)); + } + slideOut = true; + } else { + editor.putFloat("px", (windowLayoutParams.x - startX) / (float) (endX - startX)); + editor.putInt("sidex", 2); + } + if (!slideOut) { + if (Math.abs(startY - windowLayoutParams.y) <= maxDiff || windowLayoutParams.y <= ActionBar.getCurrentActionBarHeight()) { + if (animators == null) { + animators = new ArrayList<>(); + } + editor.putInt("sidey", 0); + animators.add(ObjectAnimator.ofInt(this, "y", startY)); + } else if (Math.abs(endY - windowLayoutParams.y) <= maxDiff) { + if (animators == null) { + animators = new ArrayList<>(); + } + editor.putInt("sidey", 1); + animators.add(ObjectAnimator.ofInt(this, "y", endY)); + } else { + editor.putFloat("py", (windowLayoutParams.y - startY) / (float) (endY - startY)); + editor.putInt("sidey", 2); + } + editor.commit(); + } + if (animators != null) { + if (decelerateInterpolator == null) { + decelerateInterpolator = new DecelerateInterpolator(); + } + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.setInterpolator(decelerateInterpolator); + animatorSet.setDuration(150); + if (slideOut) { + animators.add(ObjectAnimator.ofFloat(windowView, "alpha", 0.0f)); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + close(false); + if (onCloseRunnable != null) { + onCloseRunnable.run(); + } + } + }); + } + animatorSet.playTogether(animators); + animatorSet.start(); + } + } + + public int getX() { + return windowLayoutParams.x; + } + + public int getY() { + return windowLayoutParams.y; + } + + public void setX(int value) { + windowLayoutParams.x = value; + try { + windowManager.updateViewLayout(windowView, windowLayoutParams); + } catch (Exception ignore) { + + } + } + + public void setY(int value) { + windowLayoutParams.y = value; + try { + windowManager.updateViewLayout(windowView, windowLayoutParams); + } catch (Exception ignore) { + + } + } + + public static PipRoundVideoView getInstance() { + return instance; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayingGameDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayingGameDrawable.java index c37101251..429f34884 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayingGameDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayingGameDrawable.java @@ -12,14 +12,13 @@ import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.RectF; -import android.graphics.drawable.Drawable; import android.view.animation.DecelerateInterpolator; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.NotificationCenter; import org.telegram.ui.ActionBar.Theme; -public class PlayingGameDrawable extends Drawable { +public class PlayingGameDrawable extends StatusDrawable { private boolean isChat = false; private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PopupAudioView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PopupAudioView.java index 6ae2c1fae..5425262ae 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PopupAudioView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PopupAudioView.java @@ -205,7 +205,7 @@ public class PopupAudioView extends BaseCell implements SeekBar.SeekBarDelegate, private void didPressedButton() { if (buttonState == 0) { - boolean result = MediaController.getInstance().playAudio(currentMessageObject); + boolean result = MediaController.getInstance().playMessage(currentMessageObject); if (!currentMessageObject.isOut() && currentMessageObject.isContentUnread()) { if (currentMessageObject.messageOwner.to_id.channel_id == 0) { MessagesController.getInstance().markMessageContentAsRead(currentMessageObject); @@ -217,7 +217,7 @@ public class PopupAudioView extends BaseCell implements SeekBar.SeekBarDelegate, invalidate(); } } else if (buttonState == 1) { - boolean result = MediaController.getInstance().pauseAudio(currentMessageObject); + boolean result = MediaController.getInstance().pauseMessage(currentMessageObject); if (result) { buttonState = 0; invalidate(); @@ -243,7 +243,7 @@ public class PopupAudioView extends BaseCell implements SeekBar.SeekBarDelegate, } int duration = 0; - if (!MediaController.getInstance().isPlayingAudio(currentMessageObject)) { + if (!MediaController.getInstance().isPlayingMessage(currentMessageObject)) { for (int a = 0; a < currentMessageObject.getDocument().attributes.size(); a++) { TLRPC.DocumentAttribute attribute = currentMessageObject.getDocument().attributes.get(a); if (attribute instanceof TLRPC.TL_documentAttributeAudio) { @@ -275,8 +275,8 @@ public class PopupAudioView extends BaseCell implements SeekBar.SeekBarDelegate, File cacheFile = FileLoader.getPathToMessage(currentMessageObject.messageOwner); if (cacheFile.exists()) { MediaController.getInstance().removeLoadingFileObserver(this); - boolean playing = MediaController.getInstance().isPlayingAudio(currentMessageObject); - if (!playing || playing && MediaController.getInstance().isAudioPaused()) { + boolean playing = MediaController.getInstance().isPlayingMessage(currentMessageObject); + if (!playing || playing && MediaController.getInstance().isMessagePaused()) { buttonState = 0; } else { buttonState = 1; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgress.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgress.java index 282d3cf12..a29c55d24 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgress.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgress.java @@ -36,22 +36,28 @@ public class RadialProgress { private Drawable previousDrawable; private boolean hideCurrentDrawable; private int progressColor = 0xffffffff; + private Paint progressPaint; + + private int diff = AndroidUtilities.dp(4); private static DecelerateInterpolator decelerateInterpolator; - private static Paint progressPaint; private boolean alphaForPrevious = true; public RadialProgress(View parentView) { if (decelerateInterpolator == null) { decelerateInterpolator = new DecelerateInterpolator(); - progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - progressPaint.setStyle(Paint.Style.STROKE); - progressPaint.setStrokeCap(Paint.Cap.ROUND); - progressPaint.setStrokeWidth(AndroidUtilities.dp(3)); } + progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + progressPaint.setStyle(Paint.Style.STROKE); + progressPaint.setStrokeCap(Paint.Cap.ROUND); + progressPaint.setStrokeWidth(AndroidUtilities.dp(3)); parent = parentView; } + public void setStrikeWidth(int width) { + progressPaint.setStrokeWidth(width); + } + public void setProgressRect(int left, int top, int right, int bottom) { progressRect.set(left, top, right, bottom); } @@ -101,6 +107,10 @@ public class RadialProgress { } } + public void setDiff(int value) { + diff = value; + } + public void setProgressColor(int color) { progressColor = color; } @@ -188,7 +198,6 @@ public class RadialProgress { } if (currentWithRound || previousWithRound) { - int diff = AndroidUtilities.dp(4); progressPaint.setColor(progressColor); if (previousWithRound) { progressPaint.setAlpha((int) (255 * animatedAlphaValue)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RecordStatusDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RecordStatusDrawable.java index 35f795a80..0cc303f95 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RecordStatusDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RecordStatusDrawable.java @@ -11,12 +11,11 @@ package org.telegram.ui.Components; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.RectF; -import android.graphics.drawable.Drawable; import org.telegram.messenger.AndroidUtilities; import org.telegram.ui.ActionBar.Theme; -public class RecordStatusDrawable extends Drawable { +public class RecordStatusDrawable extends StatusDrawable { private boolean isChat = false; private long lastUpdateTime = 0; 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 5d50b60b9..b0e694d19 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerListView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerListView.java @@ -348,12 +348,12 @@ public class RecyclerListView extends RecyclerView { letterLayout = new StaticLayout(newLetter, letterPaint, 1000, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); oldLetterLayout = null; if (letterLayout.getLineCount() > 0) { + float lWidth = letterLayout.getLineWidth(0); + float lleft = letterLayout.getLineLeft(0); if (LocaleController.isRTL) { - float lWidth = letterLayout.getLineWidth(0); - float lleft = letterLayout.getLineLeft(0); - textX = AndroidUtilities.dp(10) + (AndroidUtilities.dp(88) - (letterLayout.getLineWidth(0) - letterLayout.getLineLeft(0))) / 2; + textX = AndroidUtilities.dp(10) + (AndroidUtilities.dp(88) - letterLayout.getLineWidth(0)) / 2 - letterLayout.getLineLeft(0); } else { - textX = (AndroidUtilities.dp(88) - (letterLayout.getLineWidth(0) - letterLayout.getLineLeft(0))) / 2; + textX = (AndroidUtilities.dp(88) - letterLayout.getLineWidth(0)) / 2 - letterLayout.getLineLeft(0); } textY = (AndroidUtilities.dp(88) - letterLayout.getHeight()) / 2; } @@ -741,7 +741,7 @@ public class RecyclerListView extends RecyclerView { onScrollListener.onScrolled(recyclerView, dx, dy); } if (selectorPosition != NO_POSITION) { - selectorRect.offset(0, -dy); + selectorRect.offset(-dx, -dy); selectorDrawable.setBounds(selectorRect); invalidate(); } else { @@ -941,6 +941,9 @@ public class RecyclerListView extends RecyclerView { @Override public boolean onInterceptTouchEvent(MotionEvent e) { + if (!isEnabled()) { + return false; + } if (disallowInterceptTouchEvents) { requestDisallowInterceptTouchEvent(true); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RoundStatusDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RoundStatusDrawable.java new file mode 100644 index 000000000..0aa32bb40 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RoundStatusDrawable.java @@ -0,0 +1,90 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Components; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.ui.ActionBar.Theme; + +public class RoundStatusDrawable extends StatusDrawable { + + private boolean isChat = false; + private long lastUpdateTime = 0; + private boolean started = false; + private float progress; + private int progressDirection = 1; + + public void setIsChat(boolean value) { + isChat = value; + } + + private void update() { + long newTime = System.currentTimeMillis(); + long dt = newTime - lastUpdateTime; + lastUpdateTime = newTime; + if (dt > 50) { + dt = 50; + } + progress += progressDirection * dt / 400.0f; + if (progressDirection > 0 && progress >= 1.0f) { + progressDirection = -1; + progress = 1; + } else if (progressDirection < 0 && progress <= 0) { + progressDirection = 1; + progress = 0; + } + invalidateSelf(); + } + + public void start() { + lastUpdateTime = System.currentTimeMillis(); + started = true; + invalidateSelf(); + } + + public void stop() { + started = false; + } + + @Override + public void draw(Canvas canvas) { + Theme.chat_statusPaint.setAlpha(55 + (int) (200 * progress)); + canvas.drawCircle(AndroidUtilities.dp(6), AndroidUtilities.dp(isChat ? 8 : 9), AndroidUtilities.dp(4), Theme.chat_statusPaint); + if (started) { + update(); + } + } + + @Override + public void setAlpha(int alpha) { + + } + + @Override + public void setColorFilter(ColorFilter cf) { + + } + + @Override + public int getOpacity() { + return 0; + } + + @Override + public int getIntrinsicWidth() { + return AndroidUtilities.dp(12); + } + + @Override + public int getIntrinsicHeight() { + return AndroidUtilities.dp(10); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RoundVideoPlayingDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RoundVideoPlayingDrawable.java new file mode 100644 index 000000000..a342bf6e5 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RoundVideoPlayingDrawable.java @@ -0,0 +1,130 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Components; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.view.View; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.ui.ActionBar.Theme; + +public class RoundVideoPlayingDrawable extends Drawable { + + private long lastUpdateTime = 0; + private boolean started = false; + private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private float progress1 = 0.47f; + private float progress2 = 0.0f; + private float progress3 = 0.32f; + private int progress1Direction = 1; + private int progress2Direction = 1; + private int progress3Direction = 1; + private View parentView; + + public RoundVideoPlayingDrawable(View view) { + super(); + parentView = view; + } + + private void update() { + long newTime = System.currentTimeMillis(); + long dt = newTime - lastUpdateTime; + lastUpdateTime = newTime; + if (dt > 50) { + dt = 50; + } + progress1 += dt / 300.0f * progress1Direction; + if (progress1 > 1.0f) { + progress1Direction = -1; + progress1 = 1.0f; + } else if (progress1 < 0.0f) { + progress1Direction = 1; + progress1 = 0.0f; + } + + progress2 += dt / 310.0f * progress2Direction; + if (progress2 > 1.0f) { + progress2Direction = -1; + progress2 = 1.0f; + } else if (progress2 < 0.0f) { + progress2Direction = 1; + progress2 = 0.0f; + } + + progress3 += dt / 320.0f * progress3Direction; + if (progress3 > 1.0f) { + progress3Direction = -1; + progress3 = 1.0f; + } else if (progress3 < 0.0f) { + progress3Direction = 1; + progress3 = 0.0f; + } + + parentView.invalidate(); + } + + public void start() { + if (started) { + return; + } + lastUpdateTime = System.currentTimeMillis(); + started = true; + parentView.invalidate(); + } + + public void stop() { + if (!started) { + return; + } + started = false; + } + + @Override + public void draw(Canvas canvas) { + paint.setColor(Theme.getColor(Theme.key_chat_mediaTimeText)); + int x = getBounds().left; + int y = getBounds().top; + for (int a = 0; a < 3; a++) { + canvas.drawRect(x + AndroidUtilities.dp(2), y + AndroidUtilities.dp(2 + 7 * progress1), x + AndroidUtilities.dp(4), y + AndroidUtilities.dp(10), paint); + canvas.drawRect(x + AndroidUtilities.dp(5), y + AndroidUtilities.dp(2 + 7 * progress2), x + AndroidUtilities.dp(7), y + AndroidUtilities.dp(10), paint); + canvas.drawRect(x + AndroidUtilities.dp(8), y + AndroidUtilities.dp(2 + 7 * progress3), x + AndroidUtilities.dp(10), y + AndroidUtilities.dp(10), paint); + } + if (started) { + update(); + } + } + + @Override + public void setAlpha(int alpha) { + + } + + @Override + public void setColorFilter(ColorFilter cf) { + + } + + @Override + public int getOpacity() { + return 0; + } + + @Override + public int getIntrinsicWidth() { + return AndroidUtilities.dp(12); + } + + @Override + public int getIntrinsicHeight() { + return AndroidUtilities.dp(12); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/SendingFileDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/SendingFileDrawable.java index e3f25a867..bbeccc253 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/SendingFileDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/SendingFileDrawable.java @@ -10,12 +10,11 @@ package org.telegram.ui.Components; import android.graphics.Canvas; import android.graphics.ColorFilter; -import android.graphics.drawable.Drawable; import org.telegram.messenger.AndroidUtilities; import org.telegram.ui.ActionBar.Theme; -public class SendingFileDrawable extends Drawable { +public class SendingFileDrawable extends StatusDrawable { private boolean isChat = false; private long lastUpdateTime = 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 8d7b6668e..68d88db32 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java @@ -8,6 +8,10 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; @@ -24,6 +28,7 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.FrameLayout; @@ -68,11 +73,15 @@ import java.util.TimerTask; public class ShareAlert extends BottomSheet implements NotificationCenter.NotificationCenterDelegate { private FrameLayout frameLayout; + private FrameLayout frameLayout2; private TextView doneButtonBadgeTextView; private TextView doneButtonTextView; private LinearLayout doneButton; private EditText nameTextView; + private EditText commentTextView; private View shadow; + private View shadow2; + private AnimatorSet animatorSet; private RecyclerListView gridView; private GridLayoutManager layoutManager; private ShareDialogsAdapter listAdapter; @@ -158,7 +167,7 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi int padding = contentSize < height ? 0 : height - (height / 5 * 3) + AndroidUtilities.dp(8); if (gridView.getPaddingTop() != padding) { ignoreLayout = true; - gridView.setPadding(0, padding, 0, AndroidUtilities.dp(8)); + gridView.setPadding(0, padding, 0, AndroidUtilities.dp(frameLayout2.getTag() != null ? 56 : 8)); ignoreLayout = false; } super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); @@ -217,10 +226,16 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi ArrayList arrayList = new ArrayList<>(); arrayList.add(sendingMessageObject); for (HashMap.Entry entry : selectedDialogs.entrySet()) { + if (frameLayout2.getTag() != null && commentTextView.length() > 0) { + SendMessagesHelper.getInstance().sendMessage(commentTextView.getText().toString(), entry.getKey(), null, null, true, null, null, null); + } SendMessagesHelper.getInstance().sendMessage(arrayList, entry.getKey()); } } else if (sendingText != null) { for (HashMap.Entry entry : selectedDialogs.entrySet()) { + if (frameLayout2.getTag() != null && commentTextView.length() > 0) { + SendMessagesHelper.getInstance().sendMessage(commentTextView.getText().toString(), entry.getKey(), null, null, true, null, null, null); + } SendMessagesHelper.getInstance().sendMessage(sendingText, entry.getKey(), null, null, true, null, null, null); } } @@ -376,6 +391,36 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi shadow.setBackgroundResource(R.drawable.header_shadow); containerView.addView(shadow, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 0)); + frameLayout2 = new FrameLayout(context); + frameLayout2.setBackgroundColor(Theme.getColor(Theme.key_dialogBackground)); + frameLayout2.setTranslationY(AndroidUtilities.dp(53)); + containerView.addView(frameLayout2, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.BOTTOM)); + frameLayout2.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return true; + } + }); + + commentTextView = new EditText(context); + commentTextView.setHint(LocaleController.getString("ShareComment", R.string.ShareComment)); + commentTextView.setMaxLines(1); + commentTextView.setSingleLine(true); + commentTextView.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); + commentTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + commentTextView.setBackgroundDrawable(null); + commentTextView.setHintTextColor(Theme.getColor(Theme.key_dialogTextHint)); + commentTextView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + commentTextView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); + AndroidUtilities.clearCursorDrawable(commentTextView); + commentTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); + frameLayout2.addView(commentTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 8, 1, 8, 0)); + + shadow2 = new View(context); + shadow2.setBackgroundResource(R.drawable.header_shadow_reverse); + shadow2.setTranslationY(AndroidUtilities.dp(53)); + containerView.addView(shadow2, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.BOTTOM | Gravity.LEFT, 0, 0, 0, 48)); + updateSelectedCount(); if (!DialogsActivity.dialogsLoaded) { @@ -447,8 +492,43 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi } } + private void showCommentTextView(final boolean show) { + if (show == (frameLayout2.getTag() != null)) { + return; + } + if (animatorSet != null) { + animatorSet.cancel(); + } + frameLayout2.setTag(show ? 1 : null); + AndroidUtilities.hideKeyboard(commentTextView); + animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(shadow2, "translationY", AndroidUtilities.dp(show ? 0 : 53)), + ObjectAnimator.ofFloat(frameLayout2, "translationY", AndroidUtilities.dp(show ? 0 : 53))); + animatorSet.setInterpolator(new DecelerateInterpolator()); + animatorSet.setDuration(180); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animation.equals(animatorSet)) { + gridView.setPadding(0, 0, 0, AndroidUtilities.dp(show ? 56 : 8)); + animatorSet = null; + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (animation.equals(animatorSet)) { + animatorSet = null; + } + } + }); + animatorSet.start(); + } + public void updateSelectedCount() { if (selectedDialogs.isEmpty()) { + showCommentTextView(false); doneButtonBadgeTextView.setVisibility(View.GONE); if (!isPublicChannel && linkToCopy == null) { doneButtonTextView.setTextColor(Theme.getColor(Theme.key_dialogTextGray4)); @@ -460,6 +540,7 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi doneButtonTextView.setText(LocaleController.getString("CopyLink", R.string.CopyLink).toUpperCase()); } } else { + showCommentTextView(true); doneButtonTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); doneButtonBadgeTextView.setVisibility(View.VISIBLE); doneButtonBadgeTextView.setText(String.format("%d", selectedDialogs.size())); @@ -497,7 +578,7 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi dialogs.add(dialog); } else { TLRPC.Chat chat = MessagesController.getInstance().getChat(-lower_id); - if (!(chat == null || ChatObject.isNotInChat(chat) || ChatObject.isChannel(chat) && !chat.creator && !chat.editor && !chat.megagroup)) { + if (!(chat == null || ChatObject.isNotInChat(chat) || ChatObject.isChannel(chat) && !chat.creator && (chat.admin_rights == null || !chat.admin_rights.post_messages) && !chat.megagroup)) { dialogs.add(dialog); } } @@ -673,7 +754,7 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi if (data != null) { TLRPC.Chat chat = TLRPC.Chat.TLdeserialize(data, data.readInt32(false), false); data.reuse(); - if (!(chat == null || ChatObject.isNotInChat(chat) || ChatObject.isChannel(chat) && !chat.creator && !chat.editor && !chat.megagroup)) { + if (!(chat == null || ChatObject.isNotInChat(chat) || ChatObject.isChannel(chat) && !chat.creator && (chat.admin_rights == null || !chat.admin_rights.post_messages) && !chat.megagroup)) { DialogSearchResult dialogSearchResult = dialogsResult.get(-(long) chat.id); dialogSearchResult.name = AndroidUtilities.generateSearchName(chat.title, null, q); dialogSearchResult.object = chat; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/StatusDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/StatusDrawable.java new file mode 100644 index 000000000..a0afc8ce5 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/StatusDrawable.java @@ -0,0 +1,17 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Components; + +import android.graphics.drawable.Drawable; + +public abstract class StatusDrawable extends Drawable { + public abstract void start(); + public abstract void stop(); + public abstract void setIsChat(boolean value); +} 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 a762a30f9..cd2a6846e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/StickersAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/StickersAlert.java @@ -19,7 +19,11 @@ import android.graphics.*; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; +import android.text.Selection; +import android.text.Spannable; +import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.text.method.LinkMovementMethod; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; @@ -34,6 +38,7 @@ import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.Emoji; import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MessagesController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; import org.telegram.messenger.query.StickersQuery; @@ -53,6 +58,8 @@ import org.telegram.ui.StickerPreviewViewer; import java.util.ArrayList; import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class StickersAlert extends BottomSheet implements NotificationCenter.NotificationCenterDelegate { @@ -65,6 +72,23 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not void onStickerSetUninstalled(); } + private static class LinkMovementMethodMy extends LinkMovementMethod { + @Override + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { + try { + boolean result = super.onTouchEvent(widget, buffer, event); + if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { + Selection.removeSelection(buffer); + } + return result; + } catch (Exception e) { + FileLog.e(e); + } + return false; + } + } + + private Pattern urlPattern; private RecyclerListView gridView; private GridAdapter adapter; private TextView titleTextView; @@ -281,23 +305,6 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not containerView.setWillNotDraw(false); containerView.setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); - titleTextView = new TextView(context); - titleTextView.setLines(1); - titleTextView.setSingleLine(true); - titleTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); - titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20); - titleTextView.setEllipsize(TextUtils.TruncateAt.MIDDLE); - titleTextView.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); - titleTextView.setGravity(Gravity.CENTER_VERTICAL); - titleTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - containerView.addView(titleTextView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); - titleTextView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return true; - } - }); - shadow[0] = new View(context); shadow[0].setBackgroundResource(R.drawable.header_shadow); shadow[0].setAlpha(0.0f); @@ -425,6 +432,20 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not } }); + titleTextView = new TextView(context); + titleTextView.setLines(1); + titleTextView.setSingleLine(true); + titleTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); + titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20); + titleTextView.setLinkTextColor(Theme.getColor(Theme.key_dialogTextLink)); + titleTextView.setHighlightColor(Theme.getColor(Theme.key_dialogLinkSelection)); + titleTextView.setEllipsize(TextUtils.TruncateAt.MIDDLE); + titleTextView.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); + titleTextView.setGravity(Gravity.CENTER_VERTICAL); + titleTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + titleTextView.setMovementMethod(new LinkMovementMethodMy()); + containerView.addView(titleTextView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); + RadialProgressView progressView = new RadialProgressView(context); emptyView.addView(progressView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER)); @@ -532,7 +553,36 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not return; } if (stickerSet != null) { - titleTextView.setText(stickerSet.set.title); + SpannableStringBuilder stringBuilder = null; + try { + if (urlPattern == null) { + urlPattern = Pattern.compile("@[a-zA-Z\\d_]{1,32}"); + } + Matcher matcher = urlPattern.matcher(stickerSet.set.title); + while (matcher.find()) { + if (stringBuilder == null) { + stringBuilder = new SpannableStringBuilder(stickerSet.set.title); + } + int start = matcher.start(); + int end = matcher.end(); + if (stickerSet.set.title.charAt(start) != '@') { + start++; + } + URLSpanNoUnderline url = new URLSpanNoUnderline(stickerSet.set.title.subSequence(start + 1, end).toString()) { + @Override + public void onClick(View widget) { + MessagesController.openByUserName(getURL(), parentFragment, 1); + dismiss(); + } + }; + if (url != null) { + stringBuilder.setSpan(url, start, end, 0); + } + } + } catch (Exception e) { + FileLog.e(e); + } + titleTextView.setText(stringBuilder != null ? stringBuilder : stickerSet.set.title); if (stickerSet.set == null || !StickersQuery.isStickerPackInstalled(stickerSet.set.id)) { setRightButton(new View.OnClickListener() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java index 98401439e..6c90e1ae2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java @@ -630,15 +630,6 @@ public class Switch extends CompoundButton { } } - /*@Override - protected int[] onCreateDrawableState(int extraSpace) { - final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); - if (isChecked()) { - mergeDrawableStates(drawableState, CHECKED_STATE_SET); - } - return drawableState; - }*/ - @Override protected void drawableStateChanged() { super.drawableStateChanged(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch2.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch2.java new file mode 100644 index 000000000..6d788c259 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch2.java @@ -0,0 +1,174 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui.Components; + +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.support.annotation.Keep; +import android.view.View; + +import org.telegram.messenger.AndroidUtilities; + +public class Switch2 extends View { + + private RectF rectF; + + private static Bitmap drawBitmap; + + private float progress; + private ObjectAnimator checkAnimator; + + private boolean attachedToWindow; + private boolean isChecked; + private boolean isDisabled; + private Paint paint; + private Paint paint2; + + public Switch2(Context context) { + super(context); + rectF = new RectF(); + if (drawBitmap == null || drawBitmap.getWidth() != AndroidUtilities.dp(24)) { + drawBitmap = Bitmap.createBitmap(AndroidUtilities.dp(24), AndroidUtilities.dp(24), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(drawBitmap); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setShadowLayer(AndroidUtilities.dp(2), 0, 0, 0x7f000000); + canvas.drawCircle(AndroidUtilities.dp(12), AndroidUtilities.dp(12), AndroidUtilities.dp(9), paint); + try { + canvas.setBitmap(null); + } catch (Exception ignore) { + + } + } + + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint2 = new Paint(Paint.ANTI_ALIAS_FLAG); + paint2.setStyle(Paint.Style.STROKE); + paint2.setStrokeCap(Paint.Cap.ROUND); + paint2.setStrokeWidth(AndroidUtilities.dp(2)); + } + + @Keep + public void setProgress(float value) { + if (progress == value) { + return; + } + progress = value; + invalidate(); + } + + public float getProgress() { + return progress; + } + + private void cancelCheckAnimator() { + if (checkAnimator != null) { + checkAnimator.cancel(); + } + } + + private void animateToCheckedState(boolean newCheckedState) { + checkAnimator = ObjectAnimator.ofFloat(this, "progress", newCheckedState ? 1 : 0); + checkAnimator.setDuration(250); + checkAnimator.start(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + attachedToWindow = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + attachedToWindow = false; + } + + public void setChecked(boolean checked, boolean animated) { + if (checked == isChecked) { + return; + } + isChecked = checked; + if (attachedToWindow && animated) { + animateToCheckedState(checked); + } else { + cancelCheckAnimator(); + setProgress(checked ? 1.0f : 0.0f); + } + } + + public void setDisabled(boolean disabled) { + isDisabled = disabled; + invalidate(); + } + + public boolean isChecked() { + return isChecked; + } + + @Override + protected void onDraw(Canvas canvas) { + if (getVisibility() != VISIBLE) { + return; + } + + int width = AndroidUtilities.dp(36); + int thumb = AndroidUtilities.dp(20); + int x = (getMeasuredWidth() - width) / 2; + int y = (getMeasuredHeight() - AndroidUtilities.dp(14)) / 2; + int tx = (int) ((width - AndroidUtilities.dp(14)) * progress) + x + AndroidUtilities.dp(7); + int ty = getMeasuredHeight() / 2; + + + int red = (int) (0xff + (0xa0 - 0xff) * progress); + int green = (int) (0xb0 + (0xd6 - 0xb0) * progress); + int blue = (int) (0xad + (0xfa - 0xad) * progress); + paint.setColor(0xff000000 | ((red & 0xff) << 16) | ((green & 0xff) << 8) | (blue & 0xff)); + + rectF.set(x, y, x + width, y + AndroidUtilities.dp(14)); + canvas.drawRoundRect(rectF, AndroidUtilities.dp(7), AndroidUtilities.dp(7), paint); + + red = (int) (0xdb + (0x44 - 0xdb) * progress); + green = (int) (0x58 + (0xa8 - 0x58) * progress); + blue = (int) (0x5c + (0xea - 0x5c) * progress); + paint.setColor(0xff000000 | ((red & 0xff) << 16) | ((green & 0xff) << 8) | (blue & 0xff)); + + canvas.drawBitmap(drawBitmap, tx - AndroidUtilities.dp(12), ty - AndroidUtilities.dp(11), null); + canvas.drawCircle(tx, ty, AndroidUtilities.dp(10), paint); + + paint2.setColor(0xffffffff); + tx -= AndroidUtilities.dp(10.8f) - AndroidUtilities.dp(1.3f) * progress; + ty -= AndroidUtilities.dp(8.5f) - AndroidUtilities.dp(0.5f) * progress; + int startX2 = (int) AndroidUtilities.dpf2(4.6f) + tx; + int startY2 = (int) (AndroidUtilities.dpf2(9.5f) + ty); + int endX2 = startX2 + AndroidUtilities.dp(2); + int endY2 = startY2 + AndroidUtilities.dp(2); + + int startX = (int) AndroidUtilities.dpf2(7.5f) + tx; + int startY = (int) AndroidUtilities.dpf2(5.4f) + ty; + int endX = startX + AndroidUtilities.dp(7); + int endY = startY + AndroidUtilities.dp(7); + + startX = (int) (startX + (startX2 - startX) * progress); + startY = (int) (startY + (startY2 - startY) * progress); + endX = (int) (endX + (endX2 - endX) * progress); + endY = (int) (endY + (endY2 - endY) * progress); + canvas.drawLine(startX, startY, endX, endY, paint2); + + startX = (int) AndroidUtilities.dpf2(7.5f) + tx; + startY = (int) AndroidUtilities.dpf2(12.5f) + ty; + endX = startX + AndroidUtilities.dp(7); + endY = startY - AndroidUtilities.dp(7); + canvas.drawLine(startX, startY, endX, endY, paint2); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/TypingDotsDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/TypingDotsDrawable.java index 2bf83d91d..7becb77a8 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/TypingDotsDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/TypingDotsDrawable.java @@ -10,14 +10,13 @@ package org.telegram.ui.Components; import android.graphics.Canvas; import android.graphics.ColorFilter; -import android.graphics.drawable.Drawable; import android.view.animation.DecelerateInterpolator; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.NotificationCenter; import org.telegram.ui.ActionBar.Theme; -public class TypingDotsDrawable extends Drawable { +public class TypingDotsDrawable extends StatusDrawable { private boolean isChat = false; private float[] scales = new float[3]; @@ -89,6 +88,7 @@ public class TypingDotsDrawable extends Drawable { } else { y = AndroidUtilities.dp(9.3f) + getBounds().top; } + Theme.chat_statusPaint.setAlpha(255); canvas.drawCircle(AndroidUtilities.dp(3), y, scales[0] * AndroidUtilities.density, Theme.chat_statusPaint); canvas.drawCircle(AndroidUtilities.dp(9), y, scales[1] * AndroidUtilities.density, Theme.chat_statusPaint); canvas.drawCircle(AndroidUtilities.dp(15), y, scales[2] * AndroidUtilities.density, Theme.chat_statusPaint); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/URLSpanBotCommand.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/URLSpanBotCommand.java index be2ed6aad..942947972 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/URLSpanBotCommand.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/URLSpanBotCommand.java @@ -9,6 +9,7 @@ package org.telegram.ui.Components; import android.text.TextPaint; +import android.text.style.WrapTogetherSpan; import org.telegram.ui.ActionBar.Theme; 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 0e00a802d..d97b5b4c7 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java @@ -16,9 +16,11 @@ import android.view.TextureView; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.exoplayer2.DefaultLoadControl; +import org.telegram.messenger.exoplayer2.DefaultRenderersFactory; import org.telegram.messenger.exoplayer2.ExoPlaybackException; import org.telegram.messenger.exoplayer2.ExoPlayer; import org.telegram.messenger.exoplayer2.ExoPlayerFactory; +import org.telegram.messenger.exoplayer2.PlaybackParameters; import org.telegram.messenger.exoplayer2.SimpleExoPlayer; import org.telegram.messenger.exoplayer2.Timeline; import org.telegram.messenger.exoplayer2.extractor.DefaultExtractorsFactory; @@ -32,7 +34,7 @@ import org.telegram.messenger.exoplayer2.source.dash.DefaultDashChunkSource; import org.telegram.messenger.exoplayer2.source.hls.HlsMediaSource; import org.telegram.messenger.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import org.telegram.messenger.exoplayer2.source.smoothstreaming.SsMediaSource; -import org.telegram.messenger.exoplayer2.trackselection.AdaptiveVideoTrackSelection; +import org.telegram.messenger.exoplayer2.trackselection.AdaptiveTrackSelection; import org.telegram.messenger.exoplayer2.trackselection.DefaultTrackSelector; import org.telegram.messenger.exoplayer2.trackselection.MappingTrackSelector; import org.telegram.messenger.exoplayer2.trackselection.TrackSelection; @@ -81,7 +83,7 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid mainHandler = new Handler(); - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER); + TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); lastReportedPlaybackState = ExoPlayer.STATE_IDLE; @@ -89,7 +91,7 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid private void ensurePleyaerCreated() { if (player == null) { - player = ExoPlayerFactory.newSimpleInstance(ApplicationLoader.applicationContext, trackSelector, new DefaultLoadControl(), null, SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF); + player = ExoPlayerFactory.newSimpleInstance(ApplicationLoader.applicationContext, trackSelector, new DefaultLoadControl(), null, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); player.addListener(this); player.setVideoListener(this); player.setVideoTextureView(textureView); @@ -168,6 +170,9 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid } public void setTextureView(TextureView texture) { + if (textureView == texture) { + return; + } textureView = texture; if (player == null) { return; @@ -205,6 +210,10 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid return player != null ? player.getCurrentPosition() : 0; } + public boolean isMuted() { + return player.getVolume() == 0.0f; + } + public void setMute(boolean value) { if (player == null) { return; @@ -216,6 +225,13 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid } } + public void setVolume(float volume) { + if (player == null) { + return; + } + player.setVolume(volume); + } + public void seekTo(long positionMs) { if (player == null) { return; @@ -243,6 +259,12 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid return player != null && lastReportedPlaybackState == ExoPlayer.STATE_BUFFERING; } + public void setStreamType(int type) { + if (player != null) { + player.setAudioStreamType(type); + } + } + @Override public void onLoadingChanged(boolean isLoading) { @@ -293,6 +315,11 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid delegate.onSurfaceTextureUpdated(surfaceTexture); } + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + + } + private void maybeReportPlayerState() { boolean playWhenReady = player.getPlayWhenReady(); int playbackState = player.getPlaybackState(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java index 8f8f37147..126f5508b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java @@ -44,12 +44,17 @@ public class VideoTimelineView extends View { private int frameWidth; private int frameHeight; private int framesToLoad; - private float maxProgressDiff; + private float maxProgressDiff = 1.0f; + private float minProgressDiff = 0.0f; + private boolean isRoundFrames; + private Rect rect1; + private Rect rect2; public interface VideoTimelineViewDelegate { void onLeftProgressChanged(float progress); - void onRifhtProgressChanged(float progress); + void didStartDragging(); + void didStopDragging(); } public VideoTimelineView(Context context) { @@ -68,6 +73,10 @@ public class VideoTimelineView extends View { return progressRight; } + public void setMinProgressDiff(float value) { + minProgressDiff = value; + } + public void setMaxProgressDiff(float value) { maxProgressDiff = value; if (progressRight - progressLeft > maxProgressDiff) { @@ -76,6 +85,14 @@ public class VideoTimelineView 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 public boolean onTouchEvent(MotionEvent event) { if (event == null) { @@ -89,25 +106,39 @@ public class VideoTimelineView extends View { int endX = (int) (width * progressRight) + AndroidUtilities.dp(16); if (event.getAction() == MotionEvent.ACTION_DOWN) { + getParent().requestDisallowInterceptTouchEvent(true); + if (mediaMetadataRetriever == null) { + return false; + } int additionWidth = AndroidUtilities.dp(12); if (startX - additionWidth <= x && x <= startX + additionWidth && y >= 0 && y <= getMeasuredHeight()) { + if (delegate != null) { + delegate.didStartDragging(); + } pressedLeft = true; pressDx = (int) (x - startX); - getParent().requestDisallowInterceptTouchEvent(true); invalidate(); return true; } else if (endX - additionWidth <= x && x <= endX + additionWidth && y >= 0 && y <= getMeasuredHeight()) { + if (delegate != null) { + delegate.didStartDragging(); + } pressedRight = true; pressDx = (int) (x - endX); - getParent().requestDisallowInterceptTouchEvent(true); invalidate(); return true; } } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { if (pressedLeft) { + if (delegate != null) { + delegate.didStopDragging(); + } pressedLeft = false; return true; } else if (pressedRight) { + if (delegate != null) { + delegate.didStopDragging(); + } pressedRight = false; return true; } @@ -122,6 +153,11 @@ public class VideoTimelineView extends View { progressLeft = (float) (startX - AndroidUtilities.dp(16)) / (float) width; if (progressRight - progressLeft > maxProgressDiff) { progressRight = progressLeft + maxProgressDiff; + } else if (minProgressDiff != 0 && progressRight - progressLeft < minProgressDiff) { + progressLeft = progressRight - minProgressDiff; + if (progressLeft < 0) { + progressLeft = 0; + } } if (delegate != null) { delegate.onLeftProgressChanged(progressLeft); @@ -138,6 +174,11 @@ public class VideoTimelineView extends View { progressRight = (float) (endX - AndroidUtilities.dp(16)) / (float) width; if (progressRight - progressLeft > maxProgressDiff) { progressLeft = progressRight - maxProgressDiff; + } else if (minProgressDiff != 0 && progressRight - progressLeft < minProgressDiff) { + progressRight = progressLeft + minProgressDiff; + if (progressRight > 1.0f) { + progressRight = 1.0f; + } } if (delegate != null) { delegate.onRifhtProgressChanged(progressRight); @@ -149,7 +190,12 @@ public class VideoTimelineView extends View { return false; } + public void setColor(int color) { + paint.setColor(color); + } + public void setVideoPath(String path) { + destroy(); mediaMetadataRetriever = new MediaMetadataRetriever(); progressLeft = 0.0f; progressRight = 1.0f; @@ -160,6 +206,7 @@ public class VideoTimelineView extends View { } catch (Exception e) { FileLog.e(e); } + invalidate(); } public void setDelegate(VideoTimelineViewDelegate delegate) { @@ -171,9 +218,14 @@ public class VideoTimelineView extends View { return; } if (frameNum == 0) { - frameHeight = AndroidUtilities.dp(40); - framesToLoad = (getMeasuredWidth() - AndroidUtilities.dp(16)) / frameHeight; - frameWidth = (int) Math.ceil((float) (getMeasuredWidth() - AndroidUtilities.dp(16)) / (float) framesToLoad); + 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); + } frameTimeOffset = videoLength / framesToLoad; } currentTask = new AsyncTask() { @@ -187,7 +239,7 @@ public class VideoTimelineView extends View { return null; } try { - bitmap = mediaMetadataRetriever.getFrameAtTime(frameTimeOffset * frameNum * 1000); + bitmap = mediaMetadataRetriever.getFrameAtTime(frameTimeOffset * frameNum * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); if (isCancelled()) { return null; } @@ -269,7 +321,7 @@ public class VideoTimelineView extends View { int endX = (int) (width * progressRight) + AndroidUtilities.dp(16); canvas.save(); - canvas.clipRect(AndroidUtilities.dp(16), 0, width + AndroidUtilities.dp(20), AndroidUtilities.dp(44)); + canvas.clipRect(AndroidUtilities.dp(16), 0, width + AndroidUtilities.dp(20), getMeasuredHeight()); if (frames.isEmpty() && currentTask == null) { reloadFrames(0); } else { @@ -277,19 +329,28 @@ public class VideoTimelineView extends View { for (int a = 0; a < frames.size(); a++) { Bitmap bitmap = frames.get(a); if (bitmap != null) { - canvas.drawBitmap(bitmap, AndroidUtilities.dp(16) + offset * frameWidth, AndroidUtilities.dp(2), null); + int x = AndroidUtilities.dp(16) + offset * (isRoundFrames ? frameWidth / 2 : frameWidth); + int y = AndroidUtilities.dp(2); + 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); + } } offset++; } } - canvas.drawRect(AndroidUtilities.dp(16), AndroidUtilities.dp(2), startX, AndroidUtilities.dp(42), paint2); - canvas.drawRect(endX + AndroidUtilities.dp(4), AndroidUtilities.dp(2), AndroidUtilities.dp(16) + width + AndroidUtilities.dp(4), AndroidUtilities.dp(42), paint2); + int top = AndroidUtilities.dp(2); - canvas.drawRect(startX, 0, startX + AndroidUtilities.dp(2), AndroidUtilities.dp(44), paint); - canvas.drawRect(endX + AndroidUtilities.dp(2), 0, endX + AndroidUtilities.dp(4), AndroidUtilities.dp(44), paint); - canvas.drawRect(startX + AndroidUtilities.dp(2), 0, endX + AndroidUtilities.dp(4), AndroidUtilities.dp(2), paint); - canvas.drawRect(startX + AndroidUtilities.dp(2), AndroidUtilities.dp(42), endX + AndroidUtilities.dp(4), AndroidUtilities.dp(44), paint); + canvas.drawRect(AndroidUtilities.dp(16), top, startX, getMeasuredHeight() - top, paint2); + canvas.drawRect(endX + AndroidUtilities.dp(4), top, AndroidUtilities.dp(16) + width + AndroidUtilities.dp(4), getMeasuredHeight() - top, paint2); + + canvas.drawRect(startX, 0, startX + AndroidUtilities.dp(2), getMeasuredHeight(), paint); + canvas.drawRect(endX + AndroidUtilities.dp(2), 0, endX + AndroidUtilities.dp(4), getMeasuredHeight(), paint); + canvas.drawRect(startX + AndroidUtilities.dp(2), 0, endX + AndroidUtilities.dp(4), top, paint); + canvas.drawRect(startX + AndroidUtilities.dp(2), getMeasuredHeight() - top, endX + AndroidUtilities.dp(4), getMeasuredHeight(), paint); canvas.restore(); canvas.drawCircle(startX, getMeasuredHeight() / 2, AndroidUtilities.dp(7), paint); 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 8b6341fcb..6c2174b71 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/WebPlayerView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/WebPlayerView.java @@ -177,7 +177,7 @@ public class WebPlayerView extends ViewGroup implements VideoPlayer.VideoPlayerD private static final Pattern stmtVarPattern = Pattern.compile("var\\s"); private static final Pattern stmtReturnPattern = Pattern.compile("return(?:\\s+|$)"); private static final Pattern exprParensPattern = Pattern.compile("[()]"); - private static final Pattern playerIdPattern = Pattern.compile(".*?-([a-zA-Z0-9_-]+)(?:/watch_as3|/html5player(?:-new)?|/base)?\\.([a-z]+)$"); + private static final Pattern playerIdPattern = Pattern.compile(".*?-([a-zA-Z0-9_-]+)(?:/watch_as3|/html5player(?:-new)?|(?:/[a-z]{2}_[A-Z]{2})?/base)?\\.([a-z]+)$"); private static final String exprName = "[a-zA-Z_$][a-zA-Z_$0-9]*"; private abstract class function { @@ -265,11 +265,20 @@ public class WebPlayerView extends ViewGroup implements VideoPlayer.VideoPlayerD //ignore } - matcher = Pattern.compile(String.format(Locale.US, "(%s)\\.([^(]+)(?:\\(+([^()]*)\\))?$", exprName)).matcher(expr); + matcher = Pattern.compile(String.format(Locale.US, "(%s)\\[(.+)\\]$", exprName)).matcher(expr); + if (matcher.find()) { + String val = matcher.group(1); + interpretExpression(matcher.group(2), localVars, allowRecursion - 1); + return; + } + + matcher = Pattern.compile(String.format(Locale.US, "(%s)(?:\\.([^(]+)|\\[([^]]+)\\])\\s*(?:\\(+([^()]*)\\))?$", exprName)).matcher(expr); if (matcher.find()) { String variable = matcher.group(1); - String member = matcher.group(2); - String arg_str = matcher.group(3); + String m1 = matcher.group(2); + String m2 = matcher.group(3); + String member = (TextUtils.isEmpty(m1) ? m2 : m1).replace("\"", ""); + String arg_str = matcher.group(4); if (localVars.get(variable) == null) { extractObject(variable); } @@ -344,9 +353,10 @@ public class WebPlayerView extends ViewGroup implements VideoPlayer.VideoPlayerD } private HashMap extractObject(String objname) throws Exception { + String funcName = "(?:[a-zA-Z$0-9]+|\"[a-zA-Z$0-9]+\"|'[a-zA-Z$0-9]+')"; HashMap obj = new HashMap<>(); // ?P - Matcher matcher = Pattern.compile(String.format(Locale.US, "(?:var\\s+)?%s\\s*=\\s*\\{\\s*(([a-zA-Z$0-9]+\\s*:\\s*function\\(.*?\\)\\s*\\{.*?\\}(?:,\\s*)?)*)\\}\\s*;", Pattern.quote(objname))).matcher(jsCode); + Matcher matcher = Pattern.compile(String.format(Locale.US, "(?:var\\s+)?%s\\s*=\\s*\\{\\s*((%s\\s*:\\s*function\\(.*?\\)\\s*\\{.*?\\}(?:,\\s*)?)*)\\}\\s*;", Pattern.quote(objname), funcName)).matcher(jsCode); String fields = null; while (matcher.find()) { String code = matcher.group(); @@ -360,7 +370,7 @@ public class WebPlayerView extends ViewGroup implements VideoPlayer.VideoPlayerD break; } // ?P ?P ?P - matcher = Pattern.compile("([a-zA-Z$0-9]+)\\s*:\\s*function\\(([a-z,]+)\\)\\{([^}]+)\\}").matcher(fields); + matcher = Pattern.compile(String.format("(%s)\\s*:\\s*function\\(([a-z,]+)\\)\\{([^}]+)\\}", funcName)).matcher(fields); while (matcher.find()) { String[] argnames = matcher.group(2).split(","); buildFunction(argnames, matcher.group(3)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/voip/CallSwipeView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/voip/CallSwipeView.java index 09a7f4ab8..36db6dac3 100755 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/voip/CallSwipeView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/voip/CallSwipeView.java @@ -17,7 +17,6 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; -import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -37,23 +36,14 @@ public class CallSwipeView extends View { private Listener listener; private Path arrow = new Path(); private AnimatorSet arrowAnim; - private boolean animatingArrows =false; + private boolean animatingArrows = false; + private boolean canceled = false; public CallSwipeView(Context context) { super(context); init(); } - public CallSwipeView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - public CallSwipeView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - private void init() { arrowsPaint = new Paint(Paint.ANTI_ALIAS_FLAG); arrowsPaint.setColor(0xFFFFFFFF); @@ -73,18 +63,19 @@ public class CallSwipeView extends View { arrowAnim = new AnimatorSet(); arrowAnim.playTogether(anims); arrowAnim.addListener(new AnimatorListenerAdapter() { - private boolean canceled = false; private long startTime; - private Runnable restarter=new Runnable(){ + private Runnable restarter = new Runnable() { @Override - public void run(){ - arrowAnim.start(); + public void run() { + if (arrowAnim != null) { + arrowAnim.start(); + } } }; @Override public void onAnimationEnd(Animator animation) { - if(System.currentTimeMillis()-startTime getWidth() - getDraggedViewWidth())) { @@ -138,8 +139,8 @@ public class CallSwipeView extends View { } else if (ev.getAction() == MotionEvent.ACTION_MOVE) { viewToDrag.setTranslationX(Math.max(dragFromRight ? -(getWidth() - getDraggedViewWidth()) : 0, Math.min(ev.getX() - dragStartX, dragFromRight ? 0 : (getWidth() - getDraggedViewWidth())))); invalidate(); - } else if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction()==MotionEvent.ACTION_CANCEL) { - if (Math.abs(viewToDrag.getTranslationX()) >= getWidth() - getDraggedViewWidth() && ev.getAction()==MotionEvent.ACTION_UP) { + } else if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) { + if (Math.abs(viewToDrag.getTranslationX()) >= getWidth() - getDraggedViewWidth() && ev.getAction() == MotionEvent.ACTION_UP) { listener.onDragComplete(); } else { listener.onDragCancel(); @@ -157,13 +158,18 @@ public class CallSwipeView extends View { } public void startAnimatingArrows() { - if(animatingArrows) + if (animatingArrows || arrowAnim == null) return; animatingArrows = true; - arrowAnim.start(); + if (arrowAnim != null) { + arrowAnim.start(); + } } public void reset() { + if (arrowAnim == null || canceled) { + return; + } listener.onDragCancel(); viewToDrag.animate().translationX(0).setDuration(200).start(); invalidate(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/voip/VoIPHelper.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/voip/VoIPHelper.java index 662529ffd..521a74123 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/voip/VoIPHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/voip/VoIPHelper.java @@ -3,27 +3,56 @@ package org.telegram.ui.Components.voip; import android.Manifest; import android.annotation.TargetApi; import android.app.Activity; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.provider.Settings; +import android.text.InputType; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.ContactsController; +import org.telegram.messenger.FileLoader; import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MediaController; +import org.telegram.messenger.MessagesController; import org.telegram.messenger.R; +import org.telegram.messenger.SendMessagesHelper; import org.telegram.messenger.voip.VoIPService; import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.RequestDelegate; +import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.AlertDialog; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Cells.CheckBoxCell; +import org.telegram.ui.Components.BetterRatingView; +import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.VoIPActivity; +import java.io.File; +import java.util.Collections; +import java.util.Set; + public class VoIPHelper{ private static long lastCallRequestTime=0; + private static final int VOIP_SUPPORT_ID=4244000; + public static void startCall(TLRPC.User user, final Activity activity, TLRPC.TL_userFull userFull){ if(userFull!=null && userFull.phone_calls_private){ new AlertDialog.Builder(activity) @@ -138,4 +167,162 @@ public class VoIPHelper{ }); } } + + public static File getLogsDir(){ + //File logsDir=new File(ApplicationLoader.applicationContext.getExternalCacheDir(), "voip_logs"); + File logsDir=new File(ApplicationLoader.applicationContext.getCacheDir(), "voip_logs"); + if(!logsDir.exists()) + logsDir.mkdirs(); + return logsDir; + } + + public static boolean canRateCall(TLRPC.TL_messageActionPhoneCall call){ + if(!(call.reason instanceof TLRPC.TL_phoneCallDiscardReasonBusy) && !(call.reason instanceof TLRPC.TL_phoneCallDiscardReasonMissed)){ + SharedPreferences prefs=ApplicationLoader.applicationContext.getSharedPreferences("notifications", Context.MODE_PRIVATE); + Set hashes=prefs.getStringSet("calls_access_hashes", (Set)Collections.EMPTY_SET); + for(String hash:hashes){ + String[] d=hash.split(" "); + if(d.length<2) + continue; + if(d[0].equals(call.call_id+"")){ + return true; + } + } + } + return false; + } + + public static void showRateAlert(Context context, TLRPC.TL_messageActionPhoneCall call){ + SharedPreferences prefs=context.getSharedPreferences("notifications", Context.MODE_PRIVATE); + Set hashes=prefs.getStringSet("calls_access_hashes", (Set)Collections.EMPTY_SET); + for(String hash:hashes){ + String[] d=hash.split(" "); + if(d.length<2) + continue; + if(d[0].equals(call.call_id+"")){ + try{ + long accessHash=Long.parseLong(d[1]); + showRateAlert(context, null, call.call_id, accessHash); + }catch(Exception x){} + return; + } + } + } + + public static void showRateAlert(final Context context, final Runnable onDismiss, final long callID, final long accessHash){ + final File log=new File(getLogsDir(), callID+".log"); + LinearLayout alertView=new LinearLayout(context); + alertView.setOrientation(LinearLayout.VERTICAL); + + int pad = AndroidUtilities.dp(16); + alertView.setPadding(pad, pad, pad, 0); + + TextView text = new TextView(context); + text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + text.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); + text.setGravity(Gravity.CENTER); + text.setText(LocaleController.getString("VoipRateCallAlert", R.string.VoipRateCallAlert)); + alertView.addView(text); + + final BetterRatingView bar = new BetterRatingView(context); + alertView.addView(bar, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL, 0, 16, 0, 0)); + + final EditText commentBox = new EditText(context); + commentBox.setHint(LocaleController.getString("CallReportHint", R.string.CallReportHint)); + commentBox.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + commentBox.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); + commentBox.setHintTextColor(Theme.getColor(Theme.key_dialogTextHint)); + commentBox.setBackgroundDrawable(Theme.createEditTextDrawable(context, true)); + commentBox.setPadding(0, AndroidUtilities.dp(4), 0, AndroidUtilities.dp(4)); + commentBox.setTextSize(18); + commentBox.setVisibility(View.GONE); + alertView.addView(commentBox, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 8, 8, 8, 0)); + + final boolean[] includeLogs={true}; + final CheckBoxCell checkbox=new CheckBoxCell(context, true); + View.OnClickListener checkClickListener=new View.OnClickListener(){ + @Override + public void onClick(View v){ + includeLogs[0]=!includeLogs[0]; + checkbox.setChecked(includeLogs[0], true); + } + }; + checkbox.setText(LocaleController.getString("CallReportIncludeLogs", R.string.CallReportIncludeLogs), null, true, false); + checkbox.setClipToPadding(false); + checkbox.setOnClickListener(checkClickListener); + alertView.addView(checkbox, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, -8, 0, -8, 0)); + + final TextView logsText = new TextView(context); + logsText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + logsText.setTextColor(Theme.getColor(Theme.key_dialogTextGray3)); + logsText.setText(LocaleController.getString("CallReportLogsExplain", R.string.CallReportLogsExplain)); + logsText.setPadding(AndroidUtilities.dp(8), 0, AndroidUtilities.dp(8), 0); + logsText.setOnClickListener(checkClickListener); + alertView.addView(logsText); + + checkbox.setVisibility(View.GONE); + logsText.setVisibility(View.GONE); + if(!log.exists()){ + includeLogs[0]=false; + } + + AlertDialog alert=new AlertDialog.Builder(context) + .setTitle(LocaleController.getString("CallMessageReportProblem", R.string.CallMessageReportProblem)) + .setView(alertView) + .setPositiveButton(LocaleController.getString("Send", R.string.Send), new DialogInterface.OnClickListener(){ + @Override + public void onClick(DialogInterface dialog, int which){ + TLRPC.TL_phone_setCallRating req = new TLRPC.TL_phone_setCallRating(); + req.rating = bar.getRating(); + if (req.rating < 5) + req.comment = commentBox.getText().toString(); + else + req.comment=""; + req.peer = new TLRPC.TL_inputPhoneCall(); + req.peer.access_hash = accessHash; + req.peer.id = callID; + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (response instanceof TLRPC.TL_updates) { + TLRPC.TL_updates updates = (TLRPC.TL_updates) response; + MessagesController.getInstance().processUpdates(updates, false); + if(includeLogs[0] && log.exists()){ + SendMessagesHelper.prepareSendingDocument(log.getAbsolutePath(), log.getAbsolutePath(), null, "text/plain", VOIP_SUPPORT_ID, null, null); + Toast.makeText(context, LocaleController.getString("CallReportSent", R.string.CallReportSent), Toast.LENGTH_LONG).show(); + } + } + } + }); + //SendMessagesHelper.getInstance().sendMessage(commentBox.getText().toString(), VOIP_SUPPORT_ID, null, null, true, null, null, null); + } + }) + .setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null) + .setOnDismissListener(new DialogInterface.OnDismissListener(){ + @Override + public void onDismiss(DialogInterface dialog){ + if(onDismiss!=null) + onDismiss.run(); + } + }) + .show(); + + final View btn = alert.getButton(DialogInterface.BUTTON_POSITIVE); + btn.setEnabled(false); + bar.setOnRatingChangeListener(new BetterRatingView.OnRatingChangeListener() { + @Override + public void onRatingChanged(int rating) { + btn.setEnabled(rating > 0); + commentBox.setHint(rating<4 ? LocaleController.getString("CallReportHint", R.string.CallReportHint) : LocaleController.getString("VoipFeedbackCommentHint", R.string.VoipFeedbackCommentHint)); + commentBox.setVisibility(rating < 5 && rating > 0 ? View.VISIBLE : View.GONE); + if (commentBox.getVisibility() == View.GONE) { + ((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(commentBox.getWindowToken(), 0); + } + if(log.exists()){ + checkbox.setVisibility(rating<4 ? View.VISIBLE : View.GONE); + logsText.setVisibility(rating<4 ? View.VISIBLE : View.GONE); + } + } + }); + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java index 315946aeb..4d803af38 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java @@ -76,6 +76,7 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter private EmptyTextProgressView emptyView; private RecyclerListView listView; private SearchAdapter searchListViewAdapter; + private ActionBarMenuItem addItem; private boolean searchWas; private boolean searching; @@ -87,6 +88,7 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter private boolean creatingChat; private boolean allowBots = true; private boolean needForwardCount = true; + private boolean needFinishFragment = true; private boolean addingToChannel; private int chat_id; private String selectAlertString = null; @@ -102,7 +104,7 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter private final static int add_button = 1; public interface ContactsActivityDelegate { - void didSelectContact(TLRPC.User user, String param); + void didSelectContact(TLRPC.User user, String param, ContactsActivity activity); } public ContactsActivity(Bundle args) { @@ -127,6 +129,7 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter needForwardCount = arguments.getBoolean("needForwardCount", true); allowBots = arguments.getBoolean("allowBots", true); addingToChannel = arguments.getBoolean("addingToChannel", false); + needFinishFragment = arguments.getBoolean("needFinishFragment", true); chat_id = arguments.getInt("chat_id", 0); } else { needPhonebook = true; @@ -185,10 +188,16 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter @Override public void onSearchExpand() { searching = true; + if (addItem != null) { + addItem.setVisibility(View.GONE); + } } @Override public void onSearchCollapse() { + if (addItem != null) { + addItem.setVisibility(View.VISIBLE); + } searchListViewAdapter.searchDialogs(null); searching = false; searchWas = false; @@ -222,10 +231,10 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter }); item.getSearchField().setHint(LocaleController.getString("Search", R.string.Search)); if (!createSecretChat && !returnAsResult) { - menu.addItem(add_button, R.drawable.add); + addItem = menu.addItem(add_button, R.drawable.add); } - searchListViewAdapter = new SearchAdapter(context, ignoreUsers, allowUsernameSearch, false, false, allowBots); + searchListViewAdapter = new SearchAdapter(context, ignoreUsers, allowUsernameSearch, false, false, allowBots, 0); listViewAdapter = new ContactsAdapter(context, onlyUsers ? 1 : 0, needPhonebook, ignoreUsers, chat_id != 0); fragmentView = new FrameLayout(context); @@ -487,10 +496,12 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter } } else { if (delegate != null) { - delegate.didSelectContact(user, param); + delegate.didSelectContact(user, param, this); delegate = null; } - finishFragment(); + if (needFinishFragment) { + finishFragment(); + } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DataSettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DataSettingsActivity.java index f0c62a363..bc62441a0 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/DataSettingsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/DataSettingsActivity.java @@ -62,6 +62,9 @@ public class DataSettingsActivity extends BaseFragment { private int callsSectionRow; private int useLessDataForCallsRow; private int callsSection2Row; + private int proxySectionRow; + private int proxyRow; + private int proxySection2Row; private int rowCount; @Override @@ -79,16 +82,19 @@ public class DataSettingsActivity extends BaseFragment { mobileUsageRow = rowCount++; wifiUsageRow = rowCount++; roamingUsageRow = rowCount++; + usageSection2Row = rowCount++; if (MessagesController.getInstance().callsEnabled) { - usageSection2Row = rowCount++; callsSectionRow = rowCount++; useLessDataForCallsRow = rowCount++; + callsSection2Row = rowCount++; } else { - usageSection2Row = -1; + callsSection2Row = -1; callsSectionRow = -1; useLessDataForCallsRow = -1; } - callsSection2Row = rowCount++; + proxySectionRow = rowCount++; + proxyRow = rowCount++; + proxySection2Row = rowCount++; return true; } @@ -100,6 +106,7 @@ public class DataSettingsActivity extends BaseFragment { if (AndroidUtilities.isTablet()) { actionBar.setOccupyStatusBar(false); } + actionBar.setAllowOverlayTitle(true); actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { @Override public void onItemClick(int id) { @@ -127,7 +134,7 @@ public class DataSettingsActivity extends BaseFragment { if (getParentActivity() == null) { return; } - final boolean maskValues[] = new boolean[6]; + final boolean maskValues[] = new boolean[7]; BottomSheet.Builder builder = new BottomSheet.Builder(getParentActivity()); int mask = 0; @@ -143,24 +150,27 @@ public class DataSettingsActivity extends BaseFragment { builder.setApplyBottomPadding(false); LinearLayout linearLayout = new LinearLayout(getParentActivity()); linearLayout.setOrientation(LinearLayout.VERTICAL); - for (int a = 0; a < 6; a++) { + for (int a = 0; a < 7; a++) { String name = null; if (a == 0) { maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_PHOTO) != 0; name = LocaleController.getString("LocalPhotoCache", R.string.LocalPhotoCache); } else if (a == 1) { maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_AUDIO) != 0; - name = LocaleController.getString("LocalAudioCache", R.string.LocalAudioCache); + name = LocaleController.getString("AudioAutodownload", R.string.AudioAutodownload); } else if (a == 2) { + maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_VIDEOMESSAGE) != 0; + name = LocaleController.getString("VideoMessagesAutodownload", R.string.VideoMessagesAutodownload); + } else if (a == 3) { maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_VIDEO) != 0; name = LocaleController.getString("LocalVideoCache", R.string.LocalVideoCache); - } else if (a == 3) { + } else if (a == 4) { maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_DOCUMENT) != 0; name = LocaleController.getString("FilesDataUsage", R.string.FilesDataUsage); - } else if (a == 4) { + } else if (a == 5) { maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_MUSIC) != 0; name = LocaleController.getString("AttachMusic", R.string.AttachMusic); - } else if (a == 5) { + } else if (a == 6) { maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_GIF) != 0; name = LocaleController.getString("LocalGifCache", R.string.LocalGifCache); } @@ -195,19 +205,21 @@ public class DataSettingsActivity extends BaseFragment { FileLog.e(e); } int newMask = 0; - for (int a = 0; a < 6; a++) { + for (int a = 0; a < 7; a++) { if (maskValues[a]) { if (a == 0) { newMask |= MediaController.AUTODOWNLOAD_MASK_PHOTO; } else if (a == 1) { newMask |= MediaController.AUTODOWNLOAD_MASK_AUDIO; } else if (a == 2) { - newMask |= MediaController.AUTODOWNLOAD_MASK_VIDEO; + newMask |= MediaController.AUTODOWNLOAD_MASK_VIDEOMESSAGE; } else if (a == 3) { - newMask |= MediaController.AUTODOWNLOAD_MASK_DOCUMENT; + newMask |= MediaController.AUTODOWNLOAD_MASK_VIDEO; } else if (a == 4) { - newMask |= MediaController.AUTODOWNLOAD_MASK_MUSIC; + newMask |= MediaController.AUTODOWNLOAD_MASK_DOCUMENT; } else if (a == 5) { + newMask |= MediaController.AUTODOWNLOAD_MASK_MUSIC; + } else if (a == 6) { newMask |= MediaController.AUTODOWNLOAD_MASK_GIF; } } @@ -271,6 +283,8 @@ public class DataSettingsActivity extends BaseFragment { presentFragment(new DataUsageActivity(2)); } else if (position == wifiUsageRow) { presentFragment(new DataUsageActivity(1)); + } else if (position == proxyRow) { + presentFragment(new ProxySettingsActivity()); } } }); @@ -310,7 +324,7 @@ public class DataSettingsActivity extends BaseFragment { public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (holder.getItemViewType()) { case 0: { - if (position == callsSection2Row || position == usageSection2Row && usageSection2Row == -1) { + if (position == proxySection2Row) { 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)); @@ -342,6 +356,8 @@ public class DataSettingsActivity extends BaseFragment { textCell.setText(LocaleController.getString("RoamingUsage", R.string.RoamingUsage), false); } else if (position == wifiUsageRow) { textCell.setText(LocaleController.getString("WiFiUsage", R.string.WiFiUsage), true); + } else if (position == proxyRow) { + textCell.setText(LocaleController.getString("ProxySettings", R.string.ProxySettings), true); } break; } @@ -353,6 +369,8 @@ public class DataSettingsActivity extends BaseFragment { headerCell.setText(LocaleController.getString("DataUsage", R.string.DataUsage)); } else if (position == callsSectionRow) { headerCell.setText(LocaleController.getString("Calls", R.string.Calls)); + } else if (position == proxySectionRow) { + headerCell.setText(LocaleController.getString("Proxy", R.string.Proxy)); } break; } @@ -381,7 +399,13 @@ public class DataSettingsActivity extends BaseFragment { if (text.length() != 0) { text += ", "; } - text += LocaleController.getString("LocalAudioCache", R.string.LocalAudioCache); + text += LocaleController.getString("AudioAutodownload", R.string.AudioAutodownload); + } + if ((mask & MediaController.AUTODOWNLOAD_MASK_VIDEOMESSAGE) != 0) { + if (text.length() != 0) { + text += ", "; + } + text += LocaleController.getString("VideoMessagesAutodownload", R.string.VideoMessagesAutodownload); } if ((mask & MediaController.AUTODOWNLOAD_MASK_VIDEO) != 0) { if (text.length() != 0) { @@ -421,7 +445,7 @@ public class DataSettingsActivity extends BaseFragment { public boolean isEnabled(RecyclerView.ViewHolder holder) { int position = holder.getAdapterPosition(); return position == wifiDownloadRow || position == mobileDownloadRow || position == roamingDownloadRow || position == storageUsageRow || - position == useLessDataForCallsRow || position == mobileUsageRow || position == roamingUsageRow || position == wifiUsageRow; + position == useLessDataForCallsRow || position == mobileUsageRow || position == roamingUsageRow || position == wifiUsageRow || position == proxyRow; } @Override @@ -450,13 +474,13 @@ public class DataSettingsActivity extends BaseFragment { @Override public int getItemViewType(int position) { - if (position == mediaDownloadSection2Row || position == usageSection2Row || position == callsSection2Row) { + if (position == mediaDownloadSection2Row || position == usageSection2Row || position == callsSection2Row || position == proxySection2Row) { return 0; - } else if (position == storageUsageRow || position == useLessDataForCallsRow || position == roamingUsageRow || position == wifiUsageRow || position == mobileUsageRow) { + } else if (position == storageUsageRow || position == useLessDataForCallsRow || position == roamingUsageRow || position == wifiUsageRow || position == mobileUsageRow || position == proxyRow) { return 1; } else if (position == wifiDownloadRow || position == mobileDownloadRow || position == roamingDownloadRow) { return 3; - } else if (position == mediaDownloadSectionRow || position == callsSectionRow || position == usageSectionRow) { + } else if (position == mediaDownloadSectionRow || position == callsSectionRow || position == usageSectionRow || position == proxySectionRow) { return 2; } else { return 1; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java index 927d05c23..83238bf30 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java @@ -25,7 +25,6 @@ import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; -import android.support.annotation.Px; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; @@ -1373,6 +1372,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. new ThemeDescription(fragmentContextView, 0, new Class[]{FragmentContextView.class}, new String[]{"titleTextView"}, null, null, null, Theme.key_returnToCallText), new ThemeDescription(null, 0, null, null, null, null, Theme.key_dialogBackground), + new ThemeDescription(null, 0, null, null, null, null, Theme.key_dialogBackgroundGray), new ThemeDescription(null, 0, null, null, null, null, Theme.key_dialogTextBlack), new ThemeDescription(null, 0, null, null, null, null, Theme.key_dialogTextLink), new ThemeDescription(null, 0, null, null, null, null, Theme.key_dialogLinkSelection), diff --git a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java index eedf00a39..8a5e126c8 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java @@ -89,7 +89,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen private int chatId; - private int maxCount = 5000; + private int maxCount = MessagesController.getInstance().maxMegagroupCount; private int chatType = ChatObject.CHAT_TYPE_CHAT; private boolean isAlwaysShare; private boolean isNeverShare; @@ -942,7 +942,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen @Override public void onViewRecycled(RecyclerView.ViewHolder holder) { - if (holder.getItemViewType() == 1) { + if (holder.itemView instanceof GroupCreateUserCell) { ((GroupCreateUserCell) holder.itemView).recycle(); } } @@ -963,7 +963,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen if (query == null) { searchResult.clear(); searchResultNames.clear(); - searchAdapterHelper.queryServerSearch(null, false, false, false); + searchAdapterHelper.queryServerSearch(null, true, false, false, false, 0, false); notifyDataSetChanged(); } else { searchTimer = new Timer(); @@ -980,7 +980,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - searchAdapterHelper.queryServerSearch(query, false, false, false); + searchAdapterHelper.queryServerSearch(query, true, false, false, false, 0, false); Utilities.searchQueue.postRunnable(new Runnable() { @Override public void run() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java index ca47031f3..03f5e4359 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java @@ -11,9 +11,14 @@ package org.telegram.ui; import android.animation.ObjectAnimator; import android.animation.StateListAnimator; import android.app.Activity; +import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.os.Build; @@ -21,33 +26,102 @@ import android.os.Bundle; import android.os.Parcelable; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; +import android.util.TypedValue; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.ScrollView; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.BuildVars; import org.telegram.messenger.LocaleController; +import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; import org.telegram.tgnet.ConnectionsManager; -import org.telegram.ui.ActionBar.Theme; +import org.telegram.tgnet.RequestDelegate; +import org.telegram.tgnet.TLObject; +import org.telegram.tgnet.TLRPC; +import org.telegram.ui.Components.LayoutHelper; -public class IntroActivity extends Activity { +public class IntroActivity extends Activity implements NotificationCenter.NotificationCenterDelegate { + + private class BottomPagesView extends View { + + private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private float progress; + private int scrollPosition; + private int currentPage; + private DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator(); + private RectF rect = new RectF(); + private float animatedProgress; + + public BottomPagesView(Context context) { + super(context); + } + + public void setPageOffset(int position, float offset) { + progress = offset; + scrollPosition = position; + invalidate(); + } + + public void setCurrentPage(int page) { + currentPage = page; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + float d = AndroidUtilities.dp(5); + paint.setColor(0xffbbbbbb); + int x; + currentPage = viewPager.getCurrentItem(); + for (int a = 0; a < 7; a++) { + if (a == currentPage) { + continue; + } + x = a * AndroidUtilities.dp(11); + rect.set(x, 0, x + AndroidUtilities.dp(5), AndroidUtilities.dp(5)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(2.5f), AndroidUtilities.dp(2.5f), paint); + } + paint.setColor(0xff2ca5e0); + x = currentPage * AndroidUtilities.dp(11); + if (progress != 0) { + if (scrollPosition >= currentPage) { + rect.set(x, 0, x + AndroidUtilities.dp(5) + AndroidUtilities.dp(11) * progress, AndroidUtilities.dp(5)); + } else { + rect.set(x - AndroidUtilities.dp(11) * (1.0f - progress), 0, x + AndroidUtilities.dp(5), AndroidUtilities.dp(5)); + } + } else { + rect.set(x, 0, x + AndroidUtilities.dp(5), AndroidUtilities.dp(5)); + } + canvas.drawRoundRect(rect, AndroidUtilities.dp(2.5f), AndroidUtilities.dp(2.5f), paint); + } + } private ViewPager viewPager; private ImageView topImage1; private ImageView topImage2; - private ViewGroup bottomPages; + private BottomPagesView bottomPages; + private TextView textView; private int lastPage = 0; private boolean justCreated = false; private boolean startPressed = false; private int[] icons; private int[] titles; + private String[] titlesString; private int[] messages; + private String[] messagesString; + + private LocaleController.LocaleInfo localeInfo; @Override protected void onCreate(Bundle savedInstanceState) { @@ -55,17 +129,6 @@ public class IntroActivity extends Activity { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); - if (AndroidUtilities.isTablet()) { - setContentView(R.layout.intro_layout_tablet); - View imageView = findViewById(R.id.background_image_intro); - BitmapDrawable drawable = (BitmapDrawable) getResources().getDrawable(R.drawable.catstile); - drawable.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); - imageView.setBackgroundDrawable(drawable); - } else { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - setContentView(R.layout.intro_layout); - } - if (LocaleController.isRTL) { icons = new int[]{ R.drawable.intro7, @@ -85,6 +148,15 @@ public class IntroActivity extends Activity { R.string.Page2Title, R.string.Page1Title }; + titlesString = new String[]{ + "Page7Title", + "Page6Title", + "Page5Title", + "Page4Title", + "Page3Title", + "Page2Title", + "Page1Title" + }; messages = new int[]{ R.string.Page7Message, R.string.Page6Message, @@ -94,6 +166,15 @@ public class IntroActivity extends Activity { R.string.Page2Message, R.string.Page1Message }; + messagesString = new String[]{ + "Page7Message", + "Page6Message", + "Page5Message", + "Page4Message", + "Page3Message", + "Page2Message", + "Page1Message" + }; } else { icons = new int[]{ R.drawable.intro1, @@ -113,6 +194,15 @@ public class IntroActivity extends Activity { R.string.Page6Title, R.string.Page7Title }; + titlesString = new String[]{ + "Page1Title", + "Page2Title", + "Page3Title", + "Page4Title", + "Page5Title", + "Page6Title", + "Page7Title" + }; messages = new int[]{ R.string.Page1Message, R.string.Page2Message, @@ -122,27 +212,44 @@ public class IntroActivity extends Activity { R.string.Page6Message, R.string.Page7Message }; + messagesString = new String[]{ + "Page1Message", + "Page2Message", + "Page3Message", + "Page4Message", + "Page5Message", + "Page6Message", + "Page7Message" + }; } - viewPager = (ViewPager) findViewById(R.id.intro_view_pager); - TextView startMessagingButton = (TextView) findViewById(R.id.start_messaging_button); - startMessagingButton.setText(LocaleController.getString("StartMessaging", R.string.StartMessaging).toUpperCase()); - if (Build.VERSION.SDK_INT >= 21) { - StateListAnimator animator = new StateListAnimator(); - animator.addState(new int[]{android.R.attr.state_pressed}, ObjectAnimator.ofFloat(startMessagingButton, "translationZ", AndroidUtilities.dp(2), AndroidUtilities.dp(4)).setDuration(200)); - animator.addState(new int[]{}, ObjectAnimator.ofFloat(startMessagingButton, "translationZ", AndroidUtilities.dp(4), AndroidUtilities.dp(2)).setDuration(200)); - startMessagingButton.setStateListAnimator(animator); - } - topImage1 = (ImageView) findViewById(R.id.icon_image1); - topImage2 = (ImageView) findViewById(R.id.icon_image2); - bottomPages = (ViewGroup) findViewById(R.id.bottom_pages); + + ScrollView scrollView = new ScrollView(this); + scrollView.setFillViewport(true); + + FrameLayout frameLayout = new FrameLayout(this); + frameLayout.setBackgroundColor(0xfffafafa); + scrollView.addView(frameLayout, LayoutHelper.createScroll(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP)); + + FrameLayout frameLayout2 = new FrameLayout(this); + frameLayout.addView(frameLayout2, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 0, 88, 0, 0)); + + topImage1 = new ImageView(this); + topImage1.setImageResource(R.drawable.intro1); + frameLayout2.addView(topImage1, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER)); + + topImage2 = new ImageView(this); topImage2.setVisibility(View.GONE); + frameLayout2.addView(topImage2, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER)); + + viewPager = new ViewPager(this); viewPager.setAdapter(new IntroAdapter()); viewPager.setPageMargin(0); viewPager.setOffscreenPageLimit(1); + frameLayout.addView(viewPager, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - + bottomPages.setPageOffset(position, positionOffset); } @Override @@ -214,6 +321,20 @@ public class IntroActivity extends Activity { } }); + TextView startMessagingButton = new TextView(this); + startMessagingButton.setText(LocaleController.getString("StartMessaging", R.string.StartMessaging).toUpperCase()); + startMessagingButton.setPadding(AndroidUtilities.dp(20), AndroidUtilities.dp(10), AndroidUtilities.dp(20), AndroidUtilities.dp(10)); + startMessagingButton.setGravity(Gravity.CENTER); + startMessagingButton.setTextColor(0xffffffff); + startMessagingButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + startMessagingButton.setBackgroundResource(R.drawable.regbtn_states); + if (Build.VERSION.SDK_INT >= 21) { + StateListAnimator animator = new StateListAnimator(); + animator.addState(new int[]{android.R.attr.state_pressed}, ObjectAnimator.ofFloat(startMessagingButton, "translationZ", AndroidUtilities.dp(2), AndroidUtilities.dp(4)).setDuration(200)); + animator.addState(new int[]{}, ObjectAnimator.ofFloat(startMessagingButton, "translationZ", AndroidUtilities.dp(4), AndroidUtilities.dp(2)).setDuration(200)); + startMessagingButton.setStateListAnimator(animator); + } + frameLayout.addView(startMessagingButton, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 10, 0, 10, 76)); startMessagingButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -237,7 +358,53 @@ public class IntroActivity extends Activity { }); } + bottomPages = new BottomPagesView(this); + frameLayout.addView(bottomPages, LayoutHelper.createFrame(77, 5, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 350, 0, 0)); + + textView = new TextView(this); + textView.setTextColor(0xff1393d2); + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + frameLayout.addView(textView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 30, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0, 0, 20)); + textView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (startPressed || localeInfo == null) { + return; + } + LocaleController.getInstance().applyLanguage(localeInfo, true); + startPressed = true; + Intent intent2 = new Intent(IntroActivity.this, LaunchActivity.class); + intent2.putExtra("fromIntro", true); + startActivity(intent2); + finish(); + } + }); + + if (AndroidUtilities.isTablet()) { + FrameLayout frameLayout3 = new FrameLayout(this); + setContentView(frameLayout3); + + View imageView = new ImageView(this); + BitmapDrawable drawable = (BitmapDrawable) getResources().getDrawable(R.drawable.catstile); + drawable.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); + imageView.setBackgroundDrawable(drawable); + frameLayout3.addView(imageView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + FrameLayout frameLayout4 = new FrameLayout(this); + frameLayout4.setBackgroundResource(R.drawable.btnshadow); + frameLayout4.addView(scrollView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + frameLayout3.addView(frameLayout4, LayoutHelper.createFrame(498, 528, Gravity.CENTER)); + } else { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + setContentView(scrollView); + } + + checkContinueText(); justCreated = true; + NotificationCenter.getInstance().addObserver(this, NotificationCenter.suggestedLangpack); + + AndroidUtilities.handleProxyIntent(this, getIntent()); } @Override @@ -255,12 +422,81 @@ public class IntroActivity extends Activity { } AndroidUtilities.checkForCrashes(this); AndroidUtilities.checkForUpdates(this); + ConnectionsManager.getInstance().setAppPaused(false, false); } @Override protected void onPause() { super.onPause(); AndroidUtilities.unregisterUpdates(); + ConnectionsManager.getInstance().setAppPaused(true, false); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.suggestedLangpack); + } + + private void checkContinueText() { + LocaleController.LocaleInfo englishInfo = null; + LocaleController.LocaleInfo systemInfo = null; + LocaleController.LocaleInfo currentLocaleInfo = LocaleController.getInstance().getCurrentLocaleInfo(); + String systemLang = LocaleController.getSystemLocaleStringIso639().toLowerCase(); + String arg = systemLang.contains("-") ? systemLang.split("-")[0] : systemLang; + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + preferences.edit().putString("language_showed2", LocaleController.getSystemLocaleStringIso639().toLowerCase()).commit(); + for (int a = 0; a < LocaleController.getInstance().languages.size(); a++) { + LocaleController.LocaleInfo info = LocaleController.getInstance().languages.get(a); + if (info.shortName.equals("en")) { + englishInfo = info; + } + if (info.shortName.replace("_", "-").equals(systemLang) || info.shortName.equals(arg)) { + systemInfo = info; + } + if (englishInfo != null && systemInfo != null) { + break; + } + } + if (englishInfo == null || systemInfo == null || englishInfo == systemInfo) { + return; + } + TLRPC.TL_langpack_getStrings req = new TLRPC.TL_langpack_getStrings(); + if (systemInfo != currentLocaleInfo) { + req.lang_code = systemInfo.shortName.replace("_", "-"); + localeInfo = systemInfo; + } else { + req.lang_code = englishInfo.shortName.replace("_", "-"); + localeInfo = englishInfo; + } + req.keys.add("ContinueOnThisLanguage"); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (response != null) { + TLRPC.Vector vector = (TLRPC.Vector) response; + if (vector.objects.isEmpty()) { + return; + } + final TLRPC.LangPackString string = (TLRPC.LangPackString) vector.objects.get(0); + if (string instanceof TLRPC.TL_langPackString) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + textView.setText(string.value); + } + }); + } + } + } + }, ConnectionsManager.RequestFlagWithoutLogin); + } + + @Override + public void didReceivedNotification(int id, Object... args) { + if (id == NotificationCenter.suggestedLangpack) { + checkContinueText(); + } } private class IntroAdapter extends PagerAdapter { @@ -271,15 +507,26 @@ public class IntroActivity extends Activity { @Override public Object instantiateItem(ViewGroup container, int position) { - View view = View.inflate(container.getContext(), R.layout.intro_view_layout, null); - TextView headerTextView = (TextView) view.findViewById(R.id.header_text); - TextView messageTextView = (TextView) view.findViewById(R.id.message_text); - container.addView(view, 0); + FrameLayout frameLayout = new FrameLayout(container.getContext()); - headerTextView.setText(getString(titles[position])); - messageTextView.setText(AndroidUtilities.replaceTags(getString(messages[position]))); + TextView headerTextView = new TextView(container.getContext()); + headerTextView.setTextColor(0xff212121); + headerTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 26); + headerTextView.setGravity(Gravity.CENTER); + frameLayout.addView(headerTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 18, 244, 18, 0)); - return view; + TextView messageTextView = new TextView(container.getContext()); + messageTextView.setTextColor(0xff808080); + messageTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + messageTextView.setGravity(Gravity.CENTER); + frameLayout.addView(messageTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 16, 286, 16, 0)); + + container.addView(frameLayout, 0); + + headerTextView.setText(LocaleController.getString(titlesString[position], titles[position])); + messageTextView.setText(AndroidUtilities.replaceTags(LocaleController.getString(messagesString[position], messages[position]))); + + return frameLayout; } @Override @@ -290,15 +537,7 @@ public class IntroActivity extends Activity { @Override public void setPrimaryItem(ViewGroup container, int position, Object object) { super.setPrimaryItem(container, position, object); - int count = bottomPages.getChildCount(); - for (int a = 0; a < count; a++) { - View child = bottomPages.getChildAt(a); - if (a == position) { - child.setBackgroundColor(0xff2ca5e0); - } else { - child.setBackgroundColor(0xffbbbbbb); - } - } + bottomPages.setCurrentPage(position); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LanguageSelectActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LanguageSelectActivity.java index f02211aca..58faae282 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LanguageSelectActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LanguageSelectActivity.java @@ -25,7 +25,7 @@ import org.telegram.messenger.support.widget.RecyclerView; import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ActionBar.ThemeDescription; -import org.telegram.ui.Cells.TextSettingsCell; +import org.telegram.ui.Cells.LanguageCell; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; import org.telegram.ui.ActionBar.ActionBarMenuItem; @@ -35,6 +35,8 @@ import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.RecyclerListView; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.Timer; import java.util.TimerTask; @@ -49,7 +51,26 @@ public class LanguageSelectActivity extends BaseFragment { private boolean searching; private Timer searchTimer; - public ArrayList searchResult; + private ArrayList searchResult; + private ArrayList sortedLanguages; + + @Override + public boolean onFragmentCreate() { + sortedLanguages = new ArrayList<>(LocaleController.getInstance().languages); + final LocaleController.LocaleInfo currentLocale = LocaleController.getInstance().getCurrentLocaleInfo(); + Collections.sort(sortedLanguages, new Comparator() { + @Override + public int compare(LocaleController.LocaleInfo o, LocaleController.LocaleInfo o2) { + if (o == currentLocale) { + return -1; + } else if (o2 == currentLocale) { + return 1; + } + return o.name.compareTo(o2.name); + } + }); + return super.onFragmentCreate(); + } @Override public View createView(Context context) { @@ -129,13 +150,13 @@ public class LanguageSelectActivity extends BaseFragment { localeInfo = searchResult.get(position); } } else { - if (position >= 0 && position < LocaleController.getInstance().sortedLanguages.size()) { - localeInfo = LocaleController.getInstance().sortedLanguages.get(position); + if (position >= 0 && position < sortedLanguages.size()) { + localeInfo = sortedLanguages.get(position); } } if (localeInfo != null) { LocaleController.getInstance().applyLanguage(localeInfo, true); - parentLayout.rebuildAllFragmentViews(false); + parentLayout.rebuildAllFragmentViews(false, false); } finishFragment(); } @@ -150,11 +171,11 @@ public class LanguageSelectActivity extends BaseFragment { localeInfo = searchResult.get(position); } } else { - if (position >= 0 && position < LocaleController.getInstance().sortedLanguages.size()) { - localeInfo = LocaleController.getInstance().sortedLanguages.get(position); + if (position >= 0 && position < sortedLanguages.size()) { + localeInfo = sortedLanguages.get(position); } } - if (localeInfo == null || localeInfo.pathToFile == null || getParentActivity() == null) { + if (localeInfo == null || localeInfo.pathToFile == null || getParentActivity() == null || localeInfo.isRemote()) { return false; } final LocaleController.LocaleInfo finalLocaleInfo = localeInfo; @@ -243,7 +264,8 @@ public class LanguageSelectActivity extends BaseFragment { long time = System.currentTimeMillis(); ArrayList resultArray = new ArrayList<>(); - for (LocaleController.LocaleInfo c : LocaleController.getInstance().sortedLanguages) { + for (int a = 0; a < sortedLanguages.size(); a++) { + LocaleController.LocaleInfo c = sortedLanguages.get(a); if (c.name.toLowerCase().startsWith(query) || c.nameEnglish.toLowerCase().startsWith(query)) { resultArray.add(c); } @@ -287,31 +309,33 @@ public class LanguageSelectActivity extends BaseFragment { } return searchResult.size(); } else { - if (LocaleController.getInstance().sortedLanguages == null) { - return 0; - } - return LocaleController.getInstance().sortedLanguages.size(); + return sortedLanguages.size(); } } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new RecyclerListView.Holder(new TextSettingsCell(mContext)); + return new RecyclerListView.Holder(new LanguageCell(mContext, false)); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - TextSettingsCell textSettingsCell = (TextSettingsCell) holder.itemView; + LanguageCell textSettingsCell = (LanguageCell) holder.itemView; LocaleController.LocaleInfo localeInfo; boolean last; if (search) { localeInfo = searchResult.get(position); last = position == searchResult.size() - 1; } else { - localeInfo = LocaleController.getInstance().sortedLanguages.get(position); - last = position == LocaleController.getInstance().sortedLanguages.size() - 1; + localeInfo = sortedLanguages.get(position); + last = position == sortedLanguages.size() - 1; } - textSettingsCell.setText(localeInfo.name, !last); + if (localeInfo.isLocal()) { + textSettingsCell.setLanguage(localeInfo, String.format("%1$s (%2$s)", localeInfo.name, LocaleController.getString("LanguageCustom", R.string.LanguageCustom)), !last); + } else { + textSettingsCell.setLanguage(localeInfo, null, !last); + } + textSettingsCell.setLanguageSelected(localeInfo == LocaleController.getInstance().getCurrentLocaleInfo()); } @Override @@ -339,7 +363,9 @@ public class LanguageSelectActivity extends BaseFragment { new ThemeDescription(listView, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider), - new ThemeDescription(listView, 0, new Class[]{TextSettingsCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(listView, 0, new Class[]{LanguageCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText), + new ThemeDescription(listView, 0, new Class[]{LanguageCell.class}, new String[]{"textView2"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText3), + new ThemeDescription(listView, 0, new Class[]{LanguageCell.class}, new String[]{"checkImage"}, null, null, null, Theme.key_featuredStickers_addedIcon), }; } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java index 0b3533d50..c9ea7f5e9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java @@ -37,6 +37,7 @@ import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; +import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.Toast; @@ -46,6 +47,7 @@ import org.telegram.messenger.BuildVars; import org.telegram.messenger.ChatObject; import org.telegram.messenger.ContactsController; import org.telegram.messenger.ImageLoader; +import org.telegram.messenger.MediaController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.MessagesController; import org.telegram.messenger.MessagesStorage; @@ -59,6 +61,7 @@ import org.telegram.messenger.LocaleController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; import org.telegram.messenger.browser.Browser; +import org.telegram.messenger.camera.CameraController; import org.telegram.messenger.query.DraftQuery; import org.telegram.messenger.support.widget.LinearLayoutManager; import org.telegram.tgnet.ConnectionsManager; @@ -71,10 +74,12 @@ import org.telegram.ui.Adapters.DrawerLayoutAdapter; import org.telegram.ui.ActionBar.ActionBarLayout; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.DrawerLayoutContainer; +import org.telegram.ui.Cells.LanguageCell; import org.telegram.ui.Components.EmbedBottomSheet; import org.telegram.ui.Components.JoinGroupAlert; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.PasscodeView; +import org.telegram.ui.Components.PipRoundVideoView; import org.telegram.ui.Components.RecyclerListView; import org.telegram.ui.Components.StickersAlert; import org.telegram.ui.ActionBar.Theme; @@ -117,6 +122,11 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa private AlertDialog visibleDialog; private RecyclerListView sideMenu; + private AlertDialog localeDialog; + private boolean loadingLocaleDialog; + private HashMap systemLocaleStrings; + private HashMap englishLocaleStrings; + private Intent passcodeSaveIntent; private boolean passcodeSaveIntentIsNew; private boolean passcodeSaveIntentIsRestore; @@ -143,6 +153,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa Map state = preferences.getAll(); if (state.isEmpty()) { Intent intent2 = new Intent(this, IntroActivity.class); + intent2.setData(intent.getData()); startActivity(intent2); super.onCreate(savedInstanceState); finish(); @@ -330,7 +341,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa sideMenu.setLayoutParams(layoutParams); sideMenu.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @Override - public void onItemClick(View view, int position) { + public void onItemClick(final View view, int position) { int id = drawerLayoutAdapter.getId(position); if (position == 0) { Bundle args = new Bundle(); @@ -369,15 +380,30 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa presentFragment(new ContactsActivity(null)); drawerLayoutContainer.closeDrawer(false); } else if (id == 7) { - try { - Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TEXT, ContactsController.getInstance().getInviteText()); - startActivityForResult(Intent.createChooser(intent, LocaleController.getString("InviteFriends", R.string.InviteFriends)), 500); - } catch (Exception e) { - FileLog.e(e); + if (BuildVars.DEBUG_PRIVATE_VERSION) { + /*AlertDialog.Builder builder = new AlertDialog.Builder(LaunchActivity.this); + builder.setTopImage(R.drawable.permissions_contacts, 0xff35a8e0); + builder.setMessage(AndroidUtilities.replaceTags(LocaleController.getString("ContactsPermissionAlert", R.string.ContactsPermissionAlert))); + builder.setPositiveButton(LocaleController.getString("ContactsPermissionAlertContinue", R.string.ContactsPermissionAlertContinue), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + + } + }); + builder.setNegativeButton(LocaleController.getString("ContactsPermissionAlertNotNow", R.string.ContactsPermissionAlertNotNow), null); + showAlertDialog(builder);*/ + showLanguageAlert(true); + } else { + try { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, ContactsController.getInstance().getInviteText()); + startActivityForResult(Intent.createChooser(intent, LocaleController.getString("InviteFriends", R.string.InviteFriends)), 500); + } catch (Exception e) { + FileLog.e(e); + } + drawerLayoutContainer.closeDrawer(false); } - drawerLayoutContainer.closeDrawer(false); } else if (id == 8) { presentFragment(new SettingsActivity()); drawerLayoutContainer.closeDrawer(false); @@ -412,6 +438,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa NotificationCenter.getInstance().addObserver(this, NotificationCenter.wasUnableToFindCurrentLocation); NotificationCenter.getInstance().addObserver(this, NotificationCenter.didSetNewWallpapper); NotificationCenter.getInstance().addObserver(this, NotificationCenter.didSetPasscode); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.reloadInterface); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.suggestedLangpack); if (actionBarLayout.fragmentsStack.isEmpty()) { if (!UserConfig.isClientActivated()) { @@ -542,7 +570,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } catch (Exception e) { FileLog.e(e); } - + MediaController.getInstance().setBaseActivity(this, true); } private void checkLayout() { @@ -631,6 +659,9 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } private boolean handleIntent(Intent intent, boolean isNew, boolean restore, boolean fromPassword) { + if (AndroidUtilities.handleProxyIntent(this, intent)) { + return true; + } int flags = intent.getFlags(); if (!fromPassword && (AndroidUtilities.needShowPasscode(true) || UserConfig.isWaitingForPasscodeEnter)) { showPasscodeActivity(); @@ -906,6 +937,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa String username = null; String group = null; String sticker = null; + String instantView[] = null; String botUser = null; String botChat = null; String message = null; @@ -918,7 +950,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa if (scheme != null) { if ((scheme.equals("http") || scheme.equals("https"))) { String host = data.getHost().toLowerCase(); - if (host.equals("telegram.me") || host.equals("t.me") || host.equals("telegram.dog")) { + if (host.equals("telegram.me") || host.equals("t.me") || host.equals("telegram.dog") || host.equals("telesco.pe")) { String path = data.getPath(); if (path != null && path.length() > 1) { path = path.substring(1); @@ -926,6 +958,12 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa group = path.replace("joinchat/", ""); } else if (path.startsWith("addstickers/")) { sticker = path.replace("addstickers/", ""); + } else if (path.startsWith("iv/")) { + instantView[0] = data.getQueryParameter("url"); + instantView[1] = data.getQueryParameter("rhash"); + if (TextUtils.isEmpty(instantView[0]) || TextUtils.isEmpty(instantView[1])) { + instantView = null; + } } else if (path.startsWith("msg/") || path.startsWith("share/")) { message = data.getQueryParameter("url"); if (message == null) { @@ -1045,8 +1083,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa presentFragment(new CancelAccountDeletionActivity(args)); } }); - } else if (username != null || group != null || sticker != null || message != null || game != null) { - runLinkRequest(username, group, sticker, botUser, botChat, message, hasUrl, messageId, game, 0); + } else if (username != null || group != null || sticker != null || message != null || game != null || instantView != null) { + runLinkRequest(username, group, sticker, botUser, botChat, message, hasUrl, messageId, game, instantView, 0); } else { try { Cursor cursor = getContentResolver().query(intent.getData(), null, null, null, null); @@ -1261,7 +1299,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa return false; } - private void runLinkRequest(final String username, final String group, final String sticker, final String botUser, final String botChat, final String message, final boolean hasUrl, final Integer messageId, final String game, final int state) { + private void runLinkRequest(final String username, final String group, final String sticker, final String botUser, final String botChat, final String message, final boolean hasUrl, final Integer messageId, final String game, final String[] instantView, final int state) { final AlertDialog progressDialog = new AlertDialog(this, 1); progressDialog.setMessage(LocaleController.getString("Loading", R.string.Loading)); progressDialog.setCanceledOnTouchOutside(false); @@ -1462,7 +1500,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { - runLinkRequest(username, group, sticker, botUser, botChat, message, hasUrl, messageId, game, 1); + runLinkRequest(username, group, sticker, botUser, botChat, message, hasUrl, messageId, game, instantView, 1); } }); builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); @@ -1582,6 +1620,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } }); presentFragment(fragment, false, true); + } else if (instantView != null) { + } if (requestId != 0) { @@ -1616,6 +1656,15 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa visibleDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { + if (visibleDialog != null && visibleDialog == localeDialog) { + try { + String shorname = LocaleController.getInstance().getCurrentLocaleInfo().shortName; + Toast.makeText(LaunchActivity.this, getStringForLanguageAlert(shorname.equals("en") ? englishLocaleStrings : systemLocaleStrings, "ChangeLanguageLater", R.string.ChangeLanguageLater), Toast.LENGTH_LONG).show(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + localeDialog = null; + } visibleDialog = null; } }); @@ -1697,7 +1746,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa captions.add(sendingText); sendingText = null; } - SendMessagesHelper.prepareSendingPhotos(null, photoPathsArray, dialog_id, null, captions, null, null); + SendMessagesHelper.prepareSendingPhotos(null, photoPathsArray, dialog_id, null, captions, null, null, false); } if (sendingText != null) { @@ -1740,6 +1789,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa NotificationCenter.getInstance().removeObserver(this, NotificationCenter.wasUnableToFindCurrentLocation); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didSetNewWallpapper); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didSetPasscode); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.reloadInterface); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.suggestedLangpack); } public void presentFragment(BaseFragment fragment) { @@ -1802,6 +1853,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa ContactsController.getInstance().readContacts(); return; } else if (requestCode == 3) { + CameraController.getInstance().initCamera(); return; } else if (requestCode == 19 || requestCode == 20) { showAlert = false; @@ -1860,6 +1912,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa @Override protected void onPause() { super.onPause(); + UserConfig.lastAppPauseTime = System.currentTimeMillis(); ApplicationLoader.mainInterfacePaused = true; Utilities.stageQueue.postRunnable(new Runnable() { @Override @@ -1902,6 +1955,11 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa SecretPhotoViewer.getInstance().destroyPhotoViewer(); ArticleViewer.getInstance().destroyArticleViewer(); StickerPreviewViewer.getInstance().destroy(); + PipRoundVideoView pipRoundVideoView = PipRoundVideoView.getInstance(); + MediaController.getInstance().setBaseActivity(this, false); + if (pipRoundVideoView != null) { + pipRoundVideoView.close(false); + } Theme.destroyResources(); EmbedBottomSheet embedBottomSheet = EmbedBottomSheet.getInstance(); if (embedBottomSheet != null) { @@ -1938,6 +1996,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa @Override protected void onResume() { super.onResume(); + showLanguageAlert(false); ApplicationLoader.mainInterfacePaused = false; Utilities.stageQueue.postRunnable(new Runnable() { @Override @@ -1968,6 +2027,13 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa if (PhotoViewer.getInstance().isVisible()) { PhotoViewer.getInstance().onResume(); } + PipRoundVideoView pipRoundVideoView = PipRoundVideoView.getInstance(); + if (pipRoundVideoView != null && MediaController.getInstance().isMessagePaused()) { + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject != null) { + MediaController.getInstance().seekToProgress(messageObject, messageObject.audioProgress); + } + } } @Override @@ -1975,6 +2041,10 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa AndroidUtilities.checkDisplaySize(this, newConfig); super.onConfigurationChanged(newConfig); checkLayout(); + PipRoundVideoView pipRoundVideoView = PipRoundVideoView.getInstance(); + if (pipRoundVideoView != null) { + pipRoundVideoView.onConfigurationChanged(); + } EmbedBottomSheet embedBottomSheet = EmbedBottomSheet.getInstance(); if (embedBottomSheet != null) { embedBottomSheet.onConfigurationChanged(newConfig); @@ -2108,6 +2178,180 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa FileLog.e(e); } } + } else if (id == NotificationCenter.reloadInterface) { + rebuildAllFragments(true); + } else if (id == NotificationCenter.suggestedLangpack) { + showLanguageAlert(false); + } + } + + private String getStringForLanguageAlert(HashMap map, String key, int intKey) { + String value = map.get(key); + if (value == null) { + return LocaleController.getString(key, intKey); + } + return value; + } + + private void showLanguageAlertInternal(LocaleController.LocaleInfo systemInfo, LocaleController.LocaleInfo englishInfo, String systemLang) { + try { + loadingLocaleDialog = false; + boolean firstSystem = systemInfo.builtIn || LocaleController.getInstance().isCurrentLocalLocale(); + AlertDialog.Builder builder = new AlertDialog.Builder(LaunchActivity.this); + builder.setTitle(getStringForLanguageAlert(systemLocaleStrings, "ChooseYourLanguage", R.string.ChooseYourLanguage)); + builder.setSubtitle(getStringForLanguageAlert(englishLocaleStrings, "ChooseYourLanguage", R.string.ChooseYourLanguage)); + LinearLayout linearLayout = new LinearLayout(LaunchActivity.this); + linearLayout.setOrientation(LinearLayout.VERTICAL); + final LanguageCell[] cells = new LanguageCell[2]; + final LocaleController.LocaleInfo[] selectedLanguage = new LocaleController.LocaleInfo[1]; + final LocaleController.LocaleInfo[] locales = new LocaleController.LocaleInfo[2]; + final String englishName = getStringForLanguageAlert(systemLocaleStrings, "English", R.string.English); + locales[0] = firstSystem ? systemInfo : englishInfo; + locales[1] = firstSystem ? englishInfo : systemInfo; + selectedLanguage[0] = firstSystem ? systemInfo : englishInfo; + + for (int a = 0; a < 2; a++) { + cells[a] = new LanguageCell(LaunchActivity.this, true); + cells[a].setLanguage(locales[a], locales[a] == englishInfo ? englishName : null, true); + cells[a].setTag(a); + cells[a].setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_dialogButtonSelector), 2)); + cells[a].setLanguageSelected(a == 0); + linearLayout.addView(cells[a], LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); + cells[a].setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Integer tag = (Integer) v.getTag(); + selectedLanguage[0] = ((LanguageCell) v).getCurrentLocale(); + for (int a = 0; a < cells.length; a++) { + cells[a].setLanguageSelected(a == tag); + } + } + }); + } + LanguageCell cell = new LanguageCell(LaunchActivity.this, true); + cell.setValue(getStringForLanguageAlert(systemLocaleStrings, "ChooseYourLanguageOther", R.string.ChooseYourLanguageOther), getStringForLanguageAlert(englishLocaleStrings, "ChooseYourLanguageOther", R.string.ChooseYourLanguageOther)); + cell.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + localeDialog = null; + drawerLayoutContainer.closeDrawer(true); + presentFragment(new LanguageSelectActivity()); + if (visibleDialog != null) { + visibleDialog.dismiss(); + visibleDialog = null; + } + } + }); + linearLayout.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); + builder.setView(linearLayout); + builder.setNegativeButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + LocaleController.getInstance().applyLanguage(selectedLanguage[0], true); + rebuildAllFragments(true); + } + }); + localeDialog = showAlertDialog(builder); + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + preferences.edit().putString("language_showed2", systemLang).commit(); + } catch (Exception e) { + FileLog.e(e); + } + } + + private void showLanguageAlert(boolean force) { + try { + if (loadingLocaleDialog) { + return; + } + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + String showedLang = preferences.getString("language_showed2", ""); + final String systemLang = LocaleController.getSystemLocaleStringIso639().toLowerCase(); + if (!force && showedLang.equals(systemLang)) { + return; + } + + final LocaleController.LocaleInfo infos[] = new LocaleController.LocaleInfo[2]; + String arg = systemLang.contains("-") ? systemLang.split("-")[0] : systemLang; + for (int a = 0; a < LocaleController.getInstance().languages.size(); a++) { + LocaleController.LocaleInfo info = LocaleController.getInstance().languages.get(a); + if (info.shortName.equals("en")) { + infos[0] = info; + } + if (info.shortName.replace("_", "-").equals(systemLang) || info.shortName.equals(arg)) { + infos[1] = info; + } + if (infos[0] != null && infos[1] != null) { + break; + } + } + if (infos[0] == null || infos[1] == null || infos[0] == infos[1]) { + return; + } + + systemLocaleStrings = null; + englishLocaleStrings = null; + loadingLocaleDialog = true; + + TLRPC.TL_langpack_getStrings req = new TLRPC.TL_langpack_getStrings(); + req.lang_code = infos[1].shortName.replace("_", "-"); + req.keys.add("English"); + req.keys.add("ChooseYourLanguage"); + req.keys.add("ChooseYourLanguageOther"); + req.keys.add("ChangeLanguageLater"); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + final HashMap keys = new HashMap<>(); + if (response != null) { + TLRPC.Vector vector = (TLRPC.Vector) response; + for (int a = 0; a < vector.objects.size(); a++) { + final TLRPC.LangPackString string = (TLRPC.LangPackString) vector.objects.get(a); + keys.put(string.key, string.value); + } + } + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + systemLocaleStrings = keys; + if (englishLocaleStrings != null && systemLocaleStrings != null) { + showLanguageAlertInternal(infos[1], infos[0], systemLang); + } + } + }); + } + }, ConnectionsManager.RequestFlagWithoutLogin); + + req = new TLRPC.TL_langpack_getStrings(); + req.lang_code = infos[0].shortName.replace("_", "-"); + req.keys.add("English"); + req.keys.add("ChooseYourLanguage"); + req.keys.add("ChooseYourLanguageOther"); + req.keys.add("ChangeLanguageLater"); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + final HashMap keys = new HashMap<>(); + if (response != null) { + TLRPC.Vector vector = (TLRPC.Vector) response; + for (int a = 0; a < vector.objects.size(); a++) { + final TLRPC.LangPackString string = (TLRPC.LangPackString) vector.objects.get(a); + keys.put(string.key, string.value); + } + } + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + englishLocaleStrings = keys; + if (englishLocaleStrings != null && systemLocaleStrings != null) { + showLanguageAlertInternal(infos[1], infos[0], systemLang); + } + } + }); + } + }, ConnectionsManager.RequestFlagWithoutLogin); + } catch (Exception e) { + FileLog.e(e); } } @@ -2158,15 +2402,60 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } private void updateCurrentConnectionState() { - String text = null; + String title = null; + String subtitle = null; + Runnable action = null; if (currentConnectionState == ConnectionsManager.ConnectionStateWaitingForNetwork) { - text = LocaleController.getString("WaitingForNetwork", R.string.WaitingForNetwork); + title = LocaleController.getString("WaitingForNetwork", R.string.WaitingForNetwork); } else if (currentConnectionState == ConnectionsManager.ConnectionStateConnecting) { - text = LocaleController.getString("Connecting", R.string.Connecting); + title = LocaleController.getString("Connecting", R.string.Connecting); + action = new Runnable() { + @Override + public void run() { + if (AndroidUtilities.isTablet()) { + if (!layerFragmentsStack.isEmpty() && layerFragmentsStack.get(layerFragmentsStack.size() - 1) instanceof ProxySettingsActivity) { + return; + } + } else { + if (!mainFragmentsStack.isEmpty() && mainFragmentsStack.get(mainFragmentsStack.size() - 1) instanceof ProxySettingsActivity) { + return; + } + } + presentFragment(new ProxySettingsActivity()); + } + }; } else if (currentConnectionState == ConnectionsManager.ConnectionStateUpdating) { - text = LocaleController.getString("Updating", R.string.Updating); + title = LocaleController.getString("Updating", R.string.Updating); + } else if (currentConnectionState == ConnectionsManager.ConnectionStateConnectingToProxy) { + title = LocaleController.getString("ConnectingToProxy", R.string.ConnectingToProxy); + subtitle = LocaleController.getString("ConnectingToProxyTapToDisable", R.string.ConnectingToProxyTapToDisable); + action = new Runnable() { + @Override + public void run() { + if (actionBarLayout == null || actionBarLayout.fragmentsStack.isEmpty()) { + return; + } + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + BaseFragment fragment = actionBarLayout.fragmentsStack.get(actionBarLayout.fragmentsStack.size() - 1); + AlertDialog.Builder builder = new AlertDialog.Builder(LaunchActivity.this); + builder.setTitle(LocaleController.getString("Proxy", R.string.Proxy)); + builder.setMessage(LocaleController.formatString("ConnectingToProxyDisableAlert", R.string.ConnectingToProxyDisableAlert, preferences.getString("proxy_ip", ""))); + builder.setPositiveButton(LocaleController.getString("ConnectingToProxyDisable", R.string.ConnectingToProxyDisable), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + SharedPreferences.Editor editor = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).edit(); + editor.putBoolean("proxy_enabled", false); + editor.commit(); + ConnectionsManager.native_setProxySettings("", 0, "", ""); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + fragment.showDialog(builder.create()); + } + }; } - actionBarLayout.setTitleOverlayText(text); + actionBarLayout.setTitleOverlayText(title, subtitle, action); } @Override @@ -2531,11 +2820,9 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa public void rebuildAllFragments(boolean last) { if (layersActionBarLayout != null) { - layersActionBarLayout.rebuildAllFragmentViews(last); - layersActionBarLayout.showLastFragment(); + layersActionBarLayout.rebuildAllFragmentViews(last, true); } else { - actionBarLayout.rebuildAllFragmentViews(last); - actionBarLayout.showLastFragment(); + actionBarLayout.rebuildAllFragmentViews(last, true); } } @@ -2543,10 +2830,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa public void onRebuildAllFragments(ActionBarLayout layout) { if (AndroidUtilities.isTablet()) { if (layout == layersActionBarLayout) { - rightActionBarLayout.rebuildAllFragmentViews(true); - rightActionBarLayout.showLastFragment(); - actionBarLayout.rebuildAllFragmentViews(true); - actionBarLayout.showLastFragment(); + rightActionBarLayout.rebuildAllFragmentViews(true, true); + actionBarLayout.rebuildAllFragmentViews(true, true); } } drawerLayoutAdapter.notifyDataSetChanged(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java index 49151b479..ca54bb02e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java @@ -160,6 +160,7 @@ public class LoginActivity extends BaseFragment { }); ActionBarMenu menu = actionBar.createMenu(); + actionBar.setAllowOverlayTitle(true); doneButton = menu.addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); fragmentView = new ScrollView(context); @@ -412,15 +413,24 @@ public class LoginActivity extends BaseFragment { showDialog(builder.create()); } - private void needShowProgress() { + private void needShowProgress(final int reqiestId) { if (getParentActivity() == null || getParentActivity().isFinishing() || progressDialog != null) { return; } - progressDialog = new AlertDialog(getParentActivity(), 1); - progressDialog.setMessage(LocaleController.getString("Loading", R.string.Loading)); + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity(), 1); + builder.setMessage(LocaleController.getString("Loading", R.string.Loading)); + if (reqiestId != 0) { + builder.setPositiveButton(LocaleController.getString("Cancel", R.string.Cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ConnectionsManager.getInstance().cancelRequest(reqiestId, true); + progressDialog = null; + } + }); + } + progressDialog = builder.show(); progressDialog.setCanceledOnTouchOutside(false); progressDialog.setCancelable(false); - progressDialog.show(); } public void needHideProgress() { @@ -1038,8 +1048,7 @@ public class LoginActivity extends BaseFragment { } params.putString("phoneFormated", phone); nextPressed = true; - needShowProgress(); - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(final TLObject response, final TLRPC.TL_error error) { AndroidUtilities.runOnUIThread(new Runnable() { @@ -1072,6 +1081,7 @@ public class LoginActivity extends BaseFragment { }); } }, ConnectionsManager.RequestFlagFailOnServerErrors | ConnectionsManager.RequestFlagWithoutLogin | ConnectionsManager.RequestFlagTryDifferentDc | ConnectionsManager.RequestFlagEnableUnauthorized); + needShowProgress(reqId); } public void fillNumber() { @@ -1383,12 +1393,11 @@ public class LoginActivity extends BaseFragment { params.putString("phoneFormated", requestPhone); nextPressed = true; - needShowProgress(); TLRPC.TL_auth_resendCode req = new TLRPC.TL_auth_resendCode(); req.phone_number = requestPhone; req.phone_code_hash = phoneHash; - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(final TLObject response, final TLRPC.TL_error error) { AndroidUtilities.runOnUIThread(new Runnable() { @@ -1419,6 +1428,7 @@ public class LoginActivity extends BaseFragment { }); } }, ConnectionsManager.RequestFlagFailOnServerErrors | ConnectionsManager.RequestFlagWithoutLogin); + needShowProgress(0); } @Override @@ -1677,8 +1687,7 @@ public class LoginActivity extends BaseFragment { req.phone_code = code; req.phone_code_hash = phoneHash; destroyTimer(); - needShowProgress(); - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(final TLObject response, final TLRPC.TL_error error) { AndroidUtilities.runOnUIThread(new Runnable() { @@ -1789,6 +1798,7 @@ public class LoginActivity extends BaseFragment { }); } }, ConnectionsManager.RequestFlagFailOnServerErrors | ConnectionsManager.RequestFlagWithoutLogin); + needShowProgress(0); } @Override @@ -1971,7 +1981,7 @@ public class LoginActivity extends BaseFragment { @Override public void onClick(View view) { if (has_recovery) { - needShowProgress(); + needShowProgress(0); TLRPC.TL_auth_requestPasswordRecovery req = new TLRPC.TL_auth_requestPasswordRecovery(); ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override @@ -2044,7 +2054,7 @@ public class LoginActivity extends BaseFragment { builder.setPositiveButton(LocaleController.getString("ResetMyAccountWarningReset", R.string.ResetMyAccountWarningReset), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { - needShowProgress(); + needShowProgress(0); TLRPC.TL_account_deleteAccount req = new TLRPC.TL_account_deleteAccount(); req.reason = "Forgot password"; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @@ -2164,7 +2174,7 @@ public class LoginActivity extends BaseFragment { FileLog.e(e); } - needShowProgress(); + needShowProgress(0); byte[] hash = new byte[current_salt.length * 2 + oldPasswordBytes.length]; System.arraycopy(current_salt, 0, hash, 0, current_salt.length); System.arraycopy(oldPasswordBytes, 0, hash, current_salt.length, oldPasswordBytes.length); @@ -2324,7 +2334,7 @@ public class LoginActivity extends BaseFragment { builder.setPositiveButton(LocaleController.getString("ResetMyAccountWarningReset", R.string.ResetMyAccountWarningReset), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { - needShowProgress(); + needShowProgress(0); TLRPC.TL_account_deleteAccount req = new TLRPC.TL_account_deleteAccount(); req.reason = "Forgot password"; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @@ -2571,7 +2581,7 @@ public class LoginActivity extends BaseFragment { onPasscodeError(false); return; } - needShowProgress(); + needShowProgress(0); TLRPC.TL_auth_recoverPassword req = new TLRPC.TL_auth_recoverPassword(); req.code = code; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @@ -2791,7 +2801,7 @@ public class LoginActivity extends BaseFragment { req.phone_number = requestPhone; req.first_name = firstNameField.getText().toString(); req.last_name = lastNameField.getText().toString(); - needShowProgress(); + needShowProgress(0); ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(final TLObject response, final TLRPC.TL_error error) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ManageSpaceActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ManageSpaceActivity.java index 502cb36e5..b0836d65b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ManageSpaceActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ManageSpaceActivity.java @@ -34,7 +34,6 @@ import org.telegram.ui.ActionBar.ActionBarLayout; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.DrawerLayoutContainer; import org.telegram.ui.Components.LayoutHelper; -import org.telegram.ui.ActionBar.Theme; import java.util.ArrayList; @@ -293,8 +292,10 @@ public class ManageSpaceActivity extends Activity implements ActionBarLayout.Act text = LocaleController.getString("Connecting", R.string.Connecting); } else if (currentConnectionState == ConnectionsManager.ConnectionStateUpdating) { text = LocaleController.getString("Updating", R.string.Updating); + } else if (currentConnectionState == ConnectionsManager.ConnectionStateConnectingToProxy) { + text = LocaleController.getString("ConnectingToProxy", R.string.ConnectingToProxy); } - actionBarLayout.setTitleOverlayText(text); + actionBarLayout.setTitleOverlayText(text, null, null); } @Override @@ -359,8 +360,7 @@ public class ManageSpaceActivity extends Activity implements ActionBarLayout.Act public void onRebuildAllFragments(ActionBarLayout layout) { if (AndroidUtilities.isTablet()) { if (layout == layersActionBarLayout) { - actionBarLayout.rebuildAllFragmentViews(true); - actionBarLayout.showLastFragment(); + actionBarLayout.rebuildAllFragmentViews(true, true); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java index 77326ed59..59c4561c2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java @@ -743,11 +743,11 @@ public class MediaActivity extends BaseFragment implements NotificationCenter.No sharedMediaData[type].addMessage(message, false, enc); } sharedMediaData[type].endReached[loadIndex] = (Boolean) args[5]; - if (loadIndex == 0 && sharedMediaData[selectedMode].messages.isEmpty() && mergeDialogId != 0) { - sharedMediaData[selectedMode].loading = true; - SharedMediaQuery.loadMedia(mergeDialogId, 0, 50, sharedMediaData[selectedMode].max_id[1], type, true, classGuid); + if (loadIndex == 0 && sharedMediaData[type].endReached[loadIndex] && mergeDialogId != 0) { + sharedMediaData[type].loading = true; + SharedMediaQuery.loadMedia(mergeDialogId, 0, 50, sharedMediaData[type].max_id[1], type, true, classGuid); } - if (!sharedMediaData[selectedMode].loading) { + if (!sharedMediaData[type].loading) { if (progressView != null) { progressView.setVisibility(View.GONE); } @@ -776,7 +776,7 @@ public class MediaActivity extends BaseFragment implements NotificationCenter.No } } if (selectedMode == 1 || selectedMode == 3 || selectedMode == 4) { - searchItem.setVisibility(!sharedMediaData[selectedMode].messages.isEmpty() && !searching ? View.VISIBLE : View.GONE); + searchItem.setVisibility(!sharedMediaData[type].messages.isEmpty() && !searching ? View.VISIBLE : View.GONE); } } } else if (id == NotificationCenter.messagesDeleted) { @@ -976,7 +976,7 @@ public class MediaActivity extends BaseFragment implements NotificationCenter.No public boolean isPhotoChecked(int index) { return false; } @Override - public void setPhotoChecked(int index) { } + public void setPhotoChecked(int index, VideoEditedInfo videoEditedInfo) { } @Override public boolean cancelButtonPressed() { return true; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PaymentFormActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PaymentFormActivity.java index 4dff23c0b..6735335fc 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PaymentFormActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PaymentFormActivity.java @@ -14,11 +14,18 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.FragmentManager; +import android.app.FragmentTransaction; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Typeface; +import android.net.Uri; import android.os.Build; +import android.os.Bundle; import android.os.Vibrator; import android.telephony.TelephonyManager; import android.text.Editable; @@ -42,6 +49,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.WindowManager; +import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.webkit.CookieManager; import android.webkit.JavascriptInterface; @@ -54,17 +62,40 @@ import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.BooleanResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.wallet.Cart; +import com.google.android.gms.wallet.FullWallet; +import com.google.android.gms.wallet.FullWalletRequest; +import com.google.android.gms.wallet.LineItem; +import com.google.android.gms.wallet.MaskedWallet; +import com.google.android.gms.wallet.MaskedWalletRequest; +import com.google.android.gms.wallet.PaymentMethodTokenizationParameters; +import com.google.android.gms.wallet.PaymentMethodTokenizationType; +import com.google.android.gms.wallet.Wallet; +import com.google.android.gms.wallet.WalletConstants; +import com.google.android.gms.wallet.fragment.WalletFragment; +import com.google.android.gms.wallet.fragment.WalletFragmentInitParams; +import com.google.android.gms.wallet.fragment.WalletFragmentMode; +import com.google.android.gms.wallet.fragment.WalletFragmentOptions; +import com.google.android.gms.wallet.fragment.WalletFragmentStyle; import com.stripe.android.Stripe; import com.stripe.android.TokenCallback; import com.stripe.android.exception.APIConnectionException; import com.stripe.android.exception.APIException; import com.stripe.android.model.Card; import com.stripe.android.model.Token; +import com.stripe.android.net.StripeApiHandler; +import com.stripe.android.net.TokenParser; +import org.json.JSONException; import org.json.JSONObject; import org.telegram.PhoneFormat.PhoneFormat; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.ContactsController; import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessageObject; @@ -73,7 +104,6 @@ import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; import org.telegram.messenger.UserConfig; import org.telegram.messenger.Utilities; -import org.telegram.messenger.browser.Browser; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.RequestDelegate; import org.telegram.tgnet.TLObject; @@ -139,13 +169,13 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen private HashMap codesMap = new HashMap<>(); private HashMap phoneFormatMap = new HashMap<>(); -// private GoogleApiClient googleApiClient; -// private WalletFragment walletFragment; + private GoogleApiClient googleApiClient; private EditText[] inputFields; private RadioCell[] radioCells; private ActionBarMenuItem doneItem; private ContextProgressView progressView; + private ContextProgressView progressViewButton; private AnimatorSet doneItemAnimation; private WebView webView; private ScrollView scrollView; @@ -157,6 +187,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen private TextCheckCell checkCell1; private TextInfoPrivacyCell bottomCell[] = new TextInfoPrivacyCell[2]; private TextSettingsCell settingsCell1; + private FrameLayout androidPayContainer; private LinearLayout linearLayout2; private PaymentFormActivityDelegate delegate; @@ -164,13 +195,15 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen private TextView payTextView; private FrameLayout bottomLayout; private PaymentInfoCell paymentInfoCell; - private TextDetailSettingsCell detailSettingsCell[] = new TextDetailSettingsCell[6]; + private TextDetailSettingsCell detailSettingsCell[] = new TextDetailSettingsCell[7]; private boolean need_card_country; private boolean need_card_postcode; private boolean need_card_name; private String stripeApiKey; + private TLRPC.User botUser; + private boolean ignoreOnTextChange; private boolean ignoreOnPhoneChange; private boolean ignoreOnCardChange; @@ -184,6 +217,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen private String cardName; private boolean webviewLoading; private String countryName; + private String totalPriceDecimal; private TLRPC.TL_payments_paymentForm paymentForm; private TLRPC.TL_payments_validatedRequestedInfo requestedInfo; private TLRPC.TL_shippingOption shippingOption; @@ -192,11 +226,17 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen private boolean donePressed; private boolean canceled; + private boolean isWebView; + private boolean saveShippingInfo; private boolean saveCardInfo; private final static int done_button = 1; + private static final int LOAD_MASKED_WALLET_REQUEST_CODE = 1000; + private static final int LOAD_FULL_WALLET_REQUEST_CODE = 1001; + private final static int fragment_container_id = 4000; + private interface PaymentFormActivityDelegate { void didSelectNewCard(String tokenJson, String card, boolean saveCard); } @@ -265,9 +305,9 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen paymentForm.users = receipt.users; shippingOption = receipt.shipping; messageObject = message; - TLRPC.User user = MessagesController.getInstance().getUser(receipt.bot_id); - if (user != null) { - currentBotName = user.first_name; + botUser = MessagesController.getInstance().getUser(receipt.bot_id); + if (botUser != null) { + currentBotName = botUser.first_name; } else { currentBotName = ""; } @@ -317,9 +357,10 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen shippingOption = shipping; messageObject = message; saveCardInfo = saveCard; - TLRPC.User user = MessagesController.getInstance().getUser(form.bot_id); - if (user != null) { - currentBotName = user.first_name; + isWebView = !"stripe".equals(paymentForm.native_provider); + botUser = MessagesController.getInstance().getUser(form.bot_id); + if (botUser != null) { + currentBotName = botUser.first_name; } else { currentBotName = ""; } @@ -346,7 +387,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); if (Build.VERSION.SDK_INT >= 23) { try { - if (currentStep == 2) { + if (currentStep == 2 && !paymentForm.invoice.test) { getParentActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); } else if (UserConfig.passcodeHash.length() == 0 || UserConfig.allowScreenCapture) { getParentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); @@ -355,16 +396,16 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen FileLog.e(e); } } -// if (googleApiClient != null) { -// googleApiClient.connect(); -// } + if (googleApiClient != null) { + googleApiClient.connect(); + } } @Override public void onPause() { -// if (googleApiClient != null) { -// googleApiClient.disconnect(); -// } + if (googleApiClient != null) { + googleApiClient.disconnect(); + } } @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) @@ -432,7 +473,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen ActionBarMenu menu = actionBar.createMenu(); - if (currentStep == 0 || currentStep == 1 || currentStep == 2 || currentStep == 3) { + if (currentStep == 0 || currentStep == 1 || currentStep == 2 || currentStep == 3 || currentStep == 4) { doneItem = menu.addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); progressView = new ContextProgressView(context, 1); doneItem.addView(progressView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); @@ -896,13 +937,19 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen } } } else if (currentStep == 2) { - if (!"stripe".equals(paymentForm.native_provider)) { + if (isWebView) { webviewLoading = true; - showEditDoneProgress(true); + showEditDoneProgress(true, true); progressView.setVisibility(View.VISIBLE); doneItem.setEnabled(false); doneItem.getImageView().setVisibility(View.INVISIBLE); - webView = new WebView(context); + webView = new WebView(context) { + @Override + public boolean onTouchEvent(MotionEvent event) { + getParent().requestDisallowInterceptTouchEvent(true); + return super.onTouchEvent(event); + } + }; webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setDomStorageEnabled(true); @@ -910,9 +957,8 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); CookieManager cookieManager = CookieManager.getInstance(); cookieManager.setAcceptThirdPartyCookies(webView, true); - webView.addJavascriptInterface(new TelegramWebviewProxy(), "TelegramWebviewProxy"); } - + webView.addJavascriptInterface(new TelegramWebviewProxy(), "TelegramWebviewProxy"); webView.setWebViewClient(new WebViewClient() { @Override public void onLoadResource(WebView view, String url) { @@ -923,7 +969,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); webviewLoading = false; - showEditDoneProgress(false); + showEditDoneProgress(true, false); updateSavePaymentField(); } }); @@ -976,44 +1022,47 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen FileLog.e(e); } - /*googleApiClient = new GoogleApiClient.Builder(context) - .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { - @Override - public void onConnected(Bundle bundle) { - - } - - @Override - public void onConnectionSuspended(int i) { - - } - }) - .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() { - @Override - public void onConnectionFailed(ConnectionResult connectionResult) { - - } - }) - .addApi(Wallet.API, new Wallet.WalletOptions.Builder() - .setEnvironment(WalletConstants.ENVIRONMENT_TEST) - .setTheme(WalletConstants.THEME_LIGHT) - .build()) - .build(); - - Wallet.Payments.isReadyToPay(googleApiClient).setResultCallback( - new ResultCallback() { - @Override - public void onResult(BooleanResult booleanResult) { - if (booleanResult.getStatus().isSuccess()) { - if (booleanResult.getValue()) { - showAndroidPay(false); - } - } else { + if (Build.VERSION.SDK_INT >= 19) { + googleApiClient = new GoogleApiClient.Builder(context) + .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { + @Override + public void onConnected(Bundle bundle) { } + + @Override + public void onConnectionSuspended(int i) { + + } + }) + .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() { + @Override + public void onConnectionFailed(ConnectionResult connectionResult) { + + } + }) + .addApi(Wallet.API, new Wallet.WalletOptions.Builder() + .setEnvironment(paymentForm.invoice.test ? WalletConstants.ENVIRONMENT_TEST : WalletConstants.ENVIRONMENT_PRODUCTION) + .setTheme(WalletConstants.THEME_LIGHT) + .build()) + .build(); + + Wallet.Payments.isReadyToPay(googleApiClient).setResultCallback( + new ResultCallback() { + @Override + public void onResult(BooleanResult booleanResult) { + if (booleanResult.getStatus().isSuccess()) { + if (booleanResult.getValue()) { + showAndroidPay(); + } + } else { + + } + } } - } - );*/ + ); + googleApiClient.connect(); + } inputFields = new EditText[FIELDS_COUNT_CARD]; for (int a = 0; a < FIELDS_COUNT_CARD; a++) { @@ -1034,13 +1083,6 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen linearLayout2.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); container.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - if (allowDivider) { - View divider = new View(context); - dividers.add(divider); - divider.setBackgroundColor(Theme.getColor(Theme.key_divider)); - container.addView(divider, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1, Gravity.LEFT | Gravity.BOTTOM)); - } - View.OnTouchListener onTouchListener = null; inputFields[a] = new EditText(context); inputFields[a].setTag(a); @@ -1429,6 +1471,19 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen bottomCell[0].setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); updateSavePaymentField(); linearLayout2.addView(bottomCell[0], LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + } else if (a == FIELD_CARD) { + androidPayContainer = new FrameLayout(context); + androidPayContainer.setId(fragment_container_id); + androidPayContainer.setBackgroundDrawable(Theme.getSelectorDrawable(true)); + androidPayContainer.setVisibility(View.GONE); + container.addView(androidPayContainer, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, 0, 0, 4, 0)); + } + + if (allowDivider) { + View divider = new View(context); + dividers.add(divider); + divider.setBackgroundColor(Theme.getColor(Theme.key_divider)); + container.addView(divider, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1, Gravity.LEFT | Gravity.BOTTOM)); } if (a == FIELD_CARD_COUNTRY && !need_card_country || a == FIELD_CARD_POSTCODE && !need_card_postcode || a == FIELD_CARDNAME && !need_card_name) { @@ -1444,18 +1499,6 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen } else { inputFields[FIELD_CVV].setImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_EXTRACT_UI); } - - /*settingsCell1 = new TextSettingsCell(context); - settingsCell1.setBackgroundDrawable(Theme.getSelectorDrawable(true)); - settingsCell1.setText("Android Pay", false); - settingsCell1.setVisibility(View.GONE); - linearLayout2.addView(settingsCell1, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - settingsCell1.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showAndroidPay(true); - } - });*/ } } else if (currentStep == 1) { int count = requestedInfo.shipping_options.size(); @@ -1634,42 +1677,59 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen }); } + TLRPC.User providerUser = null; + for (int a = 0; a < paymentForm.users.size(); a++) { + TLRPC.User user = paymentForm.users.get(a); + if (user.id == paymentForm.provider_id) { + providerUser = user; + } + } + final String providerName; + if (providerUser != null) { + detailSettingsCell[1] = new TextDetailSettingsCell(context); + detailSettingsCell[1].setBackgroundDrawable(Theme.getSelectorDrawable(true)); + detailSettingsCell[1].setTextAndValue(providerName = ContactsController.formatName(providerUser.first_name, providerUser.last_name), LocaleController.getString("PaymentCheckoutProvider", R.string.PaymentCheckoutProvider), true); + linearLayout2.addView(detailSettingsCell[1]); + } else { + providerName = ""; + } + if (validateRequest != null) { if (validateRequest.info.shipping_address != null) { String address = String.format("%s %s, %s, %s, %s, %s", validateRequest.info.shipping_address.street_line1, validateRequest.info.shipping_address.street_line2, validateRequest.info.shipping_address.city, validateRequest.info.shipping_address.state, validateRequest.info.shipping_address.country_iso2, validateRequest.info.shipping_address.post_code); - detailSettingsCell[1] = new TextDetailSettingsCell(context); - detailSettingsCell[1].setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - detailSettingsCell[1].setTextAndValue(address, LocaleController.getString("PaymentShippingAddress", R.string.PaymentShippingAddress), true); - linearLayout2.addView(detailSettingsCell[1]); - } - - if (validateRequest.info.name != null) { detailSettingsCell[2] = new TextDetailSettingsCell(context); detailSettingsCell[2].setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - detailSettingsCell[2].setTextAndValue(validateRequest.info.name, LocaleController.getString("PaymentCheckoutName", R.string.PaymentCheckoutName), true); + detailSettingsCell[2].setTextAndValue(address, LocaleController.getString("PaymentShippingAddress", R.string.PaymentShippingAddress), true); linearLayout2.addView(detailSettingsCell[2]); } - if (validateRequest.info.phone != null) { + if (validateRequest.info.name != null) { detailSettingsCell[3] = new TextDetailSettingsCell(context); detailSettingsCell[3].setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - detailSettingsCell[3].setTextAndValue(PhoneFormat.getInstance().format(validateRequest.info.phone), LocaleController.getString("PaymentCheckoutPhoneNumber", R.string.PaymentCheckoutPhoneNumber), true); + detailSettingsCell[3].setTextAndValue(validateRequest.info.name, LocaleController.getString("PaymentCheckoutName", R.string.PaymentCheckoutName), true); linearLayout2.addView(detailSettingsCell[3]); } - if (validateRequest.info.email != null) { + if (validateRequest.info.phone != null) { detailSettingsCell[4] = new TextDetailSettingsCell(context); detailSettingsCell[4].setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - detailSettingsCell[4].setTextAndValue(validateRequest.info.email, LocaleController.getString("PaymentCheckoutEmail", R.string.PaymentCheckoutEmail), true); + detailSettingsCell[4].setTextAndValue(PhoneFormat.getInstance().format(validateRequest.info.phone), LocaleController.getString("PaymentCheckoutPhoneNumber", R.string.PaymentCheckoutPhoneNumber), true); linearLayout2.addView(detailSettingsCell[4]); } - if (shippingOption != null) { + if (validateRequest.info.email != null) { detailSettingsCell[5] = new TextDetailSettingsCell(context); detailSettingsCell[5].setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - detailSettingsCell[5].setTextAndValue(shippingOption.title, LocaleController.getString("PaymentCheckoutShippingMethod", R.string.PaymentCheckoutShippingMethod), false); + detailSettingsCell[5].setTextAndValue(validateRequest.info.email, LocaleController.getString("PaymentCheckoutEmail", R.string.PaymentCheckoutEmail), true); linearLayout2.addView(detailSettingsCell[5]); } + + if (shippingOption != null) { + detailSettingsCell[6] = new TextDetailSettingsCell(context); + detailSettingsCell[6].setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + detailSettingsCell[6].setTextAndValue(shippingOption.title, LocaleController.getString("PaymentCheckoutShippingMethod", R.string.PaymentCheckoutShippingMethod), false); + linearLayout2.addView(detailSettingsCell[6]); + } } if (currentStep == 4) { @@ -1679,22 +1739,29 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen bottomLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - builder.setTitle(LocaleController.getString("PaymentTransactionReview", R.string.PaymentTransactionReview)); - builder.setMessage(LocaleController.formatString("PaymentTransactionMessage", R.string.PaymentTransactionMessage, totalPrice, currentBotName, currentItemName)); - builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - showEditDoneProgress(true); - setDonePressed(true); - sendData(); + if (botUser != null && !botUser.verified) { + String botKey = "payment_warning_" + botUser.id; + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE); + if (!preferences.getBoolean(botKey, false)) { + preferences.edit().putBoolean(botKey, true).commit(); + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("PaymentWarning", R.string.PaymentWarning)); + builder.setMessage(LocaleController.formatString("PaymentWarningText", R.string.PaymentWarningText, currentBotName, providerName)); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + showPayAlert(totalPrice); + } + }); + showDialog(builder.create()); + } else { + showPayAlert(totalPrice); } - }); - builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); - showDialog(builder.create()); + } else { + showPayAlert(totalPrice); + } } }); - payTextView = new TextView(context); payTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText6)); payTextView.setText(LocaleController.formatString("PaymentCheckoutPay", R.string.PaymentCheckoutPay, totalPrice)); @@ -1703,13 +1770,76 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen payTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); bottomLayout.addView(payTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); - progressView = new ContextProgressView(context, 0); - progressView.setVisibility(View.INVISIBLE); - bottomLayout.addView(progressView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + progressViewButton = new ContextProgressView(context, 0); + progressViewButton.setVisibility(View.INVISIBLE); + bottomLayout.addView(progressViewButton, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); View shadow = new View(context); shadow.setBackgroundResource(R.drawable.header_shadow_reverse); frameLayout.addView(shadow, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.LEFT | Gravity.BOTTOM, 0, 0, 0, 48)); + + doneItem.setEnabled(false); + doneItem.getImageView().setVisibility(View.INVISIBLE); + + webView = new WebView(context) { + @Override + public boolean onTouchEvent(MotionEvent event) { + getParent().requestDisallowInterceptTouchEvent(true); + return super.onTouchEvent(event); + } + }; + webView.setBackgroundColor(0xffffffff); + webView.getSettings().setJavaScriptEnabled(true); + webView.getSettings().setDomStorageEnabled(true); + + if (Build.VERSION.SDK_INT >= 21) { + webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + CookieManager cookieManager = CookieManager.getInstance(); + cookieManager.setAcceptThirdPartyCookies(webView, true); + } + + webView.setWebViewClient(new WebViewClient() { + @Override + public void onLoadResource(WebView view, String url) { + try { + Uri uri = Uri.parse(url); + if ("t.me".equals(uri.getHost())) { + goToNextStep(); + return; + } + } catch (Exception ignore) { + + } + super.onLoadResource(view, url); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + webviewLoading = false; + showEditDoneProgress(true, false); + updateSavePaymentField(); + } + + + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + try { + Uri uri = Uri.parse(url); + if ("t.me".equals(uri.getHost())) { + goToNextStep(); + return true; + } + } catch (Exception ignore) { + + } + return false; + } + }); + + frameLayout.addView(webView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + webView.setVisibility(View.GONE); } sectionCell[1] = new ShadowSectionCell(context); @@ -1719,6 +1849,21 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen return fragmentView; } + private void showPayAlert(final String totalPrice) { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("PaymentTransactionReview", R.string.PaymentTransactionReview)); + builder.setMessage(LocaleController.formatString("PaymentTransactionMessage", R.string.PaymentTransactionMessage, totalPrice, currentBotName, currentItemName)); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + setDonePressed(true); + sendData(); + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + showDialog(builder.create()); + } + private String getTotalPriceString(ArrayList prices) { int amount = 0; for (int a = 0; a < prices.size(); a++) { @@ -1732,7 +1877,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen for (int a = 0; a < prices.size(); a++) { amount += prices.get(a).amount; } - return LocaleController.getInstance().formatCurrencyDecimalString(amount, paymentForm.invoice.currency); + return LocaleController.getInstance().formatCurrencyDecimalString(amount, paymentForm.invoice.currency, false); } @Override @@ -1781,7 +1926,9 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen protected void onTransitionAnimationEnd(boolean isOpen, boolean backward) { if (isOpen && !backward) { if (webView != null) { - webView.loadUrl(paymentForm.url); + if (currentStep != 4) { + webView.loadUrl(paymentForm.url); + } } else if (currentStep == 2) { inputFields[FIELD_CARD].requestFocus(); AndroidUtilities.showKeyboard(inputFields[FIELD_CARD]); @@ -1796,65 +1943,131 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen public void didReceivedNotification(int id, Object... args) { if (id == NotificationCenter.didSetTwoStepPassword) { paymentForm.password_missing = false; + paymentForm.can_save_credentials = true; updateSavePaymentField(); } else if (id == NotificationCenter.didRemovedTwoStepPassword) { paymentForm.password_missing = true; + paymentForm.can_save_credentials = false; updateSavePaymentField(); } else if (id == NotificationCenter.paymentFinished) { removeSelfFromStack(); } } - /*private void showAndroidPay(boolean show) { - if (show) { - if (getParentActivity() == null) { - return; - } - Activity parentActivity = getParentActivity(); - AlertDialog.Builder builder = new AlertDialog.Builder(parentActivity); - FrameLayout frameLayout = new FrameLayout(parentActivity); - walletFragment.onCreate(null); - walletFragment.onCreateView(parentActivity.getLayoutInflater(), frameLayout, null); - walletFragment.onStart(); - walletFragment.onResume(); - builder.setView(frameLayout); - builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); - showDialog(builder.create()); - } else { - WalletFragmentOptions.Builder optionsBuilder = WalletFragmentOptions.newBuilder(); - optionsBuilder.setEnvironment(WalletConstants.ENVIRONMENT_TEST); - optionsBuilder.setMode(WalletFragmentMode.BUY_BUTTON); - walletFragment = WalletFragment.newInstance(optionsBuilder.build()); - - ArrayList arrayList = new ArrayList<>(); - arrayList.addAll(paymentForm.invoice.prices); - if (shippingOption != null) { - arrayList.addAll(shippingOption.prices); - } - final String totalPrice = getTotalPriceDecimalString(arrayList); - - MaskedWalletRequest maskedWalletRequest = MaskedWalletRequest.newBuilder() - .setPaymentMethodTokenizationParameters(PaymentMethodTokenizationParameters.newBuilder() - .setPaymentMethodTokenizationType(PaymentMethodTokenizationType.PAYMENT_GATEWAY) - .addParameter("gateway", "stripe") - .addParameter("stripe:publishableKey", stripeApiKey) - .addParameter("stripe:version", StripeApiHandler.VERSION) - .build()) - - //.setShippingAddressRequired(true) - .setEstimatedTotalPrice(totalPrice) - .setCurrencyCode(paymentForm.invoice.currency) - .build(); - - WalletFragmentInitParams initParams = WalletFragmentInitParams.newBuilder() - .setMaskedWalletRequest(maskedWalletRequest) - .setMaskedWalletRequestCode(91) - .build(); - - walletFragment.initialize(initParams); - settingsCell1.setVisibility(View.VISIBLE); + private void showAndroidPay() { + if (getParentActivity() == null || bottomCell[0] == null) { + return; } - }*/ + + WalletFragmentOptions.Builder optionsBuilder = WalletFragmentOptions.newBuilder(); + optionsBuilder.setEnvironment(paymentForm.invoice.test ? WalletConstants.ENVIRONMENT_TEST : WalletConstants.ENVIRONMENT_PRODUCTION); + optionsBuilder.setMode(WalletFragmentMode.BUY_BUTTON); + + WalletFragmentStyle walletFragmentStyle = new WalletFragmentStyle() + .setBuyButtonText(WalletFragmentStyle.BuyButtonText.LOGO_ONLY) + .setBuyButtonAppearance(WalletFragmentStyle.BuyButtonAppearance.ANDROID_PAY_LIGHT_WITH_BORDER) + .setBuyButtonWidth(WalletFragmentStyle.Dimension.WRAP_CONTENT); + + optionsBuilder.setFragmentStyle(walletFragmentStyle); + WalletFragment walletFragment = WalletFragment.newInstance(optionsBuilder.build()); + FragmentManager fragmentManager = getParentActivity().getFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + fragmentTransaction.replace(fragment_container_id, walletFragment); + fragmentTransaction.commit(); + + ArrayList arrayList = new ArrayList<>(); + arrayList.addAll(paymentForm.invoice.prices); + if (shippingOption != null) { + arrayList.addAll(shippingOption.prices); + } + totalPriceDecimal = getTotalPriceDecimalString(arrayList); + + MaskedWalletRequest maskedWalletRequest = MaskedWalletRequest.newBuilder() + .setPaymentMethodTokenizationParameters(PaymentMethodTokenizationParameters.newBuilder() + .setPaymentMethodTokenizationType(PaymentMethodTokenizationType.PAYMENT_GATEWAY) + .addParameter("gateway", "stripe") + .addParameter("stripe:publishableKey", stripeApiKey) + .addParameter("stripe:version", StripeApiHandler.VERSION) + .build()) + + .setEstimatedTotalPrice(totalPriceDecimal) + .setCurrencyCode(paymentForm.invoice.currency) + .build(); + + WalletFragmentInitParams initParams = WalletFragmentInitParams.newBuilder() + .setMaskedWalletRequest(maskedWalletRequest) + .setMaskedWalletRequestCode(LOAD_MASKED_WALLET_REQUEST_CODE) + .build(); + + walletFragment.initialize(initParams); + androidPayContainer.setVisibility(View.VISIBLE); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(androidPayContainer, "alpha", 0.0f, 1.0f)); + animatorSet.setInterpolator(new DecelerateInterpolator()); + animatorSet.setDuration(180); + animatorSet.start(); + } + + @Override + public void onActivityResultFragment(int requestCode, int resultCode, Intent data) { + if (requestCode == LOAD_MASKED_WALLET_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + showEditDoneProgress(true, true); + setDonePressed(true); + + MaskedWallet maskedWallet = data.getParcelableExtra(WalletConstants.EXTRA_MASKED_WALLET); + + Cart.Builder cardBuilder = Cart.newBuilder() + .setCurrencyCode(paymentForm.invoice.currency) + .setTotalPrice(totalPriceDecimal); + + ArrayList arrayList = new ArrayList<>(); + arrayList.addAll(paymentForm.invoice.prices); + if (shippingOption != null) { + arrayList.addAll(shippingOption.prices); + } + for (int a = 0; a < arrayList.size(); a++) { + TLRPC.TL_labeledPrice price = arrayList.get(a); + String amount = LocaleController.getInstance().formatCurrencyDecimalString(price.amount, paymentForm.invoice.currency, false); + cardBuilder.addLineItem(LineItem.newBuilder() + .setCurrencyCode(paymentForm.invoice.currency) + .setQuantity("1") + .setDescription(price.label) + .setTotalPrice(amount) + .setUnitPrice(amount).build()); + } + FullWalletRequest fullWalletRequest = FullWalletRequest.newBuilder() + .setCart(cardBuilder.build()) + .setGoogleTransactionId(maskedWallet.getGoogleTransactionId()) + .build(); + Wallet.Payments.loadFullWallet(googleApiClient, fullWalletRequest, LOAD_FULL_WALLET_REQUEST_CODE); + } else { + showEditDoneProgress(true, false); + setDonePressed(false); + } + } else if (requestCode == LOAD_FULL_WALLET_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + FullWallet fullWallet = data.getParcelableExtra(WalletConstants.EXTRA_FULL_WALLET); + String tokenJSON = fullWallet.getPaymentMethodToken().getToken(); + + try { + Token token = TokenParser.parseToken(tokenJSON); + paymentJson = String.format(Locale.US, "{\"type\":\"%1$s\", \"id\":\"%2$s\"}", token.getType(), token.getId()); + Card card = token.getCard(); + cardName = card.getType() + " *" + card.getLast4(); + goToNextStep(); + showEditDoneProgress(true, false); + setDonePressed(false); + } catch (JSONException ignore) { + showEditDoneProgress(true, false); + setDonePressed(false); + } + } else { + showEditDoneProgress(true, false); + setDonePressed(false); + } + } + } private void goToNextStep() { if (currentStep == 0) { @@ -1876,7 +2089,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen } else { nextStep = 2; } - presentFragment(new PaymentFormActivity(paymentForm, messageObject, nextStep, requestedInfo, null, null, cardName, validateRequest, saveCardInfo)); + presentFragment(new PaymentFormActivity(paymentForm, messageObject, nextStep, requestedInfo, null, null, cardName, validateRequest, saveCardInfo), isWebView); } else if (currentStep == 1) { int nextStep; if (paymentForm.saved_credentials != null) { @@ -1894,13 +2107,13 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen } else { nextStep = 2; } - presentFragment(new PaymentFormActivity(paymentForm, messageObject, nextStep, requestedInfo, shippingOption, null, cardName, validateRequest, saveCardInfo)); + presentFragment(new PaymentFormActivity(paymentForm, messageObject, nextStep, requestedInfo, shippingOption, null, cardName, validateRequest, saveCardInfo), isWebView); } else if (currentStep == 2) { if (delegate != null) { delegate.didSelectNewCard(paymentJson, cardName, saveCardInfo); finishFragment(); } else { - presentFragment(new PaymentFormActivity(paymentForm, messageObject, 4, requestedInfo, shippingOption, paymentJson, cardName, validateRequest, saveCardInfo)); + presentFragment(new PaymentFormActivity(paymentForm, messageObject, 4, requestedInfo, shippingOption, paymentJson, cardName, validateRequest, saveCardInfo), isWebView); } } else if (currentStep == 3) { int nextStep; @@ -1917,10 +2130,10 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen } private void updateSavePaymentField() { - if (bottomCell[0] == null) { + if (bottomCell[0] == null || sectionCell[2] == null) { return; } - if (paymentForm.can_save_credentials && (webView == null || webView != null && !webviewLoading)) { + if ((paymentForm.password_missing || paymentForm.can_save_credentials) && (webView == null || webView != null && !webviewLoading)) { SpannableStringBuilder text = new SpannableStringBuilder(LocaleController.getString("PaymentCardSavePaymentInformationInfoLine1", R.string.PaymentCardSavePaymentInformationInfoLine1)); if (paymentForm.password_missing) { text.append("\n"); @@ -2000,7 +2213,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen private boolean sendCardData() { if (paymentForm.saved_credentials != null && !saveCardInfo && paymentForm.can_save_credentials) { - TLRPC.TL_payments_clearSavedInfo req = new TLRPC.TL_payments_clearSavedInfo(); + /*TLRPC.TL_payments_clearSavedInfo req = new TLRPC.TL_payments_clearSavedInfo(); req.credentials = true; paymentForm.saved_credentials = null; UserConfig.tmpPassword = null; @@ -2010,7 +2223,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen public void run(TLObject response, TLRPC.TL_error error) { } - }); + });*/ } Integer month; Integer year; @@ -2053,7 +2266,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen shakeField(FIELD_CARD_POSTCODE); return false; } - showEditDoneProgress(true); + showEditDoneProgress(true, true); try { Stripe stripe = new Stripe(stripeApiKey); stripe.createToken(card, new TokenCallback() { @@ -2066,7 +2279,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen @Override public void run() { goToNextStep(); - showEditDoneProgress(false); + showEditDoneProgress(true, false); setDonePressed(false); } }); @@ -2076,7 +2289,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen if (canceled) { return; } - showEditDoneProgress(false); + showEditDoneProgress(true, false); setDonePressed(false); if (error instanceof APIConnectionException || error instanceof APIException) { AlertsCreator.showSimpleToast(PaymentFormActivity.this, LocaleController.getString("PaymentConnectionFailed", R.string.PaymentConnectionFailed)); @@ -2096,7 +2309,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen if (canceled) { return; } - showEditDoneProgress(true); + showEditDoneProgress(true, true); validateRequest = new TLRPC.TL_payments_validateRequestedInfo(); validateRequest.save = saveShippingInfo; validateRequest.msg_id = messageObject.getId(); @@ -2110,7 +2323,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen validateRequest.info.flags |= 2; } if (paymentForm.invoice.email_requested) { - validateRequest.info.email = inputFields[FIELD_EMAIL].getText().toString(); + validateRequest.info.email = inputFields[FIELD_EMAIL].getText().toString().trim(); validateRequest.info.flags |= 4; } if (paymentForm.invoice.shipping_address_requested) { @@ -2144,7 +2357,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen } goToNextStep(); setDonePressed(false); - showEditDoneProgress(false); + showEditDoneProgress(true, false); } }); } else { @@ -2152,7 +2365,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen @Override public void run() { setDonePressed(false); - showEditDoneProgress(false); + showEditDoneProgress(true, false); if (error != null) { switch (error.text) { case "REQ_INFO_NAME_INVALID": @@ -2205,7 +2418,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen info.flags |= 2; } if (paymentForm.invoice.email_requested) { - info.email = inputFields[FIELD_EMAIL].getText().toString(); + info.email = inputFields[FIELD_EMAIL].getText().toString().trim(); info.flags |= 4; } if (paymentForm.invoice.shipping_address_requested) { @@ -2225,7 +2438,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen if (canceled) { return; } - showEditDoneProgress(true); + showEditDoneProgress(false, true); final TLRPC.TL_payments_sendPaymentForm req = new TLRPC.TL_payments_sendPaymentForm(); req.msg_id = messageObject.getId(); if (UserConfig.tmpPassword != null && paymentForm.saved_credentials != null) { @@ -2262,8 +2475,15 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - Browser.openUrl(getParentActivity(), ((TLRPC.TL_payments_paymentVerficationNeeded) response).url, false); - goToNextStep(); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.paymentFinished); + setDonePressed(false); + webView.setVisibility(View.VISIBLE); + webviewLoading = true; + showEditDoneProgress(true, true); + progressView.setVisibility(View.VISIBLE); + doneItem.setEnabled(false); + doneItem.getImageView().setVisibility(View.INVISIBLE); + webView.loadUrl(((TLRPC.TL_payments_paymentVerficationNeeded) response).url); } }); } @@ -2273,7 +2493,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen public void run() { AlertsCreator.processError(error, PaymentFormActivity.this, req); setDonePressed(false); - showEditDoneProgress(false); + showEditDoneProgress(false, false); } }); } @@ -2318,7 +2538,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen return; } final String password = inputFields[FIELD_SAVEDPASSWORD].getText().toString(); - showEditDoneProgress(true); + showEditDoneProgress(true, true); setDonePressed(true); final TLRPC.TL_account_getPassword req = new TLRPC.TL_account_getPassword(); ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @@ -2354,7 +2574,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - showEditDoneProgress(false); + showEditDoneProgress(true, false); setDonePressed(false); if (response != null) { passwordOk = true; @@ -2381,7 +2601,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen } } else { AlertsCreator.processError(error, PaymentFormActivity.this, req); - showEditDoneProgress(false); + showEditDoneProgress(true, false); setDonePressed(false); } } @@ -2390,11 +2610,11 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen }, ConnectionsManager.RequestFlagFailOnServerErrors); } - private void showEditDoneProgress(final boolean show) { + private void showEditDoneProgress(final boolean animateDoneItem, final boolean show) { if (doneItemAnimation != null) { doneItemAnimation.cancel(); } - if (doneItem != null) { + if (animateDoneItem && doneItem != null) { doneItemAnimation = new AnimatorSet(); if (show) { progressView.setVisibility(View.VISIBLE); @@ -2449,22 +2669,22 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen } else if (payTextView != null) { doneItemAnimation = new AnimatorSet(); if (show) { - progressView.setVisibility(View.VISIBLE); + progressViewButton.setVisibility(View.VISIBLE); bottomLayout.setEnabled(false); doneItemAnimation.playTogether( ObjectAnimator.ofFloat(payTextView, "scaleX", 0.1f), ObjectAnimator.ofFloat(payTextView, "scaleY", 0.1f), ObjectAnimator.ofFloat(payTextView, "alpha", 0.0f), - ObjectAnimator.ofFloat(progressView, "scaleX", 1.0f), - ObjectAnimator.ofFloat(progressView, "scaleY", 1.0f), - ObjectAnimator.ofFloat(progressView, "alpha", 1.0f)); + ObjectAnimator.ofFloat(progressViewButton, "scaleX", 1.0f), + ObjectAnimator.ofFloat(progressViewButton, "scaleY", 1.0f), + ObjectAnimator.ofFloat(progressViewButton, "alpha", 1.0f)); } else { payTextView.setVisibility(View.VISIBLE); bottomLayout.setEnabled(true); doneItemAnimation.playTogether( - ObjectAnimator.ofFloat(progressView, "scaleX", 0.1f), - ObjectAnimator.ofFloat(progressView, "scaleY", 0.1f), - ObjectAnimator.ofFloat(progressView, "alpha", 0.0f), + ObjectAnimator.ofFloat(progressViewButton, "scaleX", 0.1f), + ObjectAnimator.ofFloat(progressViewButton, "scaleY", 0.1f), + ObjectAnimator.ofFloat(progressViewButton, "alpha", 0.0f), ObjectAnimator.ofFloat(payTextView, "scaleX", 1.0f), ObjectAnimator.ofFloat(payTextView, "scaleY", 1.0f), ObjectAnimator.ofFloat(payTextView, "alpha", 1.0f)); @@ -2475,7 +2695,7 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen public void onAnimationEnd(Animator animation) { if (doneItemAnimation != null && doneItemAnimation.equals(animation)) { if (!show) { - progressView.setVisibility(View.INVISIBLE); + progressViewButton.setVisibility(View.INVISIBLE); } else { payTextView.setVisibility(View.INVISIBLE); } @@ -2513,6 +2733,8 @@ public class PaymentFormActivity extends BaseFragment implements NotificationCen arrayList.add(new ThemeDescription(linearLayout2, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider)); arrayList.add(new ThemeDescription(progressView, 0, null, null, null, null, Theme.key_contextProgressInner2)); arrayList.add(new ThemeDescription(progressView, 0, null, null, null, null, Theme.key_contextProgressOuter2)); + arrayList.add(new ThemeDescription(progressViewButton, 0, null, null, null, null, Theme.key_contextProgressInner2)); + arrayList.add(new ThemeDescription(progressViewButton, 0, null, null, null, null, Theme.key_contextProgressOuter2)); if (inputFields != null) { for (int a = 0; a < inputFields.length; a++) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java index 04946d8ce..fa2ebd550 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java @@ -10,9 +10,6 @@ package org.telegram.ui; import android.app.Activity; import android.content.Context; -import android.content.res.Configuration; -import android.os.Build; -import android.text.TextUtils; import android.view.Gravity; import android.view.MotionEvent; import android.view.Surface; @@ -36,7 +33,6 @@ import org.telegram.messenger.support.widget.RecyclerView; 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.BaseFragment; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Cells.PhotoPickerAlbumsCell; @@ -52,13 +48,12 @@ import java.util.HashMap; public class PhotoAlbumPickerActivity extends BaseFragment implements NotificationCenter.NotificationCenterDelegate { public interface PhotoAlbumPickerActivityDelegate { - void didSelectPhotos(ArrayList photos, ArrayList captions, ArrayList> masks, ArrayList webPhotos); + void didSelectPhotos(ArrayList photos, ArrayList captions, ArrayList videos, ArrayList> masks, ArrayList webPhotos); void didSelectVideo(String path, VideoEditedInfo info, long estimatedSize, long estimatedDuration, String caption); void startPhotoSelectActivity(); } private ArrayList albumsSorted = null; - private ArrayList videoAlbumsSorted = null; private HashMap selectedPhotos = new HashMap<>(); private HashMap selectedWebPhotos = new HashMap<>(); private HashMap recentImagesWebKeys = new HashMap<>(); @@ -72,21 +67,15 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati private ListAdapter listAdapter; private FrameLayout progressView; private TextView emptyView; - private TextView dropDown; - private ActionBarMenuItem dropDownContainer; private PickerBottomLayout pickerBottomLayout; private boolean sendPressed; private boolean singlePhoto; private boolean allowGifs; private boolean allowCaption; - private int selectedMode; private ChatActivity chatActivity; private PhotoAlbumPickerActivityDelegate delegate; - private final static int item_photos = 2; - private final static int item_video = 3; - public PhotoAlbumPickerActivity(boolean singlePhoto, boolean allowGifs, boolean allowCaption, ChatActivity chatActivity) { super(); this.chatActivity = chatActivity; @@ -130,22 +119,6 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati finishFragment(false); delegate.startPhotoSelectActivity(); } - } else if (id == item_photos) { - if (selectedMode == 0) { - return; - } - selectedMode = 0; - dropDown.setText(LocaleController.getString("PickerPhotos", R.string.PickerPhotos)); - emptyView.setText(LocaleController.getString("NoPhotos", R.string.NoPhotos)); - listAdapter.notifyDataSetChanged(); - } else if (id == item_video) { - if (selectedMode == 1) { - return; - } - selectedMode = 1; - dropDown.setText(LocaleController.getString("PickerVideo", R.string.PickerVideo)); - emptyView.setText(LocaleController.getString("NoVideo", R.string.NoVideo)); - listAdapter.notifyDataSetChanged(); } } }); @@ -158,50 +131,7 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati FrameLayout frameLayout = (FrameLayout) fragmentView; frameLayout.setBackgroundColor(0xff000000); - if (!singlePhoto) { - selectedMode = 0; - - dropDownContainer = new ActionBarMenuItem(context, menu, 0, 0); - dropDownContainer.setSubMenuOpenSide(1); - dropDownContainer.addSubItem(item_photos, LocaleController.getString("PickerPhotos", R.string.PickerPhotos)); - dropDownContainer.addSubItem(item_video, LocaleController.getString("PickerVideo", R.string.PickerVideo)); - actionBar.addView(dropDownContainer); - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) dropDownContainer.getLayoutParams(); - layoutParams.height = LayoutHelper.MATCH_PARENT; - layoutParams.width = LayoutHelper.WRAP_CONTENT; - layoutParams.rightMargin = AndroidUtilities.dp(40); - layoutParams.leftMargin = AndroidUtilities.isTablet() ? AndroidUtilities.dp(64) : AndroidUtilities.dp(56); - layoutParams.gravity = Gravity.TOP | Gravity.LEFT; - dropDownContainer.setLayoutParams(layoutParams); - dropDownContainer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - dropDownContainer.toggleSubMenu(); - } - }); - - dropDown = new TextView(context); - dropDown.setGravity(Gravity.LEFT); - dropDown.setSingleLine(true); - dropDown.setLines(1); - dropDown.setMaxLines(1); - dropDown.setEllipsize(TextUtils.TruncateAt.END); - dropDown.setTextColor(0xffffffff); - dropDown.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - dropDown.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_arrow_drop_down, 0); - dropDown.setCompoundDrawablePadding(AndroidUtilities.dp(4)); - dropDown.setPadding(0, 0, AndroidUtilities.dp(10), 0); - dropDown.setText(LocaleController.getString("PickerPhotos", R.string.PickerPhotos)); - dropDownContainer.addView(dropDown); - layoutParams = (FrameLayout.LayoutParams) dropDown.getLayoutParams(); - layoutParams.width = LayoutHelper.WRAP_CONTENT; - layoutParams.height = LayoutHelper.WRAP_CONTENT; - layoutParams.leftMargin = AndroidUtilities.dp(16); - layoutParams.gravity = Gravity.CENTER_VERTICAL; - dropDown.setLayoutParams(layoutParams); - } else { - actionBar.setTitle(LocaleController.getString("Gallery", R.string.Gallery)); - } + actionBar.setTitle(LocaleController.getString("Gallery", R.string.Gallery)); listView = new RecyclerListView(context); listView.setPadding(AndroidUtilities.dp(4), 0, AndroidUtilities.dp(4), AndroidUtilities.dp(4)); @@ -288,14 +218,6 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati return fragmentView; } - @Override - public void onPause() { - super.onPause(); - if (dropDownContainer != null) { - dropDownContainer.closeSubMenu(); - } - } - @Override public void onResume() { super.onResume(); @@ -317,8 +239,11 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati if (id == NotificationCenter.albumsDidLoaded) { int guid = (Integer) args[0]; if (classGuid == guid) { - albumsSorted = (ArrayList) args[1]; - videoAlbumsSorted = (ArrayList) args[3]; + if (singlePhoto) { + albumsSorted = (ArrayList) args[2]; + } else { + albumsSorted = (ArrayList) args[1]; + } if (progressView != null) { progressView.setVisibility(View.GONE); } @@ -360,11 +285,14 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati } sendPressed = true; ArrayList photos = new ArrayList<>(); + ArrayList videos = new ArrayList<>(); ArrayList captions = new ArrayList<>(); ArrayList> masks = new ArrayList<>(); for (HashMap.Entry entry : selectedPhotos.entrySet()) { MediaController.PhotoEntry photoEntry = entry.getValue(); - if (photoEntry.imagePath != null) { + if (photoEntry.isVideo) { + videos.add(photoEntry); + } else if (photoEntry.imagePath != null) { photos.add(photoEntry.imagePath); captions.add(photoEntry.caption != null ? photoEntry.caption.toString() : null); masks.add(!photoEntry.stickers.isEmpty() ? new ArrayList<>(photoEntry.stickers) : null); @@ -415,7 +343,7 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati MessagesStorage.getInstance().putWebRecent(recentGifImages); } - delegate.didSelectPhotos(photos, captions, masks, webPhotos); + delegate.didSelectPhotos(photos, captions, videos, masks, webPhotos); } private void fixLayout() { @@ -446,20 +374,6 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati columnsCount = 4; } listAdapter.notifyDataSetChanged(); - - if (dropDownContainer != null) { - if (!AndroidUtilities.isTablet()) { - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) dropDownContainer.getLayoutParams(); - layoutParams.topMargin = (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); - dropDownContainer.setLayoutParams(layoutParams); - } - - if (!AndroidUtilities.isTablet() && ApplicationLoader.applicationContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - dropDown.setTextSize(18); - } else { - dropDown.setTextSize(20); - } - } } private void openPhotoPicker(MediaController.AlbumEntry albumEntry, int type) { @@ -512,14 +426,10 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati @Override public int getItemCount() { - if (singlePhoto || selectedMode == 0) { - if (singlePhoto) { - return albumsSorted != null ? (int) Math.ceil(albumsSorted.size() / (float) columnsCount) : 0; - } - return 1 + (albumsSorted != null ? (int) Math.ceil(albumsSorted.size() / (float) columnsCount) : 0); - } else { - return (videoAlbumsSorted != null ? (int) Math.ceil(videoAlbumsSorted.size() / (float) columnsCount) : 0); + if (singlePhoto) { + return albumsSorted != null ? (int) Math.ceil(albumsSorted.size() / (float) columnsCount) : 0; } + return 1 + (albumsSorted != null ? (int) Math.ceil(albumsSorted.size() / (float) columnsCount) : 0); } @Override @@ -560,25 +470,16 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati photoPickerAlbumsCell.setAlbumsCount(columnsCount); for (int a = 0; a < columnsCount; a++) { int index; - if (singlePhoto || selectedMode == 1) { + if (singlePhoto) { index = position * columnsCount + a; } else { index = (position - 1) * columnsCount + a; } - if (singlePhoto || selectedMode == 0) { - if (index < albumsSorted.size()) { - MediaController.AlbumEntry albumEntry = albumsSorted.get(index); - photoPickerAlbumsCell.setAlbum(a, albumEntry); - } else { - photoPickerAlbumsCell.setAlbum(a, null); - } + if (index < albumsSorted.size()) { + MediaController.AlbumEntry albumEntry = albumsSorted.get(index); + photoPickerAlbumsCell.setAlbum(a, albumEntry); } else { - if (index < videoAlbumsSorted.size()) { - MediaController.AlbumEntry albumEntry = videoAlbumsSorted.get(index); - photoPickerAlbumsCell.setAlbum(a, albumEntry); - } else { - photoPickerAlbumsCell.setAlbum(a, null); - } + photoPickerAlbumsCell.setAlbum(a, null); } } photoPickerAlbumsCell.requestLayout(); @@ -587,7 +488,7 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati @Override public int getItemViewType(int i) { - if (singlePhoto || selectedMode == 1) { + if (singlePhoto) { return 0; } if (i == 0) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java index b20b76e84..cd8a208d3 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java @@ -15,7 +15,6 @@ import android.graphics.Bitmap; import android.graphics.Rect; import android.os.AsyncTask; import android.os.Build; -import android.os.Bundle; import android.view.Gravity; import android.view.Surface; import android.view.View; @@ -129,9 +128,6 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen this.singlePhoto = onlyOnePhoto; this.chatActivity = chatActivity; this.allowCaption = allowCaption; - if (selectedAlbum != null && selectedAlbum.isVideo) { - singlePhoto = true; - } } @Override @@ -302,7 +298,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen listView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @Override public void onItemClick(View view, int position) { - if (selectedAlbum != null && selectedAlbum.isVideo) { + /*if (selectedAlbum != null && selectedAlbum.isVideo) { if (position < 0 || position >= selectedAlbum.photos.size()) { return; } @@ -326,6 +322,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen videoEditedInfo.resultWidth = resultWidth; videoEditedInfo.resultHeight = resultHeight; videoEditedInfo.originalPath = videoPath; + videoEditedInfo.muted = videoEditedInfo.bitrate == -1; delegate.didSelectVideo(videoPath, videoEditedInfo, estimatedSize, estimatedDuration, caption); } }); @@ -340,7 +337,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen delegate.didSelectVideo(path, null, 0, 0, null); finishFragment(); } - } else { + } else {*/ ArrayList arrayList; if (selectedAlbum != null) { arrayList = (ArrayList) selectedAlbum.photos; @@ -359,7 +356,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen } PhotoViewer.getInstance().setParentActivity(getParentActivity()); PhotoViewer.getInstance().openPhotoForSelect(arrayList, position, singlePhoto ? 1 : 0, PhotoPickerActivity.this, chatActivity); - } + //} } }); @@ -541,7 +538,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen object.imageReceiver = cell.photoImage.getImageReceiver(); object.thumb = object.imageReceiver.getBitmap(); object.scale = cell.photoImage.getScaleX(); - cell.checkBox.setVisibility(View.GONE); + cell.showCheck(false); return object; } return null; @@ -627,7 +624,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen } } if (num == index) { - cell.checkBox.setVisibility(View.VISIBLE); + cell.showCheck(true); break; } } @@ -640,9 +637,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen View view = listView.getChildAt(a); if (view instanceof PhotoPickerPhotoCell) { PhotoPickerPhotoCell cell = (PhotoPickerPhotoCell) view; - if (cell.checkBox.getVisibility() != View.VISIBLE) { - cell.checkBox.setVisibility(View.VISIBLE); - } + cell.showCheck(true); } } } @@ -663,7 +658,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen } @Override - public void setPhotoChecked(int index) { + public void setPhotoChecked(int index, VideoEditedInfo videoEditedInfo) { boolean add = true; if (selectedAlbum != null) { if (index < 0 || index >= selectedAlbum.photos.size()) { @@ -672,9 +667,11 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen MediaController.PhotoEntry photoEntry = selectedAlbum.photos.get(index); if (selectedPhotos.containsKey(photoEntry.imageId)) { selectedPhotos.remove(photoEntry.imageId); + photoEntry.editedInfo = null; add = false; } else { selectedPhotos.put(photoEntry.imageId, photoEntry); + photoEntry.editedInfo = videoEditedInfo; } } else { MediaController.SearchImage photoEntry; @@ -723,6 +720,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen return; } MediaController.PhotoEntry photoEntry = selectedAlbum.photos.get(index); + photoEntry.editedInfo = videoEditedInfo; selectedPhotos.put(photoEntry.imageId, photoEntry); } } else if (selectedPhotos.isEmpty()) { @@ -1238,8 +1236,13 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen } else if (photoEntry.path != null) { imageView.setOrientation(photoEntry.orientation, true); if (photoEntry.isVideo) { + cell.videoInfoContainer.setVisibility(View.VISIBLE); + int minutes = photoEntry.duration / 60; + int seconds = photoEntry.duration - minutes * 60; + cell.videoTextView.setText(String.format("%d:%02d", minutes, seconds)); imageView.setImage("vthumb://" + photoEntry.imageId + ":" + photoEntry.path, null, mContext.getResources().getDrawable(R.drawable.nophotos)); } else { + cell.videoInfoContainer.setVisibility(View.INVISIBLE); imageView.setImage("thumb://" + photoEntry.imageId + ":" + photoEntry.path, null, mContext.getResources().getDrawable(R.drawable.nophotos)); } } else { @@ -1263,6 +1266,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen } else { imageView.setImageResource(R.drawable.nophotos); } + cell.videoInfoContainer.setVisibility(View.INVISIBLE); cell.setChecked(selectedWebPhotos.containsKey(photoEntry.id), false); if (photoEntry.document != null) { showing = PhotoViewer.getInstance().isShowingImage(FileLoader.getPathToAttach(photoEntry.document, true).getAbsolutePath()); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java index 68950692d..7ab664992 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java @@ -171,6 +171,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private ImageView muteItem; private ImageView captionItem; private ImageView compressItem; + private AnimatorSet compressItemAnimation; private AnimatorSet currentActionBarAnimation; private PhotoCropView photoCropView; @@ -272,8 +273,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private String currentPathObject; private Bitmap currentThumb; private boolean ignoreDidSetImage; + boolean fromCamera; private int avatarsDialogId; + private boolean isEvent; private long currentDialogId; private long mergeDialogId; private int totalImagesCount; @@ -542,6 +545,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public int clipBottomAddition; public int clipTopAddition; public float scale = 1.0f; + public boolean isEvent; } public static class EmptyPhotoViewerProvider implements PhotoViewerProvider { @@ -571,7 +575,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } @Override - public void setPhotoChecked(int index) { + public void setPhotoChecked(int index, VideoEditedInfo videoEditedInfo) { } @@ -617,7 +621,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat boolean isPhotoChecked(int index); - void setPhotoChecked(int index); + void setPhotoChecked(int index, VideoEditedInfo videoEditedInfo); boolean cancelButtonPressed(); @@ -768,7 +772,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat bottomTouchEnabled = true; } } - return child != aspectRatioFrameLayout && super.drawChild(canvas, child, drawingTime); + try { + return child != aspectRatioFrameLayout && super.drawChild(canvas, child, drawingTime); + } catch (Throwable ignore) { + return true; + } } } @@ -1075,7 +1083,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat f = FileLoader.getPathToMessage(currentMessageObject.messageOwner); } } else if (currentFileLocation != null) { - f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0); + f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0 || isEvent); } if (f.exists()) { @@ -1339,7 +1347,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (currentMessageObject != null) { f = FileLoader.getPathToMessage(currentMessageObject.messageOwner); } else if (currentFileLocation != null) { - f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0); + f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0 || isEvent); } if (f != null && f.exists()) { @@ -1553,7 +1561,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return true; } } else if (currentFileLocation != null) { - File f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0); + File f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0 || isEvent); if (f.exists()) { return true; } @@ -1798,6 +1806,16 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat videoPlayerSeekbar.setProgress(0); updateVideoInfo(); } + + @Override + public void didStartDragging() { + + } + + @Override + public void didStopDragging() { + + } }); videoTimelineViewContainer.addView(videoTimelineView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 44, Gravity.LEFT | Gravity.TOP, 0, 8, 0, 0)); @@ -1870,33 +1888,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public void onClick(View view) { if (placeProvider != null && !doneButtonPressed) { - VideoEditedInfo videoEditedInfo = null; - if (captionItem.getVisibility() == View.VISIBLE) { - videoEditedInfo = new VideoEditedInfo(); - videoEditedInfo.startTime = startTime; - videoEditedInfo.endTime = endTime; - videoEditedInfo.rotationValue = rotationValue; - videoEditedInfo.originalWidth = originalWidth; - videoEditedInfo.originalHeight = originalHeight; - videoEditedInfo.bitrate = bitrate; - videoEditedInfo.originalPath = currentPlayingVideoFile.getAbsolutePath(); - videoEditedInfo.estimatedSize = estimatedSize; - videoEditedInfo.estimatedDuration = estimatedDuration; - - if (compressItem.getVisibility() == View.GONE || compressItem.getVisibility() == View.VISIBLE && selectedCompression == compressionsCount - 1) { - videoEditedInfo.resultWidth = originalWidth; - videoEditedInfo.resultHeight = originalHeight; - videoEditedInfo.bitrate = muteVideo ? -1 : originalBitrate; - } else { - if (muteVideo) { - selectedCompression = 1; - updateWidthHeightBitrateForCompression(); - } - videoEditedInfo.resultWidth = resultWidth; - videoEditedInfo.resultHeight = resultHeight; - videoEditedInfo.bitrate = muteVideo ? -1 : bitrate; - } - } + VideoEditedInfo videoEditedInfo = getCurrentVideoEditedInfo(); placeProvider.sendButtonPressed(currentIndex, videoEditedInfo); doneButtonPressed = true; closePhoto(false, false); @@ -2078,7 +2070,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat @Override public void onClick(View v) { if (placeProvider != null) { - placeProvider.setPhotoChecked(currentIndex); + placeProvider.setPhotoChecked(currentIndex, getCurrentVideoEditedInfo()); checkImageView.setChecked(placeProvider.isPhotoChecked(currentIndex), true); updateSelectedCount(); } @@ -2312,6 +2304,39 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } + private VideoEditedInfo getCurrentVideoEditedInfo() { + if (captionItem == null || captionItem.getVisibility() != View.VISIBLE || Build.VERSION.SDK_INT < 16 || currentPlayingVideoFile == null) { + return null; + } + VideoEditedInfo videoEditedInfo = new VideoEditedInfo(); + videoEditedInfo.startTime = startTime; + videoEditedInfo.endTime = endTime; + videoEditedInfo.rotationValue = rotationValue; + videoEditedInfo.originalWidth = originalWidth; + videoEditedInfo.originalHeight = originalHeight; + videoEditedInfo.bitrate = bitrate; + videoEditedInfo.originalPath = currentPlayingVideoFile.getAbsolutePath(); + videoEditedInfo.estimatedSize = estimatedSize; + videoEditedInfo.estimatedDuration = estimatedDuration; + + if (!muteVideo && (compressItem.getTag() == null || selectedCompression == compressionsCount - 1)) { + videoEditedInfo.resultWidth = originalWidth; + videoEditedInfo.resultHeight = originalHeight; + videoEditedInfo.bitrate = muteVideo ? -1 : originalBitrate; + videoEditedInfo.muted = muteVideo; + } else { + if (muteVideo) { + selectedCompression = 1; + updateWidthHeightBitrateForCompression(); + } + videoEditedInfo.resultWidth = resultWidth; + videoEditedInfo.resultHeight = resultHeight; + videoEditedInfo.bitrate = muteVideo ? -1 : bitrate; + videoEditedInfo.muted = muteVideo; + } + return videoEditedInfo; + } + private void closeCaptionEnter(boolean apply) { if (currentIndex < 0 || currentIndex >= imagesArrLocals.size()) { return; @@ -2325,7 +2350,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } if (captionEditText.getFieldCharSequence().length() != 0 && !placeProvider.isPhotoChecked(currentIndex)) { - placeProvider.setPhotoChecked(currentIndex); + placeProvider.setPhotoChecked(currentIndex, getCurrentVideoEditedInfo()); checkImageView.setChecked(placeProvider.isPhotoChecked(currentIndex), true); updateSelectedCount(); } @@ -2336,7 +2361,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat lastTitle = null; } if (captionItem.getVisibility() == View.VISIBLE) { - actionBar.setTitle(muteVideo ? LocaleController.getString("AttachGif", R.string.AttachGif) : LocaleController.getString("AttachVideo", R.string.AttachVideo)); + if (fromCamera) { + actionBar.setTitle(muteVideo ? LocaleController.getString("AttachGif", R.string.AttachGif) : LocaleController.getString("AttachVideo", R.string.AttachVideo)); + } actionBar.setSubtitle(muteVideo ? null : currentSubtitle); captionItem.setImageResource(captionEditText.getFieldCharSequence().length() == 0 ? R.drawable.photo_text : R.drawable.photo_text2); } @@ -2378,12 +2405,12 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat @SuppressLint("NewApi") private void preparePlayer(File file, boolean playWhenReady, boolean preview) { - if (parentActivity == null) { - return; - } if (!preview) { currentPlayingVideoFile = file; } + if (parentActivity == null || Build.VERSION.SDK_INT < 16) { + return; + } inPreview = preview; releasePlayer(); if (videoTextureView == null) { @@ -2629,7 +2656,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (sendPhotoType == 0 && placeProvider != null) { placeProvider.updatePhotoAtIndex(currentIndex); if (!placeProvider.isPhotoChecked(currentIndex)) { - placeProvider.setPhotoChecked(currentIndex); + placeProvider.setPhotoChecked(currentIndex, null); checkImageView.setChecked(placeProvider.isPhotoChecked(currentIndex), true); updateSelectedCount(); } @@ -3147,6 +3174,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (captionTextView.getTag() != null) { arrayList.add(ObjectAnimator.ofFloat(captionTextView, "alpha", show ? 1.0f : 0.0f)); } + if (videoTimelineViewContainer != null) { + arrayList.add(ObjectAnimator.ofFloat(videoTimelineViewContainer, "alpha", show ? 1.0f : 0.0f)); + } currentActionBarAnimation = new AnimatorSet(); currentActionBarAnimation.playTogether(arrayList); if (!show) { @@ -3175,6 +3205,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (captionTextView.getTag() != null) { captionTextView.setAlpha(show ? 1.0f : 0.0f); } + if (videoTimelineViewContainer != null) { + videoTimelineViewContainer.setAlpha(show ? 1.0f : 0.0f); + } if (!show) { actionBar.setVisibility(View.GONE); if (canShowBottom) { @@ -3303,6 +3336,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat currentMessageObject = null; currentFileLocation = null; currentPathObject = null; + fromCamera = false; currentBotInlineResult = null; currentIndex = -1; currentFileNames[0] = null; @@ -3333,6 +3367,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat currentUserAvatarLocation = null; containerView.setPadding(0, 0, 0, 0); currentThumb = object != null ? object.thumb : null; + isEvent = object != null && object.isEvent; menuItem.setVisibility(View.VISIBLE); bottomLayout.setVisibility(View.VISIBLE); bottomLayout.setTranslationY(0); @@ -3393,7 +3428,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (messageObject != null && messages == null) { imagesArr.add(messageObject); - if (currentAnimation != null) { + if (currentAnimation != null || messageObject.eventId != 0) { needSearchImageInArr = false; } else if (!(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaInvoice) && !(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) && (messageObject.messageOwner.action == null || messageObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty)) { needSearchImageInArr = true; @@ -3444,8 +3479,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (obj instanceof MediaController.PhotoEntry) { if (((MediaController.PhotoEntry) obj).isVideo) { cropItem.setVisibility(View.GONE); - bottomLayout.setVisibility(View.VISIBLE); - bottomLayout.setTranslationY(-AndroidUtilities.dp(48)); + if (Build.VERSION.SDK_INT >= 16) { + bottomLayout.setVisibility(View.VISIBLE); + bottomLayout.setTranslationY(-AndroidUtilities.dp(48)); + } } else { cropItem.setVisibility(View.VISIBLE); } @@ -3482,7 +3519,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat updateSelectedCount(); } - if (currentAnimation == null) { + if (currentAnimation == null && !isEvent) { if (currentDialogId != 0 && totalImagesCount == 0) { SharedMediaQuery.getMediaCount(currentDialogId, SharedMediaQuery.MEDIA_PHOTOVIDEO, classGuid, true); if (mergeDialogId != 0) { @@ -3534,6 +3571,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat boolean isVideo = false; boolean sameImage = false; boolean isInvoice; + String videoPath = null; if (!imagesArr.isEmpty()) { if (currentIndex < 0 || currentIndex >= imagesArr.size()) { @@ -3676,7 +3714,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (old != null && currentFileLocation != null && old.local_id == currentFileLocation.local_id && old.volume_id == currentFileLocation.volume_id) { sameImage = true; } - actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, currentIndex + 1, imagesArrLocations.size())); + if (isEvent) { + actionBar.setTitle(LocaleController.getString("AttachPhoto", R.string.AttachPhoto)); + } else { + actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, currentIndex + 1, imagesArrLocations.size())); + } menuItem.showSubItem(gallery_menu_save); allowShare = true; shareButton.setVisibility(videoPlayerControlFrameLayout == null || videoPlayerControlFrameLayout.getVisibility() != View.VISIBLE ? View.VISIBLE : View.GONE); @@ -3691,7 +3733,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return; } Object object = imagesArrLocals.get(index); - boolean fromCamera = false; CharSequence caption = null; if (object instanceof MediaController.PhotoEntry) { MediaController.PhotoEntry photoEntry = ((MediaController.PhotoEntry) object); @@ -3699,6 +3740,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat fromCamera = photoEntry.bucketId == 0 && photoEntry.dateTaken == 0 && imagesArrLocals.size() == 1; caption = photoEntry.caption; isVideo = photoEntry.isVideo; + videoPath = photoEntry.path; } else if (object instanceof TLRPC.BotInlineResult) { TLRPC.BotInlineResult botInlineResult = currentBotInlineResult = ((TLRPC.BotInlineResult) object); if (botInlineResult.document != null) { @@ -3734,6 +3776,39 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat actionBar.setTitle(LocaleController.getString("AttachPhoto", R.string.AttachPhoto)); } } else { + if (isVideo) { + if (Build.VERSION.SDK_INT >= 16) { + muteItem.setVisibility(View.VISIBLE); + compressItem.setVisibility(View.VISIBLE); + bottomLayout.setTranslationY(-AndroidUtilities.dp(48)); + bottomLayout.setVisibility(View.VISIBLE); + processOpenVideo(currentPathObject); + videoTimelineViewContainer.setVisibility(View.VISIBLE); + } else { + bottomLayout.setVisibility(View.GONE); + } + captionItem.setVisibility(View.VISIBLE); + paintItem.setVisibility(View.GONE); + cropItem.setVisibility(View.GONE); + tuneItem.setVisibility(View.GONE); + captionTextViewNew.setTranslationY(AndroidUtilities.dp(96)); + captionTextViewOld.setTranslationY(AndroidUtilities.dp(96)); + } else { + muteItem.setVisibility(View.GONE); + captionItem.setVisibility(View.GONE); + compressItem.setVisibility(View.GONE); + paintItem.setVisibility(View.VISIBLE); + cropItem.setVisibility(View.VISIBLE); + tuneItem.setVisibility(View.VISIBLE); + bottomLayout.setVisibility(View.GONE); + bottomLayout.setTranslationY(0); + if (videoTimelineViewContainer != null) { + videoTimelineViewContainer.setVisibility(View.GONE); + } + captionTextViewNew.setTranslationY(0); + captionTextViewOld.setTranslationY(0); + actionBar.setSubtitle(null); + } actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, currentIndex + 1, imagesArrLocals.size())); } if (sendPhotoType == 0) { @@ -3795,6 +3870,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat canZoom = !imagesArrLocals.isEmpty() || (currentFileNames[0] != null && !isVideo && photoProgressViews[0].backgroundState != 0); updateMinMax(scale); } + if (isVideo && videoPath != null) { + preparePlayer(new File(videoPath), false, false); + } if (prevIndex == -1) { setImages(); @@ -3916,7 +3994,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } else if (currentFileLocation != null) { TLRPC.FileLocation location = imagesArrLocations.get(index); - f = FileLoader.getPathToAttach(location, avatarsDialogId != 0); + f = FileLoader.getPathToAttach(location, avatarsDialogId != 0 || isEvent); } else if (currentPathObject != null) { f = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_DOCUMENT), currentFileNames[a]); if (!f.exists()) { @@ -3972,8 +4050,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat TLRPC.FileLocation photo = null; int imageSize = 0; String filter = null; + boolean isVideo = false; if (object instanceof MediaController.PhotoEntry) { MediaController.PhotoEntry photoEntry = (MediaController.PhotoEntry) object; + isVideo = photoEntry.isVideo; if (!photoEntry.isVideo) { if (photoEntry.imagePath != null) { path = photoEntry.imagePath; @@ -3982,6 +4062,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat path = photoEntry.path; } filter = String.format(Locale.US, "%d_%d", size, size); + } else { + path = "vthumb://" + photoEntry.imageId + ":" + photoEntry.path; } } else if (object instanceof TLRPC.BotInlineResult) { TLRPC.BotInlineResult botInlineResult = ((TLRPC.BotInlineResult) object); @@ -4026,7 +4108,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else if (photo != null) { imageReceiver.setImage(photo, null, filter, placeHolder != null ? new BitmapDrawable(null, placeHolder) : null, null, String.format(Locale.US, "%d_%d", size, size), imageSize, null, false); } else { - imageReceiver.setImage(path, filter, placeHolder != null ? new BitmapDrawable(null, placeHolder) : null, null, imageSize); + imageReceiver.setImage(path, filter, placeHolder != null ? new BitmapDrawable(null, placeHolder) : (isVideo && parentActivity != null ? parentActivity.getResources().getDrawable(R.drawable.nophotos) : null), null, imageSize); } } else { imageReceiver.setImageBitmap((Bitmap) null); @@ -4070,7 +4152,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat size[0] = -1; } TLRPC.PhotoSize thumbLocation = messageObject != null ? FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, 100) : null; - imageReceiver.setImage(fileLocation, null, null, placeHolder != null ? new BitmapDrawable(null, placeHolder) : null, thumbLocation != null ? thumbLocation.location : null, "b", size[0], null, avatarsDialogId != 0); + imageReceiver.setImage(fileLocation, null, null, placeHolder != null ? new BitmapDrawable(null, placeHolder) : null, thumbLocation != null ? thumbLocation.location : null, "b", size[0], null, avatarsDialogId != 0 || isEvent); } } else { imageReceiver.setNeedsQualityThumb(false); @@ -4092,6 +4174,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return isVisible && !disableShowCheck && object != null && currentFileLocation != null && object.local_id == currentFileLocation.local_id && object.volume_id == currentFileLocation.volume_id && object.dc_id == currentFileLocation.dc_id; } + public boolean isShowingImage(TLRPC.BotInlineResult object) { + return isVisible && !disableShowCheck && object != null && currentBotInlineResult != null && object.id == currentBotInlineResult.id; + } + public boolean isShowingImage(String object) { return isVisible && !disableShowCheck && object != null && currentPathObject != null && object.equals(currentPathObject); } @@ -4812,6 +4898,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return true; } + if (qualityChooseView != null && qualityChooseView.getVisibility() == View.VISIBLE) { + return true; + } + if (currentEditMode == 1) { return true; } @@ -5531,6 +5621,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private long estimatedDuration; private long originalSize; + private Runnable currentLoadingVideoRunnable; private MessageObject videoPreviewMessageObject; private boolean tryStartRequestPreviewOnFinish; private boolean loadInitialVideo; @@ -5626,15 +5717,18 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - int width = MeasureSpec.getSize(widthMeasureSpec); circleSize = AndroidUtilities.dp(12); gapSize = AndroidUtilities.dp(2); sideSide = AndroidUtilities.dp(18); - lineSize = (getMeasuredWidth() - circleSize * compressionsCount - gapSize * 8 - sideSide * 2) / (compressionsCount - 1); } @Override protected void onDraw(Canvas canvas) { + if (compressionsCount != 1) { + lineSize = (getMeasuredWidth() - circleSize * compressionsCount - gapSize * 8 - sideSide * 2) / (compressionsCount - 1); + } else { + lineSize = (getMeasuredWidth() - circleSize * compressionsCount - gapSize * 8 - sideSide * 2); + } int cy = getMeasuredHeight() / 2 + AndroidUtilities.dp(6); for (int a = 0; a < compressionsCount; a++) { int cx = sideSide + (lineSize + gapSize * 2 + circleSize) * a + circleSize / 2; @@ -5671,20 +5765,24 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat videoPlayer.setMute(muteVideo); } if (muteVideo) { - actionBar.setTitle(LocaleController.getString("AttachGif", R.string.AttachGif)); + if (fromCamera) { + actionBar.setTitle(LocaleController.getString("AttachGif", R.string.AttachGif)); + } actionBar.setSubtitle(null); muteItem.setImageResource(R.drawable.volume_off); - if (compressItem.getVisibility() == View.VISIBLE) { + if (compressItem.getTag() != null) { compressItem.setClickable(false); compressItem.setAlpha(0.5f); compressItem.setEnabled(false); } videoTimelineView.setMaxProgressDiff(30000.0f / videoDuration); } else { - actionBar.setTitle(LocaleController.getString("AttachVideo", R.string.AttachVideo)); + if (fromCamera) { + actionBar.setTitle(LocaleController.getString("AttachVideo", R.string.AttachVideo)); + } actionBar.setSubtitle(currentSubtitle); muteItem.setImageResource(R.drawable.volume_on); - if (compressItem.getVisibility() == View.VISIBLE) { + if (compressItem.getTag() != null) { compressItem.setClickable(true); compressItem.setAlpha(1.0f); compressItem.setEnabled(true); @@ -5727,7 +5825,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat int width; int height; - if (compressItem.getVisibility() == View.GONE || compressItem.getVisibility() == View.VISIBLE && selectedCompression == compressionsCount - 1) { + if (compressItem.getTag() == null || selectedCompression == compressionsCount - 1) { width = rotationValue == 90 || rotationValue == 270 ? originalHeight : originalWidth; height = rotationValue == 90 || rotationValue == 270 ? originalWidth : originalHeight; estimatedSize = (int) (originalSize * ((float) estimatedDuration / videoDuration)); @@ -5929,137 +6027,195 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat qualityChooseViewAnimation.start(); } - private boolean processOpenVideo(String videoPath) { - try { - videoPreviewMessageObject = null; - compressItem.setVisibility(View.GONE); - muteVideo = false; - videoTimelineView.setVideoPath(videoPath); - compressionsCount = -1; - File file = new File(videoPath); - originalSize = file.length(); - - IsoFile isoFile = new IsoFile(videoPath); - List boxes = Path.getPaths(isoFile, "/moov/trak/"); - TrackHeaderBox trackHeaderBox = null; - boolean isAvc = true; - boolean isMp4A = true; - - Box boxTest = Path.getPath(isoFile, "/moov/trak/mdia/minf/stbl/stsd/mp4a/"); - if (boxTest == null) { - isMp4A = false; - } - - if (!isMp4A) { - return false; - } - - boxTest = Path.getPath(isoFile, "/moov/trak/mdia/minf/stbl/stsd/avc1/"); - if (boxTest == null) { - isAvc = false; - } - - for (int b = 0; b < boxes.size(); b++) { - Box box = boxes.get(b); - TrackBox trackBox = (TrackBox) box; - long sampleSizes = 0; - long trackBitrate = 0; - try { - MediaBox mediaBox = trackBox.getMediaBox(); - MediaHeaderBox mediaHeaderBox = mediaBox.getMediaHeaderBox(); - SampleSizeBox sampleSizeBox = mediaBox.getMediaInformationBox().getSampleTableBox().getSampleSizeBox(); - long[] sizes = sampleSizeBox.getSampleSizes(); - for (int a = 0; a < sizes.length; a++) { - sampleSizes += sizes[a]; - } - videoDuration = (float) mediaHeaderBox.getDuration() / (float) mediaHeaderBox.getTimescale(); - trackBitrate = (int) (sampleSizes * 8 / videoDuration); - } catch (Exception e) { - FileLog.e(e); - } - TrackHeaderBox headerBox = trackBox.getTrackHeaderBox(); - if (headerBox.getWidth() != 0 && headerBox.getHeight() != 0) { - trackHeaderBox = headerBox; - originalBitrate = bitrate = (int) (trackBitrate / 100000 * 100000); - if (bitrate > 900000) { - bitrate = 900000; - } - videoFramesSize += sampleSizes; - } else { - audioFramesSize += sampleSizes; - } - } - if (trackHeaderBox == null) { - return false; - } - - Matrix matrix = trackHeaderBox.getMatrix(); - if (matrix.equals(Matrix.ROTATE_90)) { - rotationValue = 90; - } else if (matrix.equals(Matrix.ROTATE_180)) { - rotationValue = 180; - } else if (matrix.equals(Matrix.ROTATE_270)) { - rotationValue = 270; - } - resultWidth = originalWidth = (int) trackHeaderBox.getWidth(); - resultHeight = originalHeight = (int) trackHeaderBox.getHeight(); - - videoDuration *= 1000; - - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - selectedCompression = preferences.getInt("compress_video2", 1); - if (originalWidth > 1280 || originalHeight > 1280) { - compressionsCount = 5; - } else if (originalWidth > 848 || originalHeight > 848) { - compressionsCount = 4; - } else if (originalWidth > 640 || originalHeight > 640) { - compressionsCount = 3; - } else if (originalWidth > 480 || originalHeight > 480) { - compressionsCount = 2; - } else { - compressionsCount = 1; - } - updateWidthHeightBitrateForCompression(); - - if (!isAvc && (resultWidth == originalWidth || resultHeight == originalHeight)) { - return false; - } - } catch (Exception e) { - FileLog.e(e); - return false; + private void processOpenVideo(final String videoPath) { + if (currentLoadingVideoRunnable != null) { + Utilities.globalQueue.cancelRunnable(currentLoadingVideoRunnable); + currentLoadingVideoRunnable = null; } + videoPreviewMessageObject = null; + setCompressItemEnabled(false, true); + muteVideo = false; + videoTimelineView.setVideoPath(videoPath); + compressionsCount = -1; + File file = new File(videoPath); + originalSize = file.length(); - compressItem.setVisibility(compressionsCount > 1 ? View.VISIBLE : View.GONE); - if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 18 && compressItem.getVisibility() == View.VISIBLE) { - try { - MediaCodecInfo codecInfo = MediaController.selectCodec(MediaController.MIME_TYPE); - if (codecInfo == null) { - compressItem.setVisibility(View.GONE); - } 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")) { - compressItem.setVisibility(View.GONE); - } else { - if (MediaController.selectColorFormat(codecInfo, MediaController.MIME_TYPE) == 0) { - compressItem.setVisibility(View.GONE); + Utilities.globalQueue.postRunnable(currentLoadingVideoRunnable = new Runnable() { + @Override + public void run() { + if (currentLoadingVideoRunnable != this) { + return; + } + TrackHeaderBox trackHeaderBox = null; + boolean isAvc = true; + try { + IsoFile isoFile = new IsoFile(videoPath); + List boxes = Path.getPaths(isoFile, "/moov/trak/"); + boolean isMp4A = true; + + Box boxTest = Path.getPath(isoFile, "/moov/trak/mdia/minf/stbl/stsd/mp4a/"); + if (boxTest == null) { + isMp4A = false; + } + + if (!isMp4A) { + return; + } + + boxTest = Path.getPath(isoFile, "/moov/trak/mdia/minf/stbl/stsd/avc1/"); + if (boxTest == null) { + isAvc = false; + } + + for (int b = 0; b < boxes.size(); b++) { + if (currentLoadingVideoRunnable != this) { + return; + } + Box box = boxes.get(b); + TrackBox trackBox = (TrackBox) box; + long sampleSizes = 0; + long trackBitrate = 0; + try { + MediaBox mediaBox = trackBox.getMediaBox(); + MediaHeaderBox mediaHeaderBox = mediaBox.getMediaHeaderBox(); + SampleSizeBox sampleSizeBox = mediaBox.getMediaInformationBox().getSampleTableBox().getSampleSizeBox(); + long[] sizes = sampleSizeBox.getSampleSizes(); + for (int a = 0; a < sizes.length; a++) { + if (currentLoadingVideoRunnable != this) { + return; + } + sampleSizes += sizes[a]; + } + videoDuration = (float) mediaHeaderBox.getDuration() / (float) mediaHeaderBox.getTimescale(); + trackBitrate = (int) (sampleSizes * 8 / videoDuration); + } catch (Exception e) { + FileLog.e(e); + } + if (currentLoadingVideoRunnable != this) { + return; + } + TrackHeaderBox headerBox = trackBox.getTrackHeaderBox(); + if (headerBox.getWidth() != 0 && headerBox.getHeight() != 0) { + trackHeaderBox = headerBox; + originalBitrate = bitrate = (int) (trackBitrate / 100000 * 100000); + if (bitrate > 900000) { + bitrate = 900000; + } + videoFramesSize += sampleSizes; + } else { + audioFramesSize += sampleSizes; } } + } catch (Exception e) { + FileLog.e(e); + return; } - } catch (Exception e) { - compressItem.setVisibility(View.GONE); - FileLog.e(e); + if (trackHeaderBox == null) { + return; + } + final boolean isAvcFinal = isAvc; + final TrackHeaderBox trackHeaderBoxFinal = trackHeaderBox; + if (currentLoadingVideoRunnable != this) { + return; + } + currentLoadingVideoRunnable = null; + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (parentActivity == null) { + return; + } + Matrix matrix = trackHeaderBoxFinal.getMatrix(); + if (matrix.equals(Matrix.ROTATE_90)) { + rotationValue = 90; + } else if (matrix.equals(Matrix.ROTATE_180)) { + rotationValue = 180; + } else if (matrix.equals(Matrix.ROTATE_270)) { + rotationValue = 270; + } + resultWidth = originalWidth = (int) trackHeaderBoxFinal.getWidth(); + resultHeight = originalHeight = (int) trackHeaderBoxFinal.getHeight(); + + if (!isAvcFinal && (resultWidth == originalWidth || resultHeight == originalHeight)) { + return; + } + + videoDuration *= 1000; + + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + selectedCompression = preferences.getInt("compress_video2", 1); + if (originalWidth > 1280 || originalHeight > 1280) { + compressionsCount = 5; + } else if (originalWidth > 848 || originalHeight > 848) { + compressionsCount = 4; + } else if (originalWidth > 640 || originalHeight > 640) { + compressionsCount = 3; + } else if (originalWidth > 480 || originalHeight > 480) { + compressionsCount = 2; + } else { + compressionsCount = 1; + } + updateWidthHeightBitrateForCompression(); + + setCompressItemEnabled(compressionsCount > 1, true); + if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 18 && compressItem.getTag() != null) { + try { + MediaCodecInfo codecInfo = MediaController.selectCodec(MediaController.MIME_TYPE); + if (codecInfo == null) { + 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")) { + setCompressItemEnabled(false, true); + } else { + if (MediaController.selectColorFormat(codecInfo, MediaController.MIME_TYPE) == 0) { + setCompressItemEnabled(false, true); + } + } + } + } catch (Exception e) { + setCompressItemEnabled(false, true); + FileLog.e(e); + } + } + qualityChooseView.invalidate(); + + updateVideoInfo(); + updateMuteButton(); + } + }); } + }); + } + + private void setCompressItemEnabled(boolean enabled, boolean animated) { + if (compressItem == null) { + return; + } + if (enabled && compressItem.getTag() != null || !enabled && compressItem.getTag() == null) { + return; + } + compressItem.setTag(enabled ? 1 : null); + compressItem.setEnabled(enabled); + compressItem.setClickable(enabled); + if (compressItemAnimation != null) { + compressItemAnimation.cancel(); + compressItemAnimation = null; + } + if (animated) { + compressItemAnimation = new AnimatorSet(); + compressItemAnimation.playTogether(ObjectAnimator.ofFloat(compressItem, "alpha", enabled ? 1.0f : 0.5f)); + compressItemAnimation.setDuration(180); + compressItemAnimation.setInterpolator(decelerateInterpolator); + compressItemAnimation.start(); + } else { + compressItem.setAlpha(enabled ? 1.0f : 0.5f); } - - updateVideoInfo(); - updateMuteButton(); - - return true; } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java index f3d4c6e8c..ad202d55e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java @@ -65,9 +65,11 @@ import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.PlayingGameDrawable; import org.telegram.ui.Components.PopupAudioView; import org.telegram.ui.Components.RecordStatusDrawable; +import org.telegram.ui.Components.RoundStatusDrawable; import org.telegram.ui.Components.SendingFileDrawable; import org.telegram.ui.Components.SizeNotifierFrameLayout; import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.StatusDrawable; import org.telegram.ui.Components.TypingDotsDrawable; import java.io.File; @@ -92,10 +94,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC private ArrayList imageViews = new ArrayList<>(); private ArrayList audioViews = new ArrayList<>(); private VelocityTracker velocityTracker = null; - private TypingDotsDrawable typingDotsDrawable; - private RecordStatusDrawable recordStatusDrawable; - private SendingFileDrawable sendingFileDrawable; - private PlayingGameDrawable playingGameDrawable; + private StatusDrawable[] statusDrawables = new StatusDrawable[5]; private int classGuid; private TLRPC.User currentUser; @@ -180,15 +179,16 @@ public class PopupNotificationActivity extends Activity implements NotificationC NotificationCenter.getInstance().addObserver(this, NotificationCenter.appDidLogout); NotificationCenter.getInstance().addObserver(this, NotificationCenter.pushMessagesUpdated); NotificationCenter.getInstance().addObserver(this, NotificationCenter.updateInterfaces); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioProgressDidChanged); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidReset); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.messagePlayingDidReset); NotificationCenter.getInstance().addObserver(this, NotificationCenter.contactsDidLoaded); NotificationCenter.getInstance().addObserver(this, NotificationCenter.emojiDidLoaded); - typingDotsDrawable = new TypingDotsDrawable(); - recordStatusDrawable = new RecordStatusDrawable(); - sendingFileDrawable = new SendingFileDrawable(); - playingGameDrawable = new PlayingGameDrawable(); + statusDrawables[0] = new TypingDotsDrawable(); + statusDrawables[1] = new RecordStatusDrawable(); + statusDrawables[2] = new SendingFileDrawable(); + statusDrawables[3] = new PlayingGameDrawable(); + statusDrawables[4] = new RoundStatusDrawable(); SizeNotifierFrameLayout contentView = new SizeNotifierFrameLayout(this) { @Override @@ -320,6 +320,16 @@ public class PopupNotificationActivity extends Activity implements NotificationC } + @Override + public void onSwitchRecordMode(boolean video) { + + } + + @Override + public void onPreAudioVideoRecord() { + + } + @Override public void onMessageEditEnd(boolean loading) { @@ -361,6 +371,21 @@ public class PopupNotificationActivity extends Activity implements NotificationC public void needStartRecordVideo(int state) { } + + @Override + public void needStartRecordAudio(int state) { + + } + + @Override + public void needChangeVideoPreviewState(int state, float seekProgress) { + + } + + @Override + public void needShowMediaBanHint() { + + } }); messageContainer = new FrameLayoutTouch(this); @@ -1161,34 +1186,14 @@ public class PopupNotificationActivity extends Activity implements NotificationC if (start) { try { Integer type = MessagesController.getInstance().printingStringsTypes.get(currentMessageObject.getDialogId()); - if (type == 0) { - onlineTextView.setCompoundDrawablesWithIntrinsicBounds(typingDotsDrawable, null, null, null); - onlineTextView.setCompoundDrawablePadding(AndroidUtilities.dp(4)); - typingDotsDrawable.start(); - recordStatusDrawable.stop(); - sendingFileDrawable.stop(); - playingGameDrawable.stop(); - } else if (type == 1) { - onlineTextView.setCompoundDrawablesWithIntrinsicBounds(recordStatusDrawable, null, null, null); - onlineTextView.setCompoundDrawablePadding(AndroidUtilities.dp(4)); - recordStatusDrawable.start(); - typingDotsDrawable.stop(); - sendingFileDrawable.stop(); - playingGameDrawable.stop(); - } else if (type == 2) { - onlineTextView.setCompoundDrawablesWithIntrinsicBounds(sendingFileDrawable, null, null, null); - onlineTextView.setCompoundDrawablePadding(AndroidUtilities.dp(4)); - sendingFileDrawable.start(); - typingDotsDrawable.stop(); - recordStatusDrawable.stop(); - playingGameDrawable.stop(); - } else if (type == 3) { - onlineTextView.setCompoundDrawablesWithIntrinsicBounds(playingGameDrawable, null, null, null); - onlineTextView.setCompoundDrawablePadding(AndroidUtilities.dp(4)); - playingGameDrawable.start(); - typingDotsDrawable.stop(); - recordStatusDrawable.stop(); - sendingFileDrawable.stop(); + onlineTextView.setCompoundDrawablesWithIntrinsicBounds(statusDrawables[type], null, null, null); + onlineTextView.setCompoundDrawablePadding(AndroidUtilities.dp(4)); + for (int a = 0; a < statusDrawables.length; a++) { + if (a == type) { + statusDrawables[a].start(); + } else { + statusDrawables[a].stop(); + } } } catch (Exception e) { FileLog.e(e); @@ -1196,10 +1201,9 @@ public class PopupNotificationActivity extends Activity implements NotificationC } else { onlineTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); onlineTextView.setCompoundDrawablePadding(0); - typingDotsDrawable.stop(); - recordStatusDrawable.stop(); - recordStatusDrawable.stop(); - sendingFileDrawable.stop(); + for (int a = 0; a < statusDrawables.length; a++) { + statusDrawables[a].stop(); + } } } @@ -1259,7 +1263,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC updateSubtitle(); } } - } else if (id == NotificationCenter.audioDidReset) { + } else if (id == NotificationCenter.messagePlayingDidReset) { Integer mid = (Integer) args[0]; if (messageContainer != null) { int count = messageContainer.getChildCount(); @@ -1274,7 +1278,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC } } } - } else if (id == NotificationCenter.audioProgressDidChanged) { + } else if (id == NotificationCenter.messagePlayingProgressDidChanged) { Integer mid = (Integer) args[0]; if (messageContainer != null) { int count = messageContainer.getChildCount(); @@ -1330,8 +1334,8 @@ public class PopupNotificationActivity extends Activity implements NotificationC NotificationCenter.getInstance().removeObserver(this, NotificationCenter.appDidLogout); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.pushMessagesUpdated); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.updateInterfaces); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioProgressDidChanged); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidReset); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.messagePlayingDidReset); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.contactsDidLoaded); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.emojiDidLoaded); if (chatActivityEnterView != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PrivacySettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PrivacySettingsActivity.java index 503aea66b..cfd265cae 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PrivacySettingsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PrivacySettingsActivity.java @@ -11,9 +11,11 @@ package org.telegram.ui; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; +import android.content.SharedPreferences; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; +import android.widget.LinearLayout; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; @@ -33,8 +35,10 @@ import org.telegram.messenger.UserConfig; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ActionBar.ThemeDescription; +import org.telegram.ui.Cells.CheckBoxCell; import org.telegram.ui.Cells.HeaderCell; import org.telegram.ui.Cells.TextCheckCell; import org.telegram.ui.Cells.TextInfoPrivacyCell; @@ -63,11 +67,19 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio private int deleteAccountSectionRow; private int deleteAccountRow; private int deleteAccountDetailRow; + private int paymentsSectionRow; + private int paymentsClearRow; + private int paymentsDetailRow; private int secretSectionRow; private int secretWebpageRow; private int secretDetailRow; + private int callsSectionRow; + private int callsP2PRow; + private int callsDetailRow; private int rowCount; + private boolean clear[] = new boolean[2]; + @Override public boolean onFragmentCreate() { super.onFragmentCreate(); @@ -93,6 +105,9 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio deleteAccountSectionRow = rowCount++; deleteAccountRow = rowCount++; deleteAccountDetailRow = rowCount++; + paymentsSectionRow = rowCount++; + paymentsClearRow = rowCount++; + paymentsDetailRow = rowCount++; if (MessagesController.getInstance().secretWebpagePreview != 1) { secretSectionRow = rowCount++; secretWebpageRow = rowCount++; @@ -102,6 +117,15 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio secretWebpageRow = -1; secretDetailRow = -1; } + if(MessagesController.getInstance().callsEnabled){ + callsSectionRow=rowCount++; + callsP2PRow=rowCount++; + callsDetailRow=rowCount++; + }else{ + callsSectionRow=-1; + callsP2PRow=-1; + callsDetailRow=-1; + } NotificationCenter.getInstance().addObserver(this, NotificationCenter.privacyRulesUpdated); @@ -229,6 +253,73 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio if (view instanceof TextCheckCell) { ((TextCheckCell) view).setChecked(MessagesController.getInstance().secretWebpagePreview == 1); } + } else if (position == callsP2PRow) { + SharedPreferences prefs=getParentActivity().getSharedPreferences("mainconfig", Context.MODE_PRIVATE); + boolean enableP2p=!prefs.getBoolean("calls_p2p", true); + prefs.edit().putBoolean("calls_p2p", enableP2p).commit(); + if (view instanceof TextCheckCell) { + ((TextCheckCell) view).setChecked(enableP2p); + } + } else if (position == paymentsClearRow) { + BottomSheet.Builder builder = new BottomSheet.Builder(getParentActivity()); + builder.setApplyTopPadding(false); + builder.setApplyBottomPadding(false); + LinearLayout linearLayout = new LinearLayout(getParentActivity()); + linearLayout.setOrientation(LinearLayout.VERTICAL); + for (int a = 0; a < 2; a++) { + String name = null; + if (a == 0) { + name = LocaleController.getString("PrivacyClearShipping", R.string.PrivacyClearShipping); + } else if (a == 1) { + name = LocaleController.getString("PrivacyClearPayment", R.string.PrivacyClearPayment); + } + clear[a] = true; + CheckBoxCell checkBoxCell = new CheckBoxCell(getParentActivity(), true); + checkBoxCell.setTag(a); + checkBoxCell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + linearLayout.addView(checkBoxCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); + checkBoxCell.setText(name, null, true, true); + checkBoxCell.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); + checkBoxCell.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + CheckBoxCell cell = (CheckBoxCell) v; + int num = (Integer) cell.getTag(); + clear[num] = !clear[num]; + cell.setChecked(clear[num], true); + } + }); + } + BottomSheet.BottomSheetCell cell = new BottomSheet.BottomSheetCell(getParentActivity(), 1); + cell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + cell.setTextAndIcon(LocaleController.getString("ClearButton", R.string.ClearButton).toUpperCase(), 0); + cell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText)); + cell.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + if (visibleDialog != null) { + visibleDialog.dismiss(); + } + } catch (Exception e) { + FileLog.e(e); + } + TLRPC.TL_payments_clearSavedInfo req = new TLRPC.TL_payments_clearSavedInfo(); + req.credentials = clear[1]; + req.info = clear[0]; + UserConfig.tmpPassword = null; + UserConfig.saveConfig(false); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + + } + }); + } + }); + linearLayout.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); + builder.setCustomView(linearLayout); + showDialog(builder.create()); } } }); @@ -318,7 +409,8 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio position == groupsRow && !ContactsController.getInstance().getLoadingGroupInfo() || position == lastSeenRow && !ContactsController.getInstance().getLoadingLastSeenInfo() || position == callsRow && !ContactsController.getInstance().getLoadingCallsInfo() || - position == deleteAccountRow && !ContactsController.getInstance().getLoadingDeleteInfo(); + position == deleteAccountRow && !ContactsController.getInstance().getLoadingDeleteInfo() || + position == paymentsClearRow || position == callsP2PRow; } @Override @@ -402,13 +494,15 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio } } textCell.setTextAndValue(LocaleController.getString("DeleteAccountIfAwayFor", R.string.DeleteAccountIfAwayFor), value, false); + } else if (position == paymentsClearRow) { + textCell.setText(LocaleController.getString("PrivacyPaymentsClear", R.string.PrivacyPaymentsClear), false); } break; case 1: TextInfoPrivacyCell privacyCell = (TextInfoPrivacyCell) holder.itemView; if (position == deleteAccountDetailRow) { privacyCell.setText(LocaleController.getString("DeleteAccountHelp", R.string.DeleteAccountHelp)); - privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, secretSectionRow == -1 ? R.drawable.greydivider_bottom : R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); + privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); } else if (position == groupsDetailRow) { privacyCell.setText(LocaleController.getString("GroupsAndChannelsHelp", R.string.GroupsAndChannelsHelp)); privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); @@ -417,6 +511,12 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); } else if (position == secretDetailRow) { privacyCell.setText(""); + privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, callsSectionRow == -1 ? R.drawable.greydivider_bottom : R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); + } else if (position == paymentsDetailRow) { + privacyCell.setText(LocaleController.getString("PrivacyPaymentsClearInfo", R.string.PrivacyPaymentsClearInfo)); + privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, secretSectionRow == -1 && callsSectionRow == -1 ? R.drawable.greydivider_bottom : R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); + } else if (position == callsDetailRow) { + privacyCell.setText(LocaleController.getString("PrivacyCallsP2PHelp", R.string.PrivacyCallsP2PHelp)); privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); } break; @@ -430,12 +530,19 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio headerCell.setText(LocaleController.getString("DeleteAccountTitle", R.string.DeleteAccountTitle)); } else if (position == secretSectionRow) { headerCell.setText(LocaleController.getString("SecretChat", R.string.SecretChat)); + } else if (position == paymentsSectionRow) { + headerCell.setText(LocaleController.getString("PrivacyPayments", R.string.PrivacyPayments)); + } else if (position == callsSectionRow) { + headerCell.setText(LocaleController.getString("Calls", R.string.Calls)); } break; case 3: TextCheckCell textCheckCell = (TextCheckCell) holder.itemView; if (position == secretWebpageRow) { textCheckCell.setTextAndCheck(LocaleController.getString("SecretWebPage", R.string.SecretWebPage), MessagesController.getInstance().secretWebpagePreview == 1, true); + } else if (position == callsP2PRow) { + SharedPreferences prefs=getParentActivity().getSharedPreferences("mainconfig", Context.MODE_PRIVATE); + textCheckCell.setTextAndCheck(LocaleController.getString("PrivacyCallsP2PTitle", R.string.PrivacyCallsP2PTitle), prefs.getBoolean("calls_p2p", true), true); } break; } @@ -443,13 +550,13 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio @Override public int getItemViewType(int position) { - if (position == lastSeenRow || position == blockedRow || position == deleteAccountRow || position == sessionsRow || position == passwordRow || position == passcodeRow || position == groupsRow) { + if (position == lastSeenRow || position == blockedRow || position == deleteAccountRow || position == sessionsRow || position == passwordRow || position == passcodeRow || position == groupsRow || position == paymentsClearRow) { return 0; - } else if (position == deleteAccountDetailRow || position == groupsDetailRow || position == sessionsDetailRow || position == secretDetailRow) { + } else if (position == deleteAccountDetailRow || position == groupsDetailRow || position == sessionsDetailRow || position == secretDetailRow || position == paymentsDetailRow || position==callsDetailRow) { return 1; - } else if (position == securitySectionRow || position == deleteAccountSectionRow || position == privacySectionRow || position == secretSectionRow) { + } else if (position == securitySectionRow || position == deleteAccountSectionRow || position == privacySectionRow || position == secretSectionRow || position == paymentsSectionRow || position==callsSectionRow) { return 2; - } else if (position == secretWebpageRow) { + } else if (position == secretWebpageRow || position==callsP2PRow) { 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 265cbf465..5e3330a30 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java @@ -124,6 +124,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private AvatarDrawable avatarDrawable; private ActionBarMenuItem animatingItem; private ActionBarMenuItem callItem; + private ActionBarMenuItem editItem; private TopView topView; private int user_id; private int chat_id; @@ -136,6 +137,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private HashMap participantsMap = new HashMap<>(); private boolean usersEndReached; + private int banFromGroup; private boolean openAnimationInProgress; private boolean recreateMenuAfterAnimation; private boolean playProfileAnimation; @@ -153,6 +155,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private TLRPC.EncryptedChat currentEncryptedChat; private TLRPC.Chat currentChat; private TLRPC.BotInfo botInfo; + private TLRPC.ChannelParticipant currentChannelParticipant; private int totalMediaCount = -1; private int totalMediaCountMerge = -1; @@ -171,6 +174,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private final static int convert_to_supergroup = 13; private final static int add_shortcut = 14; private final static int call_item = 15; + private final static int search_members = 16; private int emptyRow; private int emptyRowChat; @@ -186,8 +190,6 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private int settingsNotificationsRow; private int sharedMediaRow; private int membersRow; - private int managementRow; - private int blockedUsersRow; private int leaveChannelRow; private int groupsInCommonRow; private int startSecretChatRow; @@ -239,7 +241,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. @Override public boolean onFragmentCreate() { user_id = arguments.getInt("user_id", 0); - chat_id = getArguments().getInt("chat_id", 0); + chat_id = arguments.getInt("chat_id", 0); + banFromGroup = arguments.getInt("ban_chat_id", 0); if (user_id != 0) { dialog_id = arguments.getLong("dialog_id", 0); if (dialog_id != 0) { @@ -555,6 +558,12 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (user != null) { VoIPHelper.startCall(user, getParentActivity(), MessagesController.getInstance().getUserFull(user.id)); } + } else if (id == search_members) { + Bundle args = new Bundle(); + args.putInt("chat_id", chat_id); + args.putInt("type", 2); + args.putBoolean("open_search", true); + presentFragment(new ChannelUsersActivity(args)); } } }); @@ -586,7 +595,6 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } }; listView.setTag(6); - listView.setPadding(0, AndroidUtilities.dp(88), 0, 0); listView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); listView.setVerticalScrollBarEnabled(false); listView.setItemAnimator(null); @@ -776,16 +784,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } else if (position == leaveChannelRow) { leaveChatPressed(); - } else if (position == membersRow || position == blockedUsersRow || position == managementRow) { + } else if (position == membersRow) { Bundle args = new Bundle(); args.putInt("chat_id", chat_id); - if (position == blockedUsersRow) { - args.putInt("type", 0); - } else if (position == managementRow) { - args.putInt("type", 1); - } else if (position == membersRow) { - args.putInt("type", 2); - } + args.putInt("type", 2); presentFragment(new ChannelUsersActivity(args)); } else if (position == convertRow) { AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); @@ -814,6 +816,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } boolean allowKick = false; boolean allowSetAdmin = false; + boolean canEditAdmin = false; + final TLRPC.ChannelParticipant channelParticipant; final TLRPC.ChatParticipant user; if (!sortedUsers.isEmpty()) { @@ -824,19 +828,15 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. selectedUser = user.user_id; if (ChatObject.isChannel(currentChat)) { - TLRPC.ChannelParticipant channelParticipant = ((TLRPC.TL_chatChannelParticipant) user).channelParticipant; - if (user.user_id != UserConfig.getClientUserId()) { - if (currentChat.creator) { - allowKick = true; - } else if (channelParticipant instanceof TLRPC.TL_channelParticipant) { - if (currentChat.editor || channelParticipant.inviter_id == UserConfig.getClientUserId()) { - allowKick = true; - } - } + channelParticipant = ((TLRPC.TL_chatChannelParticipant) user).channelParticipant; + if (user.user_id == UserConfig.getClientUserId()) { + return false; } TLRPC.User u = MessagesController.getInstance().getUser(user.user_id); - allowSetAdmin = channelParticipant instanceof TLRPC.TL_channelParticipant && !u.bot; + allowSetAdmin = channelParticipant instanceof TLRPC.TL_channelParticipant || channelParticipant instanceof TLRPC.TL_channelParticipantBanned; + canEditAdmin = !(channelParticipant instanceof TLRPC.TL_channelParticipantAdmin || channelParticipant instanceof TLRPC.TL_channelParticipantCreator) || channelParticipant.can_edit; } else { + channelParticipant = null; if (user.user_id != UserConfig.getClientUserId()) { if (currentChat.creator) { allowKick = true; @@ -846,65 +846,94 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } } + if (!allowKick) { + return false; + } } - if (!allowKick) { + + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + + ArrayList items = new ArrayList<>(); + final ArrayList actions = new ArrayList<>(); + if (currentChat.megagroup) { + if (allowSetAdmin && ChatObject.canAddAdmins(currentChat)) { + items.add(LocaleController.getString("SetAsAdmin", R.string.SetAsAdmin)); + actions.add(0); + } + if (ChatObject.canBlockUsers(currentChat) && canEditAdmin) { + items.add(LocaleController.getString("KickFromSupergroup", R.string.KickFromSupergroup)); + actions.add(1); + items.add(LocaleController.getString("KickFromGroup", R.string.KickFromGroup)); + actions.add(2); + } + } else { + items.add(chat_id > 0 ? LocaleController.getString("KickFromGroup", R.string.KickFromGroup) : LocaleController.getString("KickFromBroadcast", R.string.KickFromBroadcast)); + actions.add(2); + } + if (items.isEmpty()) { return false; } - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - if (currentChat.megagroup && currentChat.creator && allowSetAdmin) { - CharSequence[] items = new CharSequence[]{LocaleController.getString("SetAsAdmin", R.string.SetAsAdmin), LocaleController.getString("KickFromGroup", R.string.KickFromGroup)}; - builder.setItems(items, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - if (i == 0) { - TLRPC.TL_chatChannelParticipant channelParticipant = ((TLRPC.TL_chatChannelParticipant) user); - - channelParticipant.channelParticipant = new TLRPC.TL_channelParticipantEditor(); - channelParticipant.channelParticipant.inviter_id = UserConfig.getClientUserId(); - channelParticipant.channelParticipant.user_id = user.user_id; - channelParticipant.channelParticipant.date = user.date; - - final TLRPC.TL_channels_editAdmin req = new TLRPC.TL_channels_editAdmin(); - req.channel = MessagesController.getInputChannel(chat_id); - req.user_id = MessagesController.getInputUser(selectedUser); - req.role = new TLRPC.TL_channelRoleEditor(); - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(TLObject response, final TLRPC.TL_error error) { - if (error == null) { - MessagesController.getInstance().processUpdates((TLRPC.Updates) response, false); - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - MessagesController.getInstance().loadFullChat(chat_id, 0, true); - } - }, 1000); + builder.setItems(items.toArray(new CharSequence[items.size()]), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, final int i) { + if (actions.get(i) == 2) { + kickUser(selectedUser); + } else { + ChannelRightsEditActivity fragment = new ChannelRightsEditActivity(user.user_id, chat_id, channelParticipant.admin_rights, channelParticipant.banned_rights, actions.get(i), true); + fragment.setDelegate(new ChannelRightsEditActivity.ChannelRightsEditActivityDelegate() { + @Override + public void didSetRights(int rights, TLRPC.TL_channelAdminRights rightsAdmin, TLRPC.TL_channelBannedRights rightsBanned) { + if (actions.get(i) == 0) { + TLRPC.TL_chatChannelParticipant channelParticipant = ((TLRPC.TL_chatChannelParticipant) user); + if (rights == 1) { + channelParticipant.channelParticipant = new TLRPC.TL_channelParticipantAdmin(); } else { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - AlertsCreator.processError(error, ProfileActivity.this, req, false); + channelParticipant.channelParticipant = new TLRPC.TL_channelParticipant(); + } + channelParticipant.channelParticipant.inviter_id = UserConfig.getClientUserId(); + channelParticipant.channelParticipant.user_id = user.user_id; + channelParticipant.channelParticipant.date = user.date; + channelParticipant.channelParticipant.banned_rights = rightsBanned; + channelParticipant.channelParticipant.admin_rights = rightsAdmin; + } else if (actions.get(i) == 1) { + if (rights == 0) { + if (currentChat.megagroup && info != null && info.participants != null) { + boolean changed = false; + for (int a = 0; a < info.participants.participants.size(); a++) { + TLRPC.ChannelParticipant p = ((TLRPC.TL_chatChannelParticipant) info.participants.participants.get(a)).channelParticipant; + if (p.user_id == user.user_id) { + if (info != null) { + info.participants_count--; + } + info.participants.participants.remove(a); + changed = true; + break; + } } - }); + if (info != null && info.participants != null) { + for (int a = 0; a < info.participants.participants.size(); a++) { + TLRPC.ChatParticipant p = info.participants.participants.get(a); + if (p.user_id == user.user_id) { + info.participants.participants.remove(a); + changed = true; + break; + } + } + } + if (changed) { + updateOnlineCount(); + updateRowsIds(); + listAdapter.notifyDataSetChanged(); + } + } } } - }); - } else if (i == 1) { - kickUser(selectedUser); - } + } + }); + presentFragment(fragment); } - }); - } else { - CharSequence[] items = new CharSequence[]{chat_id > 0 ? LocaleController.getString("KickFromGroup", R.string.KickFromGroup) : LocaleController.getString("KickFromBroadcast", R.string.KickFromBroadcast)}; - builder.setItems(items, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - if (i == 0) { - kickUser(selectedUser); - } - } - }); - } + } + }); showDialog(builder.create()); return true; } else { @@ -913,6 +942,65 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } }); + if (banFromGroup != 0) { + if (currentChannelParticipant == null) { + TLRPC.TL_channels_getParticipant req = new TLRPC.TL_channels_getParticipant(); + req.channel = MessagesController.getInputChannel(banFromGroup); + req.user_id = MessagesController.getInputUser(user_id); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, TLRPC.TL_error error) { + if (response != null) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + currentChannelParticipant = ((TLRPC.TL_channels_channelParticipant) response).participant; + } + }); + } + } + }); + } + FrameLayout frameLayout1 = new FrameLayout(context) { + @Override + protected void onDraw(Canvas canvas) { + int bottom = Theme.chat_composeShadowDrawable.getIntrinsicHeight(); + Theme.chat_composeShadowDrawable.setBounds(0, 0, getMeasuredWidth(), bottom); + Theme.chat_composeShadowDrawable.draw(canvas); + canvas.drawRect(0, bottom, getMeasuredWidth(), getMeasuredHeight(), Theme.chat_composeBackgroundPaint); + } + }; + frameLayout1.setWillNotDraw(false); + + frameLayout.addView(frameLayout1, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 51, Gravity.LEFT | Gravity.BOTTOM)); + frameLayout1.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ChannelRightsEditActivity fragment = new ChannelRightsEditActivity(user_id, banFromGroup, null, currentChannelParticipant != null ? currentChannelParticipant.banned_rights : null, 1, true); + fragment.setDelegate(new ChannelRightsEditActivity.ChannelRightsEditActivityDelegate() { + @Override + public void didSetRights(int rights, TLRPC.TL_channelAdminRights rightsAdmin, TLRPC.TL_channelBannedRights rightsBanned) { + removeSelfFromStack(); + } + }); + presentFragment(fragment); + } + }); + + TextView textView = new TextView(context); + textView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteRedText)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + textView.setGravity(Gravity.CENTER); + textView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + textView.setText(LocaleController.getString("BanFromTheGroup", R.string.BanFromTheGroup)); + frameLayout1.addView(textView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER, 0, 1, 0, 0)); + + listView.setPadding(0, AndroidUtilities.dp(88), 0, AndroidUtilities.dp(48)); + listView.setBottomGlowOffset(AndroidUtilities.dp(48)); + } else { + listView.setPadding(0, AndroidUtilities.dp(88), 0, 0); + } + topView = new TopView(context); topView.setBackgroundColor(AvatarDrawable.getProfileBackColorForId(user_id != 0 || ChatObject.isChannel(chat_id) && !currentChat.megagroup ? 5 : chat_id)); frameLayout.addView(topView); @@ -988,7 +1076,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. writeButton.setPadding(0, AndroidUtilities.dp(3), 0, 0); } else if (chat_id != 0) { boolean isChannel = ChatObject.isChannel(currentChat); - if (isChannel && !currentChat.creator && (!currentChat.megagroup || !currentChat.editor) || !isChannel && !currentChat.admin && !currentChat.creator && currentChat.admins_enabled) { + if (isChannel && !ChatObject.canEditInfo(currentChat) || !isChannel && !currentChat.admin && !currentChat.creator && currentChat.admins_enabled) { writeButton.setImageResource(R.drawable.floating_message); writeButton.setPadding(0, AndroidUtilities.dp(3), 0, 0); } else { @@ -1034,7 +1122,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } else if (chat_id != 0) { boolean isChannel = ChatObject.isChannel(currentChat); - if (isChannel && !currentChat.creator && (!currentChat.megagroup || !currentChat.editor) || !isChannel && !currentChat.admin && !currentChat.creator && currentChat.admins_enabled) { + if (isChannel && !ChatObject.canEditInfo(currentChat) || !isChannel && !currentChat.admin && !currentChat.creator && currentChat.admins_enabled) { if (playProfileAnimation && parentLayout.fragmentsStack.get(parentLayout.fragmentsStack.size() - 2) instanceof ChatActivity) { finishFragment(); } else { @@ -1305,7 +1393,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. args.putBoolean("needForwardCount", !ChatObject.isChannel(currentChat)); //args.putBoolean("allowUsernameSearch", false); if (chat_id > 0) { - if (currentChat.creator) { + if (ChatObject.canAddViaLink(currentChat)) { args.putInt("chat_id", currentChat.id); } args.putString("selectAlertString", LocaleController.getString("AddToTheGroup", R.string.AddToTheGroup)); @@ -1313,7 +1401,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. ContactsActivity fragment = new ContactsActivity(args); fragment.setDelegate(new ContactsActivity.ContactsActivityDelegate() { @Override - public void didSelectContact(TLRPC.User user, String param) { + public void didSelectContact(TLRPC.User user, String param, ContactsActivity activity) { MessagesController.getInstance().addUserToChat(chat_id, user, info, param != null ? Utilities.parseInt(param) : 0, null, ProfileActivity.this); } }); @@ -1433,7 +1521,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else { width = AndroidUtilities.displaySize.x; } - width = (int) (width - AndroidUtilities.dp(118 + 8 + (40 + (callItem != null ? 48 : 0)) * (1.0f - diff)) - nameTextView[a].getTranslationX()); + width = (int) (width - AndroidUtilities.dp(118 + 8 + (40 + (callItem != null || editItem != null ? 48 : 0)) * (1.0f - diff)) - nameTextView[a].getTranslationX()); float width2 = nameTextView[a].getPaint().measureText(nameTextView[a].getText().toString()) * nameTextView[a].getScaleX() + nameTextView[a].getSideDrawablesSize(); layoutParams = (FrameLayout.LayoutParams) nameTextView[a].getLayoutParams(); if (width < width2) { @@ -1832,6 +1920,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. callItem.setAlpha(0.0f); animators.add(ObjectAnimator.ofFloat(callItem, "alpha", 1.0f)); } + if (editItem != null) { + editItem.setAlpha(0.0f); + animators.add(ObjectAnimator.ofFloat(editItem, "alpha", 1.0f)); + } animatorSet.playTogether(animators); } else { initialAnimationExtraHeight = extraHeight; @@ -1854,6 +1946,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. callItem.setAlpha(1.0f); animators.add(ObjectAnimator.ofFloat(callItem, "alpha", 0.0f)); } + if (editItem != null) { + editItem.setAlpha(1.0f); + animators.add(ObjectAnimator.ofFloat(editItem, "alpha", 0.0f)); + } animatorSet.playTogether(animators); } animatorSet.addListener(new AnimatorListenerAdapter() { @@ -1960,7 +2056,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } @Override - public void setPhotoChecked(int index) { + public void setPhotoChecked(int index, VideoEditedInfo videoEditedInfo) { } @Override @@ -2054,6 +2150,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } private void fetchUsersFromChannelInfo() { + if (currentChat == null || !currentChat.megagroup) { + return; + } if (info instanceof TLRPC.TL_channelFull && info.participants != null) { for (int a = 0; a < info.participants.participants.size(); a++) { TLRPC.ChatParticipant chatParticipant = info.participants.participants.get(a); @@ -2065,35 +2164,6 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private void kickUser(int uid) { if (uid != 0) { MessagesController.getInstance().deleteUserFromChat(chat_id, MessagesController.getInstance().getUser(uid), info); - if (currentChat.megagroup && info != null && info.participants != null) { - boolean changed = false; - for (int a = 0; a < info.participants.participants.size(); a++) { - TLRPC.ChannelParticipant p = ((TLRPC.TL_chatChannelParticipant) info.participants.participants.get(a)).channelParticipant; - if (p.user_id == uid) { - if (info != null) { - info.participants_count--; - } - info.participants.participants.remove(a); - changed = true; - break; - } - } - if (info != null && info.participants != null) { - for (int a = 0; a < info.participants.participants.size(); a++) { - TLRPC.ChatParticipant p = info.participants.participants.get(a); - if (p.user_id == uid) { - info.participants.participants.remove(a); - changed = true; - break; - } - } - } - if (changed) { - updateOnlineCount(); - updateRowsIds(); - listAdapter.notifyDataSetChanged(); - } - } } else { NotificationCenter.getInstance().removeObserver(this, NotificationCenter.closeChats); if (AndroidUtilities.isTablet()) { @@ -2133,11 +2203,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. emptyRowChat = -1; membersSectionRow = -1; membersRow = -1; - managementRow = -1; leaveChannelRow = -1; loadMoreMembersRow = -1; groupsInCommonRow = -1; - blockedUsersRow = -1; rowCount = 0; if (user_id != 0) { @@ -2191,21 +2259,15 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (!currentChat.megagroup && info != null && (currentChat.creator || info.can_view_participants)) { membersRow = rowCount++; } - if (!ChatObject.isNotInChat(currentChat) && !currentChat.megagroup && (currentChat.creator || currentChat.editor || currentChat.moderator)) { - managementRow = rowCount++; - } - if (!ChatObject.isNotInChat(currentChat) && currentChat.megagroup && (currentChat.editor || currentChat.creator)) { - blockedUsersRow = rowCount++; - } if (!currentChat.creator && !currentChat.left && !currentChat.kicked && !currentChat.megagroup) { leaveChannelRow = rowCount++; } - if (currentChat.megagroup && (currentChat.editor || currentChat.creator || currentChat.democracy)) { + if (currentChat.megagroup && (currentChat.admin_rights != null && currentChat.admin_rights.invite_users || currentChat.creator || currentChat.democracy)) { if (info == null || info.participants_count < MessagesController.getInstance().maxMegagroupCount) { addMemberRow = rowCount++; } } - if (info != null && info.participants != null && !info.participants.participants.isEmpty()) { + if (info != null && currentChat.megagroup && info.participants != null && !info.participants.participants.isEmpty()) { emptyRowChat = rowCount++; membersSectionRow = rowCount++; emptyRowChat2 = rowCount++; @@ -2330,7 +2392,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else { if (currentChat.megagroup && info.participants_count <= 200) { if (onlineCount > 1 && info.participants_count != 0) { - newString = String.format("%s, %s", LocaleController.formatPluralString("Members", info.participants_count), LocaleController.formatPluralString("Online", onlineCount)); + newString = String.format("%s, %s", LocaleController.formatPluralString("Members", info.participants_count), LocaleController.formatPluralString("OnlineCount", onlineCount)); } else { newString = LocaleController.formatPluralString("Members", info.participants_count); } @@ -2346,7 +2408,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. count = info.participants.participants.size(); } if (count != 0 && onlineCount > 1) { - newString = String.format("%s, %s", LocaleController.formatPluralString("Members", count), LocaleController.formatPluralString("Online", onlineCount)); + newString = String.format("%s, %s", LocaleController.formatPluralString("Members", count), LocaleController.formatPluralString("OnlineCount", onlineCount)); } else { newString = LocaleController.formatPluralString("Members", count); } @@ -2448,7 +2510,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. TLRPC.Chat chat = MessagesController.getInstance().getChat(chat_id); if (writeButton != null) { boolean isChannel = ChatObject.isChannel(currentChat); - if (isChannel && !currentChat.creator && (!currentChat.megagroup || !currentChat.editor) || !isChannel && !currentChat.admin && !currentChat.creator && currentChat.admins_enabled) { + if (isChannel && !ChatObject.canChangeChatInfo(currentChat) || !isChannel && !currentChat.admin && !currentChat.creator && currentChat.admins_enabled) { writeButton.setImageResource(R.drawable.floating_message); writeButton.setPadding(0, AndroidUtilities.dp(3), 0, 0); } else { @@ -2457,23 +2519,36 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } if (ChatObject.isChannel(chat)) { - if (chat.creator || chat.megagroup && chat.editor) { - item = menu.addItem(10, R.drawable.ic_ab_other); - item.addSubItem(edit_channel, LocaleController.getString("ChannelEdit", R.string.ChannelEdit)); - } - if (!chat.creator && !chat.left && !chat.kicked && chat.megagroup) { + if (ChatObject.hasAdminRights(chat)) { + editItem = menu.addItem(edit_channel, R.drawable.menu_settings); if (item == null) { item = menu.addItem(10, R.drawable.ic_ab_other); } - item.addSubItem(leave_group, LocaleController.getString("LeaveMegaMenu", R.string.LeaveMegaMenu)); + if (chat.megagroup) { + item.addSubItem(edit_channel, LocaleController.getString("ManageGroupMenu", R.string.ManageGroupMenu)); + } else { + item.addSubItem(edit_channel, LocaleController.getString("ManageChannelMenu", R.string.ManageChannelMenu)); + } + } + if (chat.megagroup) { + if (item == null) { + item = menu.addItem(10, R.drawable.ic_ab_other); + } + item.addSubItem(search_members, LocaleController.getString("SearchMembers", R.string.SearchMembers)); + if (!chat.creator && !chat.left && !chat.kicked) { + item.addSubItem(leave_group, LocaleController.getString("LeaveMegaMenu", R.string.LeaveMegaMenu)); + } } } else { + if (!chat.admins_enabled || chat.creator || chat.admin) { + editItem = menu.addItem(edit_name, R.drawable.group_edit_profile); + } item = menu.addItem(10, R.drawable.ic_ab_other); if (chat.creator && chat_id > 0) { item.addSubItem(set_admins, LocaleController.getString("SetAdmins", R.string.SetAdmins)); } if (!chat.admins_enabled || chat.creator || chat.admin) { - item.addSubItem(edit_name, LocaleController.getString("EditName", R.string.EditName)); + item.addSubItem(edit_name, LocaleController.getString("ChannelEdit", R.string.ChannelEdit)); } if (chat.creator && (info == null || info.participants.participants.size() > 0)) { item.addSubItem(convert_to_supergroup, LocaleController.getString("ConvertGroupMenu", R.string.ConvertGroupMenu)); @@ -2768,30 +2843,18 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. textCell.setText(LocaleController.getString("UpgradeGroup", R.string.UpgradeGroup)); textCell.setTag(Theme.key_windowBackgroundWhiteGreenText2); textCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGreenText2)); - } else if (i == membersRow) { - if (info != null) { - textCell.setTextAndValue(LocaleController.getString("ChannelMembers", R.string.ChannelMembers), String.format("%d", info.participants_count)); - } else { - textCell.setText(LocaleController.getString("ChannelMembers", R.string.ChannelMembers)); - } - } else if (i == managementRow) { - if (info != null) { - textCell.setTextAndValue(LocaleController.getString("ChannelAdministrators", R.string.ChannelAdministrators), String.format("%d", info.admins_count)); - } else { - textCell.setText(LocaleController.getString("ChannelAdministrators", R.string.ChannelAdministrators)); - } - } else if (i == blockedUsersRow) { - if (info != null) { - textCell.setTextAndValue(LocaleController.getString("ChannelBlockedUsers", R.string.ChannelBlockedUsers), String.format("%d", info.kicked_count)); - } else { - textCell.setText(LocaleController.getString("ChannelBlockedUsers", R.string.ChannelBlockedUsers)); - } } else if (i == addMemberRow) { if (chat_id > 0) { textCell.setText(LocaleController.getString("AddMember", R.string.AddMember)); } else { textCell.setText(LocaleController.getString("AddRecipient", R.string.AddRecipient)); } + } else if (i == membersRow) { + if (info != null) { + textCell.setTextAndValue(LocaleController.getString("ChannelMembers", R.string.ChannelMembers), String.format("%d", info.participants_count)); + } else { + textCell.setText(LocaleController.getString("ChannelMembers", R.string.ChannelMembers)); + } } break; case 4: @@ -2807,7 +2870,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. TLRPC.ChannelParticipant channelParticipant = ((TLRPC.TL_chatChannelParticipant) part).channelParticipant; if (channelParticipant instanceof TLRPC.TL_channelParticipantCreator) { userCell.setIsAdmin(1); - } else if (channelParticipant instanceof TLRPC.TL_channelParticipantEditor || channelParticipant instanceof TLRPC.TL_channelParticipantModerator) { + } else if (channelParticipant instanceof TLRPC.TL_channelParticipantAdmin) { userCell.setIsAdmin(2); } else { userCell.setIsAdmin(0); @@ -2849,8 +2912,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. i == sharedMediaRow || i == startSecretChatRow || i == usernameRow || i == userInfoRow || i == groupsInCommonRow; } else if (chat_id != 0) { return i == convertRow || i == settingsNotificationsRow || i == sharedMediaRow || i > emptyRowChat2 && i < membersEndRow || - i == addMemberRow || i == channelNameRow || i == leaveChannelRow || i == membersRow || i == managementRow || - i == blockedUsersRow || i == channelInfoRow; + i == addMemberRow || i == channelNameRow || i == leaveChannelRow || i == channelInfoRow || i == membersRow; } return false; } @@ -2868,7 +2930,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. return 1; } else if (i == phoneRow || i == usernameRow || i == channelNameRow) { return 2; - } else if (i == leaveChannelRow || i == sharedMediaRow || i == settingsTimerRow || i == settingsNotificationsRow || i == startSecretChatRow || i == settingsKeyRow || i == membersRow || i == managementRow || i == blockedUsersRow || i == convertRow || i == addMemberRow || i == groupsInCommonRow) { + } else if (i == leaveChannelRow || i == sharedMediaRow || i == settingsTimerRow || i == settingsNotificationsRow || i == startSecretChatRow || i == settingsKeyRow || i == convertRow || i == addMemberRow || i == groupsInCommonRow || i == membersRow) { return 3; } else if (i > emptyRowChat2 && i < membersEndRow) { return 4; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProxySettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProxySettingsActivity.java new file mode 100644 index 000000000..bf2a9e505 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProxySettingsActivity.java @@ -0,0 +1,480 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2017. + */ + +package org.telegram.ui; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.text.Editable; +import android.text.InputType; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.PasswordTransformationMethod; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.NotificationCenter; +import org.telegram.messenger.R; +import org.telegram.messenger.Utilities; +import org.telegram.tgnet.ConnectionsManager; +import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.ActionBarMenuItem; +import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.ActionBar.ThemeDescription; +import org.telegram.ui.Cells.HeaderCell; +import org.telegram.ui.Cells.ShadowSectionCell; +import org.telegram.ui.Cells.TextCheckCell; +import org.telegram.ui.Cells.TextInfoPrivacyCell; +import org.telegram.ui.Components.LayoutHelper; + +import java.net.URLEncoder; +import java.util.ArrayList; + +public class ProxySettingsActivity extends BaseFragment implements NotificationCenter.NotificationCenterDelegate { + + private final static int FIELD_IP = 0; + private final static int FIELD_PORT = 1; + private final static int FIELD_USER = 2; + private final static int FIELD_PASSWORD = 3; + + private ActionBarMenuItem shareItem; + private EditText[] inputFields; + private ScrollView scrollView; + private LinearLayout linearLayout2; + private HeaderCell headerCell; + private ArrayList dividers = new ArrayList<>(); + private ShadowSectionCell sectionCell; + private TextInfoPrivacyCell bottomCell; + private TextInfoPrivacyCell useForCallsInfoCell; + private TextCheckCell checkCell1; + private TextCheckCell useForCallsCell; + + private boolean useProxySettings; + private boolean useProxyForCalls; + + private boolean ignoreOnTextChange; + + private static final int share_item = 1; + + @Override + public void onResume() { + super.onResume(); + AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); + } + + @Override + public boolean onFragmentCreate() { + NotificationCenter.getInstance().addObserver(this, NotificationCenter.proxySettingsChanged); + return super.onFragmentCreate(); + } + + @Override + public void onFragmentDestroy() { + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.proxySettingsChanged); + SharedPreferences.Editor editor = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).edit(); + editor.putBoolean("proxy_enabled", useProxySettings); + editor.putBoolean("proxy_enabled_calls", useProxyForCalls); + String address = inputFields[FIELD_IP].getText().toString(); + String password = inputFields[FIELD_PASSWORD].getText().toString(); + String user = inputFields[FIELD_USER].getText().toString(); + int port = Utilities.parseInt(inputFields[FIELD_PORT].getText().toString()); + editor.putString("proxy_ip", address); + editor.putString("proxy_pass", password); + editor.putString("proxy_user", user); + editor.putInt("proxy_port", port); + editor.commit(); + if (useProxySettings) { + ConnectionsManager.native_setProxySettings(address, port, user, password); + } else { + ConnectionsManager.native_setProxySettings("", 0, "", ""); + } + super.onFragmentDestroy(); + } + + @Override + public View createView(Context context) { + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + useProxySettings = preferences.getBoolean("proxy_enabled", false); + useProxyForCalls = preferences.getBoolean("proxy_enabled_calls", false); + + actionBar.setTitle(LocaleController.getString("ProxySettings", R.string.ProxySettings)); + actionBar.setBackButtonImage(R.drawable.ic_ab_back); + actionBar.setAllowOverlayTitle(true); + + actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { + @Override + public void onItemClick(int id) { + if (id == -1) { + finishFragment(); + } else if (id == share_item) { + if (getParentActivity() == null) { + return; + } + StringBuilder params = new StringBuilder(""); + String address = inputFields[FIELD_IP].getText().toString(); + String password = inputFields[FIELD_PASSWORD].getText().toString(); + String user = inputFields[FIELD_USER].getText().toString(); + String port = inputFields[FIELD_PORT].getText().toString(); + try { + if (!TextUtils.isEmpty(address)) { + params.append("server=").append(URLEncoder.encode(address, "UTF-8")); + } + if (!TextUtils.isEmpty(port)) { + if (params.length() != 0) { + params.append("&"); + } + params.append("port=").append(URLEncoder.encode(port, "UTF-8")); + } + if (!TextUtils.isEmpty(user)) { + if (params.length() != 0) { + params.append("&"); + } + params.append("user=").append(URLEncoder.encode(user, "UTF-8")); + } + if (!TextUtils.isEmpty(password)) { + if (params.length() != 0) { + params.append("&"); + } + params.append("pass=").append(URLEncoder.encode(password, "UTF-8")); + } + } catch (Exception ignore) { + return; + } + if (params.length() == 0) { + return; + } + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, "https://t.me/socks?" + params.toString()); + Intent chooserIntent = Intent.createChooser(shareIntent, LocaleController.getString("ShareLink", R.string.ShareLink)); + chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getParentActivity().startActivity(chooserIntent); + } + } + }); + + shareItem = actionBar.createMenu().addItem(share_item, R.drawable.abc_ic_menu_share_mtrl_alpha); + + fragmentView = new FrameLayout(context); + FrameLayout frameLayout = (FrameLayout) fragmentView; + fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); + + scrollView = new ScrollView(context); + scrollView.setFillViewport(true); + AndroidUtilities.setScrollViewEdgeEffectColor(scrollView, Theme.getColor(Theme.key_actionBarDefault)); + frameLayout.addView(scrollView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + linearLayout2 = new LinearLayout(context); + linearLayout2.setOrientation(LinearLayout.VERTICAL); + scrollView.addView(linearLayout2, new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + checkCell1 = new TextCheckCell(context); + checkCell1.setBackgroundDrawable(Theme.getSelectorDrawable(true)); + checkCell1.setTextAndCheck(LocaleController.getString("UseProxySettings", R.string.UseProxySettings), useProxySettings, false); + linearLayout2.addView(checkCell1, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + checkCell1.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + useProxySettings = !useProxySettings; + checkCell1.setChecked(useProxySettings); + if(!useProxySettings) + useForCallsCell.setChecked(false); + useForCallsCell.setEnabled(useProxySettings); + } + }); + + sectionCell = new ShadowSectionCell(context); + linearLayout2.addView(sectionCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + /*headerCell = new HeaderCell(context); + headerCell.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + headerCell.setText(LocaleController.getString("PaymentShippingAddress", R.string.PaymentShippingAddress)); + linearLayout2.addView(headerCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT));*/ + + inputFields = new EditText[4]; + for (int a = 0; a < 4; a++) { + FrameLayout container = new FrameLayout(context); + linearLayout2.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); + container.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + + boolean allowDivider = a != FIELD_PASSWORD; + if (allowDivider) { + View divider = new View(context); + dividers.add(divider); + divider.setBackgroundColor(Theme.getColor(Theme.key_divider)); + container.addView(divider, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1, Gravity.LEFT | Gravity.BOTTOM)); + } + + inputFields[a] = new EditText(context); + inputFields[a].setTag(a); + inputFields[a].setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + inputFields[a].setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + inputFields[a].setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + inputFields[a].setBackgroundDrawable(null); + AndroidUtilities.clearCursorDrawable(inputFields[a]); + + if (a == FIELD_IP) { + inputFields[a].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) { + checkShareButton(); + } + }); + } else if (a == FIELD_PORT) { + inputFields[a].setInputType(InputType.TYPE_CLASS_NUMBER); + inputFields[a].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) { + if (ignoreOnTextChange) { + return; + } + EditText phoneField = inputFields[FIELD_PORT]; + int start = phoneField.getSelectionStart(); + String chars = "0123456789"; + String str = phoneField.getText().toString(); + StringBuilder builder = new StringBuilder(str.length()); + for (int a = 0; a < str.length(); a++) { + String ch = str.substring(a, a + 1); + if (chars.contains(ch)) { + builder.append(ch); + } + } + ignoreOnTextChange = true; + boolean changed; + int port = Utilities.parseInt(builder.toString()); + if (port < 0 || port > 65535 || !str.equals(builder.toString())) { + if (port < 0) { + phoneField.setText("0"); + } else if (port > 65535) { + phoneField.setText("65535"); + } else { + phoneField.setText(builder.toString()); + } + } else { + if (start >= 0) { + phoneField.setSelection(start <= phoneField.length() ? start : phoneField.length()); + } + } + ignoreOnTextChange = false; + checkShareButton(); + } + }); + } else if (a == FIELD_PASSWORD) { + inputFields[a].setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + inputFields[a].setTypeface(Typeface.DEFAULT); + inputFields[a].setTransformationMethod(PasswordTransformationMethod.getInstance()); + } else { + inputFields[a].setInputType(InputType.TYPE_CLASS_TEXT); + } + inputFields[a].setImeOptions(EditorInfo.IME_ACTION_NEXT | EditorInfo.IME_FLAG_NO_EXTRACT_UI); + switch (a) { + case FIELD_IP: + inputFields[a].setHint(LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress)); + inputFields[a].setText(preferences.getString("proxy_ip", "")); + break; + case FIELD_PASSWORD: + inputFields[a].setHint(LocaleController.getString("UseProxyPassword", R.string.UseProxyPassword)); + inputFields[a].setText(preferences.getString("proxy_pass", "")); + break; + case FIELD_PORT: + inputFields[a].setHint(LocaleController.getString("UseProxyPort", R.string.UseProxyPort)); + inputFields[a].setText("" + preferences.getInt("proxy_port", 1080)); + break; + case FIELD_USER: + inputFields[a].setHint(LocaleController.getString("UseProxyUsername", R.string.UseProxyUsername)); + inputFields[a].setText(preferences.getString("proxy_user", "")); + break; + } + inputFields[a].setSelection(inputFields[a].length()); + + + + inputFields[a].setPadding(0, 0, 0, AndroidUtilities.dp(6)); + inputFields[a].setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); + container.addView(inputFields[a], LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 17, 12, 17, 6)); + + inputFields[a].setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) { + if (i == EditorInfo.IME_ACTION_NEXT) { + int num = (Integer) textView.getTag(); + if (num + 1 < inputFields.length) { + num++; + inputFields[num].requestFocus(); + } + return true; + } else if (i == EditorInfo.IME_ACTION_DONE) { + finishFragment(); + return true; + } + return false; + } + }); + } + + bottomCell = new TextInfoPrivacyCell(context); + bottomCell.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + bottomCell.setText(LocaleController.getString("UseProxyInfo", R.string.UseProxyInfo)); + linearLayout2.addView(bottomCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + useForCallsCell = new TextCheckCell(context); + useForCallsCell.setBackgroundDrawable(Theme.getSelectorDrawable(true)); + useForCallsCell.setTextAndCheck(LocaleController.getString("UseProxyForCalls", R.string.UseProxyForCalls), useProxyForCalls, false); + useForCallsCell.setEnabled(useProxySettings); + linearLayout2.addView(useForCallsCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + useForCallsCell.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + useProxyForCalls = !useProxyForCalls; + useForCallsCell.setChecked(useProxyForCalls); + } + }); + + useForCallsInfoCell = new TextInfoPrivacyCell(context); + useForCallsInfoCell.setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + useForCallsInfoCell.setText(LocaleController.getString("UseProxyForCallsInfo", R.string.UseProxyForCallsInfo)); + linearLayout2.addView(useForCallsInfoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + checkShareButton(); + + return fragmentView; + } + + private void checkShareButton() { + if (inputFields[FIELD_IP] == null || inputFields[FIELD_PORT] == null) { + return; + } + if (inputFields[FIELD_IP].length() != 0 && Utilities.parseInt(inputFields[FIELD_PORT].getText().toString()) != 0) { + shareItem.setAlpha(1.0f); + shareItem.setEnabled(true); + } else { + shareItem.setAlpha(0.5f); + shareItem.setEnabled(false); + } + } + + @Override + protected void onTransitionAnimationEnd(boolean isOpen, boolean backward) { + if (isOpen && !backward) { + inputFields[FIELD_IP].requestFocus(); + AndroidUtilities.showKeyboard(inputFields[FIELD_IP]); + } + } + + @Override + public void didReceivedNotification(int id, Object... args) { + if (id == NotificationCenter.proxySettingsChanged) { + if (checkCell1 == null) { + return; + } + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + useProxySettings = preferences.getBoolean("proxy_enabled", false); + if (!useProxySettings) { + checkCell1.setChecked(false); + } else { + checkCell1.setChecked(true); + for (int a = 0; a < 4; a++) { + switch (a) { + case FIELD_IP: + inputFields[a].setText(preferences.getString("proxy_ip", "")); + break; + case FIELD_PASSWORD: + inputFields[a].setText(preferences.getString("proxy_pass", "")); + break; + case FIELD_PORT: + inputFields[a].setText("" + preferences.getInt("proxy_port", 1080)); + break; + case FIELD_USER: + inputFields[a].setText(preferences.getString("proxy_user", "")); + break; + } + } + } + } + } + + @Override + public ThemeDescription[] getThemeDescriptions() { + ArrayList arrayList = new ArrayList<>(); + arrayList.add(new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(scrollView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCH, null, null, null, null, Theme.key_actionBarDefaultSearch)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCHPLACEHOLDER, null, null, null, null, Theme.key_actionBarDefaultSearchPlaceholder)); + arrayList.add(new ThemeDescription(linearLayout2, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider)); + + if (inputFields != null) { + for (int a = 0; a < inputFields.length; a++) { + arrayList.add(new ThemeDescription((View) inputFields[a].getParent(), ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + } + } else { + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + } + arrayList.add(new ThemeDescription(headerCell, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); + arrayList.add(new ThemeDescription(headerCell, 0, new Class[]{HeaderCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader)); + arrayList.add(new ThemeDescription(sectionCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow)); + arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow)); + arrayList.add(new ThemeDescription(bottomCell, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4)); + arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_LINKCOLOR, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteLinkText)); + for (int a = 0; a < dividers.size(); a++) { + arrayList.add(new ThemeDescription(dividers.get(a), ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_divider)); + } + + arrayList.add(new ThemeDescription(checkCell1, 0, new Class[]{TextCheckCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(checkCell1, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchThumb)); + arrayList.add(new ThemeDescription(checkCell1, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchTrack)); + arrayList.add(new ThemeDescription(checkCell1, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchThumbChecked)); + arrayList.add(new ThemeDescription(checkCell1, 0, new Class[]{TextCheckCell.class}, new String[]{"checkBox"}, null, null, null, Theme.key_switchTrackChecked)); + arrayList.add(new ThemeDescription(checkCell1, ThemeDescription.FLAG_SELECTORWHITE, null, null, null, null, Theme.key_windowBackgroundWhite)); + arrayList.add(new ThemeDescription(checkCell1, ThemeDescription.FLAG_SELECTORWHITE, null, null, null, null, Theme.key_listSelector)); + + return arrayList.toArray(new ThemeDescription[arrayList.size()]); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java index e5bd4559f..97075dd97 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java @@ -157,6 +157,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter private int sendLogsRow; private int clearLogsRow; private int switchBackendButtonRow; + private int dumpCallStatsRow; private int versionRow; private int contactsSectionRow; private int contactsReimportRow; @@ -272,6 +273,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter if (BuildVars.DEBUG_VERSION) { sendLogsRow = rowCount++; clearLogsRow = rowCount++; + dumpCallStatsRow = rowCount++; switchBackendButtonRow = rowCount++; } versionRow = rowCount++; @@ -423,7 +425,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter return; } final TextView message = new TextView(getParentActivity()); - Spannable spanned = new SpannableString(Html.fromHtml(LocaleController.getString("AskAQuestionInfo", R.string.AskAQuestionInfo))); + Spannable spanned = new SpannableString(Html.fromHtml(LocaleController.getString("AskAQuestionInfo", R.string.AskAQuestionInfo).replace("\n", "
    "))); URLSpan[] spans = spanned.getSpans(0, spanned.length(), URLSpan.class); for (int a = 0; a < spans.length; a++) { URLSpan span = spans[a]; @@ -611,6 +613,15 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter linearLayout.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); builder.setCustomView(linearLayout); showDialog(builder.create()); + } else if (position == dumpCallStatsRow) { + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + boolean dump = preferences.getBoolean("dbg_dump_call_stats", false); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean("dbg_dump_call_stats", !dump); + editor.commit(); + if (view instanceof TextCheckCell) { + ((TextCheckCell) view).setChecked(!dump); + } } } }); @@ -860,7 +871,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter } @Override - public void setPhotoChecked(int index) { + public void setPhotoChecked(int index, VideoEditedInfo videoEditedInfo) { } @Override @@ -1218,7 +1229,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter textCell.setText(LocaleController.getString("ImportContacts", R.string.ImportContacts), true); } else if (position == stickersRow) { int count = StickersQuery.getUnreadStickerSets().size(); - textCell.setTextAndValue(LocaleController.getString("Stickers", R.string.Stickers), count != 0 ? String.format("%d", count) : "", true); + textCell.setTextAndValue(LocaleController.getString("StickersName", R.string.StickersName), count != 0 ? String.format("%d", count) : "", true); } else if (position == privacyPolicyRow) { textCell.setText(LocaleController.getString("PrivacyPolicy", R.string.PrivacyPolicy), true); } else if (position == emojiRow) { @@ -1243,6 +1254,8 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter textCell.setTextAndValueAndCheck(LocaleController.getString("ChromeCustomTabs", R.string.ChromeCustomTabs), LocaleController.getString("ChromeCustomTabsInfo", R.string.ChromeCustomTabsInfo), MediaController.getInstance().canCustomTabs(), false, true); } else if (position == directShareRow) { textCell.setTextAndValueAndCheck(LocaleController.getString("DirectShare", R.string.DirectShare), LocaleController.getString("DirectShareInfo", R.string.DirectShareInfo), MediaController.getInstance().canDirectShare(), false, true); + } else if (position == dumpCallStatsRow) { + textCell.setTextAndCheck("Dump detailed call stats", preferences.getBoolean("dbg_dump_call_stats", false), true); } break; } @@ -1293,7 +1306,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter position == clearLogsRow || position == languageRow || position == usernameRow || position == switchBackendButtonRow || position == telegramFaqRow || position == contactsSortRow || position == contactsReimportRow || position == saveToGalleryRow || position == stickersRow || position == raiseToSpeakRow || position == privacyPolicyRow || position == customTabsRow || position == directShareRow || position == versionRow || - position == emojiRow || position == dataRow || position == themeRow; + position == emojiRow || position == dataRow || position == themeRow || position == dumpCallStatsRow; } @Override @@ -1363,7 +1376,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter } if (position == settingsSectionRow || position == supportSectionRow || position == messagesSectionRow || position == contactsSectionRow) { return 1; - } else if (position == enableAnimationsRow || position == sendByEnterRow || position == saveToGalleryRow || position == autoplayGifsRow || position == raiseToSpeakRow || position == customTabsRow || position == directShareRow) { + } else if (position == enableAnimationsRow || position == sendByEnterRow || position == saveToGalleryRow || position == autoplayGifsRow || position == raiseToSpeakRow || position == customTabsRow || position == directShareRow || position == dumpCallStatsRow) { return 3; } else if (position == notificationRow || position == themeRow || position == backgroundRow || position == askQuestionRow || position == sendLogsRow || position == privacyRow || position == clearLogsRow || position == switchBackendButtonRow || position == telegramFaqRow || position == contactsReimportRow || position == textSizeRow || position == languageRow || position == contactsSortRow || position == stickersRow || position == privacyPolicyRow || position == emojiRow || position == dataRow) { return 2; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java index 6e2a76183..5dc80604f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java @@ -152,7 +152,7 @@ public class StickersActivity extends BaseFragment implements NotificationCenter actionBar.setBackButtonImage(R.drawable.ic_ab_back); actionBar.setAllowOverlayTitle(true); if (currentType == StickersQuery.TYPE_IMAGE) { - actionBar.setTitle(LocaleController.getString("Stickers", R.string.Stickers)); + actionBar.setTitle(LocaleController.getString("StickersName", R.string.StickersName)); } else { actionBar.setTitle(LocaleController.getString("Masks", R.string.Masks)); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java index eee21c073..6b0118198 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java @@ -184,7 +184,7 @@ public class ThemeActivity extends BaseFragment { Theme.ThemeInfo themeInfo = Theme.themes.get(position); Theme.applyTheme(themeInfo); if (parentLayout != null) { - parentLayout.rebuildAllFragmentViews(false); + parentLayout.rebuildAllFragmentViews(false, false); } finishFragment(); } @@ -295,8 +295,7 @@ public class ThemeActivity extends BaseFragment { } else if (which == 1) { if (parentLayout != null) { Theme.applyTheme(themeInfo); - parentLayout.rebuildAllFragmentViews(true); - parentLayout.showLastFragment(); + parentLayout.rebuildAllFragmentViews(true, true); new ThemeEditorView().show(getParentActivity(), themeInfo.name); } } else { @@ -310,8 +309,7 @@ public class ThemeActivity extends BaseFragment { @Override public void onClick(DialogInterface dialogInterface, int i) { if (Theme.deleteTheme(themeInfo)) { - parentLayout.rebuildAllFragmentViews(true); - parentLayout.showLastFragment(); + parentLayout.rebuildAllFragmentViews(true, true); } if (listAdapter != null) { listAdapter.notifyDataSetChanged(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ThemePreviewActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ThemePreviewActivity.java index 0d4490dba..2c0dcba27 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ThemePreviewActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ThemePreviewActivity.java @@ -330,7 +330,7 @@ public class ThemePreviewActivity extends BaseFragment implements NotificationCe @Override public void onClick(View v) { Theme.applyPreviousTheme(); - parentLayout.rebuildAllFragmentViews(false); + parentLayout.rebuildAllFragmentViews(false, false); finishFragment(); } }); @@ -348,7 +348,7 @@ public class ThemePreviewActivity extends BaseFragment implements NotificationCe @Override public void onClick(View v) { applied = true; - parentLayout.rebuildAllFragmentViews(false); + parentLayout.rebuildAllFragmentViews(false, false); Theme.applyThemeFile(themeFile, applyingTheme.name, false); finishFragment(); } @@ -383,7 +383,7 @@ public class ThemePreviewActivity extends BaseFragment implements NotificationCe @Override public boolean onBackPressed() { Theme.applyPreviousTheme(); - parentLayout.rebuildAllFragmentViews(false); + parentLayout.rebuildAllFragmentViews(false, false); return super.onBackPressed(); } @@ -739,7 +739,7 @@ public class ThemePreviewActivity extends BaseFragment implements NotificationCe } @Override - public boolean needPlayAudio(MessageObject messageObject) { + public boolean needPlayMessage(MessageObject messageObject) { return false; } @@ -804,7 +804,7 @@ public class ThemePreviewActivity extends BaseFragment implements NotificationCe } @Override - public void didPressedInstantButton(ChatMessageCell cell) { + public void didPressedInstantButton(ChatMessageCell cell, int type) { } }); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/VideoEditorActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/VideoEditorActivity.java index 4c9f3f028..fd3f4dc20 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/VideoEditorActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/VideoEditorActivity.java @@ -692,10 +692,7 @@ public class VideoEditorActivity extends BaseFragment implements NotificationCen } } if (delegate != null) { - if (muteVideo) { - - } - if (compressItem.getVisibility() == View.GONE || compressItem.getVisibility() == View.VISIBLE && selectedCompression == compressionsCount - 1) { + if (!muteVideo && (compressItem.getVisibility() == View.GONE || compressItem.getVisibility() == View.VISIBLE && selectedCompression == compressionsCount - 1)) { delegate.didFinishEditVideo(videoPath, startTime, endTime, originalWidth, originalHeight, rotationValue, originalWidth, originalHeight, muteVideo ? -1 : originalBitrate, estimatedSize, esimatedDuration, currentCaption != null ? currentCaption.toString() : null); } else { if (muteVideo) { @@ -838,6 +835,16 @@ public class VideoEditorActivity extends BaseFragment implements NotificationCen videoSeekBarView.setProgress(videoTimelineView.getLeftProgress()); updateVideoInfo(); } + + @Override + public void didStartDragging() { + + } + + @Override + public void didStopDragging() { + + } }); frameLayout.addView(videoTimelineView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 44, Gravity.LEFT | Gravity.BOTTOM, 0, 0, 0, 67)); @@ -1588,6 +1595,7 @@ public class VideoEditorActivity extends BaseFragment implements NotificationCen Box boxTest = Path.getPath(isoFile, "/moov/trak/mdia/minf/stbl/stsd/mp4a/"); if (boxTest == null) { + FileLog.d("audio track not found"); isMp4A = false; } @@ -1597,6 +1605,7 @@ public class VideoEditorActivity extends BaseFragment implements NotificationCen boxTest = Path.getPath(isoFile, "/moov/trak/mdia/minf/stbl/stsd/avc1/"); if (boxTest == null) { + FileLog.d("video track not found"); isAvc = false; } @@ -1631,6 +1640,7 @@ public class VideoEditorActivity extends BaseFragment implements NotificationCen } } if (trackHeaderBox == null) { + FileLog.d("video track header box not found"); return false; } @@ -1652,6 +1662,7 @@ public class VideoEditorActivity extends BaseFragment implements NotificationCen updateWidthHeightBitrateForCompression(); if (!isAvc && (resultWidth == originalWidth || resultHeight == originalHeight)) { + FileLog.d("video is not mp4"); return false; } } catch (Exception e) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/VoIPActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/VoIPActivity.java index c155dede5..8daf551e3 100755 --- a/TMessagesProj/src/main/java/org/telegram/ui/VoIPActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/VoIPActivity.java @@ -17,6 +17,7 @@ import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; +import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -26,6 +27,7 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; @@ -35,11 +37,13 @@ import android.media.AudioManager; import android.os.Build; import android.os.Bundle; import android.support.v7.graphics.Palette; +import android.text.Layout; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextPaint; import android.text.TextUtils; import android.text.style.CharacterStyle; +import android.text.style.ForegroundColorSpan; import android.util.TypedValue; import android.view.Gravity; import android.view.KeyEvent; @@ -49,11 +53,13 @@ import android.view.Window; import android.view.WindowManager; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; +import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.NumberPicker; import android.widget.RelativeLayout; +import android.widget.ScrollView; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; @@ -126,9 +132,6 @@ public class VoIPActivity extends Activity implements VoIPService.StateListener, private boolean retrying; private AnimatorSet retryAnim; - private int audioBitrate = 25; - private int packetLossPercent = 5; - @Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); @@ -749,6 +752,10 @@ public class VoIPActivity extends Activity implements VoIPService.StateListener, @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode == 101) { + if(VoIPService.getSharedInstance()==null){ + finish(); + return; + } if (grantResults.length>0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { VoIPService.getSharedInstance().acceptIncomingCall(); callAccepted(); @@ -791,81 +798,84 @@ public class VoIPActivity extends Activity implements VoIPService.StateListener, } } + private CharSequence getFormattedDebugString(){ + String in=VoIPService.getSharedInstance().getDebugString(); + SpannableString ss=new SpannableString(in); + + int offset=0; + do{ + int lineEnd=in.indexOf('\n', offset+1); + if(lineEnd==-1) + lineEnd=in.length(); + String line=in.substring(offset, lineEnd); + if(line.contains("IN_USE")){ + ss.setSpan(new ForegroundColorSpan(0xFF00FF00), offset, lineEnd, 0); + }else{ + if(line.contains(": ")){ + ss.setSpan(new ForegroundColorSpan(0xAAFFFFFF), offset, offset+line.indexOf(':')+1, 0); + } + } + }while((offset=in.indexOf('\n', offset+1))!=-1); + + return ss; + } + private void showDebugAlert() { if(VoIPService.getSharedInstance()==null) return; - final AlertDialog dlg = new AlertDialog.Builder(this) - .setTitle("libtgvoip v" + VoIPController.getVersion() + " debug") - .setMessage(VoIPService.getSharedInstance().getDebugString()) - .setPositiveButton("Close", null) - .create(); + final LinearLayout debugOverlay=new LinearLayout(this); + debugOverlay.setOrientation(LinearLayout.VERTICAL); + debugOverlay.setBackgroundColor(0xCC000000); + int pad=AndroidUtilities.dp(16); + debugOverlay.setPadding(pad, pad*2, pad, pad*2); + + TextView title=new TextView(this); + title.setTextColor(0xFFFFFFFF); + title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + title.setTypeface(Typeface.DEFAULT_BOLD); + title.setGravity(Gravity.CENTER); + title.setText("libtgvoip v"+VoIPController.getVersion()); + debugOverlay.addView(title, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 0, 0, 0, 16)); + + ScrollView scroll=new ScrollView(this); + final TextView debugText=new TextView(this); + debugText.setTypeface(Typeface.MONOSPACE); + debugText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 11); + debugText.setMaxWidth(AndroidUtilities.dp(350)); + debugText.setTextColor(0xFFFFFFFF); + debugText.setText(getFormattedDebugString()); + scroll.addView(debugText); + debugOverlay.addView(scroll, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, 1f)); + + TextView closeBtn=new TextView(this); + closeBtn.setBackgroundColor(0xFFFFFFFF); + closeBtn.setTextColor(0xFF000000); + closeBtn.setPadding(pad, pad, pad, pad); + closeBtn.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + closeBtn.setText(LocaleController.getString("Close", R.string.Close)); + debugOverlay.addView(closeBtn, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL, 0, 16, 0, 0)); + + final WindowManager wm=(WindowManager) getSystemService(WINDOW_SERVICE); + wm.addView(debugOverlay, new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, 0, PixelFormat.TRANSLUCENT)); + + closeBtn.setOnClickListener(new View.OnClickListener(){ + @Override + public void onClick(View v){ + wm.removeView(debugOverlay); + } + }); + final Runnable r = new Runnable() { @Override public void run() { - if (!dlg.isShowing() || isFinishing() || VoIPService.getSharedInstance() == null) { + if (isFinishing() || VoIPService.getSharedInstance() == null) { return; } - dlg.setMessage(VoIPService.getSharedInstance().getDebugString()); - dlg.getWindow().getDecorView().postDelayed(this, 500); + debugText.setText(getFormattedDebugString()); + debugOverlay.postDelayed(this, 500); } }; - dlg.show(); - dlg.getWindow().getDecorView().postDelayed(r, 500); - } - - private void showDebugCtlAlert() { - new AlertDialog.Builder(this) - .setItems(new String[]{"Set audio bitrate", "Set expect packet loss %", "Disable p2p", "Enable p2p", "Disable AEC", "Enable AEC"}, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case 0: - showNumberPickerDialog(8, 32, audioBitrate, "Audio bitrate (kbit/s)", new NumberPicker.OnValueChangeListener() { - @Override - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - audioBitrate = newVal; - VoIPService.getSharedInstance().debugCtl(1, newVal * 1000); - } - }); - break; - case 1: - showNumberPickerDialog(0, 100, packetLossPercent, "Expected packet loss %", new NumberPicker.OnValueChangeListener() { - @Override - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - packetLossPercent = newVal; - VoIPService.getSharedInstance().debugCtl(2, newVal); - } - }); - break; - case 2: - VoIPService.getSharedInstance().debugCtl(3, 0); - break; - case 3: - VoIPService.getSharedInstance().debugCtl(3, 1); - break; - case 4: - VoIPService.getSharedInstance().debugCtl(4, 0); - break; - case 5: - VoIPService.getSharedInstance().debugCtl(4, 1); - break; - } - } - }) - .show(); - } - - private void showNumberPickerDialog(int min, int max, int value, String title, NumberPicker.OnValueChangeListener listener) { - NumberPicker picker = new NumberPicker(this); - picker.setMinValue(min); - picker.setMaxValue(max); - picker.setValue(value); - picker.setOnValueChangedListener(listener); - new AlertDialog.Builder(this) - .setTitle(title) - .setView(picker) - .setPositiveButton("Done", null) - .show(); + debugOverlay.postDelayed(r, 500); } private void startUpdatingCallDuration() { @@ -875,7 +885,7 @@ public class VoIPActivity extends Activity implements VoIPService.StateListener, if (isFinishing() || VoIPService.getSharedInstance() == null) { return; } - if(callState==VoIPService.STATE_ESTABLISHED){ + if(callState==VoIPService.STATE_ESTABLISHED || callState==VoIPService.STATE_RECONNECTING){ long duration=VoIPService.getSharedInstance().getCallDuration()/1000; durationText.setText(duration>3600 ? String.format("%d:%02d:%02d", duration/3600, duration%3600/60, duration%60) : String.format("%d:%02d", duration/60, duration%60)); durationText.postDelayed(this, 500); @@ -1058,6 +1068,7 @@ public class VoIPActivity extends Activity implements VoIPService.StateListener, @Override public void onStateChanged(final int state) { + final int prevState=callState; callState=state; runOnUiThread(new Runnable() { @Override @@ -1110,6 +1121,8 @@ public class VoIPActivity extends Activity implements VoIPService.StateListener, setStateTextAnimated(LocaleController.getString("VoipRequesting", R.string.VoipRequesting), true); } else if (state == VoIPService.STATE_HANGING_UP) { setStateTextAnimated(LocaleController.getString("VoipHangingUp", R.string.VoipHangingUp), true); + endBtnIcon.setAlpha(.5f); + endBtn.setEnabled(false); } else if (state == VoIPService.STATE_ENDED) { setStateTextAnimated(LocaleController.getString("VoipCallEnded", R.string.VoipCallEnded), false); stateText.postDelayed(new Runnable() { @@ -1128,8 +1141,8 @@ public class VoIPActivity extends Activity implements VoIPService.StateListener, } }, 2000);*/ showRetry(); - } else if (state == VoIPService.STATE_ESTABLISHED) { - if(!wasFirstStateChange){ + } else if (state == VoIPService.STATE_ESTABLISHED || state==VoIPService.STATE_RECONNECTING) { + if(!wasFirstStateChange && state==VoIPService.STATE_ESTABLISHED){ int count=getSharedPreferences("mainconfig", MODE_PRIVATE).getInt("call_emoji_tooltip_count", 0); if(count<3){ setEmojiTooltipVisible(true); @@ -1143,13 +1156,15 @@ public class VoIPActivity extends Activity implements VoIPService.StateListener, getSharedPreferences("mainconfig", MODE_PRIVATE).edit().putInt("call_emoji_tooltip_count", count+1).apply(); } } - setStateTextAnimated("0:00", false); - startUpdatingCallDuration(); - updateKeyView(); - if (emojiWrap.getVisibility() != View.VISIBLE) { - emojiWrap.setVisibility(View.VISIBLE); - emojiWrap.setAlpha(0f); - emojiWrap.animate().alpha(1).setDuration(200).setInterpolator(new DecelerateInterpolator()).start(); + if(prevState!=VoIPService.STATE_ESTABLISHED && prevState!=VoIPService.STATE_RECONNECTING){ + setStateTextAnimated("0:00", false); + startUpdatingCallDuration(); + updateKeyView(); + if(emojiWrap.getVisibility()!=View.VISIBLE){ + emojiWrap.setVisibility(View.VISIBLE); + emojiWrap.setAlpha(0f); + emojiWrap.animate().alpha(1).setDuration(200).setInterpolator(new DecelerateInterpolator()).start(); + } } } else if (state == VoIPService.STATE_FAILED) { setStateTextAnimated(LocaleController.getString("VoipFailed", R.string.VoipFailed), false); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/VoIPFeedbackActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/VoIPFeedbackActivity.java index c3b6a0b7a..8d452de51 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/VoIPFeedbackActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/VoIPFeedbackActivity.java @@ -25,6 +25,7 @@ import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Components.BetterRatingView; import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.voip.VoIPHelper; public class VoIPFeedbackActivity extends Activity { @Override @@ -36,83 +37,12 @@ public class VoIPFeedbackActivity extends Activity { setContentView(new View(this)); - LinearLayout ll = new LinearLayout(this); - ll.setOrientation(LinearLayout.VERTICAL); - int pad = AndroidUtilities.dp(16); - ll.setPadding(pad, pad, pad, pad); - - TextView text = new TextView(this); - text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); - text.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); - text.setGravity(Gravity.CENTER); - text.setText(LocaleController.getString("VoipRateCallAlert", R.string.VoipRateCallAlert)); - ll.addView(text); - - final BetterRatingView bar = new BetterRatingView(this); - ll.addView(bar, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL, 0, 16, 0, 0)); - - final EditText commentBox = new EditText(this); - commentBox.setHint(LocaleController.getString("VoipFeedbackCommentHint", R.string.VoipFeedbackCommentHint)); - commentBox.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE); - commentBox.setVisibility(View.GONE); - commentBox.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); - commentBox.setHintTextColor(Theme.getColor(Theme.key_dialogTextHint)); - commentBox.setBackgroundDrawable(Theme.createEditTextDrawable(this, true)); - ll.addView(commentBox, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL, 0, 16, 0, 0)); - - AlertDialog alert = new AlertDialog.Builder(this) - .setTitle(LocaleController.getString("AppName", R.string.AppName)) - .setView(ll) - .setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - TLRPC.TL_phone_setCallRating req = new TLRPC.TL_phone_setCallRating(); - req.rating = bar.getRating(); - if (req.rating < 5) - req.comment = commentBox.getText().toString(); - else - req.comment=""; - req.peer = new TLRPC.TL_inputPhoneCall(); - req.peer.access_hash = getIntent().getLongExtra("call_access_hash", 0); - req.peer.id = getIntent().getLongExtra("call_id", 0); - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(TLObject response, TLRPC.TL_error error) { - if (response instanceof TLRPC.TL_updates) { - TLRPC.TL_updates updates = (TLRPC.TL_updates) response; - MessagesController.getInstance().processUpdates(updates, false); - } - } - }); - finish(); - } - }) - .setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - finish(); - } - }) - .show(); - alert.setCanceledOnTouchOutside(true); - alert.setOnCancelListener(new DialogInterface.OnCancelListener() { + VoIPHelper.showRateAlert(this, new Runnable(){ @Override - public void onCancel(DialogInterface dialog) { + public void run(){ finish(); } - }); - final View btn = alert.getButton(DialogInterface.BUTTON_POSITIVE); - btn.setEnabled(false); - bar.setOnRatingChangeListener(new BetterRatingView.OnRatingChangeListener() { - @Override - public void onRatingChanged(int rating) { - btn.setEnabled(rating > 0); - commentBox.setVisibility(rating < 5 && rating > 0 ? View.VISIBLE : View.GONE); - if (commentBox.getVisibility() == View.GONE) { - ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(commentBox.getWindowToken(), 0); - } - } - }); + }, getIntent().getLongExtra("call_id", 0), getIntent().getLongExtra("call_access_hash", 0)); } @Override diff --git a/TMessagesProj/src/main/res/drawable-hdpi/corner_in_bl.png b/TMessagesProj/src/main/res/drawable-hdpi/corner_in_bl.png index de2693153..76fac7c47 100755 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/corner_in_bl.png and b/TMessagesProj/src/main/res/drawable-hdpi/corner_in_bl.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/corner_in_br.png b/TMessagesProj/src/main/res/drawable-hdpi/corner_in_br.png index 4bdd96326..336ffd5de 100755 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/corner_in_br.png and b/TMessagesProj/src/main/res/drawable-hdpi/corner_in_br.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/corner_in_tl.png b/TMessagesProj/src/main/res/drawable-hdpi/corner_in_tl.png index 26868c2a3..50d82e546 100755 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/corner_in_tl.png and b/TMessagesProj/src/main/res/drawable-hdpi/corner_in_tl.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/corner_in_tr.png b/TMessagesProj/src/main/res/drawable-hdpi/corner_in_tr.png index 7351bcc97..c8f77c77c 100755 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/corner_in_tr.png and b/TMessagesProj/src/main/res/drawable-hdpi/corner_in_tr.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/corner_out_bl.png b/TMessagesProj/src/main/res/drawable-hdpi/corner_out_bl.png index 6209ba0be..919027012 100755 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/corner_out_bl.png and b/TMessagesProj/src/main/res/drawable-hdpi/corner_out_bl.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/corner_out_br.png b/TMessagesProj/src/main/res/drawable-hdpi/corner_out_br.png index e4783db81..d3774e9eb 100755 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/corner_out_br.png and b/TMessagesProj/src/main/res/drawable-hdpi/corner_out_br.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/corner_out_tl.png b/TMessagesProj/src/main/res/drawable-hdpi/corner_out_tl.png index 0c22f84ec..69b666400 100755 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/corner_out_tl.png and b/TMessagesProj/src/main/res/drawable-hdpi/corner_out_tl.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/corner_out_tr.png b/TMessagesProj/src/main/res/drawable-hdpi/corner_out_tr.png index 64cab85f3..223a637da 100755 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/corner_out_tr.png and b/TMessagesProj/src/main/res/drawable-hdpi/corner_out_tr.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/group_admin.png b/TMessagesProj/src/main/res/drawable-hdpi/group_admin.png new file mode 100755 index 000000000..a9e269ef4 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/group_admin.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/group_admin_new.png b/TMessagesProj/src/main/res/drawable-hdpi/group_admin_new.png new file mode 100755 index 000000000..de4076527 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/group_admin_new.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/group_ban_empty.png b/TMessagesProj/src/main/res/drawable-hdpi/group_ban_empty.png new file mode 100755 index 000000000..9e2c5a1a9 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/group_ban_empty.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/group_ban_new.png b/TMessagesProj/src/main/res/drawable-hdpi/group_ban_new.png new file mode 100755 index 000000000..713ac7d51 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/group_ban_new.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/group_banned.png b/TMessagesProj/src/main/res/drawable-hdpi/group_banned.png new file mode 100755 index 000000000..ca5298aed Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/group_banned.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/group_edit.png b/TMessagesProj/src/main/res/drawable-hdpi/group_edit.png new file mode 100755 index 000000000..d7d36c0ef Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/group_edit.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/group_edit_profile.png b/TMessagesProj/src/main/res/drawable-hdpi/group_edit_profile.png new file mode 100644 index 000000000..661e77566 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/group_edit_profile.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/group_log.png b/TMessagesProj/src/main/res/drawable-hdpi/group_log.png new file mode 100755 index 000000000..a5f58b209 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/group_log.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_arrow.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_arrow.png new file mode 100755 index 000000000..7a835e5cc Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_arrow_shadow.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_arrow_shadow.png new file mode 100755 index 000000000..71d8731a0 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_arrow_shadow.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_body.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_body.png new file mode 100755 index 000000000..8960f4cce Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_body.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_spiral.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_spiral.png new file mode 100755 index 000000000..4e328347c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_fast_spiral.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_bubble.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_bubble.png new file mode 100755 index 000000000..a22b99fcf Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_bubble.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_bubble_dot.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_bubble_dot.png new file mode 100755 index 000000000..4fb0563c3 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_bubble_dot.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_cam.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_cam.png new file mode 100755 index 000000000..3e9ac9c4c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_cam.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_cam_lens.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_cam_lens.png new file mode 100755 index 000000000..a2b598b86 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_cam_lens.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_pencil.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_pencil.png new file mode 100755 index 000000000..3ea76136b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_pencil.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_pin.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_pin.png new file mode 100755 index 000000000..5ee93c8fe Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_pin.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_smile.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_smile.png new file mode 100755 index 000000000..b2a06031b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_smile.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_smile_eye.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_smile_eye.png new file mode 100755 index 000000000..c38bb6b3c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_smile_eye.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_videocam.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_videocam.png new file mode 100755 index 000000000..f7787655b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_ic_videocam.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_knot_down.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_knot_down.png new file mode 100755 index 000000000..5eeab7bc9 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_knot_down.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_knot_up.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_knot_up.png new file mode 100755 index 000000000..d2c38e6de Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_knot_up.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_infinity.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_infinity.png new file mode 100755 index 000000000..cfc1182b5 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_infinity.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_infinity_white.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_infinity_white.png new file mode 100755 index 000000000..85a9f6d0b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_infinity_white.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_mask.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_mask.png new file mode 100755 index 000000000..1706123bf Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_mask.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_star.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_star.png new file mode 100755 index 000000000..32b1352a2 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_powerful_star.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_private_door.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_private_door.png new file mode 100755 index 000000000..58c3bcbf2 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_private_door.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_private_screw.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_private_screw.png new file mode 100755 index 000000000..fa2dd4f9c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_private_screw.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_tg_plane.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_tg_plane.png new file mode 100755 index 000000000..57c6124f0 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_tg_plane.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/intro_tg_sphere.png b/TMessagesProj/src/main/res/drawable-hdpi/intro_tg_sphere.png new file mode 100755 index 000000000..3a0a11455 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/intro_tg_sphere.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/lock_arrow.png b/TMessagesProj/src/main/res/drawable-hdpi/lock_arrow.png new file mode 100755 index 000000000..b3895dd1c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/lock_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/lock_middle.png b/TMessagesProj/src/main/res/drawable-hdpi/lock_middle.png new file mode 100755 index 000000000..62d360e56 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/lock_middle.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/lock_round.9.png b/TMessagesProj/src/main/res/drawable-hdpi/lock_round.9.png new file mode 100755 index 000000000..61bc758ee Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/lock_round.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/lock_round_shadow.9.png b/TMessagesProj/src/main/res/drawable-hdpi/lock_round_shadow.9.png new file mode 100755 index 000000000..56d0563c9 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/lock_round_shadow.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/lock_top.png b/TMessagesProj/src/main/res/drawable-hdpi/lock_top.png new file mode 100755 index 000000000..38aacd992 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/lock_top.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/log_info.png b/TMessagesProj/src/main/res/drawable-hdpi/log_info.png new file mode 100755 index 000000000..09c27549e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/log_info.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/menu_copy.9.png b/TMessagesProj/src/main/res/drawable-hdpi/menu_copy.9.png new file mode 100755 index 000000000..f981e1b7d Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/menu_copy.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/moon.png b/TMessagesProj/src/main/res/drawable-hdpi/moon.png new file mode 100755 index 000000000..ec5925666 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/moon.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/permissions_contacts.png b/TMessagesProj/src/main/res/drawable-hdpi/permissions_contacts.png new file mode 100755 index 000000000..1001c8c6d Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/permissions_contacts.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/popup_fixed_alert.9.png b/TMessagesProj/src/main/res/drawable-hdpi/popup_fixed_alert.9.png new file mode 100755 index 000000000..c19436fc7 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/popup_fixed_alert.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/popup_fixed_top.9.png b/TMessagesProj/src/main/res/drawable-hdpi/popup_fixed_top.9.png new file mode 100755 index 000000000..b202d3c67 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/popup_fixed_top.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/video_mute.png b/TMessagesProj/src/main/res/drawable-hdpi/video_mute.png new file mode 100755 index 000000000..7cb6d4b6d Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/video_mute.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/corner_in_bl.png b/TMessagesProj/src/main/res/drawable-mdpi/corner_in_bl.png index 51b1c2810..6c9767042 100755 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/corner_in_bl.png and b/TMessagesProj/src/main/res/drawable-mdpi/corner_in_bl.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/corner_in_br.png b/TMessagesProj/src/main/res/drawable-mdpi/corner_in_br.png index ef954307f..a74c63b80 100755 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/corner_in_br.png and b/TMessagesProj/src/main/res/drawable-mdpi/corner_in_br.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/corner_in_tl.png b/TMessagesProj/src/main/res/drawable-mdpi/corner_in_tl.png index c9d6a3985..71de37a0d 100755 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/corner_in_tl.png and b/TMessagesProj/src/main/res/drawable-mdpi/corner_in_tl.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/corner_in_tr.png b/TMessagesProj/src/main/res/drawable-mdpi/corner_in_tr.png index 1ca70a22c..3dd1a4c4f 100755 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/corner_in_tr.png and b/TMessagesProj/src/main/res/drawable-mdpi/corner_in_tr.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/corner_out_bl.png b/TMessagesProj/src/main/res/drawable-mdpi/corner_out_bl.png index 4d5e2d59f..05cf134d6 100755 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/corner_out_bl.png and b/TMessagesProj/src/main/res/drawable-mdpi/corner_out_bl.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/corner_out_br.png b/TMessagesProj/src/main/res/drawable-mdpi/corner_out_br.png index 93b55ba67..7e4abd9c7 100755 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/corner_out_br.png and b/TMessagesProj/src/main/res/drawable-mdpi/corner_out_br.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/corner_out_tl.png b/TMessagesProj/src/main/res/drawable-mdpi/corner_out_tl.png index 023cc522d..026b68398 100755 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/corner_out_tl.png and b/TMessagesProj/src/main/res/drawable-mdpi/corner_out_tl.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/corner_out_tr.png b/TMessagesProj/src/main/res/drawable-mdpi/corner_out_tr.png index c549ad936..68bc95bd1 100755 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/corner_out_tr.png and b/TMessagesProj/src/main/res/drawable-mdpi/corner_out_tr.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/group_admin.png b/TMessagesProj/src/main/res/drawable-mdpi/group_admin.png new file mode 100755 index 000000000..81cfdf57a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/group_admin.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/group_admin_new.png b/TMessagesProj/src/main/res/drawable-mdpi/group_admin_new.png new file mode 100755 index 000000000..9562a204c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/group_admin_new.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/group_ban_empty.png b/TMessagesProj/src/main/res/drawable-mdpi/group_ban_empty.png new file mode 100755 index 000000000..8c14a2834 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/group_ban_empty.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/group_ban_new.png b/TMessagesProj/src/main/res/drawable-mdpi/group_ban_new.png new file mode 100755 index 000000000..856368d57 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/group_ban_new.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/group_banned.png b/TMessagesProj/src/main/res/drawable-mdpi/group_banned.png new file mode 100755 index 000000000..66584bf30 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/group_banned.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/group_edit.png b/TMessagesProj/src/main/res/drawable-mdpi/group_edit.png new file mode 100755 index 000000000..080faa1a9 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/group_edit.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/group_edit_profile.png b/TMessagesProj/src/main/res/drawable-mdpi/group_edit_profile.png new file mode 100644 index 000000000..f30f75109 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/group_edit_profile.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/group_log.png b/TMessagesProj/src/main/res/drawable-mdpi/group_log.png new file mode 100755 index 000000000..e288b43c8 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/group_log.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_arrow.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_arrow.png new file mode 100755 index 000000000..1712dcb05 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_arrow_shadow.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_arrow_shadow.png new file mode 100755 index 000000000..be5e6940b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_arrow_shadow.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_body.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_body.png new file mode 100755 index 000000000..a0ae7455c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_body.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_spiral.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_spiral.png new file mode 100755 index 000000000..5ec4b2e4a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_fast_spiral.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_bubble.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_bubble.png new file mode 100755 index 000000000..041c59faf Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_bubble.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_bubble_dot.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_bubble_dot.png new file mode 100755 index 000000000..b23265aa7 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_bubble_dot.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_cam.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_cam.png new file mode 100755 index 000000000..71b03c12f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_cam.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_cam_lens.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_cam_lens.png new file mode 100755 index 000000000..7bd1f3203 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_cam_lens.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_pencil.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_pencil.png new file mode 100755 index 000000000..0e53de3ff Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_pencil.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_pin.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_pin.png new file mode 100755 index 000000000..8fd091b63 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_pin.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_smile.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_smile.png new file mode 100755 index 000000000..b7055d81e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_smile.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_smile_eye.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_smile_eye.png new file mode 100755 index 000000000..72f350010 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_smile_eye.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_videocam.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_videocam.png new file mode 100755 index 000000000..d3c3a5a25 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_ic_videocam.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_knot_down.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_knot_down.png new file mode 100755 index 000000000..c4c50ee6f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_knot_down.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_knot_up.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_knot_up.png new file mode 100755 index 000000000..c853bd18c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_knot_up.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_infinity.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_infinity.png new file mode 100755 index 000000000..6b0c87622 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_infinity.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_infinity_white.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_infinity_white.png new file mode 100755 index 000000000..dd6ee3861 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_infinity_white.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_mask.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_mask.png new file mode 100755 index 000000000..4efc053a1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_mask.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_star.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_star.png new file mode 100755 index 000000000..d0fefec08 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_powerful_star.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_private_door.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_private_door.png new file mode 100755 index 000000000..869068856 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_private_door.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_private_screw.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_private_screw.png new file mode 100755 index 000000000..77100ed23 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_private_screw.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_tg_plane.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_tg_plane.png new file mode 100755 index 000000000..51e6ef344 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_tg_plane.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/intro_tg_sphere.png b/TMessagesProj/src/main/res/drawable-mdpi/intro_tg_sphere.png new file mode 100755 index 000000000..73cfd009f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/intro_tg_sphere.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/lock_arrow.png b/TMessagesProj/src/main/res/drawable-mdpi/lock_arrow.png new file mode 100755 index 000000000..4c37b9e05 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/lock_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/lock_middle.png b/TMessagesProj/src/main/res/drawable-mdpi/lock_middle.png new file mode 100755 index 000000000..1e93060c2 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/lock_middle.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/lock_round.9.png b/TMessagesProj/src/main/res/drawable-mdpi/lock_round.9.png new file mode 100755 index 000000000..a8bc4f2df Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/lock_round.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/lock_round_shadow.9.png b/TMessagesProj/src/main/res/drawable-mdpi/lock_round_shadow.9.png new file mode 100755 index 000000000..f6d88b6bd Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/lock_round_shadow.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/lock_top.png b/TMessagesProj/src/main/res/drawable-mdpi/lock_top.png new file mode 100755 index 000000000..23feb7b6f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/lock_top.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/log_info.png b/TMessagesProj/src/main/res/drawable-mdpi/log_info.png new file mode 100755 index 000000000..c256730ad Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/log_info.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/menu_copy.9.png b/TMessagesProj/src/main/res/drawable-mdpi/menu_copy.9.png new file mode 100755 index 000000000..1c8dfc502 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/menu_copy.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/moon.png b/TMessagesProj/src/main/res/drawable-mdpi/moon.png new file mode 100755 index 000000000..ebc53da3e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/moon.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/permissions_contacts.png b/TMessagesProj/src/main/res/drawable-mdpi/permissions_contacts.png new file mode 100755 index 000000000..e9041c5a9 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/permissions_contacts.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/popup_fixed_alert.9.png b/TMessagesProj/src/main/res/drawable-mdpi/popup_fixed_alert.9.png new file mode 100755 index 000000000..58570d83e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/popup_fixed_alert.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/popup_fixed_top.9.png b/TMessagesProj/src/main/res/drawable-mdpi/popup_fixed_top.9.png new file mode 100755 index 000000000..9083dbb5c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/popup_fixed_top.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/video_mute.png b/TMessagesProj/src/main/res/drawable-mdpi/video_mute.png new file mode 100755 index 000000000..8c98ae8ac Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/video_mute.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_bl.png b/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_bl.png index 0620792f8..1bc2555ce 100755 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_bl.png and b/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_bl.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_br.png b/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_br.png index 40e0bba75..7947baf0d 100755 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_br.png and b/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_br.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_tl.png b/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_tl.png index 7c75fa2ce..d3af56ed6 100755 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_tl.png and b/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_tl.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_tr.png b/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_tr.png index 919358f8c..8c7ab4dbb 100755 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_tr.png and b/TMessagesProj/src/main/res/drawable-xhdpi/corner_in_tr.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_bl.png b/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_bl.png index b286218be..3edd6cd92 100755 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_bl.png and b/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_bl.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_br.png b/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_br.png index 3e859e80c..6b94a5bcc 100755 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_br.png and b/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_br.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_tl.png b/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_tl.png index 84433cf5d..bb9521b66 100755 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_tl.png and b/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_tl.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_tr.png b/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_tr.png index ba5b3c2dd..02daa0290 100755 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_tr.png and b/TMessagesProj/src/main/res/drawable-xhdpi/corner_out_tr.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/group_admin.png b/TMessagesProj/src/main/res/drawable-xhdpi/group_admin.png new file mode 100755 index 000000000..dd80eac2c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/group_admin.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/group_admin_new.png b/TMessagesProj/src/main/res/drawable-xhdpi/group_admin_new.png new file mode 100755 index 000000000..cf95adcf4 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/group_admin_new.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/group_ban_empty.png b/TMessagesProj/src/main/res/drawable-xhdpi/group_ban_empty.png new file mode 100755 index 000000000..8fb16b308 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/group_ban_empty.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/group_ban_new.png b/TMessagesProj/src/main/res/drawable-xhdpi/group_ban_new.png new file mode 100755 index 000000000..00fc6ec4a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/group_ban_new.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/group_banned.png b/TMessagesProj/src/main/res/drawable-xhdpi/group_banned.png new file mode 100755 index 000000000..ff4ea8d5e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/group_banned.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/group_edit.png b/TMessagesProj/src/main/res/drawable-xhdpi/group_edit.png new file mode 100755 index 000000000..dde469b24 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/group_edit.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/group_edit_profile.png b/TMessagesProj/src/main/res/drawable-xhdpi/group_edit_profile.png new file mode 100644 index 000000000..80828cf33 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/group_edit_profile.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/group_log.png b/TMessagesProj/src/main/res/drawable-xhdpi/group_log.png new file mode 100755 index 000000000..b02373d1f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/group_log.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_arrow.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_arrow.png new file mode 100755 index 000000000..e9592206b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_arrow_shadow.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_arrow_shadow.png new file mode 100755 index 000000000..5d904ab79 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_arrow_shadow.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_body.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_body.png new file mode 100755 index 000000000..6c1bda76f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_body.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_spiral.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_spiral.png new file mode 100755 index 000000000..7d035b8a2 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_fast_spiral.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_bubble.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_bubble.png new file mode 100755 index 000000000..e2c8ae334 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_bubble.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_bubble_dot.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_bubble_dot.png new file mode 100755 index 000000000..3db545fc6 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_bubble_dot.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_cam.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_cam.png new file mode 100755 index 000000000..4be47e374 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_cam.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_cam_lens.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_cam_lens.png new file mode 100755 index 000000000..521526252 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_cam_lens.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_pencil.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_pencil.png new file mode 100755 index 000000000..51a7b7320 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_pencil.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_pin.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_pin.png new file mode 100755 index 000000000..93f2aa5be Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_pin.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_smile.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_smile.png new file mode 100755 index 000000000..7f81d87de Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_smile.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_smile_eye.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_smile_eye.png new file mode 100755 index 000000000..2f2956cce Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_smile_eye.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_videocam.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_videocam.png new file mode 100755 index 000000000..43ce262c1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_ic_videocam.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_knot_down.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_knot_down.png new file mode 100755 index 000000000..99da27876 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_knot_down.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_knot_up.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_knot_up.png new file mode 100755 index 000000000..33f67d5a8 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_knot_up.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_infinity.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_infinity.png new file mode 100755 index 000000000..8274c8f25 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_infinity.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_infinity_white.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_infinity_white.png new file mode 100755 index 000000000..dca6ff670 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_infinity_white.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_mask.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_mask.png new file mode 100755 index 000000000..e7c5d561a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_mask.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_star.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_star.png new file mode 100755 index 000000000..3be017732 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_powerful_star.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_private_door.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_private_door.png new file mode 100755 index 000000000..4197879ee Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_private_door.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_private_screw.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_private_screw.png new file mode 100755 index 000000000..59ce382f7 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_private_screw.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_tg_plane.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_tg_plane.png new file mode 100755 index 000000000..f56072cb1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_tg_plane.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/intro_tg_sphere.png b/TMessagesProj/src/main/res/drawable-xhdpi/intro_tg_sphere.png new file mode 100755 index 000000000..c5a4816ad Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/intro_tg_sphere.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/lock_arrow.png b/TMessagesProj/src/main/res/drawable-xhdpi/lock_arrow.png new file mode 100755 index 000000000..79087de8a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/lock_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/lock_middle.png b/TMessagesProj/src/main/res/drawable-xhdpi/lock_middle.png new file mode 100755 index 000000000..28f67d6ac Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/lock_middle.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/lock_round.9.png b/TMessagesProj/src/main/res/drawable-xhdpi/lock_round.9.png new file mode 100755 index 000000000..9667fdee2 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/lock_round.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/lock_round_shadow.9.png b/TMessagesProj/src/main/res/drawable-xhdpi/lock_round_shadow.9.png new file mode 100755 index 000000000..6c9c03af6 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/lock_round_shadow.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/lock_top.png b/TMessagesProj/src/main/res/drawable-xhdpi/lock_top.png new file mode 100755 index 000000000..7b0423f60 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/lock_top.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/log_info.png b/TMessagesProj/src/main/res/drawable-xhdpi/log_info.png new file mode 100755 index 000000000..3c75bfc87 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/log_info.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/menu_copy.9.png b/TMessagesProj/src/main/res/drawable-xhdpi/menu_copy.9.png new file mode 100755 index 000000000..79bad8012 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/menu_copy.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/moon.png b/TMessagesProj/src/main/res/drawable-xhdpi/moon.png new file mode 100755 index 000000000..4ff8798b1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/moon.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/permissions_contacts.png b/TMessagesProj/src/main/res/drawable-xhdpi/permissions_contacts.png new file mode 100755 index 000000000..ad5375d93 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/permissions_contacts.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/popup_fixed_alert.9.png b/TMessagesProj/src/main/res/drawable-xhdpi/popup_fixed_alert.9.png new file mode 100755 index 000000000..150e5e6e1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/popup_fixed_alert.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/popup_fixed_top.9.png b/TMessagesProj/src/main/res/drawable-xhdpi/popup_fixed_top.9.png new file mode 100755 index 000000000..c08c8860a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/popup_fixed_top.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/video_mute.png b/TMessagesProj/src/main/res/drawable-xhdpi/video_mute.png new file mode 100755 index 000000000..dc8d33ce8 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/video_mute.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_bl.png b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_bl.png index feb15efe0..4d8ff724e 100755 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_bl.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_bl.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_br.png b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_br.png index fa742b3b2..b7e7f208c 100755 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_br.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_br.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_tl.png b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_tl.png index 5db9f5ce6..cec5eed9a 100755 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_tl.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_tl.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_tr.png b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_tr.png index a2c058a0b..20fd7f930 100755 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_tr.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_in_tr.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_bl.png b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_bl.png index 64b85cc32..80cbf39fb 100755 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_bl.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_bl.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_br.png b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_br.png index 50ae092c0..4bdd87d12 100755 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_br.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_br.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_tl.png b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_tl.png index 1e3f529ed..848917112 100755 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_tl.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_tl.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_tr.png b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_tr.png index 8b7f95fe8..4d631fa89 100755 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_tr.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/corner_out_tr.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/group_admin.png b/TMessagesProj/src/main/res/drawable-xxhdpi/group_admin.png new file mode 100755 index 000000000..1632432e2 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/group_admin.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/group_admin_new.png b/TMessagesProj/src/main/res/drawable-xxhdpi/group_admin_new.png new file mode 100755 index 000000000..afb3f6489 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/group_admin_new.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/group_ban_empty.png b/TMessagesProj/src/main/res/drawable-xxhdpi/group_ban_empty.png new file mode 100755 index 000000000..68bb80d70 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/group_ban_empty.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/group_ban_new.png b/TMessagesProj/src/main/res/drawable-xxhdpi/group_ban_new.png new file mode 100755 index 000000000..4a999047d Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/group_ban_new.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/group_banned.png b/TMessagesProj/src/main/res/drawable-xxhdpi/group_banned.png new file mode 100755 index 000000000..808379e96 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/group_banned.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/group_edit.png b/TMessagesProj/src/main/res/drawable-xxhdpi/group_edit.png new file mode 100755 index 000000000..da1949f1d Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/group_edit.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/group_edit_profile.png b/TMessagesProj/src/main/res/drawable-xxhdpi/group_edit_profile.png new file mode 100644 index 000000000..279ab9b0a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/group_edit_profile.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/group_log.png b/TMessagesProj/src/main/res/drawable-xxhdpi/group_log.png new file mode 100755 index 000000000..d63964438 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/group_log.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_arrow.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_arrow.png new file mode 100755 index 000000000..75958e57b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_arrow_shadow.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_arrow_shadow.png new file mode 100755 index 000000000..1e800196f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_arrow_shadow.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_body.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_body.png new file mode 100755 index 000000000..f73062d0e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_body.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_spiral.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_spiral.png new file mode 100755 index 000000000..47f84d686 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_fast_spiral.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_bubble.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_bubble.png new file mode 100755 index 000000000..1703e622e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_bubble.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_bubble_dot.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_bubble_dot.png new file mode 100755 index 000000000..cd316642a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_bubble_dot.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_cam.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_cam.png new file mode 100755 index 000000000..1cdd5de4f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_cam.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_cam_lens.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_cam_lens.png new file mode 100755 index 000000000..21255f1b1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_cam_lens.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_pencil.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_pencil.png new file mode 100755 index 000000000..b85402d0b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_pencil.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_pin.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_pin.png new file mode 100755 index 000000000..6548b487b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_pin.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_smile.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_smile.png new file mode 100755 index 000000000..d51bcfb87 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_smile.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_smile_eye.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_smile_eye.png new file mode 100755 index 000000000..0703cb21d Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_smile_eye.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_videocam.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_videocam.png new file mode 100755 index 000000000..15e97ed6e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_ic_videocam.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_knot_down.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_knot_down.png new file mode 100755 index 000000000..4caa342fa Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_knot_down.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_knot_up.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_knot_up.png new file mode 100755 index 000000000..7559ee614 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_knot_up.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_infinity.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_infinity.png new file mode 100755 index 000000000..8dca0dec1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_infinity.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_infinity_white.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_infinity_white.png new file mode 100755 index 000000000..a32c1f298 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_infinity_white.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_mask.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_mask.png new file mode 100755 index 000000000..d7d885b82 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_mask.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_star.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_star.png new file mode 100755 index 000000000..fa901bcf1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_powerful_star.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_private_door.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_private_door.png new file mode 100755 index 000000000..a091403d4 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_private_door.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_private_screw.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_private_screw.png new file mode 100755 index 000000000..cc3a0fb4c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_private_screw.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_tg_plane.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_tg_plane.png new file mode 100755 index 000000000..a6af63624 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_tg_plane.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/intro_tg_sphere.png b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_tg_sphere.png new file mode 100755 index 000000000..adec08025 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/intro_tg_sphere.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/lock_arrow.png b/TMessagesProj/src/main/res/drawable-xxhdpi/lock_arrow.png new file mode 100755 index 000000000..71c66fdc2 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/lock_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/lock_middle.png b/TMessagesProj/src/main/res/drawable-xxhdpi/lock_middle.png new file mode 100755 index 000000000..fb6112276 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/lock_middle.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/lock_round.9.png b/TMessagesProj/src/main/res/drawable-xxhdpi/lock_round.9.png new file mode 100755 index 000000000..82a3d5e68 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/lock_round.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/lock_round_shadow.9.png b/TMessagesProj/src/main/res/drawable-xxhdpi/lock_round_shadow.9.png new file mode 100755 index 000000000..c1940ae77 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/lock_round_shadow.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/lock_top.png b/TMessagesProj/src/main/res/drawable-xxhdpi/lock_top.png new file mode 100755 index 000000000..743dea6ba Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/lock_top.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/log_info.png b/TMessagesProj/src/main/res/drawable-xxhdpi/log_info.png new file mode 100755 index 000000000..132330fad Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/log_info.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/menu_copy.9.png b/TMessagesProj/src/main/res/drawable-xxhdpi/menu_copy.9.png new file mode 100755 index 000000000..7269d0e09 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/menu_copy.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/moon.png b/TMessagesProj/src/main/res/drawable-xxhdpi/moon.png new file mode 100755 index 000000000..1a2d70090 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/moon.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/permissions_contacts.png b/TMessagesProj/src/main/res/drawable-xxhdpi/permissions_contacts.png new file mode 100755 index 000000000..ba36b93ad Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/permissions_contacts.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/popup_fixed_alert.9.png b/TMessagesProj/src/main/res/drawable-xxhdpi/popup_fixed_alert.9.png new file mode 100755 index 000000000..87fcf07ce Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/popup_fixed_alert.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/popup_fixed_top.9.png b/TMessagesProj/src/main/res/drawable-xxhdpi/popup_fixed_top.9.png new file mode 100755 index 000000000..99bf6326c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/popup_fixed_top.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/video_mute.png b/TMessagesProj/src/main/res/drawable-xxhdpi/video_mute.png new file mode 100755 index 000000000..aae2bc97f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/video_mute.png differ diff --git a/TMessagesProj/src/main/res/layout/intro_layout.xml b/TMessagesProj/src/main/res/layout/intro_layout.xml deleted file mode 100644 index b274c11b3..000000000 --- a/TMessagesProj/src/main/res/layout/intro_layout.xml +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/TMessagesProj/src/main/res/layout/intro_layout_tablet.xml b/TMessagesProj/src/main/res/layout/intro_layout_tablet.xml deleted file mode 100644 index 77c73d20f..000000000 --- a/TMessagesProj/src/main/res/layout/intro_layout_tablet.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/TMessagesProj/src/main/res/layout/intro_view_layout.xml b/TMessagesProj/src/main/res/layout/intro_view_layout.xml deleted file mode 100644 index 030e41de5..000000000 --- a/TMessagesProj/src/main/res/layout/intro_view_layout.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/TMessagesProj/src/main/res/values-ar/strings.xml b/TMessagesProj/src/main/res/values-ar/strings.xml index 5691df2ac..173030c1d 100644 --- a/TMessagesProj/src/main/res/values-ar/strings.xml +++ b/TMessagesProj/src/main/res/values-ar/strings.xml @@ -6,8 +6,10 @@ تيليجرام نسخة تيليجرام التجريبية العربية + الإنجليزية Arabic ar + استمر بالعربية رقم هاتفك المحمول يرجى التحقق من صحة رمز بلدك وإدخال رقم هاتفك المحمول. @@ -79,6 +81,7 @@ الفاتورة التجريبية إدفع %1$s وسيلة الدفع + مزود عملية الدفع الاسم رقم الهاتف عنوان التواصل @@ -89,6 +92,8 @@ المعذرة، تم إلغاء عملية الدفع من البوت. المعذرة، تم رفض عملية الدفع. تعذر الوصور إلى خادم الدفع. فضلًا تحقق من اتصال الانترنت وحاول مرة أخرى. + تحذير + لا يمتلك تيليجرام، ولا %1$s صلاحيات الوصول إلى معلومات بطاقتك الإئتمانية. تفاصيل البطاقات الإئتمانية تتم معالجتها مباشرة عن طريق نظام الدفع, %2$s.\n\nالدفعات تذهب لشكل مباشر إلى مطور %1$s. تيليجرام لا يوفر أية ضمانات، لذلك استمر على مسؤوليتك. في حال وجود مشاكل، تواصل مع مطور %1$s أو البنك الخاص بك. محادثات جديدة الإعدادات @@ -100,6 +105,13 @@ إبدأ المراسلة بالضغط على\nأيقونة النقاط في أعلى يمين الشاشة\nأو اذهب لقسم جهات الاتصال. في إنتظار الشبكة... جاري الاتصال... + جاري الاتصال بالبروكسي... + اضغط لتعطيل البروكسي... + تعطيل + تفعيل + هل أنت متأكد من رغبتك في تفعيل هذا البروكسي؟ + يمكنك تغيير خادم البروكسي لاحقًا عن طريق الإعدادات (البيانات والتخزين) + هل أنت متأكد من رغبتك في تعطيل البروكسي %1$s؟ يمكنك إعادة تفعيله لاحقًا عن طريق الإعدادات (البيانات والتخزين). جاري التحديث... محادثة سرية جديدة في إنتظار اتصال %s … @@ -139,12 +151,14 @@ عريض مائل طبيعي + لتستطيع التواصل بشكل سلس مع الجميع، قم بالسماح لـ ]]>تيليجرام]]> بالوصول إلى جهات الاتصال الخاصة بك. + ليس الآن + الاستمرار - نوع المجموعة - نوع القناة عامة خاصة ترقية ليكون مشرف + لا يوجد مستخدمين محظورين يمكنك كتابة وصف اختياري لمجموعتك. مغادرة المجموعة حذف المجموعة @@ -168,6 +182,7 @@ un1 ثبت ملف un1 ثبت ملصق un1 ثبت رسالة صوتية + un1 ثبت رسالة مرئية un1 ثبت جهة اتصال un1 ثبت %1$s un1 ثبت خريطة @@ -176,6 +191,7 @@ تم ترقية هذه المجموعة لتصبح مجموعة خارقة تم ترقية المجموعة %1$s لتصبح مجموعة خارقة أعضاء القائمة السوداء هم أعضاء تم حذفهم من المجموعة ولا يمكنهم العودة لها إلى بدعوة من المشرف. روابط الدعوة لن تمكنهم من العودة للمجموعة. + أعضاء القائمة السوداء هم أعضاء تم حذفهم من القناة ولا يمكنهم العودة لها إلى بدعوة من المشرف. روابط الدعوة لن تمكنهم من العودة للقناة. قناة جديدة اسم القناة إضافة جهات اتصال لقناتك @@ -217,7 +233,9 @@ جاري مراجعة الاسم... %1$s متاح. أعضاء - جهات الاتصال المحظورة + قائمة المحظورين + المستخدمون المحظورون + المستخدمون المقيدون الإداريون حذف القناة حذف القناة @@ -233,15 +251,17 @@ تم حذف صورة القناة تم تغيير اسم القناة إلى un2 المعذرة، قمت بحجز معرفات عامة كثيرة. يمكنك إلغاء بعض روابط مجموعاتك وقنواتك القديمة، أو قم بجعلها خاصة. - المراقب المنشئ إداري الصامت إلغاء الصامت - إضافة إداري + إضافة مشرف + حظر المستخدم + ألغ الحظر + اضغط بإستمرار على المستخدم لإلغاء الحظر استخدم رابط دعوة هل أنت متأكد من رغبتك في تعيين %1$s كإداري؟ - حذف + إزالة المشرف فقط الإداريّون يمكنهم مشاهدة هذه القائمة. هذا المستخدم لم يقم بالدخول إلى القناة بعد. فضلًا، هل ترغب في دعوتهم؟ أي شخص يمتلك تيليجرام على جهازه سيمكنه الدخول لقناتك باستخدام هذا الرابط. @@ -271,6 +291,7 @@ %1$s قام بإرسال ملف للقناة %2$s %1$s أرسل صورة متحركة للقناة %2$s %1$s أرسل رسالة صوتية للقناة %2$s + %1$s أرسل رسالة مرئية للقناة %2$s %1$s قام بإرسال مقطع صوتي للقناة %2$s %1$s قام بإرسال ملصق للقناة %2$s %1$s أرسل %3$s ملصق للقناة %2$s @@ -282,6 +303,7 @@ %1$s أرسل ملف %1$s قام بنشر صورة متحركة %1$s أرسل رسالة صوتية + %1$s أرسل رسالة مرئية %1$s أرسل مقطع صوتي %1$s أرسل ملصق %1$s أرسل %2$s ملصق @@ -292,6 +314,110 @@ لن يتم إشعار الأعضاء عند إرسال الرسائل. وقّع الرسائل قم بإضافة أسماء المشرفين للرسائل التي يرسلونها. + عدل على المشرف + ماذا يستطيع المشرف عمله؟ + غير معلومات القناة + غير معلومات المجموعة + انشر رسالة + عدل على رسائل الآخرين + احذف رسائل الآخرين + حذف الرسائل + إضافة مشرفين جدد + إزالة المشرف + حظر مستخدمين + إضافة مستخدمين + دعوة مستخدمين من خلال الرابط + ثبت رسائل + تمت الإضافة بواسطة %1$s + لا يمكنك التعديل على حقوق هذا المشرف. + تم التقييد بواسطة %1$s + قيود المستخدم + اقرأ رسائل + ماذا يستطيع هذا المستخدم عمله؟ + أرسل رسائل + أرسل وسائط + أرسل ملصقات وصور متحركة + ضمن روابط + محظور حتى + إلى الأبد + حذف + حظر وإخراج من المجموعة + أدر المجموعة + أدر القناة + أدر المجموعة + أدر القناة + + الأفعال الحديثة + كافة الأحداث + الأحداث المختارة + كافة المشرفين + لا توجد أحداث هنا بعد\n\nأعضاء المجموعة ومشرفيها\nلم يقوموا بأي عمليات\nخلال 48 ساعة الماضية. + لا توجد أحداث هنا بعد\n\nمشرفي القناة\nلم يقوموا بأي عمليات\nخلال 48 ساعة الماضية. + ]]>لم يتم العثور على أحداث ]]>\n\nلا توجد أحداث مؤخرًا تحتوي على ما بحثت عنه. + لا توجد أحداث مؤخرًا تحتوي على \']]>%1$s]]>\' تم العثور عليها. + What is the Recent Actions? + هذه قائمة بجميع العمليات التي قام بها أعضاء المجموعة ومشرفيها خلال 48 ساعة. + هذه قائمة بجميع العمليات التي قام بها أعضاء القناة ومشرفيها خلال 48 ساعة. + un1 قام بتغيير اسم المجموعة إلى \"%1$s\" + un1 قام بتغيير اسم القناة إلى \"%1$s\" + un1 غادر المجموعة + un1 غادر القناة + un1 قام بإضافة un2 + un1 انضم للمجموعة + حظر %1$s + ألغى حظر %1$s + un1 قام بالدخول للقناة + un1 قام بتعيين صورة مجموعة جديدة + un1 قام بتعيين صورة قناة جديدة + un1 قام بحذف صورة المجموعة + un1 قام بحذف صورة القناة + un1 قام بالتعديل على هذه رسالة: + un1 قام بالتعديل على التعليق: + الرسالة الأصلية + التعليق الأصلي + إفراغ + un1 ثبت هذه رسالة: + un1 ألغى تثبيت رسالة + un1 حذف هذه رسالة: + un1 غير رابط المجموعة: + un1 غير رابط القناة: + un1 قام بحذف رابط المجموعة + un1 قام بحذف رابط القناة + الرابط السابق + un1 عدل وصف المجموعة: + un1 عدل وصف القناة: + الوصف السابق + un1 فعل دعوات المجموعة + un1 عطل الدعوات + un1 فعل التواقيع + un1 عطل التواقيع + غير القيود الخاصة بـ %1$s\n\nلمدة: %2$s + أرسل ملصقات وصور متحركة + أرسل وسائط + أرسل رسائل + ضمن روابط + اقرأ رسائل + غير صلاحيات %1$s + غير معلومات القناة + غير معلومات المجموعة + انشر رسائل + تعديل الرسائل + حذف الرسائل + إضافة مشرفين + حظر مستخدمين + إضافة مستخدمين + ثبت رسائل + كافة الأحداث + قيد جديد + مشرفون جدد + أعضاء جدد + معلومات المجموعة + معلومات القناة + إعدادات المجموعة + رسائل محذوفة + تعديل الرسائل + رسائل مثبتة + الأعضاء المغادرين رسالة جماعية جديدة أدخل اسم القائمة @@ -324,13 +450,18 @@ يكتب… يكتبون… %1$s يقوم بتسجيل رسالة صوتية... + %1$s يقوم بتسجيل رسالة مرئية... %1$s يقوم بإرسال مقطع صوتي... %1$s يقوم بإرسال صورة... استعراض لحظي + افتح مجموعة + افتح قناة + النمط الداكن سيتم تفعيله تلقائيًا في الليل %1$s يلعب لعبة... %1$s يقوم بإرسال مقطع مرئي... %1$s يقوم بإرسال ملف... - جاري تسجيل الرسالة الصوتية... + جاري تسجيل رسالة صوتية... + جاري تسجيل رسالة مرئية... جاري إرسال المقطع الصوتي... جاري إرسال الصورة... يلعب لعبة... @@ -378,6 +509,8 @@ افتح باستخدام... انسخ الرابط أرسل %1$s + أرسل كملف + أرسل كملفات هل ترغب في فتح الرابط باستخدام %1$s ؟ هل ترغب في السماح لـ %1$s ليرسل معرفك (ليس رقم هاتفك) الخاص بتيليجرام واسمك للصفحات التي تفتحها من خلال هذا البوت؟ الإبلاغ عن الرسائل المزعجة @@ -393,6 +526,7 @@ https://telegram.org/faq/can-39t-send-messages-to-non-contacts ملعومات إضافية أرسل إلى... + اكتب ملاحظة اضغط هنا للوصول للصور المتحركة المحفوظة تثبيت أشعر جميع الأعضاء @@ -415,6 +549,7 @@ %1$s المعذرة، انتهت مدة التعديل على الرسائل. إضافة اختصار + ابحث عن أعضاء تمت إضافة اختصار للشاشة الرئيسية تحدث مع نفسك أنت @@ -427,6 +562,21 @@ حذف لـ%1$s حذف لكافة الأعضاء تم نسخ النص للحافظة + اضغط باستمرار لتسجيل صوتي. اضغط مرة واحدة للتحويل للفيديو. + اضغط باستمرار لتسجيل فيديو. اضغط مرة واحدة للتحويل للصوتي. + تجاهل الرسالة الصوتية + هل أنت متأكد من رغبتك في إيقاف التسجيل وتجاهل الرسالة الصوتية؟ + تجاهل الرسالة المرئية + هل أنت متأكد من رغبتك في إيقاف التسجيل وتجاهل الرسالة المرئية؟ + تجاهل + مشرفو هذه المجموعة قاموا بوضع قيد يمنعك من إرسال الوسائط هنا حتى %1$s + مشرفو هذه المجموعة قاموا بوضع قيد يمنعك من إرسال محتوى في نفس السطر هنا حتى %1$s + مشرفو هذه المجموعة قاموا بوضع قيد يمنعك من إرسال الملصقات هنا حتى %1$s + مشرفو هذه المجموعة قاموا بوضع قيد يمنعك من الكتابة هنا حتى %1$s + مشرفو هذه المجموعة قاموا بوضع قيد يمنعك من إرسال الوسائط هنا + مشرفو هذه المجموعة قاموا بوضع قيد يمنعك من إرسال محتوى في نفس السطر هنا + مشرفو هذه المجموعة قاموا بوضع قيد يمنعك من إرسال الملصقات هنا + مشرفو هذه المجموعة قاموا بوضع قيد يمنعك من الكتابة هنا %1$s قام بتعيين عداد التدمير الذاتي إلى to %2$s لقد قمت بتعيين التدمير الذاتي إلى %1$s @@ -443,6 +593,7 @@ %1$s قام بإرسال ملف لك %1$s أرسل لك صورة متحركة %1$s قام بإرسال رسالة صوتية + %1$s قام بإرسال رسالة مرئية لك %1$s أرسل مقطع صوتي لك %1$s قام بإرسال ملصق %1$s قام بإرسال %2$s ملصق @@ -456,12 +607,13 @@ %1$s قام بإرسال ملف للمجموعة %2$s %1$s أرسل صورة متحركة للمجموعة %2$s %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 - %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 @@ -485,6 +637,7 @@ %1$s ثبت ملصق في المجموعة %2$s %1$s قام بتثبيت %3$s ملصق في المجموعة %2$s %1$s ثبت رسالة صوتية في المجموعة %2$s + %1$s ثبت رسالة مرئية في المجموعة %2$s %1$s ثبت جهة اتصال في المجموعة %2$s %1$s ثبت خريطة في المجموعة %2$s %1$s ثبت صورة متحركة في المجموعة %2$s @@ -497,6 +650,7 @@ %1$s ثبت ملصق %1$s قام بتثبيت %2$s ملصق %1$s ثبت رسالة صوتية + %1$s ثبت رسالة مرئية %1$s ثبت جهة اتصال %1$s ثبت خريطة %1$s ثبت صورة متحركة @@ -551,8 +705,10 @@ الإعدادات إضافة مشارك تعيين كمشرف + حظر من المجموعة مغادرة المجموعة وحذفها الإشعارات + قيد المستخدم إخراج من المجموعة قم بالتحديث لمجموعة خارقة التحويل إلى مجموعة خارقة @@ -584,7 +740,7 @@ مفتاح التشفير عداد التدمير الذاتي إيقاف - هذه الصورة والنص تم اشتقاقهم من مفتاح التشفير لهذه المحادثة السرية مع ]]>%1$s]]>.
    ]]>إذا كانت مطابقة لما يظهر على جهاز ]]>%2$s]]> ، التشفير من البداية للنهاية مضمون.
    ]]>للإستزادة، إطلع على telegram.org
    + هذه الصورة والنص تم اشتقاقهم من مفتاح التشفير لهذه المحادثة السرية مع ]]>%1$s]]>.\n\nإذا كانت مطابقة لما يظهر على جهاز ]]>%2$s\'s]]> ، التشفير من البداية للنهاية مضمون.\n\nللاستزادة، اطلع على telegram.org https://telegram.org/faq#secret-chats اضغط للتحويل لإيموجي غير معروف @@ -598,14 +754,14 @@ سم المستخدم يجب أن يتكوّن من ٥ حروف على الأقل. اسم المستخدم يجب ألا يتخطى ٣٢ حرف كحد أقصى. المعذرة، اسم المستخدم لا يمكن أن يبدأ برقم. - يمكنك اختيار اسم مستخدم في ]]>تيليجرام]]>. إذا قمت بذلك، سيستطيع الناس إيجادك باستخدام الاسم المستخدم والتواصل معك من دون معرفة رقمك.
    ]]>يمكنك استخدام ]]>حروف اللغة الإنجليزية]]>, ]]>وأرقامها]]> و كذلك الخط. لا بد من استخدام ]]>٥]]> حروف على الأقل.
    + يمكنك اختيار اسم مستخدم في ]]>تيليجرام]]>. إذا قمت بذلك، سيستطيع الناس إيجادك باستخدام الاسم المستخدم والتواصل معك من دون معرفة رقمك.\n\nتستطيع استخدام ]]>حروف اللغة الإنجليزية]]>, ]]>وأرقامها ]]> وكذلك الخط. لا بد من استخدام ]]>٥]]> حروف على الأقل.. هذا الرابط يقوم بفتح محادثة معك في تيليجرام:\n%1$s جارٍ مراجعة اسم المستخدم... %1$s متاح. لا يوجد حدث خطأ. - ملصقات + ملصقات إضافة ملصق إضافة أقنعة إضافة إلى الملصقات @@ -638,6 +794,8 @@ تمت أرشفة بعض من حزم الأقنعة الخاصة بك. يمكنك تفعيل هذه الحزم ان طريق إعدادات الأقنعة. نمط + داكن + أزرق هل أنت متأكد من رغبتك في حذف النمط؟ ملف النمط غير صحيح أدخل اسم النمط @@ -683,7 +841,10 @@ أخضر سماوي أبيض + بني داكن + داكن وردي + حجم النص بنفسجي برتقالي الـ LED هي إضاءة صغيرة تومض في بعض الأجهزة كإشعار برسالة جديدة. @@ -707,7 +868,8 @@ اشترك صديق في تيليجرام رسائل مثبتة اللغة - نرجو الأخذ بالعلم أن الدعم الفني في تيليجرام يقوم به مجموعة من المتطوعين. نحن نحاول الرد بسرعة قدر المستطاع، لكن ربما نستغرق القليل من الوقت.
    ]]> صفحة الأسئلة الأكثر شيوعًا]]>: يوجد بها حلول للمشاكل وإجابات لمعظم الأسئلة.
    + مخصص + نرجو الأخذ بالعلم أن الدعم الفني في تيليجرام يقوم به مجموعة من المتطوعين. نحن نحاول الرد بسرعة قدر المستطاع، لكن ربما نستغرق القليل من الوقت.\n\nيرجى الإطلاع على صفحة الأسئلة الأكثر شيوعًا]]>: يوجد بها إجابات لمعظم الأسئلة و حلول للمشاكل و]]>. اسأل أحد المتطوعين الأسئلة الشائعة عن تيليجرام https://telegram.org/faq/ar @@ -788,6 +950,19 @@ قائمة المعالجة استيراد جهات الاتصال إعادة تنزيل جهات الاتصال + يمكنك تغيير اللغة الخاصة بك من الإعدادات لاحقًا. + اختر لغتك + أخرى + إعدادات البروكسي + بروكسي + استخدم إعدادات البروكسي + الخادم + كلمة المرور + المنفذ + اسم المستخدم + إعدادات بروكسي SOCKS 5 + Use proxy for calls + Proxy servers may degrade the quality of your calls. قاعدة البيانات على الجهاز هل ترغب في مسح الرسائل المحفوظة في الذاكرة المخبئية؟ @@ -797,7 +972,7 @@ جاري الحساب... المستندات الصور - الرسائل الصوتية + رسالة صوتية/مرئية المقاطع المرئية الموسيقى الصور المتحركة @@ -806,6 +981,8 @@ الإحتفاظ بالوسائط الصور، المقاطع المرئية، وجميع الملفات المحفوظة في خوادمنا التي لم تستخدمها ]]> خلال هذه المدة سيتم حذفها لتوفير المساحة. ملفات الوسائط ستبقى في خوادم تيليجرام ويمكنك إعادة تنزيلها متى ما احتجتها مرة أخرى. إلى الأبد + الرسائل الصوتية + الرسائل المرئية الأجهزة المسجّل دخول منها الجهاز الحالي @@ -869,8 +1046,8 @@ حفظ في الجهاز %1$d من %2$d الألبوم - جميع الصور - كافة المقاطع المرئية + كافة الصور + كافة الوسائط لا توجد صور حتى الآن لا يوجد مقاطع مرئية بعد فضلًا، قم بتنزيل الوسائط أولًا @@ -1004,6 +1181,11 @@ الخصوصية والأمان الخصوصية آخر ظهور + عمليات الدفع + مسح معلومات عملية الدفع والشحن + يمكنك حذف معلومات الشحن الخاصة بك والطلب من جميع موفري خدمة الدفع حذف بطاقاتك الإئتمانية المسجلة. يرجى الأخذ بالعلم أن تيليجرام لا يقوم بتاتًا بحفظ بيانات بطاقتك الإئتمانية. + معلومات الشحن + معلومات عملية الدفع الجميع جهات الاتصال الخاصة بي لا أحد @@ -1044,6 +1226,8 @@ المعذرة، لا يمكنك إضافة هذا المستخدم للمجموعات بسبب إعدادات الخصوصية المتعلقة به. المعذرة، لا يمكنك إضافة هذا المستخدم للقنوات بسبب إعدادات الخصوصية المتعلقة به. المعذرة، لا يمكنك إنشاء مجموعة مع هؤلاء المستخدمين نظرًا لإعدادات الخصوصية المتعلقة بهم. + الند للند + تعطيل الند للند سيجعل جميع المكالمات تعتمد على خوادم تيليجرام لتجنب الإفصاح عن الـ IP الخاص لك، لكن يقوم بتنزيل جودة الصوت قليلًا. تحرير الفيديو جارٍ إرسال المقطع المرئي... @@ -1121,6 +1305,7 @@ ملف ملصق رسالة صوتية + رسالة مرئية لعبة أنت أنت أخذت لقطة للشاشة ! @@ -1189,6 +1374,11 @@ المعذرة، لا يمكنك القيام بذلك. المعذرة، لا يمكنك إضافة هذا المستخدم أو البوت للمجموعات بسبب أنك قمت بحظره. فضلًا قم بإزالة الحظر للإستمرار. انضم للمجموعة + المعذرة، لا يمكنك إضافة هذا المستخدم كمشرف لأنه ليس عضوًا في هذه المجموعة وغير مسموح لك دعوته. + المعذرة، لا يمكنك إضافة هذا المستخدم كمشرف لأنه في قائمة المحظورين ولا يمكنك رفع الحظر عنه. + المعذرة، لا يمكنك حظر هذا المستخدم لأنه مشرف في هذه المجموعة ولا يمكنك تغيير ذلك. + المعذرة، مشرفو هذه المجموعة قاموا بوضع قيد يمنعك من إرسال الملصقات. + المعذرة، مشرفو هذه المجموعة قاموا بوضع قيد يمنعك من إرسال الوسائط. تيليجرام يحتاج للسماح له بالوصول لجهات الاتصال الخاصة بك لتتمكن من محادثة أصدقائك من كافة أجهزتك. تيليجرام يحتاج للسماح له بالوصول للذاكرة الخاصة بك لتتمكن من إرسال وحفظ الصور، المقاطع المرئية، الموسيقى وغيرها من الوسائط. @@ -1207,13 +1397,13 @@ قوي مرتبط بالسحاب خصوصي - أسرع تطبيق مراسلة في العالم. ]]>كما أنه مجاني و آمن. - تيليجرام]]> يوصل الرسائل أسرع من أي تطبيق آخر. - تيليجرام]]> مجاني للأبد. بدون أية إعلانات. وبدون رسوم إشتراك. - تيليجرام]]> يحمي الرسائل الخاصة بك من هجمات المخترقين. - تيليجرام]]> لا يفرض عليك حدود لحجم الوسائط والمحادثات. - تيليجرام]]> يمكنك الوصول إلى الرسائل الخاصة بك من أجهزة متعددة. - تيليجرام]]> الرسائل مشفرة بشكل قوي وتستطيع تدمير ذاتها + تيليجرام هو أسرع]]> تطبيق مراسلة في العالم.\n,وهو كذلك مجاني]]> و آمن]]>. + تيليجرام]]> يوصل الرّسائل أسرع من\nأي تطبيق آخر. + تيليجرام]]> مجّاني دائماً. لا إعلانات.\nدون رسوم إشتراك. + تيليجرام]]> يحمي الرسائل الخاصة بك\nمن هجمات المخترقين. + تيليجرام]]> لا يفرض حدوداً على حجم\nمحادثاتك و وسائطك. + تيليجرام]]> يمكنك من الوصول إلى الرسائل الخاصة بك\nمن أجهزة متعددة. + رسائل تيليجرام]]> مشفرة بشكل قوي\nوتستطيع تدمير ذاتها. إبدأ المراسلة إعدادات الحساب @@ -1256,7 +1446,7 @@ مكالمات ملغاة مكالمة مرفوضة %1$s (%2$s) - هذه الصورة والنص تم اشتقاقهم من مفتاح التشفير لهذه المحادثة السرية مع ]]>%1$s]]>.
    ]]>إذا كانت مطابقة لما يظهر على جهاز ]]>%2$s]]> ، التشفير من البداية للنهاية مضمون.
    + هذه الصورة والنص تم اشتقاقهم من مفتاح التشفير لهذه المحادثة السرية مع ]]>%1$s]]>.\n\nإذا كانت مطابقة لما يظهر على جهاز ]]>%2$s\'s]]> ، التشفير من البداية للنهاية مضمون. لم تقم بإجراء أية مكالمة بعد. إصدار تيليجرام الخاص بـ]]>%1$s]]> غير متوافق. ينبغي عليه تحديث إصدارهم لتتمكن من الاتصال بهم. تطبيق تيليجرام الخاص بـ]]>%1$s]]> لا يدعم المكالمات. ينبغي عليه تحديث تطبيقهم لتتمكن من الاتصال بهم. @@ -1275,13 +1465,18 @@ الرجوع للمكالمة المعذرة، ]]>%1$s]]> لا يستقبل مكالمات. إذا كانت هذه الايموجيات مطابقة للتي تظهر على شاشة %1$s، فالمكالمة 100%% آمنة. + قيم المكالمة + ماذا حدث؟ + أرفق المعلومات التقنية + هذا لن يفصح عن محتوى محادثتك، لكن سيساعد في حل المشكلة بشكل أسرع. + شكرًا لمساعدتك في جعل مكالمة تيليجرام أفضل. - %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 عضوان @@ -1469,6 +1664,12 @@ %1$d رسالة صوتية معاد توجيهها %1$d رسالة صوتية معاد توجيهها %1$d رسالة صوتية معاد توجيهها + %1$d رسالة مرئية معاد توجيهها + رسالة مرئية معاد توجيهها + %1$d رسالة مرئية معاد توجيهها + %1$d رسالة مرئية معاد توجيهها + %1$d رسالة مرئية معاد توجيهها + %1$d رسالة مرئية معاد توجيهها %1$d موقع معاد توجيهه موقع معاد توجيهه %1$d موقع معاد توجيهه @@ -1496,6 +1697,10 @@ MMM dd yyyy, h:mm a MMM dd yyyy, HH:mm + MMM dd yyyy, h:mm a + MMM dd yyyy, HH:mm + MMM dd, h:mm a + MMM dd, HH:mm MMMM yyyy MMM dd dd.MM.yy diff --git a/TMessagesProj/src/main/res/values-de/strings.xml b/TMessagesProj/src/main/res/values-de/strings.xml index 4a5133a0a..d37859c92 100644 --- a/TMessagesProj/src/main/res/values-de/strings.xml +++ b/TMessagesProj/src/main/res/values-de/strings.xml @@ -6,8 +6,10 @@ Telegram Telegram Beta Deutsch + Englisch German de + Weiter auf Deutsch Dein Telefon Bitte bestätige deine Landesvorwahl und deine Telefonnummer. @@ -79,6 +81,7 @@ TEST-RECHNUNG %1$s BEZAHLEN Zahlungsart + Zahlungsbetreiber Name Telefonnummer Kontaktadresse @@ -89,6 +92,8 @@ Tut uns sehr leid. Die Zahlung wurde durch den Bot abgebrochen. Tut uns sehr leid. Die Zahlung wurde abgelehnt. Kann den Zahlungsserver nicht erreichen. Bitte prüfe deine Internetverbindung und versuche es erneut. + Warnung + Weder Telegram noch %1$s haben Zugriff auf deine Kreditkartendaten. Kreditkartendetails werden nur vom Zahlungssystem %2$s abgewickelt.\n\nZahlungen gehen direkt an den Entwickler von %1$s. Telegram kann keine Haftung übernehmen, du handelst auf eigene Gefahr. Wende dich bei Problemen bitte direkt an den Entwickler von %1$s oder an deine Bank. Neue Unterhaltung Einstellungen @@ -100,6 +105,13 @@ Tippe unten auf den Stift für deine erste\nChatnachricht oder auf den Menüknopf\num die restlichen Optionen zu öffnen. Warte auf Netzwerk... Verbinde... + Verbinde mit Proxy... + Hier tippen, um Proxy zu deaktivieren... + Deaktivieren + Aktivieren + Möchtest du wirklich diesen Proxy aktivieren? + Du kann deinen Proxyserver später in den Einstellungen (unter Daten und Speicher) ändern. + Möchtest du wirklich Proxyserver %1$s deaktivieren? Du kannst ihn später wieder in den Einstellungen (Daten und Speicher) aktivieren. Aktualisiere... Neuer Geheimer Chat Warte, bis %s online geht... @@ -139,19 +151,21 @@ Fett Kursiv Normal + Um dich nahtlos mit jedem zu verbinden, den du kennst, erlaube ]]>Telegram]]> Zugriff auf deine Kontakte. + JETZT NICHT + WEITER - Gruppenart - Kanalart Öffentlich Privat Zum Admin machen + Keine gesperrten Nutzer Beschreibe deine Gruppe (optional). Gruppe verlassen Gruppe Löschen Gruppe verlassen Gruppe löschen Du verlierst alle Nachrichten der Gruppe. - Administratoren helfen dir, deine Gruppe zu verwalten. Tippen und halten um sie zu löschen. + Administratoren helfen dir, deine Gruppe zu verwalten. Tippen und halten, um sie zu löschen. Wenn du diese Gruppe löschst, werden alle Mitglieder und alle Nachrichten entfernt. Wirklich löschen? Gruppe erstellt un1 hat dich hinzugefügt @@ -168,6 +182,7 @@ un1 hat eine Datei angeheftet un1 hat einen Sticker angeheftet un1 hat eine Sprachnachricht angeheftet + un1 hat eine Videonachricht angeheftet un1 hat einen Kontakt angeheftet un1 hat %1$s angeheftet un1 hat einen Standort angeheftet @@ -175,7 +190,8 @@ un1 hat ein Musikstück angeheftet Gruppe wurde in eine Supergruppe geändert %1$s wurde in eine Supergruppe geändert - Blockierte Nutzer können nur durch Admins erneut hinzugefügt werden. Einladungslinks funktionieren nicht. + Gesperrte Nutzer werden aus der Gruppe entfernt und können nur durch Admins erneut hinzugefügt werden. Einladungslinks funktionieren nicht. + Gesperrte Nutzer werden aus dem Kanal entfernt und können nur durch Admins erneut hinzugefügt werden. Einladungslinks funktionieren nicht. Neuer Kanal Kanalname Kontakte zum Kanal hinzufügen @@ -217,7 +233,9 @@ Überprüfe Namen... %1$s ist verfügbar. Mitglieder - Blockierte Nutzer + Sperrliste + Gesperrte Nutzer + Eingeschränke Nutzer Administratoren Kanal löschen Kanal löschen @@ -233,19 +251,21 @@ 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. - Moderator - Gründer + Ersteller Administrator STUMM STUMM AUS - Administrator hinzufügen + Admin hinzufügen + Nutzer sperren + Entsperren + Gedrückt halten um zu entsperren Per Link einladen Sicher, dass %1$s ein Administrator werden soll? - Entfernen + Admin entlassen Nur Administratoren sehen diese Liste. Dieser Nutzer ist noch nicht im Kanal; willst du ihn einladen? Jeder, der Telegram installiert hat, kann anhand dieses Links in deinen Kanal. - Administratoren helfen dir, deinen Kanal zu verwalten. Tippen und halten um sie zu löschen. + Administratoren helfen dir, deinen Kanal zu verwalten. Tippen und halten, um sie zu löschen. Möchtest du dem Kanal \'%1$s\' beitreten? Dieser Chat ist nicht mehr zugänglich. Du wurdest gesperrt und kannst öffentliche Gruppen nicht betreten. @@ -271,6 +291,7 @@ %1$s hat eine Datei an den Kanal %2$s gesendet %1$s hat ein GIF an den Kanal %2$s gesendet %1$s hat eine Sprachnachricht an den Kanal %2$s gesendet + %1$s hat eine Videonachricht an den Kanal %2$s gesendet %1$s hat ein Musikstück an den Kanal %2$s gesendet %1$s hat einen Sticker an den Kanal %2$s gesendet %1$s hat einen %3$s Sticker an den Kanal %2$s gesendet @@ -282,6 +303,7 @@ %1$s hat eine Datei gesendet %1$s hat ein GIF gesendet %1$s hat eine Sprachnachricht gesendet + %1$s hat eine Videonachricht gesendet %1$s hat ein Musikstück gesendet %1$s hat einen Sticker gesendet %1$s hat einen %2$s Sticker gesendet @@ -292,6 +314,110 @@ Mitglieder werden nicht benachrichtigt Nachrichten unterschreiben Zeigt für neue Nachrichten an, welcher Admin die Nachricht gesendet hat. + Admin bearbeiten + Was kann dieser Admin machen? + Kanal-Info ändern + Gruppen-Info ändern + Nachrichten senden + Nachrichten von anderen bearbeiten + Nachrichten von anderen löschen + Nachrichten löschen + Neue Admins hinfügen + Admin entlassen + Nutzer sperren + Nutzer hinzufügen + Nutzer per Link einladen + Nachrichten anheften + Hinzugefügt von %1$s + Du kannst die Rechte dieses Admins nicht bearbeiten. + Eingeschränkt von %1$s + Nutzerbeschränkungen + Nachrichten lesen + Was kann dieser Nutzer machen? + Nachrichten senden + Medien senden + Sticker & GIFs senden + Linkvorschau senden + Gesperrt bis + Dauerhaft + Entfernen + Sperren und aus der Gruppe entfernen + Gruppe verwalten + Kanal verwalten + Gruppe verwalten + Kanal verwalten + + Letzte Aktionen + Alle Aktionen + Ausgewählte Aktionen + Alle Admins + ]]>Noch keine Aktionen!]]>\n\nMitglieder und Admins\nhaben noch keine Aktionen in den\nletzten 48 Stunden durchgeführt. + ]]>Noch keine Aktionen!]]>\n\nMitglieder und Admins\nhaben noch keine Aktionen\nin den letzten 48 Stunden durchgeführt. + ]]>Keine Aktionen gefunden]]>\n\nKeine kürzlichen Ereignisse gefunden,\ndie deinen Suchbegriff beinhalten. + Keine kürzlichen Aktionen gefunden, die \']]>%1$s]]>\' beinhalten. + Was sind letzte Aktionen? + Das ist eine Liste aller Aktionen, die von Gruppenmitgliedern und Admins in den letzten 48 Stunden durchgeführt wurden. + Das ist eine Liste aller Aktionen, die von Gruppenmitgliedern und Admins in den letzten 48 Stunden durchgeführt wurden. + un1 hat die Gruppe \"%1$s\" umbenannt + un1 hat den Kanal in \"%1$s\" umbenannt + un1 hat die Gruppe verlassen + un1 hat den Kanal verlassen + un1 hat un2 hinzugefügt + un1 ist der Gruppe beigetreten + hat %1$s gesperrt + hat %1$s entsperrt + un1 ist dem Kanal beigetreten + un1 hat das Gruppenbild geändert + un1 hat das Kanalbild geändert + un1 hat das Gruppenbild entfernt + un1 hat das Kanalbild entfernt + un1 hat diese Nachricht bearbeitet: + un1 hat Beschriftung bearbeitet: + Original-Nachricht + Original-Beschriftung + Leer + un1 hat diese Nachricht angeheftet: + un1 hat angeheftete Nachricht entfernt + un1 hat diese Nachricht gelöscht: + un1 hat den Gruppen-Link geändert: + un1 hat den Kanal-Link geändert: + un1 hat den Gruppen-Link entfernt + un1 hat den Kanal-Link entfernt + Vorheriger Link + un1 hat die Gruppenbeschreibung geändert: + un1 hat die Kanalbeschreibung geändert: + Vorherige Beschreibung + un1 hat Gruppeneinladungen aktiviert + un1 hat Gruppeneinladungen deaktiviert + un1 hat Unterschriften aktiviert + un1 hat Unterschriften deaktiviert + hat Einschränkungen für %1$s geändert\n\nDauer: %2$s + Sticker & GIFs senden + Medien senden + Nachrichten senden + Linkvorschau senden + Nachrichten lesen + hat Berechtigungen von %1$s geändert + Kanal-Info ändern + Gruppen-Info ändern + Nachrichten senden + Nachrichten bearbeiten + Nachrichten löschen + Admins hinzufügen + Nutzer sperren + Nutzer hinzufügen + Nachrichten anheften + Alle Aktionen + Neue Einschränkungen + Neue Admins + Neue Mitglieder + Gruppen-Info + Kanal-Info + Gruppen-Einstellungen + Gelöschte Nachrichten + Bearbeitete Nachrichten + Angeheftete Nachrichten + Ehemalige Mitglieder Neue Broadcast Liste Listenname @@ -323,14 +449,19 @@ tippt… tippt... tippen… - %1$s nimmt etwas auf... + %1$s nimmt eine Sprachnachricht auf... + %1$s nimmt eine Videonachricht auf... %1$s schickt Audio... %1$s schickt Bild... SCHNELLANSICHT + GRUPPE ÖFFNEN + KANAL ÖFFNEN + Das dunkle Farbthema wird Nachts automatisch aktiviert %1$s spielt ein Spiel... %1$s schickt Video... %1$s schickt Datei... - nimmt etwas auf... + nimmt eine Sprachnachricht auf... + nimmt eine Videonachricht auf... schickt Audio... schickt Bild... spielt ein Spiel... @@ -378,6 +509,8 @@ Öffnen in... URL kopieren %1$s senden + Als Datei senden + Als Datei URL %1$s öffnen? Darf %1$s deinen Anzeigenamen und deine id (nicht deine Telefonnummer) mit Internetseiten teilen, die du mit diesem Bot öffnest? SPAM MELDEN @@ -393,6 +526,7 @@ https://telegram.org/faq/de#kann-keine-nachrichten-an-nicht-kontakte-senden Mehr Infos Sende an... + Kommentar hinterlassen... Hier tippen um gespeicherte GIFs zu sehen Anheften Mitglieder benachrichtigen @@ -415,6 +549,7 @@ %1$s Bearbeitungszeit leider abgelaufen. Verknüpfung hinzufügen + Mitglieder suchen Zum Startbildschirm hinzugefügt chatte mit dir selbst Du @@ -427,6 +562,21 @@ Bei %1$s löschen Bei allen Mitgliedern löschen Text in die Zwischenablage kopiert + Halten für Audioaufnahme. Tippen für Video. + Halten für Videoaufnahme. Tippen für Audio. + Sprachnachricht verwerfen + Möchtest du die Aufnahme beenden und wirklich die Sprachnachricht dadurch verwerfen? + Videonachricht verwerfen + Möchtest du die Aufnahme beenden und wirklich die Videonachricht dadurch verwerfen? + Verwerfen + Die Admins dieser Gruppe haben dir die Berechtigung entzogen, Medien bis %1$s zu senden + Die Admins dieser Gruppe haben dir die Berechtigung entzogen, Sticker bis %1$s zu senden + Die Admins dieser Gruppe haben dir die Berechtigung entzogen, Sticker bis %1$s zu senden + Die Admins dieser Gruppe haben dir die Berechtigung entzogen, Nachrichten bis %1$s zu senden + Die Admins dieser Gruppe haben dir die Berechtigung entzogen, Medien zu senden + Die Admins dieser Gruppe haben dir die Berechtigung entzogen, Inline-Bot Inhalte zu senden + Die Admins dieser Gruppe haben dir die Berechtigung entzogen, Sticker zu senden + Die Admins dieser Gruppe haben dir die Berechtigung entzogen, Nachrichten zu senden %1$s hat den Selbstzerstörungs-Timer auf %2$s gesetzt Du hast den Selbstzerstörungs-Timer auf %1$s gesetzt @@ -443,6 +593,7 @@ %1$s hat dir eine Datei gesendet %1$s hat dir ein GIF gesendet %1$s hat dir eine Sprachnachricht gesendet + %1$s hat dir eine Videonachricht gesendet %1$s hat dir ein Musikstück gesendet %1$s hat dir einen Sticker gesendet %1$s hat dir einen %2$s Sticker gesendet @@ -456,12 +607,13 @@ %1$s hat eine Datei an die Gruppe %2$s gesendet %1$s hat ein GIF an die Gruppe %2$s gesendet %1$s hat eine Sprachnachricht an die Gruppe %2$s gesendet + %1$s hat eine Videonachricht an die Gruppe %2$s gesendet %1$s hat ein Musikstück an die Gruppe %2$s gesendet %1$s hat einen Sticker an die Gruppe %2$s gesendet %1$s hat einen %3$s Sticker an die Gruppe %2$s gesendet %1$s hat dich in die Gruppe %2$s eingeladen - %1$s hat den Namen der Gruppe %2$s geändert - %1$s hat das Bild der Gruppe %2$s geändert + %1$s hat die Gruppe %2$s umbenannt + %1$s hat das Gruppenbild von %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 @@ -485,6 +637,7 @@ %1$s hat einen Sticker in der Gruppe %2$s angeheftet %1$s hat einen %3$s Sticker in der Gruppe %2$s angeheftet %1$s hat eine Sprachnachricht in der Gruppe %2$s angeheftet + %1$s hat eine Videonachricht in der Gruppe %2$s angeheftet %1$s hat einen Kontakt in der Gruppe %2$s angeheftet %1$s hat einen Standort in der Gruppe %2$s angeheftet %1$s hat ein GIF in der Gruppe %2$s angeheftet @@ -497,6 +650,7 @@ %1$s hat einen Sticker angeheftet %1$s hat einen %2$s Sticker angeheftet %1$s hat eine Sprachnachricht angeheftet + %1$s hat eine Videonachricht angeheftet %1$s hat einen Kontakt angeheftet %1$s hat einen Stamdort angeheftet %1$s hat ein GIF angeheftet @@ -551,8 +705,10 @@ Einstellungen Mitglied hinzufügen Administratoren + NUTZER EINSCHRÄNKEN Löschen und Gruppe verlassen Mitteilungen + Nutzer einschränken Aus der Gruppe entfernen In Supergruppe ändern In Supergruppe ändern @@ -560,7 +716,7 @@ Warnung Du kannst die Supergruppe nicht mehr in eine normale Gruppe ändern. ]]>Gruppenlimit erreicht.]]>\n\nFür weitere Funktionen und um das Limit aufzuheben in Supergruppe ändern:\n\n• Bis zu %1$s sind nun möglich\n• Neue Mitglieder sehen gesamten Verlauf\n• Gelöschte Nachrichten werden bei allen Mitgliedern entfernt\n• Admins können Nachrichten anheften\n• Gruppenersteller kann die Gruppe öffentlich machen - ]]>In Supergruppen:]]>\n\n• Neue Mitglieder sehen gesamten Verlauf\n• Nachrichten werden bei allen gelöscht\n• Admins können Nachrichten anheften\n• Gründer kann Gruppe öffentlich machen + ]]>In Supergruppen:]]>\n\n• Neue Mitglieder sehen gesamten Verlauf\n• Nachrichten sind bei allen löschbar\n• Admins können Nachrichten anheften\n• Ersteller kann Gruppe öffentlich machen ]]>Wichtig:]]> Die Änderung kann nicht rückgängig gemacht werden. Teilen @@ -584,7 +740,7 @@ Geheimer Schlüssel Selbstzerstörungs-Timer Aus - Bild und Text zeigen den aktuellen Schlüssel dieses geheimen Chats mit ]]>%1$s]]>.
    ]]>Sehen sie auf dem Gerät von ]]>%2$s]]> genau so aus, ist eure Sicherheit garantiert.
    ]]>Erfahre mehr unter telegram.org
    + Bild und Text zeigen den Schlüssel dieses geheimen Chats mit ]]>%1$s]]>.\n\nSehen sie auf dem Gerät von ]]>%2$s\'s]]> genau so aus, ist Ende-zu-Ende Verschlüsselung garantiert.\n\nErfahre mehr unter telegram.org https://telegram.org/faq/de#geheime-chats Tippen für Emoji-Ansicht Unbekannt @@ -598,14 +754,14 @@ Ein Benutzername benötigt mindestens 5 Zeichen. Ein Benutzername darf maximal 32 Zeichen haben. Benutzernamen dürfen leider nicht mit einer Zahl anfangen. - Wähle einen öffentlichen Benutzernamen, wenn du von anderen bei ]]>Telegram]]> gefunden werden willst — ohne, dass sie deine Nummer kennen müssen.
    ]]>Erlaubt sind ]]>a-z]]>, ]]>0-9]]> und Unterstriche. Die Mindestlänge beträgt ]]>5]]> Zeichen.
    + Wähle einen öffentlichen Benutzernamen, wenn du von anderen bei ]]>Telegram]]> gefunden werden willst — ohne, dass sie deine Nummer kennen müssen.\n\nErlaubt sind ]]>a–z]]>, ]]>0–9]]> und Unterstriche. Die Mindestlänge beträgt ]]>5]]> Zeichen. Dieser Link öffnet einen Chat mit dir:\n%1$s Prüfe Benutzername... %1$s ist verfügbar. Keiner Es ist ein Fehler aufgetreten. - Sticker + Sticker Sticker hinzufügen Masken hinzufügen Sticker hinzufügen @@ -638,6 +794,8 @@ Einige deiner Masken wurden archiviert. Du kannst sie in den Masken-Einstellungen erneut hinzufügen. Thema + Dunkel + Blau Möchtest du wirklich dieses Thema löschen? Themen-Datei ist fehlerhaft. Einen Namen festlegen @@ -657,7 +815,7 @@ Animationen aktivieren Freigeben Gedrückt halten um freizugeben. - Keine blockierten Benutzer + Keine blockierten Nutzer Nachrichten Benachrichtigung Vorschau @@ -683,7 +841,10 @@ Grün Cyan Weiß + Sepia + Dunkel Pink + Textgröße Violett Orange Einige Geräte besitzen ein kleines Licht, welches farbig leuchten oder blinken kann, um dich über neue Nachrichten zu informieren. @@ -693,7 +854,7 @@ Aus Einschalten Ausschalten - Blockierte Benutzer + Blockierte Nutzer Abmelden Kein Ton Standard @@ -707,7 +868,8 @@ Kontakt ist Telegram beigetreten Angeheftete Nachrichten Sprache - Bedenke bitte, dass der Telegram Support von ehrenamtlichen Helfern betreut wird. Wir versuchen so schnell wie möglich zu antworten, dies kann jedoch manchmal ein bisschen dauern.
    ]]>Bitte schau auch in den Fragen und Antworten ]]> nach. Dort findest du Antworten auf die meisten Fragen und wichtige Tipps zur Problembehandlung]]>.
    + Eigene + Bedenke bitte, dass der Telegram Support von ehrenamtlichen Helfern betreut wird. Wir versuchen so schnell wie möglich zu antworten, dies kann jedoch manchmal ein bisschen dauern.\n\nBitte schau auch in den Telegram FAQ]]> nach. Dort findest du Antworten auf die meisten Fragen und wichtige Tipps zur Problembehandlung]]>. Eine Frage stellen Fragen und Antworten https://telegram.org/faq/de @@ -788,6 +950,19 @@ Debug-Menü Kontakte importieren Kontakte neu laden + Du kannst die Sprache später in den Einstellungen ändern. + Wähle deine Sprache + Weitere + Proxy-Einstellungen + Proxy + Proxy-Einstellungen benutzen + Server + Kennwort + Port + Benutzername + SOCKS5 Proxy-Einstellungen + Proxy für Anrufe benutzen + Proxyserver können die Qualität von Anrufen beinflussen. Lokale Datenbank Textnachrichten-Cache leeren? @@ -797,7 +972,7 @@ Berechne... Dateien Bilder - Sprachnachrichten + Sprach-/Videonachrichten Videos Musik GIFs @@ -806,6 +981,8 @@ Medien behalten Bilder, Videos und andere Dateien, auf die du während dieser Zeit nicht zugegriffen]]> hast, werden von diesem Gerät gelöscht, um Speicherplatz zu sparen.\n\nAlle Medien bleiben in der Telegram Cloud gespeichert und können jederzeit wieder heruntergeladen werden. Dauerhaft + Sprachnachrichten + Videonachrichten Sitzungen Aktuelle Sitzung @@ -870,7 +1047,7 @@ %1$d von %2$d Galerie Alle Bilder - Alle Videos + Alle Medien Noch keine Bilder Noch keine Videos Medien bitte zuerst herunterladen @@ -1004,6 +1181,11 @@ Privatsphäre und Sicherheit Privatsphäre Zuletzt gesehen + Zahlungen + Zahlungsinfo entfernen + Du kannst deine Versandinfos löschen und alle Zahlungsbetreiber anweisen, deine gespeicherten Kreditkartendaten zu entfernen. Beachte bitte, dass Telegram niemals deine Kreditkartendaten speichert. + Versandinformationen + Zahlungsinformationen Jeder Meine Kontakte Niemand @@ -1044,6 +1226,8 @@ Du kannst diesen Nutzer nicht hinzufügen, weil er das nicht erlaubt. Du kannst diesen Nutzer nicht hinzufügen, weil er das nicht erlaubt. Du kannst mit diesen Nutzern keine Gruppe erstellen, weil sie es nicht erlauben. + Peer-to-Peer + Deaktivierst du Peer-to-Peer, werden alle Anrufe über die Telegram Server geleitet. Dadurch ist deine IP-Adresse nicht mehr sichtbar, die Gesprächsqualität wird jedoch leicht abnehmen. Video bearbeiten Sende Video... @@ -1121,6 +1305,7 @@ Datei Sticker Sprachnachricht + Videonachricht Spiel Du Du hast ein Bildschirmfoto gemacht! @@ -1139,7 +1324,7 @@ Du hast keinen Videoplayer. Bitte installiere einen um fortzufahren. Bitte sende eine E-Mail an sms@stel.com mit einer Beschreibung des Problems. Du hast keine Applikationen, die den Dateityp \'%1$s\' öffnen könnten. Bitte installiere eine entsprechende Anwendung um fortzufahren. - Dieser Benutzer hat noch kein Telegram. Möchtest du ihn einladen? + Dieser Nutzer hat noch kein Telegram. Möchtest du ihn einladen? Bist du sicher? %1$s dem Chat %2$s hinzufügen? Wieviele der letzten Nachrichten willst du weiterleiten? @@ -1178,7 +1363,7 @@ Verzeihung, diese Funktion ist derzeit in deinem Land nicht verfügbar. Kein Konto mit diesem Benutzernamen Keine Gruppen mit diesem Bot möglich - Möchtest du die erweitere Linkvorschau in Geheimen Chats aktivieren? Die Vorschau wird auf den Telegram Servern generiert. + Möchtest du die erweiterte Linkvorschau in Geheimen Chats aktivieren? Die Vorschau wird auf den Telegram Servern generiert. Inline Bots werden von Drittentwicklern erstellt. Symbole, die du nach dem Botnamen eingibst, werden an den jeweiligen Entwickler geschickt, damit der Bot funktioniert. Möchtest du \"Zum Sprechen ans Ohr\" für Sprachnachrichten aktivieren? Du kannst diese Nachricht nicht bearbeiten. @@ -1189,6 +1374,11 @@ Du kannst diese Aktion nicht durchführen. Du kannst diesen Nutzer oder Bot Gruppen leider nicht hinzufügen, da du ihn blockiert hast. Gebe ihn wieder frei, um fortzufahren. GRUPPE BEITRETEN + Du kannst diesen Nutzer nicht als Admin festlegen, da er kein Mitglied dieser Gruppe ist oder du keine Erlaubnis hast, ihn einzuladen. + Du kannst diesen Nutzer nicht als Admin festlegen, da er sich in der Sperrliste befindet und du ihn nicht entsperren darfst. + Du kannst diesen Nutzer nicht sperren, da er ein Admin dieser Gruppe ist und du keine Berechtigung hast, ihn zurückzustufen. + Die Admins dieser Gruppe haben dir die Berechtigung entzogen, Sticker zu senden. + Die Admins dieser Gruppe haben dir die Berechtigung entzogen, Medien zu senden. Telegram benötigt Zugriff auf deine Kontakte um dich auf all denen Geräten mit deinen Freunden zu verbinden. Telegram benötigt Zugriff auf deinen Speicher, damit du Bilder, Videos und Musik senden und speichern kannst. @@ -1207,13 +1397,13 @@ Leistungsstark Cloud-Basiert Vertraulich - Die schnellste]]> Messaging App der Welt.Kostenlos]]> und sicher]]>. - Telegram]]> stellt Nachrichten schneller]]>zu als andere Anwendungen. - Telegram]]> ist für immer kostenlos.]]>Keine Werbung. Keine Abo-Gebühr. - Telegram]]> schützt deine Nachrichten ]]>vor Hacker-Angriffen. - Telegram]]> unterstützt unbegrenzt große ]]>Chats und Mediendateien. - Telegram]]> lässt sich von verschiedenen Geräten]]>gleichzeitig nutzen. - Telegram]]>-Nachrichten sind stark verschlüsselt]]>und können sich selbst zerstören. + Die schnellste]]> Messaging App der Welt.\nKostenlos]]> und sicher]]>. + Telegram]]> übermittelt Nachrichten\nschneller als jede andere Anwendung. + Telegram]]> bleibt immer gratis.\nKeine Werbung. Keine Abo-Gebühren. + Telegram]]> schützt deine Nachrichten\nvor Hacker-Angriffen. + Telegram]]> hat keine Begrenzungen auf\ndie Größe deiner Medien oder Chats. + Telegram]]> kannst du vom Handy\nTablet oder auch Computer\nsynchronisiert benutzen. + Telegram]]>-Nachrichten sind stark verschlüsselt\nund können sich selbst zerstören. Jetzt beginnen Kontoeinstellungen @@ -1228,8 +1418,8 @@ Keine Verbindung möglich Es klingelt Besetzt - Telegram Anruf - Laufender Telegram Anruf + Telegram-Anruf + Laufender Telegram-Anruf Anruf beenden Ein anderer Anruf ist bereits aktiv Du bist mit %1$s]]> bereits im Gespräch. Möchtest du den Anruf beenden und einen neuen mit %2$s]]> starten? @@ -1256,7 +1446,7 @@ Abgebrochener Anruf Abgelehnter Anruf %1$s (%2$s) - Bild und Text zeigen den Schlüssel dieses Anrufs mit ]]>%1$s]]>.
    ]]>Sehen sie auf dem Gerät von ]]>%2$s\'s]]> genau so aus, ist Ende-zu-Ende Verschlüsselung garantiert.
    + Bild und Text zeigen den aktuellen Schlüssel dieses Sprachanrufs mit ]]>%1$s]]>.\n\nSehen sie auf dem Gerät von ]]>%2$s\'s]]> genau so aus, ist Ende-zu-Ende Verschlüsselung garantiert. Du hast noch keine Anrufe geführt. ]]>%1$s]]>s App ist nicht mit unserem Protokoll kompatibel. Dein Chatpartner muss seine App aktualisieren, bevor du anrufen kannst. ]]>%1$s]]> unterstützt keine Anrufe. Dein Chatpartner muss seine App aktualisieren, bevor du ihn anrufen kannst. @@ -1268,20 +1458,25 @@ Erneut anrufen Standard Möchtest du wirklich diesen Eintrag aus der Anrufliste löschen? - Telegram Anruf + Telegram-Anruf Ohrhörer Lautsprecher Bluetooth ZURÜCK ZUM ANRUF ]]>%1$s]]> akzeptiert leider keine Anrufe. Wenn diese Emoji genau so bei %1$s aussehen, ist euer Anruf 100%% sicher. + Anruf bewerten + Was klappte nicht? + Technische Infos senden + Das hilft uns das Problem schneller zu beseitigen. Die Inhalte deiner Konversation werden nicht angezeigt. + Danke, dass du uns hilfst, Telegram Anrufe zu verbessern. - %1$d online - %1$d online - %1$d online - %1$d online - %1$d online - %1$d online + %1$d online + %1$d online + %1$d online + %1$d online + %1$d online + %1$d online %1$d Mitglieder %1$d Mitglied %1$d Mitglieder @@ -1469,6 +1664,12 @@ %1$d weitergeleitete Sprachnachrichten %1$d weitergeleitete Sprachnachrichten %1$d weitergeleitete Sprachnachrichten + %1$d angehängte Videonachrichten + Videonachrichtenanhang + %1$d angehängte Videonachrichten + %1$d angehängte Videonachrichten + %1$d angehängte Videonachrichten + %1$d angehängte Videonachrichten %1$d weitergeleiteten Standorte Angehängter Standort %1$d angehängte Standorte @@ -1496,11 +1697,15 @@ dd. MMM yyyy, h:mm a dd. MMM yyyy, HH:mm + dd MMM yyyy, h:mm a + dd MMM yyyy, HH:mm + dd. MMM yyyy, h:mm a + dd MMM, HH:mm MMMM yyyy dd MMM dd.MM.yy dd.MM.yyyy - d MMMM + d\'.\' MMMM d MMMM, yyyy EEE HH:mm diff --git a/TMessagesProj/src/main/res/values-es/strings.xml b/TMessagesProj/src/main/res/values-es/strings.xml index 8d2237fce..76345828a 100644 --- a/TMessagesProj/src/main/res/values-es/strings.xml +++ b/TMessagesProj/src/main/res/values-es/strings.xml @@ -6,8 +6,10 @@ Telegram Telegram Beta Español + Inglés Spanish es + Seguir en inglés Tu teléfono Por favor, confirma tu código de país y pon tu número de teléfono. @@ -79,6 +81,7 @@ PROBAR FACTURA PAGAR %1$s Forma de pago + Proveedor de pago Nombre Número de teléfono Dirección de contacto @@ -89,6 +92,8 @@ Lo sentimos, el pago fue cancelado por el bot. Lo sentimos, el pago fue rechazado. No es posible acceder al servidor de pago. Por favor, revisa tu conexión a internet y reinténtalo. + Advertencia + Ni Telegram, ni %1$s tendrán acceso a la información de tu tarjeta de crédito. Los detalles de la tarjeta de crédito serán manejados sólo por el sistema de pago, %2$s.\n\nLos pagos irán directamente al desarrollador de %1$s. Telegram no puede entregar garantías, así que es bajo tu propio riesgo. En caso de problemas, contacta al desarrollador de %1$s o a tu banco. Nuevo chat Ajustes @@ -100,6 +105,13 @@ Envía mensajes tocando el botón para\nredactar, en la parte inferior derecha,\no pulsa el botón menú para más opciones. Esperando red... Conectando... + Conectando al proxy... + Toca aquí para desactivarlo... + Desactivar + Activar + ¿Quieres activar este proxy? + Puedes cambiar tu servidor proxy más tarde en los Ajustes (Datos y almacenamiento). + ¿Quieres desactivar el servidor proxy %1$s? Puedes volver a activarlo más tarde en los Ajustes (Datos y almacenamiento). Actualizando... Nuevo chat secreto Esperando que %s se conecte... @@ -139,12 +151,14 @@ Negrita Cursiva Normal + Para conectarte sin problemas con todas las personas que conoces, permite a ]]>Telegram]]> acceder a tus contactos. + AHORA NO + SEGUIR - Tipo de grupo - Tipo de canal Público Privado - Nombrar como administrador + Ascender a administrador + Sin suspendidos Puedes poner una descripción para tu grupo. Salir del grupo Eliminar grupo @@ -168,6 +182,7 @@ un1 ancló un archivo un1 ancló un sticker un1 ancló un mensaje de voz + un1 ancló un videomensaje un1 ancló un contacto un1 ancló %1$s un1 ancló un mapa @@ -176,6 +191,7 @@ Este grupo fue convertido en un supergrupo %1$s fue convertido en un supergrupo Los usuarios suspendidos son eliminados del grupo y sólo pueden volver si son invitados por un administrador. Los enlaces de invitación no funcionan para ellos. + Los usuarios suspendidos son eliminados del canal y sólo pueden volver si son invitados por un administrador. Los enlaces de invitación no funcionan para ellos. Nuevo canal Nombre del canal Añadir contactos a tu canal @@ -217,7 +233,9 @@ Verificando nombre... %1$s está disponible. Miembros - Suspendidos + Suspendidos + Usuarios suspendidos + Usuarios restringidos Administradores Eliminar canal Eliminar canal @@ -233,15 +251,17 @@ Foto del canal eliminada Nombre del canal cambiado a un2 Lo sentimos, tienes demasiados alias públicos. Puedes anular el enlace de uno de tus grupos o canales anteriores, o crear uno privado. - Moderador Creador Administrador SILENCIAR NO SILENCIAR Añadir administrador + Suspender usuario + Quitar suspensión + Toca y mantén sobre un usuario para quitar su suspensión. Invitar con un enlace ¿Quieres nombrar a %1$s como administrador? - Eliminar + Eliminar administrador Sólo los administradores del canal pueden ver esta lista. Este usuario aún no se ha unido al canal. ¿Quieres invitarlo? Cualquiera que tenga Telegram instalada podrá unirse a tu canal siguiendo este enlace. @@ -271,6 +291,7 @@ %1$s envió un archivo al canal %2$s %1$s envió un GIF al canal %2$s %1$s envió un mensaje de voz al canal %2$s + %1$s envió un videomensaje al canal %2$s %1$s envió una pista al canal %2$s %1$s envió un sticker al canal %2$s %1$s envió un %3$s sticker al canal %2$s @@ -282,6 +303,7 @@ %1$s publicó un archivo %1$s publicó un GIF %1$s publicó un mensaje de voz + %1$s publicó un videomensaje %1$s publicó una pista %1$s publicó un sticker %1$s publicó un %2$s sticker @@ -292,6 +314,110 @@ Tu publicación no será notificada Firmar los mensajes Añadir los nombres de los administradores a sus publicaciones. + Editar administrador + ¿Qué puede hacer este administrador? + Cambiar información del canal + Cambiar información del grupo + Publicar mensajes + Editar mensajes de otros + Eliminar mensajes de otros + Eliminar mensajes + Añadir administradores + Eliminar administrador + Suspender usuarios + Añadir usuarios + Invitar con un enlace + Anclar mensajes + Añadido por %1$s + No puedes editar los privilegios de este administrador. + Restringido por %1$s + Restricciones + Leer mensajes + ¿Qué puede hacer este usuario? + Enviar mensajes + Enviar multimedia + Enviar stickers y GIF + Vista previa de enlaces + Suspendido hasta + Siempre + Eliminar + Suspender y eliminar del grupo + Administrar grupo + Administrar canal + Administrar grupo + Administrar canal + + Acciones recientes + Todas las acciones + Acciones seleccionadas + Todos los administradores + ]]>Aún no hay eventos]]>\n\nLos administradores del grupo\nno han realizado acciones de servicio\nen las últimas 48 horas. + ]]>Aún no hay eventos]]>\n\nLos administradores del canal\nno han realizado acciones de servicio\nen las últimas 48 horas. + ]]>No se encontraron acciones]]>\n\nNo se encontraron acciones recientes\nque coincidan con tu solicitud. + No se encontraron acciones recientes que contengan \']]>%1$s]]>. + ¿Qué son las acciones recientes? + Esta es una lista de las acciones de servicio realizadas por los miembros y administradores del grupo en las últimas 48 horas. + Esta es una lista de las acciones de servicio realizadas por los administradores del canal en las últimas 48 horas. + un1 renombró el grupo a “%1$s” + un1 renombró el canal a “%1$s” + un1 dejó el grupo + un1 dejó el canal + un1 añadió a un2 + un1 se unió al grupo + suspendió a %1$s + quitó la suspensión a %1$s + un1 se unió al canal + un1 puso una nueva foto del grupo + un1 puso una nueva foto del canal + un1 eliminó la foto del grupo + un1 eliminó la foto del canal + un1 editó este mensaje: + un1 editó un comentario: + Mensaje original + Comentario original + Vacío + un1 ancló este mensaje: + un1 desancló un mensaje + un1 eliminó este mensaje: + un1 cambió el enlace del grupo: + un1 cambió el enlace del canal: + un1 eliminó el enlace del grupo + un1 eliminó el enlace del canal + Enlace anterior + un1 editó la descripción del grupo: + un1 editó la descripción del canal: + Descripción anterior + un1 activó las invitaciones + un1 desactivó las invitaciones + un1 activó las firmas + un1 desactivó las firmas + cambió las restricciones para %1$s\n\nDuración: %2$s + Enviar stickers y GIF + Enviar multimedia + Enviar mensajes + Vista previa de enlaces + Leer mensajes + cambió los privilegios de %1$s + Cambiar la información del canal + Cambiar la información del grupo + Publicar mensajes + Editar mensajes + Eliminar mensajes + Añadir administradores + Suspender usuarios + Añadir usuarios + Anclar mensajes + Todas las acciones + Nuevas restricciones + Nuevos administradores + Nuevos miembros + Información del grupo + Información del canal + Ajustes del grupo + Mensajes eliminados + Mensajes editados + Mensajes anclados + Miembros que se fueron Nueva difusión Nombre de la lista @@ -324,13 +450,18 @@ está escribiendo... están escribiendo... %1$s está grabando un mensaje de voz... + %1$s está grabando un videomensaje... %1$s está enviando un audio... %1$s está enviando una foto... VISTA RÁPIDA + ABRIR GRUPO + ABRIR CANAL + El tema oscuro se usará automáticamente en la noche %1$s está jugando... %1$s está enviando un vídeo... %1$s está enviando un archivo... - grabando mensaje de voz... + grabando un mensaje de voz... + grabando un videomensaje... enviando audio... enviando foto... jugando... @@ -378,6 +509,8 @@ Abrir en... Copiar URL Enviar %1$s + Enviar como archivo + Como archivo ¿Abrir %1$s? ¿Permites a %1$s entregar tu nombre e id de Telegram (no tu número de teléfono) a los sitios web que abres a través de este bot? REPORTAR SPAM @@ -393,6 +526,7 @@ https://telegram.org/faq/es#no-puedo-enviar-mensajes-a-quienes-no-son-mis-contactos Más información Enviar a... + Escribe un comentario... Toca y ve los GIF guardados Anclar Notificar a los miembros @@ -415,6 +549,7 @@ %1$s Terminó el tiempo de edición. Añadir acceso directo + Buscar miembros Acceso directo añadido al escritorio chat contigo @@ -427,6 +562,21 @@ Eliminar para %1$s Eliminar para todos Texto copiado al portapapeles + Mantén para grabar audio. Toca para vídeo. + Mantén para grabar vídeo. Toca para audio. + Descartar mensaje de voz + ¿Quieres detener la grabación y descartar tu mensaje de voz? + Descartar videomensaje + ¿Quieres detener la grabación y descartar tu videomensaje? + Descartar + Los administradores del grupo han restringido que envíes multimedia hasta el %1$s + Los administradores del grupo han restringido que envíes contenido integrado hasta el %1$s + Los administradores del grupo han restringido que envíes stickers hasta el %1$s + Los administradores del grupo han restringido que escribas hasta el %1$s + Los administradores del grupo han restringido que envíes multimedia + Los administradores del grupo han restringido que envíes contenido integrado + Los administradores del grupo han restringido que envíes stickers + Los administradores del grupo han restringido que escribas %1$s activó la autodestrucción en %2$s Activaste la autodestrucción en %1$s @@ -443,6 +593,7 @@ %1$s te envió un archivo %1$s te envió un GIF %1$s te envió un mensaje de voz + %1$s te envió un videomensaje %1$s te envió una pista %1$s te envió un sticker %1$s te envió un %2$s sticker @@ -456,11 +607,12 @@ %1$s envió un archivo al grupo %2$s %1$s envió un GIF al grupo %2$s %1$s envió un mensaje de voz al grupo %2$s + %1$s envió un videomensaje al grupo %2$s %1$s envió una pista al grupo %2$s %1$s envió un sticker al grupo %2$s %1$s envió un %3$s sticker al grupo %2$s %1$s te invitó al grupo %2$s - %1$s cambió el nombre del grupo %2$s + %1$s renombró el grupo %2$s %1$s cambió la foto del grupo %2$s %1$s invitó a %3$s al grupo %2$s %1$s volvió al grupo %2$s @@ -485,6 +637,7 @@ %1$s ancló un sticker en el grupo %2$s %1$s ancló un %3$s sticker en el grupo %2$s %1$s ancló un mensaje de voz en el grupo %2$s + %1$s ancló un videomensaje en el grupo %2$s %1$s ancló un contacto en el grupo %2$s %1$s ancló un mapa en el grupo %2$s %1$s ancló un GIF en el grupo %2$s @@ -497,6 +650,7 @@ %1$s ancló un sticker %1$s ancló un %2$s sticker %1$s ancló un mensaje de voz + %1$s ancló un videomensaje %1$s ancló un contacto %1$s ancló un mapa %1$s ancló un GIF @@ -551,8 +705,10 @@ Ajustes Añadir miembro Nombrar administradores + SUSPENDER DEL GRUPO Eliminar y salir del grupo Notificaciones + Restringir usuario Eliminar del grupo Convertir en supergrupo Convertir en supergrupo @@ -584,7 +740,7 @@ Clave de cifrado Autodestrucción Apagada - El texto e imagen derivan de la clave de cifrado para el chat secreto creado con ]]>%1$s]]>.
    ]]>Si se ven igual en el dispositivo de ]]>%2$s]]>, el cifrado end-to-end está garantizado.
    ]]>Conoce más en telegram.org
    + El texto e imagen derivan de la clave de cifrado para el chat secreto creado con ]]>%1$s]]>.\n\nSi se ven igual en el dispositivo de ]]>%2$s]]>, el cifrado end-to-end está garantizado.\n\nConoce más en telegram.org https://telegram.org/faq/es#chats-secretos Toca para ver emojis Desconocido @@ -598,14 +754,14 @@ Un alias debe tener al menos 5 caracteres. El alias no debe exceder los 32 caracteres. Lo sentimos, un alias no puede comenzar con un número. - Puedes elegir un alias en ]]>Telegram]]>. Si lo haces, otras personas te podrán encontrar por ese alias y contactarte sin saber tu número de teléfono.
    ]]>Puedes usar ]]>a–z]]>, ]]>0–9]]> y guiones bajos. La longitud mínima es de ]]>5]]> caracteres.
    + Puedes elegir un alias en ]]>Telegram]]>. Si lo haces, otras personas te podrán encontrar por ese alias y contactarte sin saber tu número de teléfono.\n\nPuedes usar ]]>a–z]]>, ]]>0–9]]> y guiones bajos. La longitud mínima es de ]]>5]]> caracteres. Este enlace abre un chat contigo en Telegram:\n%1$s Verificando alias... %1$s está disponible. Ninguno Ocurrió un error. - Stickers + Stickers Añadir stickers Añadir máscaras Añadir a stickers @@ -638,6 +794,8 @@ Algunos de tus packs de máscaras más viejos fueron archivados. Puedes reactivarlos desde los ajustes de máscaras. Tema + Oscuro + Azul ¿Quieres eliminar este tema? Archivo de tema incorrecto Pon el nombre del tema @@ -683,7 +841,10 @@ Verde Cian Blanco + Sepia + Oscuro Rosa + Tamaño del texto Violeta Naranja LED es una pequeña luz que parpadea en algunos dispositivos para señalar los mensajes nuevos. @@ -707,7 +868,8 @@ Un contacto se unió a Telegram Mensajes anclados Idioma - Por favor, considera que el soporte de Telegram está hecho por voluntarios. Respondemos lo antes posible, pero puede tomar tiempo.
    ]]>Por favor, mira las preguntas frecuentes de Telegram]]>: tienen respuestas para la mayoría de las preguntas y soluciones a problemas]]>.
    + Personalizado + Por favor, considera que el soporte de Telegram está hecho por voluntarios. Respondemos lo antes posible, pero puede tomar tiempo.\n\nPor favor, mira las preguntas frecuentes de Telegram]]>: tienen respuestas para la mayoría de las preguntas y soluciones a problemas]]>. Preguntar Preguntas frecuentes https://telegram.org/faq/es @@ -734,7 +896,7 @@ Globo en el ícono Cortas Largas - Descarga automática de multimedia + Autodescarga de multimedia Con uso de datos móviles Con conexión a Wi-Fi Con itinerancia de datos @@ -788,6 +950,19 @@ Menú de depuración Importar contactos Recargar contactos + Puedes cambiar el idioma después, en Ajustes. + Elige tu idioma + Otro + Ajustes de proxy + Proxy + Usar ajustes de proxy + Servidor + Contraseña + Puerto + Alias + Ajustes de proxy SOCKS5 + Usar proxy para llamadas + El proxy puede disminuir la calidad de tus llamadas. Base de datos local ¿Eliminar los mensajes en la caché? @@ -797,7 +972,7 @@ Calculando... Archivos Fotos - Mensajes de voz + Mensajes de voz/vídeo Vídeos Música GIF @@ -806,6 +981,8 @@ Conservar multimedia Las fotos, los vídeos y los archivos de los chats en la nube a los que no accedas]]> durante ese periodo de tiempo se eliminarán del dispositivo para liberar espacio.\n\nToda la multimedia permanecerá en la nube de Telegram y podrás volver a descargarla si la necesitas. Siempre + Mensajes de voz + Videomensajes Sesiones activas Sesión actual @@ -830,7 +1007,7 @@ Pon, otra vez, tu nuevo código Código de acceso inválido Los códigos de acceso no coinciden - Bloqueo automático + Autobloqueo El bloqueo se activará transcurrido este tiempo. en %1$s Desactivado @@ -870,7 +1047,7 @@ %1$d de %2$d Galería Todas las fotos - Todos los vídeos + Toda la multimedia Aún no hay fotos Aún sin vídeos Por favor, primero descarga la multimedia @@ -1004,6 +1181,11 @@ Privacidad y seguridad Privacidad Última conexión + Pagos + Eliminar información de pago + Puedes eliminar tu información de envío y forzar a todos los proveedores de pago a eliminar tus tarjetas de crédito guardadas. Telegram nunca almacena tus datos de tarjetas de crédito. + Información de envío + Información de pago Todos Mis contactos Nadie @@ -1044,6 +1226,8 @@ No puedes añadir este usuario a grupos debido a sus ajustes de privacidad. No puedes añadir este usuario a canales debido a sus ajustes de privacidad. No puedes crear un grupo con estos usuarios debido a sus ajustes de privacidad. + Peer-to-Peer + Al desactivar peer-to-peer todas las llamadas pasarán por los servidores de Telegram para evitar dar conocer tu dirección IP, pero disminuirá ligeramente la calidad de audio. Editar vídeo Enviando vídeo... @@ -1121,6 +1305,7 @@ Archivo Sticker Mensaje de voz + Videomensaje Juego ¡Hiciste una captura de pantalla! @@ -1149,7 +1334,7 @@ ¿Enviar mensajes a %1$s? ¿Compartir el juego con %1$s? ¿Enviar contacto a %1$s? - ¿Quieres cerrar sesión?\n\nConsidera que puedes usar Telegram en todos tus dispositivos a la vez.\n\nRecuerda que, al cerrar sesión, eliminas todos tus chats secretos. + ¿Quieres cerrar sesión?\n\nRecuerda que puedes usar Telegram en todos tus dispositivos a la vez.\n\nNo olvides que, al cerrar sesión, eliminas todos tus chats secretos. ¿Quieres terminar todas las otras sesiones? ¿Quieres eliminar y salir del grupo? ¿Quieres eliminar este chat? @@ -1189,6 +1374,11 @@ Lo sentimos, no estás autorizado para hacer esto. No puedes añadir a este usuario o bot al grupo porque lo has bloqueado. Por favor, desbloquéalo para continuar. UNIRME AL GRUPO + No puedes añadir a este usuario como administrador porque no es un miembro de este grupo y no puedes invitarlo. + No puedes añadir a este usuario como administrador porque está en la lista de suspendidos y no puedes quitar su suspensión. + No puedes suspender a este usuario porque es un administrador del grupo y no puedes degradarlo. + Los administradores del grupo han restringido que envíes stickers. + Los administradores del grupo han restringido que envíes multimedia. Telegram necesita el acceso a tus contactos, para que puedas comunicarte con ellos en todos tus dispositivos. Telegram necesita acceso a tu almacenamiento, para que puedas enviar y guardar fotos, vídeos, música y otros archivos. @@ -1207,14 +1397,14 @@ Poderosa Basada en la nube Privada - La aplicación de mensajería másveloz]]> del mundo. Es gratis]]> y segura]]>. - Telegram]]> entrega mensajes más]]>rápido que cualquier otra aplicación. - Telegram]]> es gratis para siempre.]]>Sin publicidad ni suscripciones. - Telegram]]> mantiene tus mensajes]]>a salvo del ataque de hackers. - Telegram]]> no tiene límites en]]>el tamaño de tus chats y archivos. - Telegram]]> te permite acceder a tus]]>mensajes desde múltiples dispositivos. - Telegram]]> posee mensajes fuertemente]]>cifrados y se pueden autodestruir. - Empieza a conversar + La aplicación de mensajería más veloz]]> del mundo.\nEs gratis]]> y segura]]>. + Telegram]]> entrega mensajes más rápido\nque cualquier otra aplicación. + Telegram]]> es gratis para siempre. Sin publicidad.\nSin suscripciones. + Telegram]]> mantiene tus mensajes\na salvo del ataque de hackers. + Telegram]]> no tiene límites en el\ntamaño de tus chats y archivos. + Telegram]]> te permite acceder a tus mensajes\ndesde múltiples dispositivos. + Telegram]]> posee mensajes fuertemente\ncifrados y se pueden autodestruir. + Comenzar Ajustes de la cuenta Usar menos datos @@ -1256,7 +1446,7 @@ Llamada cancelada Llamada rechazada %1$s (%2$s) - El texto e imagen derivan de la clave de cifrado para la llamada con ]]>%1$s]]>.
    ]]>Si se ven igual en el dispositivo de ]]>%2$s\'s]]> el cifrado end-to-end está garantizado.
    + El texto e imagen derivan de la clave de cifrado para la llamada con ]]>%1$s]]>.\n\nSi se ven igual en el dispositivo de ]]>%2$s]]> el cifrado end-to-end está garantizado. Aún no has realizado llamadas. La app de ]]>%1$s]]> está usando un protocolo incompatible. Necesita actualizar la app para poder llamar. La app de ]]>%1$s]]> no soporta llamadas. Necesita actualizar la app para poder llamar. @@ -1274,14 +1464,19 @@ Bluetooth VOLVER A LA LLAMADA Lo sentimos, ]]>%1$s]]> no acepta llamadas. - Si estos emojis son los mismos en la pantalla de %1$s, esta llamada es 100%% segura. + Si los emojis son los mismos para %1$s, la llamada es 100%% segura. + Evalúa esta llamada + ¿Qué ocurrió? + Incluir información técnica + Esto no revelará los contenidos de tu conversación, pero nos ayudará a resolver el problema más pronto. + Gracias por ayudarnos a mejorar las llamadas de Telegram. - %1$d en línea - %1$d en línea - %1$d en línea - %1$d en línea - %1$d en línea - %1$d en línea + %1$d en línea + %1$d en línea + %1$d en línea + %1$d en línea + %1$d en línea + %1$d en línea %1$d miembros %1$d miembro %1$d miembros @@ -1469,6 +1664,12 @@ %1$d mensajes de voz reenviados %1$d mensajes de voz reenviados %1$d mensajes de voz reenviados + %1$d reenvió videomensajes + Videomensaje reenviado + %1$d videomensajes reenviados + %1$d videomensajes reenviados + %1$d videomensajes reenviados + %1$d videomensajes reenviados %1$d ubicaciones adjuntas Ubicación adjunta %1$d ubicaciones adjuntas @@ -1496,6 +1697,10 @@ dd MMM yyyy, h:mm a dd MMM yyyy, HH:mm + dd MMM yyyy, h:mm a + dd MMM yyyy, HH:mm + dd MMM, h:mm a + dd MMM, HH:mm MMMM \'de\' yyyy dd \'de\' MMM dd.MM.yy diff --git a/TMessagesProj/src/main/res/values-it/strings.xml b/TMessagesProj/src/main/res/values-it/strings.xml index bac63eccc..3af4bcd72 100644 --- a/TMessagesProj/src/main/res/values-it/strings.xml +++ b/TMessagesProj/src/main/res/values-it/strings.xml @@ -6,8 +6,10 @@ Telegram Telegram Beta Italiano + Inglese Italian it + Continua in Italiano Il tuo telefono Conferma il prefisso nazionale e inserisci il tuo numero di telefono. @@ -79,6 +81,7 @@ FATTURA DI TEST PAGA %1$s Metodo di pagamento + Fornitore del pagamento Nome Numero di telefono Indirizzo di contatto @@ -89,6 +92,8 @@ Spiacenti, il pagamento è stato annullato dal bot. Spiacenti, il pagamento è stato rifiutato. Impossibile raggiungere il server di pagamento. Per favore controlla la tua connessione a Internet e riprova. + Attenzione + Né Telegram, né %1$s avranno accesso alle informazioni della tua carta di credito. I dettagli della tua carta di credito saranno gestiti solamente dal sistema di pagamento, %2$s.\n\nI pagamenti saranno inviati direttamente allo sviluppatore di %1$s. Telegram non può fornire alcuna garanzia, per cui procedi a tuo rischio. In caso di problemi, per favore contatta lo sviluppatore di %1$s o la tua banca. Nuova chat Impostazioni @@ -100,6 +105,13 @@ Inizia a messaggiare premendo il tasto\nnuovo messaggio in basso a destra\no apri il menù per avere più opzioni. Attendo la rete... Connetto... + Connetto al proxy... + Tocca qui per disattivare il proxy... + Disattiva + Attiva + Sei sicuro di voler attivare questo proxy? + Puoi cambiare il tuo server proxy più tardi nelle Impostazioni (Dati e archivio). + Sei sicuro di voler disattivare il server proxy %1$s? Puoi riattivarlo più tardi nelle Impostazioni (Dati e archivio). Aggiorno... Nuova chat segreta Attendo che %s si colleghi... @@ -139,12 +151,14 @@ Grassetto Corsivo Normale + Per connetterti senza problemi con tutti i tuoi conoscenti, permetti a ]]>Telegram]]> di accedere ai tuoi contatti. + NON ADESSO + CONTINUA - Tipo di gruppo - Tipo di canale Pubblico Privato Rendi amministratore + Nessun utente bloccato Puoi inserire una descrizione opzionale per il tuo gruppo. Lascia il gruppo Elimina gruppo @@ -168,6 +182,7 @@ un1 ha fissato un file un1 ha fissato uno sticker un1 ha fissato un messaggio vocale + un1 ha fissato un videomessaggio un1 ha fissato un contatto un1 ha fissato %1$s un1 ha fissato una posizione @@ -175,7 +190,8 @@ un1 ha fissato una traccia Questo gruppo è stato aggiornato a supergruppo %1$s è stato aggiornato a supergruppo. - Gli utenti in lista nera sono rimossi dal gruppo e possono tornare solo se invitati da un amministratore. I link d\'invito non funzionano per loro. + Gli utenti bloccati sono rimossi dal gruppo e possono rientrare solamente se invitati da un amministratore. I link di invito non funzionano per loro. + Gli utenti bloccati sono rimossi dal canale e possono rientrare solamente se invitati da un amministratore. I link di invito non funzionano per loro. Nuovo canale Nome del canale Aggiungi contatti al tuo canale @@ -217,7 +233,9 @@ Controllo il nome... %1$s è disponibile. Membri + Utenti bloccati Utenti bloccati + Utenti limitati Amministratori Elimina canale Elimina canale @@ -233,15 +251,17 @@ 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. - Moderatore Creatore Amministratore SILENZIA SUONA Aggiungi amministratore + Blocca utente + Sblocca + Tieni premuto sull\'utente per sbloccarlo. Invita tramite link Sei sicuro di voler rendere %1$s un amministratore? - Rimuovi + Rimuovi amministratore Solo gli amministratori del canale possono vedere questa lista. Questo utente non si è ancora unito al canale. Vuoi invitarlo? Chiunque abbia Telegram installato potrà aggiungersi al tuo canale aprendo questo link. @@ -271,6 +291,7 @@ %1$s ha inviato un file al canale %2$s %1$s ha inviato una GIF al canale %2$s %1$s ha inviato un messaggio vocale al canale %2$s + %1$s ha inviato un videomessaggio al canale %2$s %1$s ha inviato una traccia al canale %2$s %1$s ha inviato uno sticker al canale %2$s %1$s ha inviato uno %3$s sticker al canale %2$s @@ -282,6 +303,7 @@ %1$s ha pubblicato un file %1$s ha pubblicato una GIF %1$s ha pubblicato un messaggio vocale + %1$s ha pubblicato un videomessaggio %1$s ha pubblicato una traccia %1$s ha pubblicato uno sticker %1$s ha pubblicato uno %2$s sticker @@ -292,6 +314,110 @@ I post non saranno notificati ai membri Firma messaggi Aggiungi i nomi degli amministratori nei messaggi da loro pubblicati. + Modifica amministratore + Cosa può fare questo utente? + Cambiare le info del canale + Cambiare le info del gruppo + Pubblicare messaggi + Modificare messaggi di altri + Eliminare messaggi di altri + Eliminare messaggi + Aggiungere amministratori + Rimuovi amministratore + Bloccare utenti + Aggiungere utenti + Invitare utenti tramite link + Fissare messaggi + Aggiunto da %1$s + Non puoi modificare i diritti di questo amministratore. + Limitato da %1$s + Restrizioni utente + Leggere messaggi + Cosa può fare questo utente? + Inviare messaggi + Inviare media + Inviare stickers e GIF + Inviare link con anteprima + Bloccato fino a + Sempre + Rimuovi + Blocca e rimuovi dal gruppo + Gestisci gruppo + Gestisci canale + Gestisci gruppo + Gestisci canale + + Azioni recenti + Tutte le azioni + Azioni selezionate + Tutti gli amministratori + ]]>Ancora nessuna azione]]>\n\nI membri e gli amministratori del\ngruppo non hanno eseguito azioni\ndi servizio nelle ultime 48 ore. + ]]>Ancora nessuna azione]]>\n\nGli amministratori del canale non\nhanno eseguito azioni di servizio\nnelle ultime 48 ore. + ]]>Nessuna azione trovata]]>\n\nNon sono state trovate azioni\nrecenti che rispondono alla tua richiesta. + Non sono state trovate azioni recenti contenenti \']]>%1$s]]>\' . + Cosa sono le azioni recenti? + Questa è una lista di tutte le azioni di servizio effettuate dai membri e dagli amministratori del gruppo nelle ultime 48 ore. + Questa è una lista di tutte le azioni di servizio eseguite dagli amministratori del canale nelle ultime 48 ore. + un1 ha rinominato il gruppo in \"%1$s\" + un1 ha rinominato il canale in \"%1$s\" + un1 ha lasciato il gruppo + un1 ha lasciato il canale + un1 ha aggiunto un2 + un1 si è unito al gruppo + ha bloccato %1$s + ha sbloccato %1$s + un1 si è unito al canale + un1 ha impostato una nuova foto del gruppo + un1 ha impostato una nuova foto del canale + un1 ha rimosso la foto del gruppo + un1 ha rimosso la foto del canale + un1 ha modificato questo messaggio: + un1 ha modificato una didascalia: + Messaggio originale + Didascalia originale + Vuota + un1 ha fissato questo messaggio: + un1 ha tolto un messaggio + un1 ha eliminato questo messaggio: + un1 ha modificato il link del gruppo: + un1 ha modificato il link del canale: + un1 ha rimosso il link del gruppo + un1 ha rimosso il link del canale + Link precedente + un1 ha modificato la descrizione del gruppo: + un1 ha modificato la descrizione del canale: + Descrizione precedente + un1 ha attivato gli inviti del gruppo + un1 ha disattivato gli inviti del gruppo + un1 ha attivato le firme + un1 ha disattivato le firme + ha cambiato le restrizioni di %1$s\n\nDurata: %2$s + Inviare sticker e GIF + Inviare media + Inviare messaggi + Inviare link con anteprima + Leggere messaggi + ha cambiato i privilegi di %1$s + Modificare le info del canale + Modificare le info del gruppo + Pubblicare messaggi + Modificare messaggi + Eliminare messaggi + Aggiungere amministratori + Bloccare utenti + Aggiungere utenti + Fissare messaggi + Tutte le azioni + Nuove restrizioni + Nuovi amministratori + Nuovi membri + Info gruppo + Info canale + Impostazioni gruppo + Messaggi eliminati + Messaggi modificati + Messaggi fissati + Membri rimossi Nuova lista broadcast Inserisci il nome della lista @@ -323,14 +449,19 @@ sta scrivendo... sta scrivendo... stanno scrivendo... - %1$s sta registrando un audio... + %1$s sta registrando un messaggio vocale... + %1$s sta registrando un videomessaggio... %1$s sta inviando un audio... %1$s sta inviando una foto... APERTURA RAPIDA + APRI GRUPPO + APRI CANALE + Il tema scuro si attiverà automaticamente durante la notte %1$s sta giocando a un gioco... %1$s sta inviando un video... %1$s sta inviando un file... - sta registrando un audio... + sta registrando un messaggio vocale... + sta registrando un videomessaggio... sta inviando un audio... sta inviando una foto... sta giocando a un gioco... @@ -378,6 +509,8 @@ Apri in... Copia URL Invia %1$s + Invia come file + Invia come file Aprire url %1$s? Consentire a %1$s di trasmettere il tuo nome e id Telegram (non il tuo numero di telefono) alle pagine che apri tramite questo bot? SEGNALA SPAM @@ -393,6 +526,7 @@ https://telegram.org/faq/it#non-posso-inviare-messaggi-a-chi-non-far-parte-dei-miei-contatti Maggiori info Invia a... + Scrivi un commento... Tocca qui per vedere le GIF salvate Fissa Notifica tutti i membri @@ -415,6 +549,7 @@ %1$s Spiacenti, tempo di modifica scaduto. Aggiungi scorciatoia + Cerca membri Scorciatoia aggiunta alla schermata home chat con te stesso Tu @@ -427,6 +562,21 @@ Elimina per %1$s Elimina per tutti i membri Testo copiato negli appunti + Tieni premuto per l\'audio. Tocca per il video. + Tieni premuto per il video. Tocca per l\'audio. + Scarta messaggio vocale + Sei sicuro di voler interrompere la registrazione e scartare il tuo messaggio vocale? + Scarta videomessaggio + Sei sicuro di voler interrompere la registrazione e scartare il tuo videomessaggio? + Scarta + Gli amministratori di questo gruppo ti hanno vietato di inviare media qui fino al %1$s + Gli amministratori di questo gruppo ti hanno vietato di inviare contenuti inline qui fino al %1$s + Gli amministratori di questo gruppo ti hanno vietato di inviare sticker qui fino al %1$s + Gli amministratori di questo gruppo ti hanno vietato di scrivere qui fino al %1$s + Gli amministratori di questo gruppo ti hanno vietato di inviare media qui + Gli amministratori di questo gruppo ti hanno vietato di inviare contenuti inline qui + Gli amministratori di questo gruppo ti hanno vietato di inviare sticker qui + Gli amministratori di questo gruppo ti hanno vietato di scrivere qui %1$s ha impostato il timer di autodistruzione a %2$s Hai impostato il timer di autodistruzione a %1$s @@ -443,6 +593,7 @@ %1$s ti ha inviato un file %1$s ti ha inviato una GIF %1$s ti ha inviato un messaggio vocale + %1$s ti ha inviato un videomessaggio %1$s ti ha inviato una traccia %1$s ti ha inviato uno sticker %1$s ti ha inviato uno %2$s sticker @@ -456,11 +607,12 @@ %1$s ha inviato un file al gruppo %2$s %1$s ha inviato una GIF al gruppo %2$s %1$s ha inviato un messaggio vocale al gruppo %2$s + %1$s ha inviato un videomessaggio al gruppo %2$s %1$s ha inviato una traccia al gruppo %2$s %1$s ha inviato uno sticker al gruppo %2$s %1$s ha inviato uno %3$s sticker al gruppo %2$s %1$s ti ha invitato nel gruppo %2$s - %1$s ha modificato il nome del gruppo %2$s + %1$s ha rinominato il gruppo %2$s %1$s ha modificato la foto del gruppo %2$s %1$s ha invitato %3$s nel gruppo %2$s %1$s è tornato nel gruppo %2$s @@ -469,7 +621,7 @@ %1$s ti ha rimosso dal gruppo %2$s %1$s ha lasciato il gruppo %2$s %1$s si è unito a Telegram! - %1$s,\nAbbiamo rilevato un accesso al tuo account da un nuovo dispositivo il %2$s\n\nDispositivo: %3$s\nPosizione: %4$s\n\nSe non sei stato tu, puoi andare nelle Impostazioni - Privacy e sicurezza - Sessioni - Termina tutte le sessioni.\n\nSe pensi che qualcuno si sia collegato al tuo account contro il tuo volere, puoi attivare la verifica in due passaggi nelle impostazioni di Privacy e sicurezza.\n\nSinceramente\nIl Team di Telegram + %1$s,\nAbbiamo rilevato un accesso al tuo account da un nuovo dispositivo il %2$s\n\nDispositivo: %3$s\nPosizione: %4$s\n\nSe non sei stato tu, puoi andare nelle Impostazioni - Privacy e sicurezza - Sessioni - Termina tutte le sessioni.\n\nSe pensi che qualcuno si sia collegato al tuo account contro il tuo volere, puoi attivare la verifica in due passaggi nelle impostazioni di Privacy e sicurezza.\n\nCordiali saluti,\nil team di Telegram %1$s ha aggiornato la foto del profilo %1$s si è unito al gruppo %2$s tramite link d\'invito Rispondi @@ -485,6 +637,7 @@ %1$s ha fissato uno sticker nel gruppo %2$s %1$s ha fissato uno %3$s sticker nel gruppo %2$s %1$s ha fissato un messaggio vocale nel gruppo %2$s + %1$s ha fissato un videomessaggio nel gruppo %2$s %1$s ha fissato un contatto nel gruppo %2$s %1$s ha fissato una posizione nel gruppo %2$s %1$s ha fissato una GIF nel gruppo %2$s @@ -497,6 +650,7 @@ %1$s ha fissato uno sticker %1$s ha fissato uno %2$s sticker %1$s ha fissato un messaggio vocale + %1$s ha fissato un videomessaggio %1$s ha fissato un contatto %1$s ha fissato una posizione %1$s ha fissato una GIF @@ -551,8 +705,10 @@ Impostazioni Aggiungi membro Imposta amministratori + RIMUOVI DAL GRUPPO Elimina e lascia il gruppo Notifiche + Limita utente Rimuovi dal gruppo Aggiorna a supergruppo Converti in supergruppo @@ -572,7 +728,7 @@ Modifica Elimina Home - Mobile + Cellulare Lavoro Altro Principale @@ -584,7 +740,7 @@ Chiave di crittografia Timer di autodistruzione Spento - L\'immagine e il testo sono derivati dalla chiave di crittografia di questa chat segreta con ]]>%1$s]]>.
    ]]>Se sono uguali sul dispositivo di ]]>%2$s]]>, la crittografia end-to-end è garantita.
    ]]>Ulteriori informazioni su telegram.org
    + L\'immagine e il testo sono derivati dalla chiave di crittografia di questa chat segreta con ]]>%1$s]]>.\n\nSe sono uguali sul dispositivo di ]]>%2$s]]>, la crittografia end-to-end è garantita.\n\nUlteriori informazioni su telegram.org https://telegram.org/faq/it#chat-segrete Tocca per mostrare le emoji Sconosciuto @@ -598,14 +754,14 @@ Un username deve avere almeno 5 caratteri. Il massimo per un username è 32 caratteri. Spiacenti, un username non può iniziare con un numero. - Puoi scegliere un username su ]]>Telegram]]>. Se lo fai, le altre persone potranno trovarti tramite questo username e contattarti senza conoscere il tuo numero di telefono.
    ]]>Puoi usare ]]>a–z]]>, ]]>0–9]]> e underscore. La lunghezza minima è di ]]>5]]> caratteri.
    + Puoi scegliere un username su ]]>Telegram]]>. Se lo fai, le altre persone potranno trovarti tramite questo username e contattarti senza conoscere il tuo numero di telefono.\n\nPuoi usare ]]>a–z]]>, ]]>0–9]]> e underscore. La lunghezza minima è di ]]>5]]> caratteri. Questo link apre una chat con te su Telegram:\n%1$s Controllo l\'username... %1$s è disponibile. Nessuno Si è verificato un errore. - Sticker + Sticker Aggiungi sticker Aggiungi maschere Aggiungi agli sticker @@ -638,6 +794,8 @@ Alcuni dei tuoi set di maschere più vecchi sono stati stati archiviati. Puoi riattivarli nelle impostazioni delle maschere. Tema + Scuro + Blu Sei sicuro di voler eliminare questo tema? File tema non valido Inserisci il nome del tema @@ -683,7 +841,10 @@ Verde Ciano Bianco + Seppia + Scuro Rosa + Dimensione carattere Viola Arancione Il led è una piccola luce lampeggiante usata in alcuni dispositivi per indicare nuovi messaggi. @@ -707,7 +868,8 @@ Un contatto si è unito a Telegram Messaggi fissati Lingua - Per favore nota che l\'assistenza di Telegram è fornita da volontari. Proviamo a rispondere quanto prima, ma potrebbe volerci del tempo.
    ]]>Dai un\'occhiata alle domande frequenti di Telegram]]>: contengono suggerimenti importanti per risolvere i problemi]]> e risposte a quasi tutte le domande.
    + Personalizzata + Per favore nota che l\'assistenza di Telegram è fornita da volontari. Proviamo a rispondere quanto prima, ma potrebbe volerci del tempo.\n\nDai un\'occhiata alle domande frequenti di Telegram]]>: contengono suggerimenti importanti per risolvere i problemi]]> e risposte a quasi tutte le domande. Chiedi a un volontario Domande frequenti https://telegram.org/faq/it @@ -735,7 +897,7 @@ Breve Lunga Download automatico media - Quando utilizzi la rete mobile + Quando utilizzi la rete cellulare Quando connesso tramite Wi-Fi In roaming Nessun media @@ -788,6 +950,19 @@ Menù debug Importa contatti Ricarica contatti + Puoi cambiare la lingua più tardi nelle Impostazioni. + Scegli la tua lingua + Altro + Impostazioni proxy + Proxy + Usa impostazioni proxy + Server + Password + Porta + Username + Impostazioni proxy SOCKS5 + Usa il proxy per le chiamate + I server proxy potrebbero ridurre la qualità delle tue chiamate. Database locale Cancellare i messaggi salvati nella cache? @@ -797,7 +972,7 @@ Calcolo... Documenti Foto - Messaggi vocali + Messaggi vocali/video Video Musica GIF @@ -805,7 +980,9 @@ Vuota Mantieni media Foto, video e altri file dalle chat nel cloud che non hai aperto]]> in questo periodo verranno eliminati dal dispositivo per preservare lo spazio sul disco.\n\nTutti i media rimarranno nel cloud di Telegram e potranno essere riscaricati ogni volta che ne avrai bisogno. - Per sempre + Sempre + Messaggi vocali + Videomessaggi Sessioni attive Sessione corrente @@ -839,7 +1016,7 @@ Sensore touch Impronta digitale non riconosciuta. Riprova Consenti cattura schermo - Se attivata, puoi fare screenshot dell\'app, ma il sistema mostrerà le tue chat nel task switcher anche quando il codice di blocco è attivo.\n\nPotresti dover riavviare l\'app per rendere effettive le modifiche. + Se attivata, puoi fare screenshot dell\'app, ma il sistema mostrerà le tue chat nella lista delle app recenti anche quando il codice di blocco è attivo.\n\nPotresti dover riavviare l\'app per rendere effettive le modifiche. File condivisi Media condivisi @@ -870,7 +1047,7 @@ %1$d di %2$d Galleria Tutte le foto - Tutti i video + Tutti i media Ancora nessuna foto Ancora nessun video Scarica prima il file @@ -983,7 +1160,7 @@ Dati e archivio Utilizzo disco e rete Utilizzo archivio - Utilizzo dati mobile + Utilizzo dati cellulare Utilizzo dati roaming Utilizzo dati Wi-Fi Messaggi e altri dati @@ -1004,6 +1181,11 @@ Privacy e sicurezza Privacy Ultimo accesso + Pagamenti + Cancella info di pagamento + Puoi cancellare le tue info di spedizione e ordinare a tutti i fornitori dei pagamenti di rimuovere le tue carte di credito salvate. Ricorda che Telegram non archivia mai i dati della tua carta di credito. + Info di spedizione + Info di pagamento Tutti I miei contatti Nessuno @@ -1044,6 +1226,8 @@ Spiacenti, non puoi aggiungere questo utente al gruppo a causa delle sue impostazioni di privacy. Spiacenti, non puoi aggiungere questo utente al canale a causa delle sue impostazioni di privacy. Spiacenti, non puoi creare un gruppo con questi utenti a causa delle loro impostazioni di privacy. + Peer-to-Peer + Disattivando il peer-to-peer, tutte le chiamate verranno inviate tramite i server di Telegram per evitare di rivelare l\'indirizzo IP, ma la qualità audio diminuirà leggermente. Modifica video Invio video... @@ -1121,13 +1305,14 @@ File Sticker Messaggio vocale + Videomessaggio Gioco Tu Hai fatto uno screenshot! un1 ha fatto uno screenshot! Numero di telefono non valido - Numero di telefono bandito + Numero di telefono vietato Codice scaduto, effettua di nuovo l\'accesso Troppi tentativi, riprova più tardi Troppi tentativi, per favore riprova di nuovo tra %1$s @@ -1189,6 +1374,11 @@ Spiacenti, questa azione non ti è permessa. Spiacenti, non puoi aggiungere questo utente o bot al gruppo perchè lo hai bloccato. Per favore sbloccalo per procedere. UNISCITI AL GRUPPO + Spiacenti, non puoi aggiungere questo utente come amministratore perché non è un membro del gruppo e non sei autorizzato ad invitarlo. + Spiacenti, non puoi aggiungere questo utente come amministratore perché è bloccato e non puoi sbloccarlo. + Spiacenti, non puoi bloccare questo utente perché è un amministratore in questo gruppo e non sei autorizzato a degradarlo. + Spiacenti, gli amministratori di questo gruppo ti hanno vietato di inviare sticker. + Spiacenti, gli amministratori di questo gruppo ti hanno vietato di inviare media. Telegram deve accedere ai tuoi contatti per poterti connettere con i tuoi amici su tutti i tuoi dispositivi. Telegram deve accedere alla tua memoria per poter inviare e salvare foto,video, musica e altri media. @@ -1207,13 +1397,13 @@ Potente Basato sul cloud Privato - L\'app di messaggi più veloce]]> al mondo.]]>È gratuita]]> e sicura]]>. - Telegram]]> consegna i messaggi più]]>velocemente di qualsiasi altra app. - Telegram]]> sarà sempre gratuito.]]>Nessuna pubblicità. Nessun abbonamento. - Telegram]]> protegge i tuoi messaggi]]>dagli attacchi degli hacker. - Telegram]]> non ha limiti di dimensione]]>per le tue chat e i file multimediali. - Telegram]]> ti consente di accedere]]>ai tuoi messaggi da più dispositivi. - I messaggi di Telegram]]> sono fortemente]]>criptati e possono autodistruggersi. + L\'app di messaggi più veloce]]> al mondo.\nÈ gratuita]]> e sicura]]>. + Telegram]]> consegna i messaggi più\nvelocemente di qualsiasi altra app. + Telegram]]> sarà sempre gratuito.\nNessuna pubblicità. Nessun abbonamento. + Telegram]]> protegge i tuoi messaggi\ndagli attacchi degli hacker. + Telegram]]> non ha limiti di dimensione\nper le tue chat e i file multimediali. + Telegram]]> ti consente di accedere\nai tuoi messaggi da più dispositivi. + I messaggi di Telegram]]> sono fortemente\ncriptati e possono autodistruggersi. Inizia a messaggiare Impostazioni account @@ -1241,7 +1431,7 @@ Puoi decidere chi può chiamarti. Questi utenti potranno o non potranno chiamarti indipendentemente dalle impostazioni precedenti. Mai - Solo con i dati mobile + Solo con i dati cellulare Sempre Rispondi Rifiuta @@ -1256,7 +1446,7 @@ Chiamata annullata Chiamata rifiutata %1$s (%2$s) - L\'immagine e il testo sono derivati dalla chiave di crittografia di questa chiamata con ]]>%1$s]]>.
    ]]>Se sono uguali sul dispositivo di ]]>%2$s]]>, la crittografia end-to-end è garantita.
    + L\'immagine e il testo sono derivati dalla chiave di crittografia di questa chiamata con ]]>%1$s]]>.\n\nSe sono uguali sul dispositivo di ]]>%2$s]]>, la crittografia end-to-end è garantita. Non hai ancora effettuato alcuna chiamata. L\'app di ]]>%1$s]]> sta usando un protocollo non compatibile. Deve aggiornare la sua app prima che tu possa chiamarlo. L\'app di ]]>%1$s]]> non supporta le chiamate. Deve aggiornare la sua app prima che tu possa chiamarlo. @@ -1275,13 +1465,18 @@ RITORNA ALLA CHIAMATA Spiacenti, ]]>%1$s]]> non accetta le chiamate. Se le emoji sono uguali sullo schermo di %1$s, la chiamata è sicura al 100%%. + Valuta la chiamata + Cosa è andato storto? + Includi informazioni tecniche + Questo non rivelerà i contenuti della tua conversazione, ma ci aiuterà a risolvere il problema più velocemente. + Grazie per aver contribuito a rendere le chiamate di Telegram migliori. - %1$d in linea - %1$d in linea - %1$d in linea - %1$d in linea - %1$d in linea - %1$d in linea + %1$d in linea + %1$d in linea + %1$d in linea + %1$d in linea + %1$d in linea + %1$d in linea %1$d membri %1$d membro %1$d membri @@ -1469,6 +1664,12 @@ %1$d messaggi vocali inoltrati %1$d messaggi vocali inoltrati %1$d messaggi vocali inoltrati + %1$d videomessaggi inoltrati + Videomessaggio inoltrato + %1$d videomessaggi inoltrati + %1$d videomessaggi inoltrati + %1$d videomessaggi inoltrati + %1$d videomessaggi inoltrati %1$d posizioni inoltrate Posizione inoltrata %1$d posizione inoltrate @@ -1496,6 +1697,10 @@ dd MMM yyyy, h:mm a dd MMM yyyy, HH:mm + dd MMM yyyy, h:mm a + dd MMM yyyy, HH:mm + dd MMM, h:mm a + dd MMM, HH:mm MMMM yyyy dd MMM dd.MM.yy diff --git a/TMessagesProj/src/main/res/values-ko/strings.xml b/TMessagesProj/src/main/res/values-ko/strings.xml index 8dd8fc0c1..8fe6376d0 100644 --- a/TMessagesProj/src/main/res/values-ko/strings.xml +++ b/TMessagesProj/src/main/res/values-ko/strings.xml @@ -6,8 +6,10 @@ 텔레그램 텔레그램 베타 한국어 - Korean + 한국어 + 한국어 ko + 한국어로 진행 전화번호 입력 국가번호와 전화번호를 입력하세요. (대한민국 국가번호: 82) @@ -79,6 +81,7 @@ 테스트 인보이스 %1$s 결제 결제방법 + 결제 제공 업체 이름 전화번호 연락주소 @@ -89,6 +92,8 @@ 죄송합니다, 결제가 봇에 의하여 거절되었습니다. 죄송합니다, 결제가 거절되었습니다. 결제 서버로 연결을 할 수 없습니다. 인터넷 연결을 확인해주시고 다시 시도해주세요. + 경고 + 신용카드 정보는 텔레그램이나 %1$s이 권한을 가지지 않습니다. 신용카드 정보는 %2$s 결제 시스템에서만 처리가 됩니다.\n\n결제는 %1$s 개발자에게 직접 됩니다. 텔레그램은 그 어떠한 보장도 하지 않음으로 진행시 위험을 감수 하셔야 합니다. 문제발생시, %1$s 개발자나 사용하시는 은행에게 문의를 부탁드립니다. 새로운 대화 설정 @@ -100,6 +105,13 @@ 대화를 시작하려면 우측 상단의\n초대하기 버튼을 누르거나\n메뉴 버튼을 눌러 보세요. 연결 대기 중... 연결 중... + 프록시 연결 중... + 프록시를 비활성화하려면 여기를 탭하세요.. + 비활성화 + 활성화 + 이 프록시를 비활성화 하겠습니까? + 나중에 설정에서 프록시 서버를 변경 할 수 있습니다. (데이터 및 저장소) + %1$s 프록시 서버를 비활성화 하겠습니까? 나중에 설정에서 변경 할 수 있습니다. (데이터 및 저장소) 업데이트 중... 비밀대화 시작 %s님을 기다리는 중... @@ -139,12 +151,14 @@ 굵게 기울림 일반 + Telegram에 권한을 부여하여 주소록에 있는 분들과 연결하세요. + 아직 안됨 + 계속 - 그룹 종류 - 채널 종류 공개 비공개 관리자로 지명 + 차단한 사용자 없음 그룹에 추가 설명을 제공 할 수 있습니다. 그룹 나가기 그룹 삭제 @@ -152,7 +166,7 @@ 그룹 삭제 그룹에 있는 모든 메시지가 삭제됩니다. 그룹방 관리를 도울 수 있는 관리자를 추가 할 수 있습니다. 길게 탭을하면 관리자 삭제가 가능합니다. - 이 그룹방을 삭제하실 경우 모든 구성원과 메시지를 삭제를 하게되며 복구가 안됩니다. 그래도 그룹방을 삭제하시겠습니까? + 이 그룹방을 삭제하실 경우 모든 구성원과 메시지를 삭제 하게되며 복구가 안됩니다. 그래도 그룹방을 삭제하시겠습니까? 그룹 생성됨 이 그룹방에 un1님이 초대하였습니다. 정말 그룹방에서 나가겠습니까? @@ -168,6 +182,7 @@ un1 님이 파일을 고정함 un1 님이 스티커를 고정함 un1 님이 음성메시지를 고정함 + un1 님이 영상메시지를 고정함 un1 님이 연락처를 고정함 un1 님이 %1$s 고정함 un1 님이 위치를 고정함 @@ -176,6 +191,7 @@ 이 그룹방은 슈퍼그룹방으로 업그레이드 되었습니다. %1$s 그룹방은 슈퍼그룹방으로 업그레이드 되었습니다. 그룹방에서 차단되어 퇴장당한 사용자는 관리자가 초대해야지만 그룹방에 입장이 가능합니다. 초대링크로는 초대가 되지 않습니다. + 그룹방에서 차단되어 퇴장당한 사용자는 관리자가 초대해야지만 그룹방에 입장이 가능합니다. 초대링크로는 초대가 되지 않습니다. 새 채널 채널명 채널에 친구 추가 @@ -217,7 +233,9 @@ 이름 확인 중.. %1$s은 사용 가능합니다. 구성원 + 블랙리스트 차단된 사용자 + 제한된 사용자 관리자 채널 삭제 채널 삭제 @@ -233,15 +251,17 @@ 채널 사진 삭제됨 채널명이 un2로 변경됨 죄송하지만, 너무 많은 공개아이디를 생성하였습니다. 기존 공개 채널 혹은 그룹방을 비공개로 전환하거나 삭제해주세요. - 관리자 생성자 관리자 음소거 음소거 취소 관리자 추가 + 사용자 차단 + 차단해제 + 차단을 해제하려면 대화상대를 길게 누르세요. 링크를 통해 초대 %1$s님을 관리자로 추가하시겠습니까? - 삭제 + 관리자 회수 채널 관리자만 리스트를 볼 수 있습니다. 아직 이 유저가 채널에 입장하지 않았습니다. 초대하시겠습니까? 텔레그램이 설치된 분들은 링크를 타고 채널에 참여가 가능합니다. @@ -271,6 +291,7 @@ %1$s님이 %2$s 채널에 파일을 보냈습니다 %1$s님이 %2$s 채널에 GIF파일을 보냈습니다 %1$s님이 %2$s 채널에 음성메시지를 보냈습니다 + %1$s님이 %2$s 채널에 영상메시지를 보냈습니다 %1$s님이 %2$s 채널에 트랙을 보냈습니다 %1$s님이 %2$s 채널에 스티커를 보냈습니다 %1$s님이 %2$s 채널에 %3$s 스티커를 보냈습니다 @@ -282,6 +303,7 @@ %1$s 님이 파일을 보냈습니다 %1$s 님이 GIF파일을 보냈습니다 %1$s님이 음성메시지를 보냈습니다 + %1$s님이 영상메시지를 보냈습니다 %1$s님이 트랙을 보냈습니다 %1$s님이 스티커를 보냈습니다 %1$s님이 %2$s스티커를 작성하였습니다 @@ -292,6 +314,110 @@ 메시지 미알림 대상 메시지 서명 메시지 작성하는 관리자 추가 + 관리자 수정 + 관리자는 무엇을 할 수 있나요? + 채널 정보 수정 + 그룹 정보 수정 + 메시지 작성 + 다른 사용자 메시지 수정 + 다른 사용자 메시지 삭제 + 메시지 삭제 + 새 관리자 추가 + 관리자 회수 + 사용자 차단 + 사용자 추가 + 링크를 통하여 사용자 초대 + 메시지 고정 + %1$s 님이 추가 + 이 관리자의 권한을 수정할 수 없습니다. + %1$s 님이 제한함 + 사용자 제한 + 메시지 읽기 + 이 사용자는 무엇을 할 수 있나요? + 메시지 전송 + 미디어 전송 + 스티커 & GIF 전송 + 링크 저장 + 다음까지 차단 + 영원히 + 삭제 + 그룹에서 차단 후 내보내기 + 그룹 관리 + 채널 관리 + 그룹 관리 + 채널 관리 + + 최근 활동 + 모든 활동 + 선택한 활동 + 모든 관리자 + 아직 이벤트가 없습니다.\n\n그룹 사용자나 관리자가\n지난 48시간내에\n 아무런 활동이 없었습니다. + ]]>아직 이벤트가 없습니다.]]>\n\n이 채널 관리자는\n지난 48시간내에\n 아무런 활동이 없었습니다. + ]]>이벤트를 찾을 수 없습니다.]]>\n\n검색한 이벤트를 찾을 수 없습니다. + \'%1$s\' 에 대한 최근 이벤트를 찾을 수 없습니다. + 최근 활동이란 무엇인가요? + 그룹 사용자와 관리자의 지난 48시간동안 활동 리스트입니다. + 채널 관리자의 지난 48시간동안 활동 리스트입니다. + un1님이 그룹명을 \"%1$s\" 로 변경하였습니다. + un1님이 채널명을 \"%1$s\" 로 변경하였습니다. + un1님이 그룹에서 나갔습니다. + un1님이 채널에서 나갔습니다. + un1님이 un2님을 초대했습니다 + un1 님이 그룹에 참여했습니다 + %1$s 님이 차단됨 + %1$s 님이 차단해제됨 + un1님이 채널에 참여했습니다. + un1님이 그룹 사진 을 새로 설정했습니다. + un1님이 채널 사진 을 새로 설정했습니다. + un1님이 그룹 사진 을 삭제했습니다. + un1님이 채널 사진 을 삭제했습니다. + un1님이 이 메시지를 편집했습니다. + un1님이 캡션을 수정했습니다: + 원본 메시지 + 원본 캡션 + 없음 + un1님이 이 메시지를 고정했습니다: + un1 님이 메시지를 고정했습니다. + un1님이 이 메시지를 삭제했습니다: + un1님이 그룹링크를 변경했습니다: + un1님이 채널링크를 변경했습니다: + un1 님이 그룹링크를 삭제했습니다. + un1님이 채널 링크를 삭제했습니다. + 기존 링크 + un1님이 그룹정보를 변경했습니다: + un1님이 채널정보를 변경했습니다: + 기존 정보 + un1 님이 그룹 초대를 허용함 + un1 님이 그룹 초대를 불허함 + un1님이 서명을 허용했습니다. + un1 님이 서명을 불허함 + %1$s 님에 대한 제한을 변경했습니다.\n\n기한: %2$s + 스티커 & GIF 전송 + 미디어 전송 + 메시지 전송 + 링크 저장 + 메시지 읽기 + %1$s님의 권한을 수정했습니다. + 채널 정보 수정 + 그룹 정보 수정 + 메시지 작성 + 메시지 수정 + 메시지 삭제 + 관리자 추가 + 사용자 차단 + 사용자 추가 + 메시지 고정 + 모든 활동 + 새로운 제한 + 새로운 관리자 + 새로운 사용자 + 그룹 정보 + 채널 정보 + 그룹 설정 + 메시지 삭제 + 메시지 수정 + 메시지 고정 + 퇴장한 사용자 새 단체 메시지 리스트 리스트 이름을 입력하세요 @@ -324,13 +450,18 @@ 님이 입력 중... 님이 입력 중... %1$s님이 음성메시지를 녹음중입니다... + %1$s님이 영상메시지를 녹화중입니다... %1$s님이 음성파일을 전송중입니다... %1$s님이 사진 보내는 중... 즉시 보기 + 공개 그룹 + 공개 채널 + 밤시간에는 자동으로 어두운 테마가 작동됩니다 %1$s님이 게임을 플레이중입니다. %1$s님이 동영상 보내는 중... %1$s님이 파일 보내는 중... - 음성메시지를 녹음 중입니다.. + 영상메시지를 녹화중... + 영상메시지를 녹화중... 음성파일 전송 중.. 사진 전송 중.. 게임 플레이중... @@ -378,6 +509,8 @@ 다음으로 파일열기.. URL 복사 %1$s 전송 + 파일로 전송 + 파일로 보내기 %1$s 링크를 여시겠습니까? 이 bot을 통하여 %1$s이 텔레그램 이름과 아이디를(전화번호 아님) 웹페이지로 전달하시기를 허락하시나요? 스팸 신고 @@ -393,6 +526,7 @@ https://telegram.org/faq#can-39t-send-messages-to-non-contacts 더 보기 다음에게 보내기.. + 코멘트 쓰기.. 저장된 GIF 파일을 보려면 탭하세요. 고정 모두에게 알림 @@ -415,6 +549,7 @@ %1$s 죄송합니다, 수정가능시간이 만료되었습니다. 바로 가기 추가 + 사용자 검색 홈에 바로가기 추가 나와의 대화 @@ -427,6 +562,21 @@ %1$s 에게 삭제 모두에게 삭제 클립보드로 텍스트가 복하되었습니다. + 꾹 눌러 음성을 녹음하세요. 탭하여 영상으로 전환하세요. + 꾹 눌러 영상을 녹화하세요. 탭하여 음성으로 전환하세요. + 음성 메시지 지우기 + 녹음을 중단하고 음성 메시지를 지우시겠습니까? + 영상 메시지 지우기 + 녹화를 중단하고 영상 메시지를 지우시겠습니까? + 지우기 + 그룹 관리자가 회원님의 미디어 전송을 %1$s까지 제한했습니다. + 그룹 관리자가 회원님의 인라인 명령어 전송을 %1$s까지 제한했습니다. + 그룹 관리자가 회원님의 스티커 전송을 %1$s까지 제한했습니다. + 그룹 관리자가 회원님의 메시지 작성을 %1$s까지 제한했습니다. + 그룹 관리자가 회원님의 미디어 전송을 제한했습니다. + 그룹 관리자가 회원님의 인라인 명령어 전송을 제한했습니다. + 그룹 관리자가 회원님의 스티커 전송을 제한했습니다. + 그룹 관리자가 회원님의 메시지 작성을 제한했습니다. %1$s님이 자동삭제를 %2$s 후로 설정했습니다 자동삭제를 %1$s 후로 설정했습니다 @@ -443,6 +593,7 @@ %1$s님이 파일을 보냈습니다 %1$s 님께서 GIF파일을 보내셨습니다 %1$s님이 음성메시지를 보냈습니다 + %1$s 님이 영상 메시지를 보냈습니다 %1$s 님이 트랙을 보냈습니다 %1$s님이 스티커를 보냈습니다 %1$s님이 %2$s스티커를 보냈습니다 @@ -456,12 +607,13 @@ %1$s님이 %2$s 그룹에 파일을 보냈습니다 %1$s 님께서 %2$s 그룹에 GIF파일을 보냈습니다 %1$s님이 %2$s 그룹에 음성메시지를 보냈습니다 + %1$s 님이 %2$s 그룹에 영상 메시지를 보냈습니다 %1$s님이 %2$s 그룹에 트랙을 보냈습니다 %1$s님이 %2$s 그룹에 스티커를 보냈습니다 %1$s님이 %2$s 그룹에 %3$s 스티커를 보냈습니다 %1$s님이 %2$s 그룹에 초대했습니다 - %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 그룹에 참여했습니다 @@ -485,6 +637,7 @@ %1$s 님이 스티커를 %2$s 그룹방에 고정함 %1$s 님이 %3$s 스티커를 %2$s 그룹방에 고정함 %1$s 님이 음성메시지를 %2$s 그룹방에 고정함 + %1$s 님이 영상메시지를 %2$s 그룹방에 고정함 %1$s 님이 연락처를 %2$s 그룹방에 고정함 %1$s 님이 지도를 %2$s 그룹방에 고정함 %1$s 님이 GIF를 %2$s 그룹방에 고정함 @@ -497,6 +650,7 @@ %1$s 님이 스티커를 고정함 %1$s 님이 %2$s 스티커를 고정함 %1$s 님이 음성메시지를 고정함 + %1$s 님이 영상메시지를 고정함 %1$s 님이 연락처를 고정함 %1$s 님이 지도를 고정함 %1$s 님이 GIF를 고정함 @@ -551,8 +705,10 @@ 설정 대화상대 추가 관리자 설정 + 그룹으로 부터 차단됨 그룹에서 나가기 알림 + 사용자 제한 그룹에서 내보내기 슈퍼그룹으로 업그레이드 슈퍼그룹으로 변환 @@ -584,7 +740,7 @@ 암호화 키 자동삭제 타이머 해제 - 이 이미지와 텍스트는 ]]>%1$s]]>.
    ]]> 님과의 현재 비밀대화에 대한 비밀키에서 파생된 것입니다. 이 이미지와 텍스트가 ]]>%2$s\'s]]> 님의 휴대전화와 동일하다면 단말기간(end-to-end)의 암호화가 정상적으로 진행되고 있음을 보장합니다.
    ]]> 자세한 사항은 telegram.org를 참고해 주세요.
    + 이 이미지와 텍스트는 ]]>%1$s]]>님과의 비밀대화시 생성된 암호화키에서 파생되었습니다.\n\n이 이미지와 텍스트가 ]]>%2$s\'s]]> 님의 휴대전화와 동일하다면 단말기간(end-to-end)의 암호화가 정상적으로 진행되고 있음을 보장합니다.\n\n더 자세한 사항은 telegram.org를 참고해 주세요. https://telegram.org/faq#secret-chats 탭을 하여 이모티콘화 알 수 없음 @@ -598,14 +754,14 @@ 아이디는 최소 다섯 글자 이상 입력해야 합니다. 아이디는 최대 32자까지만 가능합니다. 아이디는 숫자로 시작할 수 없습니다. - 텔레그램 아이디를 설정할 수 있습니다. 아이디를 설정하면 회원님의 전화번호를 몰라도 아이디로 회원님을 찾아 대화를 나눌 수 있습니다.
    ]]>아이디는 영문, 밑줄, 숫자로 (]]>a~z]]>, ]]>_]]>, ]]>0~9]]>) ]]>다섯 글자]]> 이상으로 설정해 주세요.
    + ]]>텔레그램]]> 아이디를 설정할 수 있습니다. 아이디를 설정하면 회원님의 전화번호를 몰라도 아이디로 회원님을 찾아 대화를 나눌 수 있습니다.\n\n아이디는 ]]>a-z]]>,]]>0-9]]>와 밑줄을 사용할 수 있습니다. 아이디의 최소단위는 ]]>5]]>글자입니다. 이 링크는 다음 상대와 텔레그램 대화를 열게 됩니다:\n%1$s 아이디 확인 중... %1$s: 사용 가능합니다. 없음 오류가 발생했습니다. - 스티커 + 스티커 스티커 추가 마스크 추가 스티커 추가 @@ -638,6 +794,8 @@ 오래된 마스크중 일부가 보관되어집니다. 마스크 설정에서 다시 재설정이 가능합니다. 테마 + 다크 + 파랑 이 테마를 삭제하시겠습니까? 올바르지 않은 테마 파일 테마명을 입력하세요 @@ -683,7 +841,10 @@ 초록색 청록색 하얀색 + 세피아 + 다크 핑크색 + 폰트 크기 보라색 주황색 LED는 일부 기기에서 새로운 메시지가 수신될때 반짝입니는 빛입니다 @@ -707,7 +868,8 @@ 친구의 텔레그램 가입 알림 고정된 메시지 언어 - 텔레그램에 관한 질문은 자원봉사자들이 답변해 드립니다. 신속한 답변을 위해 노력하지만 답변이 다소 늦을 수 있습니다.
    ]]>일반적인 문제와 해결방법]]>에 대해서는 \'자주 묻는 질문]]>\'을 확인해 보세요.
    + 사용자 지정 + 텔레그램에 관한 질문은 자원봉사자들이 답변해 드립니다. 신속한 답변을 위해 노력하지만 답변이 다소 늦을 수 있습니다. 일반적인 문제와 해결방법에 대해서는 \'자주 묻는 질문\']]>을 확인해 보세요 : FAQ에는 문제해결]]> 관련된 정보와 팁이 있습니다. 질문하기 자주 묻는 질문 https://telegram.org/faq/ko @@ -788,6 +950,19 @@ 디버그 메뉴 연락처 가져오기 연락처 재동기화 + 나중에 설정에서 언어 변경이 가능합니다. + 언어 선택 + 기타 + 프록시 설정 + 프록시 + 프록시 설정 사용 + 서버 + 비밀번호 + 포트 + 아이디 + SOCKS5 프록시 설정 + Use proxy for calls + Proxy servers may degrade the quality of your calls. 로컬 데이터베이스 캐시된 텍스트 메시지를 삭제하시겠습니까? @@ -797,7 +972,7 @@ 계산중... 문서 사진 - 음성 메시지 + 음성/영상 메시지 동영상 음악 GIF파일 @@ -806,6 +981,8 @@ 미디어 저장 이 기간 동안 클라우드 채팅방에서 접근하지 않은]]> 사진이나 동영상, 기타 파일 등은 공간 절약을 위해 이 기기에서 삭제됩니다.\n\n모든 파일은 Telegram 클라우드에 여전히 남으며 필요하시면 언제든 다시 다운로드하실 수 있습니다. 영원히 + 음성 메시지 + 음성 메시지 활성화된 세션 현재 세션 @@ -838,8 +1015,8 @@ 지문인식 후 진행해주세요 터치 센서 지문인식이 실패하였습니다. 다시 시도해주세요. - 화면 캡춰 허용 - 활성화시 앱내에서 화면 캡춰가 가능합니다. 다만, 화면전환시 잠금코드가 활성화 되어져 있더라도 대화내용이 뜰 수 있습니다.\n\n적용이 안될시, 앱 재시작이 필요할 수도 있습니다. + 화면 캡처 허용 + 활성화시 앱내에서 화면 캡처가 가능합니다. 다만, 화면전환시 잠금코드가 활성화 되어져 있더라도 대화내용이 뜰 수 있습니다.\n\n적용이 안될시, 앱 재시작이 필요할 수도 있습니다. 공유한 파일 공유된 미디어 @@ -870,7 +1047,7 @@ %1$d / %2$d 앨범 모든 사진 - 모든 동영상 + 모든 미디어 사진이 없습니다. 동영상이 아직 없음 사진/동영상을 먼저 다운로드하세요 @@ -1004,6 +1181,11 @@ 개인정보 및 보안 개인정보 마지막 접속 + 결제 + 결제 및 배송정보 초기화 + 배송 정보를 삭제 및 결제사에게 저장된 신용카드 정보 삭제를 요청할 수 있습니다. 텔레그램 절대로 회원님의 신용카드 정보를 저장하지 않습니다. + 배송 정보 + 결제정보 전체 공개 내 대화상대 비공개 @@ -1044,6 +1226,8 @@ 죄송합니다, 이 이용자는 개인 설정으로 인하여 그룹에 초대 할 수 없습니다. 죄송합니다, 이 이용자는 개인 설정으로 인하여 채널에 초대 할 수 없습니다. 죄송합니다, 이 이용자들의 개인 설정으로 인하여 그룹을 생성 할 수 없습니다. + 단-대-단 + 사용자의 아이피 주소 노출을 제한하기 위하여 모든 단-대-단 기능을 텔레그램 서버로 릴레이하지만, 음성 품질이 약간 저하됩니다. 동영상 편집 동영상 보내는 중... @@ -1121,6 +1305,7 @@ 파일 스티커 음성 메시지 + 영상 메시지 게임 화면을 캡처했습니다! @@ -1189,6 +1374,11 @@ 죄송합니다, 이 작업이 제한되었습니다. 죄송합니다, 그룹에 이 이용자나 봇을 차단하였기 대문에 추가를 할 수 없습니다. 초대하시렴녀 차단해제를 부탁드립니다. 그룹방 입장 + 죄송합니다, 이 사용자는 이 그룹에 속해있지 않으며 초대가 되지 않기 때문에 관리자로 추가할 수 없습니다. + 죄송합니다, 이 사용자는 블랙리스트에 있으며 해제가 가능하지 않기 떄문에 관리자로 추가할 수 없습니다. + 죄송합니다, 이 사용자는 그룹의 관리자이며 해제권한이 없으시기 떄문에 차단할 수 없습니다. + 죄송합니다, 그룹의 관리자가 회원님의 스티커 전송을 제한했습니다. + 죄송합니다, 그룹의 관리자가 회원님의 미디어 전송을 제한했습니다. Telegram은 여러 기기에서 친구와 메시지를 주고받을 수 있도록 회원님의 연락처 접근이 필요합니다. Telegram은 사진, 비디오, 음악 및 다양한 미디어를 공유 및 저장하기 위하여 스토리지 접근이 필요합니다. @@ -1207,13 +1397,13 @@ 강력함 클라우드 기반 사생활 보호 - 세상에서 가장 빠르고 안전한
    무료]]> 메신저입니다.
    - 텔레그램]]>은 어떤 메신저보다도]]>빠르게 메시지를 전송합니다. - 텔레그램]]>은 영원히 무료입니다.]]>광고도 없고 이용료도 없습니다. - 텔레그램]]>은 그 어떤 누구로부터도]]>메시지를 안전하게 보호합니다. - 텔레그램]]>은 대화나 미디어의]]>용량에 제한이 없습니다. - 텔레그램]]>은 다른 기기에서도]]>동시에 사용할 수 있습니다. - 텔레그램]]>은 메시지를 강력하게 암호화하며]]>자동으로 삭제되게 할 수 있습니다. + 세상에서 가장 빠른]]> 메신저입니다.\n 무료]]> 이며 안전합니다]]>. + 텔레그램]]>은 어떤 메신저보다도\n빠르게 메시지를 전송합니다. + 텔레그램]]>은 영원히 무료입니다. \n광고도 없고 이용료도 없습니다. + 텔레그램]]>은 그 어떤 누구로부터도\n메시지를 안전하게 보호합니다. + 텔레그램]]>은 대화나 미디어의 용량에\n제한이 없습니다. + 텔레그램]]>은 다른 기기에서도\n동시에 사용할 수 있습니다. + 텔레그램]]>은 메시지를 강력하게 암호화하며\n자동으로 삭제되게 할 수 있습니다. 시작하기 계정설정 @@ -1256,7 +1446,7 @@ 취소된 전화 거절한 전화 %1$s (%2$s) - 이 이미지와 텍스트는 ]]>%1$s]]>.
    ]]> 님과의 현재 전화를 위하여 파생된 것입니다. 이 이미지와 텍스트가 ]]>%2$s\'s]]> 님의 휴대전화와 동일하다면 단말기간(end-to-end)의 암호화가 정상적으로 진행되고 있음을 보장합니다.
    + 이 이미지와 텍스트는 ]]>%1$s]]>님과의 통화시 생성된 암호화키에서 파생되었습니다.\n\n이 이미지와 텍스트가 ]]>%2$s\'s]]> 님의 휴대전화와 동일하다면 단말기간(end-to-end)의 암호화가 정상적으로 진행되고 있음을 보장합니다. 아직 전화 내역이 없습니다. ]]>%1$s]]>의 앱은 현재 호환되지 않은 프로토콜을 사용중입니다. 앱 업데이트가 필요합니다. ]]>%1$s]]>은 현재 전화기능을 지원하고 있지 않습니다. 앱 업데이트 후에 전화가 가능합니다. @@ -1275,13 +1465,18 @@ 전화로 이동 죄송합니다, ]]>%1$s]]> 님은 수신거부 상태입니다. %1$s님과 이모티콘이 일치한다면, 이 통화는 100%% 안전합니다 + 전화 평가 + 무엇이 잘못됬나요? + 기술적 정보 포함 + 이 작업은 대화의 내용이 노출되지 않지만, 향 후 문제해결에 도움이 됩니다. + 텔레그램 전화가 개선될 수 있도록 도와주셔서 감사합니다. - 온라인 %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명 @@ -1469,6 +1664,12 @@ %1$d 개의 전달된 음성메시지 %1$d 개의 전달된 음성메시지 %1$d 개의 전달된 음성 메시지 + %1$d 개의 전달된 영상메시지 + 전달된 영상 메시지 + %1$d 개의 전달된 영상메시지 + %1$d 개의 전달된 영상메시지 + %1$d 개의 전달된 영상메시지 + %1$d 개의 전달된 영상메시지 %1$d 개의 전달된 위치 전달된 위치 %1$d 개의 전달된 위치 @@ -1496,6 +1697,10 @@ MMM dd yyyy, h:mm a MMM dd yyyy, HH:mm + MMM dd yyyy, h:mm a + MMM dd yyyy, HH:mm + MMM dd, h:mm a + MMM dd, HH:mm MMMM yyyy M\'월\' d\'일\' yyyy.MM.dd. diff --git a/TMessagesProj/src/main/res/values-nl/strings.xml b/TMessagesProj/src/main/res/values-nl/strings.xml index fcf54fec6..623f5b9e2 100644 --- a/TMessagesProj/src/main/res/values-nl/strings.xml +++ b/TMessagesProj/src/main/res/values-nl/strings.xml @@ -6,8 +6,10 @@ Telegram Telegram-bèta Nederlands + Engels Dutch nl + Doorgaan in het Nederlands Je telefoon Bevestig je landnummer en voer je telefoonnummer in. @@ -65,7 +67,7 @@ Betaalkaart Kaartnummer Veiligheidscode (CVV) - MM/YY + MM/JJ Factuuradres Kaarthouder Achternaam @@ -79,6 +81,7 @@ TEST FACTUUR %1$s BETALEN Betalingsmethode + Betalingsprovider Naam Telefoonnummer Contactadres @@ -89,6 +92,8 @@ Sorry, de bot heeft je betaling geannuleerd. Sorry, de betaling werd geweigerd. Betalingsprovider kon niet worden bereikt, controleer je internetverbinding en probeer het opnieuw. + Waarschuwing + Noch Telegram, noch %1$s krijgen toegang tot je creditcard-informatie. Creditkaartgegevens worden alleen gebruikt door het het betalingssysteem %2$s.\n\nBetalingen gaan direct naar de ontwikkelaar van %1$s. Telegram kan geen garanties afgeven, doorgaan is op eigen risico. Neem bij problemen contact op met de ontwikkelaar van %1$s of je bank. NIeuwe chat Instellingen @@ -100,6 +105,13 @@ Begin een gesprek door op de\nopstelknop rechtsonder te drukken\nof druk op de menuknop voor meer opties. Wachten op netwerk Verbinden + Verbinden met proxy... + Tik om proxy uit te schakelen + Uitschakelen + Inschakelen + Proxy echt inschakelen + Je kunt de proxy-server aanpassen via Instellingen (Data en opslag) + Proxy %1$s echt uitschakelen? Opnieuw inschakelen kan via Instellingen (Data en opslag). Bijwerken Nieuwe geheime chat Wachten tot %s online komt @@ -126,12 +138,12 @@ RECENT CHATS %1$s uit suggesties verwijderen? - Link-voorvertoning + Link-voorbeeld Concept Geschiedenis gewist door %1$s %1$s door %2$s - Feedback over deze voorvertoning + Feedback over dit voorbeeld Sticker sturen Bundel bekijken Vastzetten @@ -139,12 +151,14 @@ Vet Cursief Normaal + Geef ]]>Telegram]]> toegang tot je contacten zodat je makkelijk contact kunt opnemen met iedereen die je kent. + NU NIET + DOORGAAN - Groepsvorm - Kanaaltype Publiek Privé Promoveren tot beheerder + Geen geblokkeerde gebruikers Optioneel kun je een groepsbeschrijving geven. Groep verlaten Groep verwijderen @@ -168,6 +182,7 @@ un1 heeft bestand vastgezet un1 heeft sticker vastgezet un1 heeft spraakbericht vastgezet + un1 heeft videobericht vastgezet un1 heeft contact vastgezet un1 heeft %1$s vastgezet un1 heeft locatie vastgezet @@ -176,6 +191,7 @@ De groep is opgewaardeerd naar een supergroep %1$s is opgewaardeerd naar een supergroep Geblokkeerde gebruikers kunnen alleen worden uitgenodigd door beheerders, uitnodigingslinks werken niet voor hen. + Geblokkeerde gebruikers kunnen alleen worden uitgenodigd door beheerders, uitnodigingslinks werken niet voor hen. Nieuw kanaal Kanaalnaam Contacten aan je kanaal toevoegen @@ -217,7 +233,9 @@ Naam controleren... %1$s is beschikbaar. Leden + Blacklist Geblokkeerde gebruikers + Beperkt Beheerders Kanaal verwijderen Kanaal verwijderen @@ -233,15 +251,17 @@ 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. - Moderator Maker Beheerder GELUID UIT GELUID AAN Beheerder toevoegen + Blokkeer gebruiker + Deblokkeer + Gebruiker vasthouden om te deblokkeren. Uitnodigen via link %1$s echt als beheerder toevoegen? - Verwijder + Beheerder ontslaan Alleen kanaal-beheerders zien deze lijst. Deze gebruiker is nog geen lid, uitnodigen? Andere Telegram-gebruikers kunnen lid worden van je groep door deze link te openen. @@ -271,6 +291,7 @@ %1$s heeft een bestand gestuurd naar het kanaal %2$s %1$s heeft een GIF gestuurd naar het kanaal %2$s %1$s heeft een spraakbericht gestuurd naar het kanaal %2$s + %1$s heeft een videobericht gestuurd naar het kanaal %2$s %1$s heeft een muziekbestand gestuurd naar het kanaal %2$s %1$s heeft een sticker gestuurd naar het kanaal %2$s %1$s heeft een %3$s sticker gestuurd naar kanaal %2$s @@ -282,16 +303,121 @@ %1$s plaatste een bestand %1$s plaatste een GIF %1$s plaatste een spraakbericht + %1$s plaatste een videobericht %1$s plaatste een muziekbestand %1$s plaatste een sticker %1$s plaatste een %2$s sticker - Wie kan leden toevoegen? + Leden toevoegen Alle leden - Alleen beheerders + Beheerders Berichtgeving voor leden Geen berichtgeving voor leden Ondertekenen Beheerdersnaam bij alle uitgaande berichten. + Beheerder wijzigen + Wat mag deze beheerder? + Informatie aanpassen + Informatie aanpassen + Berichten plaatsen + Berichten wijzigen + Berichten verwijderen + Verwijder berichten + Beheerders toevoegen + Beheerder ontslaan + Leden blokkeren + Leden toevoegen + Uitnodigen via link + Berichten vastzetten + Toegevoegd door %1$s + Je kunt de machtigingen van deze beheerder niet aanpassen. + Beperkt door %1$s + Gebruikersbeperkingen + Berichten lezen + Wat mag dit lid? + Berichten versturen + Media versturen + Stickers & GIF\'s versturen + Link-voorbeeld sturen + Blokkeer tot + Voor altijd + Verwijder + Blokkeer en verwijder uit groep + Groep beheren + Kanaal beheren + Groep beheren + Kanaal beheren + + Recente gebeurtenissen + Alle gebeurtenissen + Geselecteerde gebeurtenissen + Alle beheerders + ]]>Nog geen gebeurtenissen]]>\n\nBeheerders en leden\nhebben geen acties uitgevoerd\nin de afgelopen 48 uur. + ]]>Nog geen gebeurtenissen]]>\n\nBeheerders\nhebben geen acties uitgevoerd\nin de afgelopen 48 uur. + ]]>Geen gebeurtenissen]]>\n\nGeen recente gebeurtenissen gevonden\nvoor je zoekopdracht. + Geen recente gebeurtenissen met inhoud \']]>%1$s]]>\' gevonden + Wat is een logboek? + Dit is een lijst met alle acties van de groepsleden en beheerders in de afgelopen 48 uur. + Dit is een lijst met alle acties van de beheerders in de afgelopen 48 uur. + un1 heeft de groep hernoemd naar \"%1$s\" + un1 heeft het kanaal hernoemd naar \"%1$s\" + un1 heeft de groep verlaten + un1 heeft het kanaal verlaten + un1 heeft un2 toegevoegd + un1 is lid geworden + %1$s geblokkeerd + %1$s gedeblokkeerd + un1 is lid geworden + groepsafbeelding door un1 ingesteld: + kanaalfoto door un1 ingesteld: + groepsafbeelding door un1 verwijderd + kanaalfoto door un1 verwijderd + bericht door un1 bewerkt: + onderschrift door un1 gewijzigd: + Origineel bericht + Origineel onderschrift + Leeg + bericht door un1 vastgezet: + bericht door un1 losgemaakt + bericht door un1 verwijderd: + link door un1 gewijzigd: + link door un1 gewijzigd: + un1 heeft de link verwijderd + un1 heeft de link verwijderd + Vorige link + beschrijving door un1 gewijzigd + beschrijving door un1 gewijzigd + Vorige beschrijving + uitnodigingen door un1 ingeschakeld + uitnodigingen door un1 uitgeschakeld + ondertekenen door un1 ingeschakeld + ondertekenen door un1 uitgeschakeld + beperkingen van %1$s\n\n tot: %2$s aangepast + Stickers & GIF\'s versturen + Media versturen + Berichten versturen + Link-voorbeeld sturen + Berichten lezen + privileges van %1$s aangepast + Informatie aanpassen + Informatie aanpassen + Berichten plaatsen + Berichten wijzigen + Berichten verwijderen + Beheerders toevoegen + Leden blokkeren + Leden toevoegen + Berichten vastzetten + Alle gebeurtenissen + Nieuwe beperkingen + Nieuwe beheerders + Nieuwe leden + Groepsinformatie + Kanaalinformatie + Groepsinstellingen + Verwijderde berichten + Gewijzigde berichten + Vastgezette berichten + Vertrokken leden Nieuwe verzendlijst Naam van lijst @@ -324,13 +450,18 @@ is aan het typen zijn aan het typen %1$s neemt een spraakbericht op... + %1$s neemt een videobericht op... %1$s verstuurt een geluid... %1$s verstuurt een foto SNELLE WEERGAVE + GROEP OPENEN + KANAAL OPENEN + Donker thema schakelt automatisch in tijdens de nacht %1$s speelt een spel %1$s verstuurt een video %1$s verstuurt een bestand neemt een spraakbericht op... + neemt een videobericht op... audio versturen... foto versturen speelt een spel @@ -378,6 +509,8 @@ Open met... Link kopiëren %1$s versturen + Als bestand + Als bestand URL %1$s openen? Wil je %1$s toestaan om je naam en ID (niet je telefoonnummer) door te geven aan webpagina\'s die je opent via deze bot? SPAM MELDEN @@ -393,13 +526,14 @@ https://telegram.org/faq#can-39t-send-messages-to-non-contacts Meer informatie Versturen naar... + Opmerking toevoegen Tik hier om opgeslagen GIF\'s te bekijken Vastzetten Alle leden informeren Losmaken Wil je dit bericht vastzetten? Wil je dit bericht losmaken? - Blacklist gebruiker + Blokkeer gebruiker Spam melden Alles verwijderen van %1$s Recente emoji\'s wissen? @@ -415,6 +549,7 @@ %1$s Wijzigen kan niet meer. Snelkoppeling toevoegen + Leden zoeken Snelkoppeling aan startscherm toegevoegd Je Cloudopslag Jij @@ -427,6 +562,21 @@ Verwijder voor %1$s Verwijder voor iedereen Tekst gekopieerd naar klembord. + Hou vast voor audio-opname. Tik voor video + Hou vast voor video-opname. Tik voor audio + Spraakbericht weggooien + Wil je stoppen met opnemen en je spraakbericht weggooien? + Videobericht weggooien + Wil je stoppen met opnemen en je videobericht weggooien? + Weggooien + Je bent beperkt in het sturen van media door de groepsbeheerders tot %1$s + Je bent beperkt in het sturen van inline-content door de groepsbeheerders tot %1$s + Je bent beperkt in het sturen van stickers door de groepsbeheerders tot %1$s + Je bent beperkt in het plaatsen van berichten door de groepsbeheerders tot%1$s + Je bent beperkt in het sturen van media door de groepsbeheerders. + Je bent beperkt in het sturen van inline-content door de groepsbeheerders. + Je bent beperkt in het sturen van stickers door de groepsbeheerders. + Je bent beperkt in het plaatsen van berichten door de groepsbeheerders. %1$s heeft de zelfvernietigingstimer ingesteld op %2$s Je hebt de zelfvernietigingstimer ingesteld op %1$s @@ -443,6 +593,7 @@ %1$s heeft je een bestand gestuurd %1$s heeft je een GIF gestuurd %1$s heeft je een spraakbericht gestuurd + %1$s heeft je een videobericht gestuurd %1$s heeft je een muziekbestand gestuurd %1$s heeft je een sticker gestuurd %1$s heeft je een %2$s sticker gestuurd @@ -456,12 +607,13 @@ %1$s heeft een bestand gestuurd naar de groep %2$s %1$s heeft een GIF gestuurd naar de groep %2$s %1$s heeft een spraakbericht gestuurd naar de groep %2$s + %1$s heeft een videobericht gestuurd naar de groep %2$s %1$s heeft een muziekbestand gestuurd naar de groep %2$s %1$s heeft een sticker gestuurd naar de groep %2$s %1$s heeft een %3$s sticker gestuurd naar de groep %2$s %1$s heeft je uitgenodigd voor de groep %2$s - %1$s heeft de naam van de groep %2$s gewijzigd - %1$s heeft de afbeelding van de groep %2$s gewijzigd + %1$s heeft de groepsnam van %2$s gewijzigd + %1$s heeft de groepsfoto 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 @@ -485,6 +637,7 @@ %1$s heeft sticker vastgezet in de groep %2$s %1$s heeft een %3$s sticker vastgezet in de groep %2$s %1$s heeft spraakbericht vastgezet in de groep %2$s + %1$s heeft videobericht vastgezet in de groep %2$s %1$s heeft contact vastgezet in de groep %2$s %1$s heeft locatie vastgezet in de groep %2$s %1$s heeft GIF vastgezet in de groep %2$s @@ -497,6 +650,7 @@ %1$s heeft sticker vastgezet %1$s heeft een %2$s sticker vastgezet %1$s heeft spraakbericht vastgezet + %1$s heeft videobericht vastgezet %1$s heeft contact vastgezet %1$s heeft locatie vastgezet %1$s heeft GIF vastgezet @@ -551,8 +705,10 @@ Instellingen Lid toevoegen Beheerders instellen + LID BEPERKEN Groep verwijderen en verlaten Meldingen + Lid beperken Verwijderen uit groep Opwaarderen naar supergroep Opwaarderen naar supergroep @@ -584,7 +740,7 @@ Encryptiesleutel Zelfvernietigingstimer Uit - Deze afbeelding en tekst zijn afgeleid van de encryptiesleutel voor deze geheime chat met ]]>%1$s]]>.
    ]]>Als dit er hetzelfde uitziet op het apparaat van ]]>%2$s]]>, dan is end-to-end-encryptie gegarandeerd.
    ]]>Lees meer op telegram.org
    + Deze afbeelding en tekst zijn afgeleid van de encryptiesleutel voor deze geheime chat met ]]>%1$s]]>.\n\nAls dit er hetzelfde uitziet op het apparaat van ]]>%2$s]]>, dan is end-to-end-encryptie gegarandeerd.
    ]]>Lees meer op telegram.org
    https://telegram.org/faq#secret-chats Tik voor emoji-weergave Onbekend @@ -598,14 +754,14 @@ Je naam moet minimaal 5 tekens hebben. Je naam mag niet langer zijn dan 32 tekens. Sorry, begincijfers zijn niet toegestaan. - Je kan een gebruikersnaam kiezen voor ]]>Telegram]]>. Hiermee kunnen anderen je vinden en contact met je opnemen zonder je telefoonnummer te weten.
    ]]>Je mag ]]>a–z]]>, ]]>0–9]]> en liggend streepje gebruiken. De minimale lengte is ]]>5]]> tekens.
    + Je kan een gebruikersnaam kiezen voor ]]>Telegram]]>. Hiermee kunnen anderen je vinden en contact met je opnemen zonder je telefoonnummer te weten.\n\nJe mag ]]>a–z]]>, ]]>0–9]]> en liggend streepje gebruiken. De minimale lengte is ]]>5]]> tekens. Deze link opent een chat met je in Telegram:\n%1$s Gebruikersnaam controleren. %1$s is beschikbaar. Geen Er is een fout opgetreden. - Stickers + Stickers Stickers toevoegen Maskers toevoegen Toevoegen aan stickers @@ -638,6 +794,8 @@ Een aantal ongebruikte maskerbundels zijn gearchiveerd. Je kunt deze opnieuw activeren via de maskerinstellingen. Thema + Donker + Blauw Thema echt verwijderen? Themabestand is ongeldig Geef een naam aan je thema @@ -645,7 +803,7 @@ THEME OPSLAAN Nieuw Thema TOEPASSEN - Thema-voorvertoning + Thema-voorbeeld Selecteer kleur Nieuw thema maken Tik op het palet-icoon om een lijst met elementen voor elk scherm te zien en deze te bewerken. @@ -660,14 +818,14 @@ Geen geblokkeerde gebruikers Berichtmeldingen Waarschuwing - Voorvertoning + Voorbeeld Groepsmeldingen Geluid In-app meldingen Geluiden Trillen Trillen - Voorvertoningen + In-App voorbeeld Resetten Meldingen resetten Aangepaste meldingsinstellingen wissen voor contacten en groepen. @@ -683,7 +841,10 @@ Groen Cyaan Wit + Sepia + Donker Roze + Tekstgrootte Paars Oranje Sommige apparaten hebben een LED-functie, om d.m.v. licht aan te geven dat er nieuwe berichten zijn. @@ -707,7 +868,8 @@ Contact lid van Telegram Vastgezette berichten Taal - De ondersteuning van Telegram wordt gedaan door vrijwilligers. We doen ons best om zo snel mogelijk te antwoorden.
    ]]>Bekijk ook de veelgestelde vragen]]>: Hier staan de antwoorden op de meeste vragen en belangrijke tips voor het oplossen van problemen]]>.
    + Aangepast + De ondersteuning van Telegram wordt gedaan door vrijwilligers. We doen ons best om zo snel mogelijk te antwoorden.\n\nBekijk ook de veelgestelde vragen]]>: Hier staan de antwoorden op de meeste vragen en belangrijke tips voor het oplossen van problemen]]>. Vraag een vrijwilliger Veelgestelde vragen https://telegram.org/faq @@ -775,7 +937,7 @@ Uitgeschakeld Aantal geluidsmeldingen %1$s binnen %2$s - Linkvoorvertoningen + Link-voorbeelden Geheime chats In-app browser Externe links in de app openen @@ -788,6 +950,19 @@ Debug-menu Contacten importeren Contacten verversen + Via instellingen kun je je taal altijd aanpassen. + Kies je taal + Overige + Proxy-instellingen + Proxy + Gebruik proxy-instellingen + Server + Wachtwoord + Poort + Gebruiker + SOCKS5 proxy-instellingen + Gebruik voor oproepen + Je oproepkwaliteit kan slechter worden door het gebruik van een proxy-server. Lokale database Gecachet tekstberichten wissen? @@ -797,7 +972,7 @@ Berekenen... Bestanden Foto\'s - Spraakberichten + Spraak/Video-berichten Video\'s Muziek GIFs @@ -806,6 +981,8 @@ Media bewaren Foto\'s, video\'s en andere bestanden uit cloudchats die je gedurende deze periode niet hebt geopend]]> zullen worden verwijderd van dit apparaat om ruimte te besparen.\n\nAlle media zal in de Telegram-cloud bewaard blijven en kan opnieuw worden gedownload als je het weer nodig hebt. Voor altijd + Spraakberichten + Videoberichten Actieve sessies Huidige sessie @@ -870,7 +1047,7 @@ %1$d van %2$d Galerij Alle foto\'s - Alle video\'s + Alle media Nog geen foto\'s Nog geen video\'s Download media eerst @@ -1004,6 +1181,11 @@ Privacy en veiligheid Privacy Laatst gezien + Betalingen + Betaal- en verzendinfo wissen + Je kunt je verzendinformatie wissen en alle betalingsproviders opdracht geven om je bewaarde creditcards te verwijderen. Je creditcardgegevens worden nooit opgeslagen door Telegram. + Verzendinformatie + Betaalinformatie Iedereen Mijn contacten Niemand @@ -1044,6 +1226,8 @@ Je kunt deze gebruiker niet toevoegen aan groepen vanwege de privacyinstellingen van deze gebruiker. Je kunt deze gebruiker niet toevoegen aan kanalen vanwege de privacyinstellingen van deze gebruiker. Je kan geen groep maken met deze gebruikers vanwege de privacyinstellingen van deze gebruikers. + Peer-to-Peer + peer-to-peer uitschakelen zorgt ervoor dat alle gesprekken verlopen via Telegram-servers, dit voorkomt dat je IP-adres zichtbaar is maar verlaagt de audio-kwaliteit enigszins. Video bewerken Video versturen @@ -1121,6 +1305,7 @@ Bestand Sticker Spraakbericht + Videobericht Spel Jij Schermafdruk gemaakt! @@ -1178,7 +1363,7 @@ Sorry, deze functie is momenteel niet beschikbaar in jouw land. Er is geen Telegram-account met deze gebruikersnaam. Deze bot kan geen groepslid worden. - Wil je uitgebreide link-voorvertoning inschakelen voor geheime chats? Let op: deze worden gegenereerd op onze servers. + Wil je uitgebreide link-voorbeelden inschakelen voor geheime chats? Let op: deze worden gegenereerd op onze servers. Let op: inline-bots worden aangeboden door externe ontwikkelaars. Voor de werking van de bot worden de karakters die je na de botnaam typt naar deze ontwikkelaar verstuurd. Wil je \"Houd bij oor\" inschakelen voor spraakberichten? Je mag dit bericht niet wijzigen. @@ -1189,6 +1374,11 @@ Je mag deze actie niet uitvoeren. Je hebt deze gebruiker of bot geblokkeerd, toevoegen aan een groep kan pas na deblokkeren. LID WORDEN + Je mag geen leden toevoegen, deze gebruiker is nog geen lid en kan daarom niet worden toegevoegd als beheerder. + Je mag geen leden deblokkeren, deze gebruiker is geblokkeerd en kan daarom niet worden toegevoegd als beheerder. + Je mag geen leden degraderen, deze gebruiker is een beheerder en kan daarom niet worden geblokkeerd. + De beheerders van deze groep hebben je beperkt in het sturen van stickers. + De beheerders van deze groep hebben je beperkt in het sturen van media. Telegram heeft toegang tot je contacten nodig zodat je kan chatten met je vrienden vanaf al je apparaten. Telegram heeft toegang tot je opslaggeheugen nodig zodat je foto\'s, video\'s, muziek en andere media kunt opslaan en versturen. @@ -1207,13 +1397,13 @@ Krachtig In de cloud Privé - \'s Werelds snelste]]> berichtendienst.]]>Het is gratis]]> en veilig]]>. - Telegram]]> bezorgt berichten sneller dan]]>elke andere applicatie. - Telegram]]> is altijd gratis. Geen advertenties.]]>Geen abonnementskosten. - Telegram]]> beveiligd je berichten]]>tegen aanvallen van hackers. - Telegram]]> beperkt je niet in de grootte van]]>je media of chats. - Telegram]]> biedt toegang tot je berichten]]>vanaf meerdere apparaten. - Telegram]]> berichten zijn sterk versleuteld]]>en kunnen zichzelf vernietigen. + \'s Werelds snelste]]> berichtendienst.\nHet is gratis]]> en veilig]]>. + Telegram]]> bezorgt berichten sneller dan\nelke andere applicatie. + Telegram]]> is altijd gratis. Geen advertenties.\nGeen abonnementskosten. + Telegram]]> beveiligd je berichten\ntegen aanvallen van hackers. + Telegram]]> beperkt je niet in de grootte van\nje media of chats. + Telegram]]> biedt toegang tot je berichten\nvanaf meerdere apparaten. + Telegram]]> berichten zijn sterk versleuteld\nen kunnen zichzelf vernietigen. Begin met chatten Accountinstellingen @@ -1256,7 +1446,7 @@ Oproep geannuleerd Geweigerde oproep %1$s (%2$s) - Deze afbeelding en tekst zijn afgeleid van de encryptiesleutel voor deze oproep met ]]>%1$s]]>.
    ]]>Als dit er hetzelfde uitziet op het apparaat van ]]>%2$s]]>, dan is end-to-end-encryptie gegarandeerd.
    + Deze afbeelding en tekst zijn afgeleid van de encryptiesleutel voor deze oproep met ]]>%1$s]]>.\n\nAls dit er hetzelfde uitziet op het apparaat van ]]>%2$s]]>, dan is end-to-end-encryptie gegarandeerd. Je hebt nog geen oproepen ]]>%1$s]]> maakt gebruik van een niet-compatibel protocol voor spraakoproepen en moet eerst een update uitvoeren. ]]>%1$s]]> heeft nog geen ondersteuning voor spraakoproepen en zal eerst een update moeten uitvoeren. @@ -1274,14 +1464,19 @@ Bluetooth TERUG NAAR GESPREK Sorry, ]]>%1$s]]> neemt geen oproepen aan. - Als deze emoji\'s er bij %1$s hetzelfde uitzien is deze oproep 100%% veilig. + Als deze emoji\'s gelijk zijn bij %1$s is dit gesprek 100%% veilig. + Beoordeel deze oproep + Wat is er misgegaan? + Technische informatie versturen + We hebben geen inzage in je gesprek, maar kunnen hiermee het probleem wel sneller oplossen. + Bedankt voor je hulp bij het verbeteren van Telegram-oproepen. - %1$d online - %1$d online - %1$d online - %1$d online - %1$d online - %1$d online + %1$d online + %1$d online + %1$d online + %1$d online + %1$d online + %1$d online %1$d leden %1$d leden %1$d leden @@ -1469,6 +1664,12 @@ Bijlage: %1$d spraakberichten Bijlage: %1$d spraakberichten Bijlage: %1$d spraakberichten + Bijlage: %1$d videoberichten + Doorgestuurd videobericht + Bijlage: %1$d videoberichten + Bijlage: %1$d videoberichten + Bijlage: %1$d videoberichten + Bijlage: %1$d videoberichten Bijlage: %1$d locaties Bijlage: 1 locatie Bijlage: %1$d locaties @@ -1496,6 +1697,10 @@ dd MMM yyyy, h:mm a dd MMM yyyy, HH:mm + dd MMM yyyy, h:mm a + dd MMM yyyy, HH:mm + dd MMM, h:mm a + dd MMM, HH:mm MMMM yyyy dd MMM dd-MM-yy diff --git a/TMessagesProj/src/main/res/values-pt-rBR/strings.xml b/TMessagesProj/src/main/res/values-pt-rBR/strings.xml index 44f02ce2a..23f1b6255 100644 --- a/TMessagesProj/src/main/res/values-pt-rBR/strings.xml +++ b/TMessagesProj/src/main/res/values-pt-rBR/strings.xml @@ -6,8 +6,10 @@ Telegram Telegram Beta Português (Brasil) - Português (Brasil) + Inglês + Portuguese (Brazil) pt_BR + Continuar em Português Seu número Confirme o código de seu país e preencha seu número de telefone. @@ -47,30 +49,30 @@ Finalizar Métodos de envio Desculpe, não é possível entregar neste endereço. - Informação de envio - Endereço de envio + Dados de envio + Dados de envio Endereço 1 (Rua) Endereço 2 (Rua) Cidade Estado País - Código Postal + Código postal Destinatário Nome Completo Número de Telefone E-Mail - Salvar Informação de Envio - Você pode salvar sua informação de envio para uso futuro. - Informação de pagamento + Salvar Dados de Envio + Você pode salvar seus dados de envio para uso futuro. + Dados de pagamento Cartão de pagamento Número do Cartão Código de Segurança (CVV) - MM/YY + MM/AA Endereço de cobrança Titular do cartão Nome Sobrenome - Salvar Informação de Pagamento - Você pode salvar sua informação de pagamento para uso futuro. + Salvar Dados de Pagamento + Você pode salvar seus dados de pagamento para uso futuro. Por favor, *ative a Verificação em Duas Etapas* para habilitar isso. Revisão da Transação Você realmente deseja transferir %1$s para o bot %2$s por %3$s? @@ -79,6 +81,7 @@ FATURA DE TESTES PAGAR %1$s Método de pagamento + Provedor do pagamento Nome Número de telefone Endereço de contato @@ -89,6 +92,8 @@ Desculpe, o pagamento foi cancelado pelo bot. Desculpe, o pagamento foi recusado. Não foi possível encontrar o servidor de pagamento. Verifique sua conexão e tente novamente. + Atenção + Nem o Telegram, nem %1$s terão acesso ao número do seu cartão de crédito. Os detalhes do cartão de crédito serão tratados apenas pelo sistema de pagamentos, %2$s.\n\nPagamentos irão diretamente para o desenvolvedor de %1$s. O Telegram não pode prover qualquer garantia, então proceda por sua conta e risco. Em caso de problemas, contate o desenvolvedor de %1$s ou o seu banco. Nova conversa Configurações @@ -100,6 +105,13 @@ Comece a conversar pressionando o\nbotão \'Nova Mensagem\' no canto inferior direito\nou vá para a seção \'Contatos\'. Aguardando rede... Conectando... + Conectando ao proxy... + Toque para desabilitar o proxy... + Desativar + Ativar + Você tem certeza que deseja habilitar o proxy? + Você pode alterar seu servidor proxy nas Configurações (Dados e Armazenamento). + Você tem certeza que deseja desabilitar o servidor proxy %1$s? Você pode reabilitá-lo depois nas Configurações (Dados e Armazenamento). Atualizando... Novo Chat Secreto Esperando %s se conectar... @@ -139,16 +151,18 @@ Negrito Itálico Regular + Para se conectar com todos que você conhece, permita ao Telegram o acesso aos seus contatos. + AGORA NÃO + CONTINUAR - Tipo de Grupo - Tipo de canal Público Privado Promover a administrador + Nenhum usuário banido Você pode fornecer uma descrição opcional para seu grupo. Sair do Grupo Apagar Grupo - Sair do Grupo + Sair do grupo Apagar Grupo Você perderá todas as mensagens neste grupo. Você pode adicionar administradores para ajudar você a gerenciar seu grupo. Toque e segure para removê-los. @@ -168,6 +182,7 @@ un1 fixou um arquivo un1 fixou um sticker un1 fixou uma mensagem de voz + un1 fixou uma mensagem de vídeo un1 fixou um contato un1 fixou %1$s un1 fixou um mapa @@ -175,7 +190,8 @@ un1 fixou uma música Esse grupo foi convertido para um supergrupo %1$s foi convertido a um supergrupo - Usuários bloqueados são removidos do grupo e só podem voltar se convidados por um administrador. Convites por link não funcionam para eles. + Usuários banidos são removidos do grupo e só podem voltar se convidados por um administrador. Convites por link não funcionam para eles. + Usuários banidos são removidos do canal e só podem voltar se convidados por um administrador. Convites por link não funcionam para eles. Novo Canal Nome do canal Adicionar contatos no canal @@ -217,7 +233,9 @@ Verificando nome... %1$s está disponível. Membros - Usuários bloqueados + Lista Negra + Usuários banidos + Usuários restringidos Administradores Apagar Canal Apagar Canal @@ -233,15 +251,17 @@ 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. - Moderador Criador Administrador SILENCIAR RESTAURAR SOM Adicionar Administrador + Banir usuário + Desbanir + Toque e segure no usuário para desbanir Convidar via Link Você tem certeza que deseja colocar %1$s como adiministrador? - Remover + Descartar admin Somente os administradores do canal podem ver essa lista. Esse usuário não entrou no seu canal ainda. Você deseja enviar um convite? Qualquer um com Telegram instalado poderá entrar no seu canal abrindo este link. @@ -270,7 +290,8 @@ %1$s enviou uma localização ao canal %2$s %1$s enviou um arquivo ao canal %2$s %1$s enviou um GIF ao canal %2$s - %1$s enviou uma mensagem ao canal %2$s + %1$s enviou uma mensagem de voz ao canal %2$s + %1$s enviou uma mensagem de vídeo ao canal %2$s %1$s enviou uma música ao canal %2$s %1$s enviou um sticker ao canal %2$s %1$s enviou um %3$s sticker ao canal %2$s @@ -282,6 +303,7 @@ %1$s postou um arquivo %1$s postou um GIF %1$s postou uma mensagem de voz + %1$s postou uma mensagem de vídeo %1$s postou uma música %1$s postou um sticker %1$s postou um %2$s sticker @@ -292,6 +314,110 @@ Os membros não serão notificados quando você postar Assinar Mensagens Adicionar nomes dos administradores nas mensagens postadas. + Editar Admin + O que esse administrador pode fazer? + Alterar Info do Canal + Alterar Info do Grupo + Postar Mensagens + Editar Mensagens de Outros + Apagar Mensagens de Outros + Apagar Mensagens + Adicionar Novos Admins + Descartar Admin + Banir Usuários + Adicionar Usuários + Convidar Usuários via Link + Fixar Mensagens + Adicionado por %1$s + Você não têm permissão para editar esse admin + Restringido por %1$s + Restrições do Usuário + Ler Mensagens + O que esse usuário pode fazer? + Enviar Mensagens + Enviar Mídias + Enviar Stickers & GIFs + Preview de Links + Banido até + Sempre + Remover + Banir e remover do grupo + Gerenciar Grupo + Gerenciar Canal + Gerenciar grupo + Gerenciar canal + + Ações Recentes + Todas as ações + Ações selecionadas + Todos os Admins + ]]>Nenhuma ação ainda]]>\n\nOs membros e admins do grupo\nainda não tomaram nenhuma ação\nnas últimas 48 horas. + ]]>Nenhuma ação ainda]]>\n\nOs membros e admins do canal\nainda não tomaram nenhuma ação\nnas últimas 48 horas. + ]]>Nenhuma ação encontrada]]>\n\nNenhuma ação recente correspondente\nfoi encontrada. + Nenhuma ação recente que contenha \']]>%1$s]]>\' foi encontrada. + O que são Ações Recentes? + Essa é uma lista de todas as ações tomadas pelos membros e admins do grupo nas últimas 48 horas. + Essa é uma lista de todas as ações tomadas pelos admins do canal nas últimas 48 horas. + un1 renomeou o grupo para \"%1$s\" + un1 renomeou o canal para \"%1$s\" + un1 deixou o grupo + un1 deixou o canal + un1 adicionou un2 + un1 entrou no grupo + baniu %1$s + desbaniu %1$s + un1 entrou no canal + un1 definiu uma nova foto do grupo + un1 definiu uma nova foto do canal + un1 removeu a foto do grupo + un1 removeu foto do canal + un1 editou essa mensagem: + un1 editou a legenda: + Mensagem original: + Legenda original: + Vazio + un1 fixou essa mensagem: + un1 desafixou essa mensagem: + un1 removeu essa mensagem: + un1 alterou o link do grupo: + un1 alterou o link do canal: + un1 removeu o link do grupo + un1 removeu o link do canal + Link prévio + un1 editou a descrição do grupo + un1 editou a descrição do canal + Descrição prévia + un1 ativou o convite ao grupo + un1 desativou o convite ao grupo + un1 ativou assinaturas + un1 desativou assinaturas + alterou as restrições para %1$s\n\nDuração: %2$s + Enviar Stickers & GIFs + Enviar mídia + Enviar mensagem + Preview de Links + Ler Mensagens + alterou os privilégios de %1$s + Alterar Info do canal + Alterar Info do grupo + Postar Mensagens + Editar mensagens + Apagar mensagens + Adicionar admins + Banir usuários + Adicionar usuários + Fixar mensagens + Todas as ações + Novas restrições + Novos admins + Novos membros + Info do grupo + Info do canal + Configurações do grupo + Mensagens apagadas + Mensagens editadas + Mensagens fixadas + Membros saindo Nova Lista de Transmissão Digite o nome da lista @@ -324,13 +450,18 @@ está escrevendo... estão escrevendo... %1$s está gravando uma mensagem de voz... + %1$s está gravando uma mensagem de vídeo... %1$s está enviando um áudio... %1$s está enviando uma foto... LEITURA RÁPIDA + ABRIR GRUPO + ABRIR CANAL + O tema noturno será ativado automaticamente à noite %1$s está jogando... %1$s está enviando um vídeo... %1$s está enviando um arquivo... - gravando mensagem de voz... + gravando uma mensagem de voz... + gravando uma mensagem de vídeo... enviando áudio... enviando foto... jogando... @@ -378,6 +509,8 @@ Abrir em... Copiar URL Enviar %1$s + Enviar como arquivo + Enviar como Arquivos Abrir URL em %1$s? Permitir ao %1$s passar seu nome do Telegram e id (não o seu número de telefone) para páginas que você abrir com esse bot? REPORTAR SPAM @@ -393,6 +526,7 @@ https://telegram.org/faq/br#no-consigo-enviar-mensagens-para-no-contatos Mais informações Enviar para... + Escrever um comentário... Toque aqui para acessar os GIFs salvos Fixar Notificar todos os membros @@ -415,6 +549,7 @@ %1$s Desculpe, o tempo para editar expirou. Adicionar atalho + Buscar membros Atalho adicionado à tela de início chat consigo mesmo Você @@ -427,6 +562,21 @@ Apagar para %1$s Apagar para todos os membros Texto copiado para área de transferência + Segure para gravar um áudio. Toque para mudar para vídeo. + Segure para gravar um vídeo. Toque para mudar para áudio. + Descartar Mensagem de Voz + Você tem certeza que deseja parar de gravar e descartar sua mensagem de voz? + Descartar Mensagem de Vídeo + Você tem certeza que deseja parar de gravar e descartar sua mensagem de vídeo? + Descartar + Os administradores do grupo restringiram você de enviar mídias aqui até %1$s + Os administradores do grupo restringiram você de usar bots integrados aqui até %1$s + Os administradores do grupo restringiram você de enviar stickers aqui até %1$s + Os administradores do grupo restringiram você de escrever aqui até %1$s + Os administradores do grupo restringiram você de enviar mídias aqui + Os administradores do grupo restringiram você de usar bots integrados aqui + Os administradores do grupo restringiram você de enviar stickers aqui + Os administradores do grupo restringiram você de escrever aqui %1$s estabeleceu o tempo de autodestruição para %2$s Você estabeleceu o tempo de autodestruição para %1$s @@ -443,6 +593,7 @@ %1$s lhe enviou um arquivo %1$s te enviou um GIF %1$s enviou uma mensagem de voz + %1$s enviou uma mensagem de vídeo %1$s enviou uma música %1$s lhe enviou um sticker %1$s lhe enviou um %2$s sticker @@ -455,13 +606,14 @@ %1$s convidou o grupo %2$s para jogar %3$s %1$s enviou um arquivo para o grupo %2$s %1$s enviou um GIF para o grupo %2$s - %1$s enviou uma mensagem para o grupo %2$s + %1$s enviou uma mensagem de voz para o grupo %2$s + %1$s enviou uma mensagem de vídeo para o grupo %2$s %1$s enviou uma música ao grupo %2$s %1$s enviou um sticker ao grupo %2$s %1$s enviou um %3$s sticker ao grupo %2$s %1$s convidou você para o grupo %2$s - %1$s editou o nome do grupo %2$s - %1$s editou a foto do grupo %2$s + %1$s renomeou o grupo %2$s + %1$s alterou a foto 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 @@ -485,6 +637,7 @@ %1$s fixou um sticker no grupo %2$s %1$s fixou um %3$s sticker no grupo %2$s %1$s fixou uma mensagem de voz no grupo %2$s + %1$s fixou uma mensagem de vídeo no grupo %2$s %1$s fixou um contato no grupo %2$s %1$s fixou um mapa no grupo %2$s %1$s fixou um GIF no grupo %2$s @@ -497,6 +650,7 @@ %1$s fixou um sticker %1$s fixou um %2$s sticker %1$s fixou uma mensagem de voz + %1$s fixou uma mensagem de vídeo %1$s fixou um contato %1$s fixou um mapa %1$s fixou um GIF @@ -551,8 +705,10 @@ Configurações Adicionar membro Definir administradores + BANIR DO GRUPO Apagar e sair do grupo Notificações + Restringir usuário Remover do grupo Converter a Supergrupo Converter a Supergrupo @@ -584,7 +740,7 @@ Chave criptográfica Tempo de autodestruição Desativado - Essa imagem e texto foram derivadas da chave criptográfica para este chat secreto com ]]>%1$s]]>.
    ]]>Se você vê o mesmo no dispositivo de ]]>%2$s\'s]]>, a criptografia ponta a ponta está garantida.
    ]]>Leia mais em telegram.org
    + Essa imagem e texto foram derivadas da chave criptográfica para esse chat secreto com ]]>%1$s]]>.\n\nSe você vê o mesmo no dispositivo de ]]>%2$s]]>, a criptografia ponta a ponta está garantida. https://telegram.org/faq/br#chats-secretos Toque para emojizar Desconhecido @@ -598,14 +754,14 @@ O nome de usuário deve ter pelo menos 5 caracteres. O nome de usuário não pode exceder 32 caracteres. Desculpe, o nome de usuário não pode começar com um número. - Você pode escolher um nome de usuário no ]]>Telegram]]>. Assim, outras pessoas poderão te encontrar pelo nome de usuário e entrar em contato sem precisar saber seu telefone.
    ]]>Você pode usar ]]>a–z]]>, ]]>0–9]]> e underline. O tamanho mínimo é de ]]>5]]> caracteres.
    + Você pode escolher um nome de usuário no ]]>Telegram]]>. Assim, outras pessoas poderão te encontrar pelo nome de usuário e entrar em contato sem precisar saber seu telefone.\n\nVocê pode usar ]]>a–z]]>, ]]>0–9]]> e underline. O tamanho mínimo é ]]>5]]> caracteres. Esse link abre um chat com você mesmo no Telegram:\n%1$s Verificando nome de usuário... %1$s está disponível. Nenhum Ocorreu um erro. - Stickers + Stickers Adicionar Stickers Adicionar Máscaras Adicionar aos Stickers @@ -638,6 +794,8 @@ Alguns de seus pacotes de máscaras antigos foram arquivados. Você pode reativá-los nas Configurações de Máscaras. Tema + Escuro + Azul Você tem certeza que deseja apagar esse tema? Arquivo de tema incorreto Insira o nome do tema @@ -683,7 +841,10 @@ Verde Ciano Branco + Sépia + Escuro Rosa + Tamanho da Fonte Violeta Laranja O LED é uma pequena luz piscante em alguns dispositivos, utilizada para indicar novas mensagens. @@ -707,7 +868,8 @@ Contato entrou para o Telegram Mensagens Fixadas Idioma - Por favor entenda que o suporte do Telegram é feito por voluntários. Tentaremos responder o mais rápido possível, mas poderemos demorar um pouco.
    ]]>Por favor verifique a página de perguntas frequentes do Telegram]]>: há dicas e respostas para a maioria dos problemas]]>.
    + Personalizado + Note que o suporte do Telegram é feito por voluntários. Tentaremos responder o mais rápido possível, mas poderemos demorar um pouco. Por favor verifique a página de Perguntas Frequentes]]>: há dicas e respostas para a maioria dos problemas]]>. Pergunte a um voluntário Perguntas Frequentes https://telegram.org/faq/br @@ -788,6 +950,19 @@ Debug Menu Importar Contatos Recarregar Contatos + Você pode alterar seu idioma nas Configurações. + Escolha o seu idioma + Outro + Configurações de Proxy + Proxy + Usar configurações de proxy + Server + Senha + Porta + Nome de Usuário + Configuração de proxy SOCKS5 + Usar proxy para chamadas + Servidores proxy podem diminuir a qualidade de suas chamadas. Banco de Dados Local Limpar todos os textos em cache? @@ -797,7 +972,7 @@ Calculando... Documentos Fotos - Mensagens de voz + Mensagens de vídeo/voz Vídeos Música GIFs @@ -806,6 +981,8 @@ Manter Mídias Fotos, vídeos e outros arquivos da nuvem que você não acessou]]> durante esse período serão removidos deste dispositivo para economizar espaço em disco.\n\nTodas as mídias permanecerão na nuvem do Telegram e poderão ser baixadas novamente conforme necessário. Permanentemente + Mensagens de voz + Mensagens de vídeo Sessões Ativas Sessão atual @@ -869,8 +1046,8 @@ Salvar na galeria %1$d de %2$d Galeria - Todas as Fotos - Todos os Vídeos + Todas as fotos + Todas as mídias Ainda não há fotos Nenhum vídeo ainda Baixar o vídeo primeiro @@ -1004,6 +1181,11 @@ Privacidade e Segurança Privacidade Último Acesso + Pagamentos + Limpar dados de pagamento & envio + Você pode remover suas informações de envio e instruir aos provedores de pagamentos para remover seus cartões de crédito salvos. Note que o Telegram nunca armazena seus dados de cartão de crédito. + Dados de envio + Dados de pagamento Todos Meus Contatos Nenhum @@ -1044,6 +1226,8 @@ Desculpe, você não pode adicionar esse usuário a grupos devido às configurações de privacidade dele. Desculpe, você não pode adicionar esse usuário a canais devido às configurações de privacidade dele. Você não pode criar um grupo com esses usuários devido as configurações de privacidade deles. + Ponta a Ponta + Desativar o ponta a ponta irá transmitir as chamadas através dos servidores do Telegram, para evitar revelar o seu endereço de IP, mas pode diminuir a qualidade do áudio. Editar Vídeo Enviando vídeo... @@ -1121,6 +1305,7 @@ Arquivo Sticker Mensagem de voz + Mensagem de vídeo Jogar Você Você realizou uma captura da tela! @@ -1189,6 +1374,11 @@ Você não tem permissão para isso. Desculpe, você não pode adicionar esse usuário ou bot à grupos por tê-lo bloqueado. Desbloqueie para prosseguir. ENTRAR NO GRUPO + Você não pode adicionar esse usuário como administrador porque ele não é um membro do grupo e você não possui permissão para convidá-lo. + Você não pode adicionar esse usuário como admin porque ele está na lista negra e você não pode desbaní-lo. + Você não pode banir esse usuário porque ele é um admin do grupo e você não possui permissão para rebaixá-lo. + Desculpe, os administradores do grupo te restringiram do envio de stickers. + Desculpe, os administradores do grupo te restringiram do envio de mídias. Telegram precisa acessar seus contatos para que você possa se conectar aos seus amigos em todos os seus dispositivos. Telegram precisa acessar seu armazenamento para que você possa enviar e salvar fotos, vídeos, músicas e outras mídias. @@ -1207,13 +1397,13 @@ Poderoso Baseado na nuvem Privado - O mais rápido]]> aplicativo de mensagem do mundo. ]]>É gratuito]]> e seguro]]>. - O Telegram]]> envia mensagens mais rápido]]>que qualquer outro aplicativo. - O Telegram]]> é grátis para sempre. ]]>Sem propagandas. Sem taxas. - O Telegram]]> mantém suas mensagens]]>seguras de ataques de hackers. - O Telegram]]> não possui limites no tamanho]]>de seus arquivos e conversas. - O Telegram]]> permite você acessar suas]]> mensagens de múltiplos dispositivos. - O Telegram]]> possui mensagens fortemente]]>encriptadas e podem se auto-destruir. + O aplicativo de mensagem mais rápido]]> do mundo.\nÉ gratuito]]> e seguro]]>. + O Telegram]]> envia mensagens mais rápido que qualquer outro aplicativo. + O Telegram]]> é gratuito para sempre. Sem propagandas.\nSem taxas. + O Telegram]]> mantém suas mensagens seguras\nde ataques de hackers. + O Telegram]]> não possui limites no tamanho de\nsuas mídia e conversas. + O Telegram]]> te permite acessar suas mensagens \nde múltiplos dispositivos. + O Telegram]]> possui mensagens fortemente criptografadas\ne que podem se auto-destruir. Comece a conversar Configurações de Conta @@ -1256,7 +1446,7 @@ Chamada Cancelada Chamada Rejeitada %1$s (%2$s) - Essa imagem e texto foram derivadas da chave criptográfica para essa chamada com ]]>%1$s]]>.
    ]]>Se você vê o mesmo no dispositivo de ]]>%2$s]]>, a criptografia ponta a ponta está garantida.
    + Essa imagem e texto foram derivadas da chave criptográfica para essa chamada com ]]>%1$s]]>.\n\nSe você vê o mesmo no dispositivo de ]]>%2$s]]>, a criptografia ponta a ponta está garantida. Você ainda não realizou chamadas. O aplicativo de ]]>%1$s]]> está usando um protocolo incompatível. O aplicativo desse usuário precisa ser atualizado para que você possa chamá-lo. O aplicativo de ]]>%1$s]]> não suporta chamadas. O aplicativo desse usuário precisa ser atualizado para que você possa chamá-lo. @@ -1275,13 +1465,18 @@ RETORNAR À LIGAÇÃO Desculpe, ]]>%1$s]]> não aceita chamadas. Se os emojis na tela de %1$s são os mesmos, essa chamada é 100%% segura. + Avaliar Chamada + O que aconteceu de errado? + Incluir informação técnica + Isso não irá revelar o conteúdo de sua conversa, mas nos ajudará a resolver o problema. + Obrigado por ajudar a tornar as chamadas do Telegram melhores. - %1$d online - %1$d online - %1$d online - %1$d online - %1$d online - %1$d online + %1$d online + %1$d online + %1$d online + %1$d online + %1$d online + %1$d online %1$d membros %1$d membro %1$d membros @@ -1469,6 +1664,12 @@ %1$d mensagens de voz encaminhadas %1$d mensagens de voz encaminhadas %1$d mensagens de voz encaminhadas + %1$d mensagens de vídeo encaminhadas + Mensagem de vídeo encaminhada + %1$d mensagens de vídeo encaminhadas + %1$d mensagens de vídeo encaminhadas + %1$d mensagens de vídeo encaminhadas + %1$d mensagens de vídeo encaminhadas %1$d localizações encaminhadas Localização encaminhada %1$d localizações encaminhadas @@ -1496,6 +1697,10 @@ dd MMM yyyy, h:mm a dd MMM yyyy, h:mm a + dd MMM yyyy, h:mm a + dd MMM yyyy, h:mm a + dd MMM yyyy, h:mm a + dd MMM, HH:mm MMMM yyyy dd MMM dd.MM.yy diff --git a/TMessagesProj/src/main/res/values-v21/styles.xml b/TMessagesProj/src/main/res/values-v21/styles.xml index f98bc6044..0a540117e 100644 --- a/TMessagesProj/src/main/res/values-v21/styles.xml +++ b/TMessagesProj/src/main/res/values-v21/styles.xml @@ -18,6 +18,7 @@ @null #426482 #527da3 + #000000 + +