From ca827fa6b68df31b8f07a4d6242c591b7c7bd4d5 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Fri, 9 Sep 2016 09:38:28 +0200 Subject: [PATCH] Upgrade from Telegram 3.8.1 (787) to 3.10.1-(821). --- .gitignore | 1 + TMessagesProj/build.gradle | 10 +- .../config/debug/AndroidManifest.xml | 2 +- .../config/release/AndroidManifest.xml | 1 + TMessagesProj/jni/Android.mk | 9 +- TMessagesProj/jni/SqliteWrapper.cpp | 241 + TMessagesProj/jni/jni.c | 5 - .../jni/libyuv/include/libyuv/compare_row.h | 9 +- .../jni/libyuv/include/libyuv/convert.h | 6 +- .../jni/libyuv/include/libyuv/convert_argb.h | 6 +- .../jni/libyuv/include/libyuv/convert_from.h | 2 - .../jni/libyuv/include/libyuv/cpu_id.h | 4 +- .../libyuv/include/libyuv/planar_functions.h | 18 +- .../jni/libyuv/include/libyuv/rotate_row.h | 33 +- TMessagesProj/jni/libyuv/include/libyuv/row.h | 144 +- .../jni/libyuv/include/libyuv/scale_row.h | 68 +- .../jni/libyuv/include/libyuv/version.h | 2 +- TMessagesProj/jni/libyuv/source/convert.cc | 8 +- .../jni/libyuv/source/convert_argb.cc | 19 +- .../jni/libyuv/source/convert_from.cc | 15 +- .../jni/libyuv/source/convert_from_argb.cc | 175 +- .../jni/libyuv/source/convert_jpeg.cc | 1 + .../jni/libyuv/source/convert_to_argb.cc | 13 +- .../jni/libyuv/source/convert_to_i420.cc | 14 +- TMessagesProj/jni/libyuv/source/cpu_id.cc | 27 +- .../jni/libyuv/source/planar_functions.cc | 133 +- TMessagesProj/jni/libyuv/source/rotate.cc | 36 +- TMessagesProj/jni/libyuv/source/rotate_any.cc | 8 +- .../jni/libyuv/source/rotate_mips.cc | 16 +- .../jni/libyuv/source/rotate_neon.cc | 8 +- .../jni/libyuv/source/rotate_neon64.cc | 8 +- TMessagesProj/jni/libyuv/source/row_any.cc | 83 +- TMessagesProj/jni/libyuv/source/row_common.cc | 81 +- TMessagesProj/jni/libyuv/source/row_gcc.cc | 302 +- TMessagesProj/jni/libyuv/source/row_mips.cc | 36 +- TMessagesProj/jni/libyuv/source/row_neon.cc | 105 +- TMessagesProj/jni/libyuv/source/row_neon64.cc | 272 +- TMessagesProj/jni/libyuv/source/row_win.cc | 159 +- TMessagesProj/jni/libyuv/source/scale.cc | 113 +- TMessagesProj/jni/libyuv/source/scale_any.cc | 21 + TMessagesProj/jni/libyuv/source/scale_argb.cc | 27 +- .../jni/libyuv/source/scale_common.cc | 38 +- TMessagesProj/jni/libyuv/source/scale_gcc.cc | 60 +- TMessagesProj/jni/libyuv/source/scale_mips.cc | 42 +- TMessagesProj/jni/libyuv/source/scale_neon.cc | 30 +- .../jni/libyuv/source/scale_neon64.cc | 30 +- TMessagesProj/jni/libyuv/source/scale_win.cc | 2 +- .../jni/libyuv/source/video_common.cc | 1 + TMessagesProj/jni/sqlite.c | 11 - TMessagesProj/jni/sqlite.h | 10 - TMessagesProj/jni/sqlite/sqlite3.c | 32558 +++++++++++----- TMessagesProj/jni/sqlite/sqlite3.h | 1859 +- TMessagesProj/jni/sqlite_cursor.c | 79 - TMessagesProj/jni/sqlite_database.c | 42 - TMessagesProj/jni/sqlite_statement.c | 120 - .../jni/tgnet/ConnectionsManager.cpp | 25 - TMessagesProj/jni/tgnet/Datacenter.cpp | 2 +- TMessagesProj/jni/tgnet/MTProtoScheme.cpp | 2 + TMessagesProj/jni/tgnet/MTProtoScheme.h | 3 +- TMessagesProj/src/main/AndroidManifest.xml | 19 + .../org/telegram/PhoneFormat/PhoneFormat.java | 64 +- .../org/telegram/SQLite/SQLiteCursor.java | 23 +- .../SQLite/SQLitePreparedStatement.java | 8 +- .../telegram/messenger/AndroidUtilities.java | 305 +- .../messenger/Animation/Animator10.java | 191 - .../Animation/AnimatorListenerAdapter10.java | 50 - .../messenger/Animation/AnimatorSet10.java | 705 - .../messenger/Animation/FloatKeyframeSet.java | 114 - .../messenger/Animation/IntKeyframeSet.java | 114 - .../messenger/Animation/Keyframe.java | 187 - .../messenger/Animation/KeyframeSet.java | 200 - .../messenger/Animation/ObjectAnimator10.java | 488 - .../messenger/Animation/Property.java | 49 - .../Animation/PropertyValuesHolder.java | 545 - .../Animation/ReflectiveProperty.java | 182 - .../messenger/Animation/ValueAnimator.java | 675 - .../telegram/messenger/Animation/View10.java | 349 - .../AnimatorListenerAdapterProxy.java | 112 - .../AnimationCompat/AnimatorSetProxy.java | 137 - .../AnimationCompat/ObjectAnimatorProxy.java | 130 - .../messenger/AnimationCompat/ViewProxy.java | 248 - .../AnimatorListenerAdapterProxy.java | 45 + .../telegram/messenger/ApplicationLoader.java | 58 +- .../messenger/AutoMessageReplyReceiver.java | 2 +- .../java/org/telegram/messenger/Bitmaps.java | 6 +- .../org/telegram/messenger/BuildVars.java | 4 +- .../messenger/ContactsController.java | 162 +- .../org/telegram/messenger/DialogObject.java | 18 + .../telegram/messenger/FileLoadOperation.java | 29 +- .../org/telegram/messenger/ImageLoader.java | 45 +- .../org/telegram/messenger/ImageReceiver.java | 28 +- .../telegram/messenger/LocaleController.java | 2 +- .../telegram/messenger/MediaController.java | 135 +- .../org/telegram/messenger/MessageObject.java | 84 +- .../messenger/MessagesController.java | 1815 +- .../telegram/messenger/MessagesStorage.java | 1426 +- .../messenger/MusicBrowserService.java | 593 + .../messenger/MusicPlayerService.java | 6 +- .../org/telegram/messenger/NativeLoader.java | 43 +- .../org/telegram/messenger/NetworkAlarm.java | 60 - .../messenger/NotificationCenter.java | 9 +- .../NotificationDismissReceiver.java | 23 + .../messenger/NotificationsController.java | 79 +- .../telegram/messenger/OpenChatReceiver.java | 36 + .../telegram/messenger/SecretChatHelper.java | 71 +- .../messenger/SendMessagesHelper.java | 343 +- .../messenger/TgChooserTargetService.java | 1 - .../org/telegram/messenger/UserConfig.java | 35 +- .../org/telegram/messenger/Utilities.java | 2 +- .../telegram/messenger/VideoEditedInfo.java | 40 +- .../telegram/messenger/WearReplyReceiver.java | 2 +- .../telegram/messenger/browser/Browser.java | 10 +- .../exoplayer/AspectRatioFrameLayout.java | 86 + .../exoplayer/BehindLiveWindowException.java | 33 + .../org/telegram/messenger/exoplayer/C.java | 146 + .../messenger/exoplayer/CodecCounters.java | 62 + .../messenger/exoplayer/CryptoInfo.java | 116 + .../messenger/exoplayer/DecoderInfo.java | 69 + .../exoplayer/DefaultLoadControl.java | 278 + .../exoplayer/DummyTrackRenderer.java | 77 + .../exoplayer/ExoPlaybackException.java | 51 + .../messenger/exoplayer/ExoPlayer.java | 415 + .../messenger/exoplayer/ExoPlayerImpl.java | 230 + .../exoplayer/ExoPlayerImplInternal.java | 668 + .../exoplayer/ExoPlayerLibraryInfo.java | 51 + .../exoplayer/FrameworkSampleSource.java | 349 + .../messenger/exoplayer/LoadControl.java | 75 + .../MediaClock.java} | 20 +- .../MediaCodecAudioTrackRenderer.java | 486 + .../exoplayer/MediaCodecSelector.java | 64 + .../exoplayer/MediaCodecTrackRenderer.java | 1114 + .../messenger/exoplayer/MediaCodecUtil.java | 501 + .../MediaCodecVideoTrackRenderer.java | 611 + .../messenger/exoplayer/MediaFormat.java | 502 + .../exoplayer/MediaFormatHolder.java | 34 + .../messenger/exoplayer/ParserException.java | 41 + .../messenger/exoplayer/SampleHolder.java | 153 + .../messenger/exoplayer/SampleSource.java | 214 + .../exoplayer/SampleSourceTrackRenderer.java | 267 + .../exoplayer/SingleSampleSource.java | 301 + .../exoplayer/StandaloneMediaClock.java | 76 + .../messenger/exoplayer/TimeRange.java | 203 + .../messenger/exoplayer/TrackRenderer.java | 371 + .../VideoFrameReleaseTimeHelper.java | 305 + .../exoplayer/audio/AudioCapabilities.java | 122 + .../audio/AudioCapabilitiesReceiver.java | 104 + .../messenger/exoplayer/audio/AudioTrack.java | 1337 + .../BaseChunkSampleSourceEventListener.java | 104 + .../exoplayer/chunk/BaseMediaChunk.java | 107 + .../messenger/exoplayer/chunk/Chunk.java | 135 + .../chunk/ChunkExtractorWrapper.java | 145 + .../exoplayer/chunk/ChunkOperationHolder.java | 53 + .../exoplayer/chunk/ChunkSampleSource.java | 688 + .../exoplayer/chunk/ChunkSource.java | 139 + .../exoplayer/chunk/ContainerMediaChunk.java | 191 + .../messenger/exoplayer/chunk/DataChunk.java | 124 + .../messenger/exoplayer/chunk/Format.java | 173 + .../exoplayer/chunk/FormatEvaluator.java | 284 + .../chunk/FormatWrapper.java} | 20 +- .../exoplayer/chunk/InitializationChunk.java | 200 + .../messenger/exoplayer/chunk/MediaChunk.java | 69 + .../chunk/SingleSampleMediaChunk.java | 107 + .../chunk/VideoFormatSelectorUtil.java | 250 + .../messenger/exoplayer/drm/DrmInitData.java | 129 + .../exoplayer/drm/DrmSessionManager.java | 104 + .../drm/KeysExpiredException.java} | 11 +- .../exoplayer/drm/MediaDrmCallback.java | 49 + .../drm/StreamingDrmSessionManager.java | 518 + .../drm/UnsupportedDrmException.java | 44 + .../exoplayer/extractor/ChunkIndex.java | 86 + .../extractor/DefaultExtractorInput.java | 261 + .../extractor/DefaultTrackOutput.java | 271 + .../exoplayer/extractor/DummyTrackOutput.java | 47 + .../exoplayer/extractor/Extractor.java | 105 + .../exoplayer/extractor/ExtractorInput.java | 226 + .../exoplayer/extractor/ExtractorOutput.java | 55 + .../extractor/ExtractorSampleSource.java | 907 + .../exoplayer/extractor/GaplessInfo.java | 88 + .../extractor/PositionHolder.java} | 16 +- .../extractor/RollingSampleBuffer.java | 673 + .../exoplayer/extractor/SeekMap.java | 60 + .../exoplayer/extractor/TrackOutput.java | 79 + .../extractor/flv/AudioTagPayloadReader.java | 102 + .../exoplayer/extractor/flv/FlvExtractor.java | 286 + .../extractor/flv/ScriptTagPayloadReader.java | 212 + .../extractor/flv/TagPayloadReader.java | 110 + .../extractor/flv/VideoTagPayloadReader.java | 199 + .../extractor/mp3/ConstantBitrateSeeker.java | 59 + .../exoplayer/extractor/mp3/Id3Util.java | 299 + .../exoplayer/extractor/mp3/Mp3Extractor.java | 343 + .../exoplayer/extractor/mp3/VbriSeeker.java | 120 + .../exoplayer/extractor/mp3/XingSeeker.java | 168 + .../exoplayer/extractor/mp4/Atom.java | 299 + .../exoplayer/extractor/mp4/AtomParsers.java | 1135 + .../extractor/mp4/DefaultSampleValues.java | 32 + .../mp4/FixedSampleSizeRechunker.java | 96 + .../extractor/mp4/FragmentedMp4Extractor.java | 1019 + .../exoplayer/extractor/mp4/Mp4Extractor.java | 476 + .../exoplayer/extractor/mp4/PsshAtomUtil.java | 140 + .../exoplayer/extractor/mp4/Sniffer.java | 189 + .../exoplayer/extractor/mp4/Track.java | 100 + .../extractor/mp4/TrackEncryptionBox.java | 50 + .../extractor/mp4/TrackFragment.java | 167 + .../extractor/mp4/TrackSampleTable.java | 106 + .../exoplayer/extractor/ogg/FlacReader.java | 100 + .../exoplayer/extractor/ogg/OggExtractor.java | 86 + .../exoplayer/extractor/ogg/OggParser.java | 173 + .../exoplayer/extractor/ogg/OggSeeker.java | 79 + .../exoplayer/extractor/ogg/OggUtil.java | 211 + .../exoplayer/extractor/ogg/StreamReader.java | 44 + .../extractor/ogg/VorbisBitArray.java | 162 + .../exoplayer/extractor/ogg/VorbisReader.java | 247 + .../exoplayer/extractor/ogg/VorbisUtil.java | 489 + .../exoplayer/extractor/ts/Ac3Reader.java | 181 + .../exoplayer/extractor/ts/AdtsExtractor.java | 155 + .../exoplayer/extractor/ts/AdtsReader.java | 311 + .../exoplayer/extractor/ts/DtsReader.java | 168 + .../extractor/ts/ElementaryStreamReader.java | 60 + .../exoplayer/extractor/ts/H262Reader.java | 281 + .../exoplayer/extractor/ts/H264Reader.java | 520 + .../exoplayer/extractor/ts/H265Reader.java | 483 + .../exoplayer/extractor/ts/Id3Reader.java | 94 + .../extractor/ts/MpegAudioReader.java | 204 + .../extractor/ts/NalUnitTargetBuffer.java | 110 + .../exoplayer/extractor/ts/PsExtractor.java | 328 + .../extractor/ts/PtsTimestampAdjuster.java | 115 + .../exoplayer/extractor/ts/SeiReader.java | 65 + .../exoplayer/extractor/ts/TsExtractor.java | 641 + .../exoplayer/extractor/wav/WavExtractor.java | 135 + .../exoplayer/extractor/wav/WavHeader.java | 110 + .../extractor/wav/WavHeaderReader.java | 190 + .../extractor/webm/DefaultEbmlReader.java | 240 + .../exoplayer/extractor/webm/EbmlReader.java | 84 + .../extractor/webm/EbmlReaderOutput.java | 117 + .../exoplayer/extractor/webm/Sniffer.java | 115 + .../extractor/webm/VarintReader.java | 140 + .../extractor/webm/WebmExtractor.java | 1560 + .../exoplayer/hls/Aes128DataSource.java | 108 + .../hls/DefaultHlsTrackSelector.java | 141 + .../exoplayer/hls/HlsChunkSource.java | 956 + .../exoplayer/hls/HlsExtractorWrapper.java | 269 + .../exoplayer/hls/HlsMasterPlaylist.java | 44 + .../exoplayer/hls/HlsMediaPlaylist.java | 89 + .../exoplayer/hls/HlsParserUtil.java | 72 + .../hls/HlsPlaylist.java} | 28 +- .../exoplayer/hls/HlsPlaylistParser.java | 355 + .../exoplayer/hls/HlsSampleSource.java | 841 + .../exoplayer/hls/HlsTrackSelector.java | 59 + .../hls/PtsTimestampAdjusterProvider.java | 63 + .../messenger/exoplayer/hls/TsChunk.java | 139 + .../messenger/exoplayer/hls/Variant.java | 39 + .../exoplayer/hls/WebvttExtractor.java | 173 + .../exoplayer/metadata/MetadataParser.java | 45 + .../metadata/MetadataTrackRenderer.java | 169 + .../exoplayer/metadata/id3/ApicFrame.java | 38 + .../exoplayer/metadata/id3/BinaryFrame.java | 30 + .../exoplayer/metadata/id3/GeobFrame.java | 38 + .../exoplayer/metadata/id3/Id3Frame.java | 32 + .../exoplayer/metadata/id3/Id3Parser.java | 285 + .../exoplayer/metadata/id3/PrivFrame.java | 34 + .../metadata/id3/TextInformationFrame.java | 30 + .../exoplayer/metadata/id3/TxxxFrame.java | 34 + .../DefaultSmoothStreamingTrackSelector.java | 101 + .../SmoothStreamingChunkSource.java | 558 + .../SmoothStreamingManifest.java | 256 + .../SmoothStreamingManifestParser.java | 702 + .../SmoothStreamingTrackSelector.java | 59 + .../exoplayer/text/CaptionStyleCompat.java | 160 + .../messenger/exoplayer/text/Cue.java | 140 + .../exoplayer/text/PlayableSubtitle.java | 75 + .../messenger/exoplayer/text/Subtitle.java | 64 + .../exoplayer/text/SubtitleParser.java | 44 + .../exoplayer/text/SubtitleParserHelper.java | 189 + .../exoplayer/text/TextRenderer.java | 32 + .../exoplayer/text/TextTrackRenderer.java | 328 + .../exoplayer/text/eia608/ClosedCaption.java | 41 + .../text/eia608/ClosedCaptionCtrl.java | 101 + .../text/eia608/ClosedCaptionList.java | 39 + .../text/eia608/ClosedCaptionText.java | 27 + .../exoplayer/text/eia608/Eia608Parser.java | 267 + .../text/eia608/Eia608TrackRenderer.java | 363 + .../exoplayer/text/subrip/SubripParser.java | 128 + .../exoplayer/text/subrip/SubripSubtitle.java | 80 + .../exoplayer/text/ttml/TtmlColorParser.java | 140 + .../exoplayer/text/ttml/TtmlNode.java | 298 + .../exoplayer/text/ttml/TtmlParser.java | 492 + .../exoplayer/text/ttml/TtmlRegion.java | 39 + .../exoplayer/text/ttml/TtmlRenderUtil.java | 151 + .../exoplayer/text/ttml/TtmlStyle.java | 248 + .../exoplayer/text/ttml/TtmlSubtitle.java | 80 + .../exoplayer/text/tx3g/Tx3gParser.java | 41 + .../exoplayer/text/tx3g/Tx3gSubtitle.java | 62 + .../text/webvtt/Mp4WebvttParser.java | 100 + .../text/webvtt/Mp4WebvttSubtitle.java | 61 + .../exoplayer/text/webvtt/WebvttCue.java | 180 + .../text/webvtt/WebvttCueParser.java | 401 + .../exoplayer/text/webvtt/WebvttParser.java | 68 + .../text/webvtt/WebvttParserUtil.java | 75 + .../exoplayer/text/webvtt/WebvttSubtitle.java | 127 + .../exoplayer/upstream/Allocation.java | 53 + .../exoplayer/upstream/Allocator.java | 67 + .../exoplayer/upstream/AssetDataSource.java | 160 + .../exoplayer/upstream/BandwidthMeter.java | 52 + .../exoplayer/upstream/ByteArrayDataSink.java | 61 + .../upstream/ByteArrayDataSource.java | 70 + .../exoplayer/upstream/ContentDataSource.java | 157 + .../exoplayer/upstream/DataSink.java | 52 + .../exoplayer/upstream/DataSource.java | 72 + .../upstream/DataSourceInputStream.java | 102 + .../exoplayer/upstream/DataSpec.java | 168 + .../exoplayer/upstream/DefaultAllocator.java | 158 + .../upstream/DefaultBandwidthMeter.java | 117 + .../upstream/DefaultHttpDataSource.java | 582 + .../upstream/DefaultUriDataSource.java | 159 + .../exoplayer/upstream/FileDataSource.java | 134 + .../exoplayer/upstream/HttpDataSource.java | 165 + .../messenger/exoplayer/upstream/Loader.java | 260 + .../exoplayer/upstream/NetworkLock.java | 135 + .../upstream/PriorityDataSource.java | 58 + .../exoplayer/upstream/TeeDataSource.java | 71 + .../exoplayer/upstream/TransferListener.java | 41 + .../exoplayer/upstream/UdpDataSource.java | 186 + .../exoplayer/upstream/UriDataSource.java | 32 + .../exoplayer/upstream/UriLoadable.java | 99 + .../exoplayer/upstream/cache/Cache.java | 185 + .../upstream/cache/CacheDataSink.java | 140 + .../upstream/cache/CacheDataSource.java | 257 + .../upstream/cache/CacheEvictor.java | 34 + .../exoplayer/upstream/cache/CacheSpan.java | 150 + .../cache/LeastRecentlyUsedCacheEvictor.java | 76 + .../upstream/cache/NoOpCacheEvictor.java | 47 + .../exoplayer/upstream/cache/SimpleCache.java | 382 + .../messenger/exoplayer/util/Ac3Util.java | 240 + .../messenger/exoplayer/util/Assertions.java | 155 + .../util/Clock.java} | 27 +- .../exoplayer/util/CodecSpecificDataUtil.java | 250 + .../exoplayer/util/DebugTextViewHelper.java | 125 + .../messenger/exoplayer/util/DtsUtil.java | 116 + .../exoplayer/util/FlacSeekTable.java | 85 + .../exoplayer/util/FlacStreamInfo.java | 78 + .../messenger/exoplayer/util/FlacUtil.java | 51 + .../messenger/exoplayer/util/LongArray.java | 86 + .../exoplayer/util/ManifestFetcher.java | 410 + .../messenger/exoplayer/util/MimeTypes.java | 191 + .../exoplayer/util/MpegAudioHeader.java | 206 + .../messenger/exoplayer/util/NalUnitUtil.java | 478 + .../exoplayer/util/ParsableBitArray.java | 233 + .../exoplayer/util/ParsableByteArray.java | 433 + .../messenger/exoplayer/util/ParserUtil.java | 55 + .../exoplayer/util/PlayerControl.java | 105 + .../messenger/exoplayer/util/Predicate.java | 33 + .../exoplayer/util/PriorityHandlerThread.java | 43 + .../exoplayer/util/SlidingPercentile.java | 157 + .../messenger/exoplayer/util/SystemClock.java | 28 + .../messenger/exoplayer/util/TraceUtil.java | 61 + .../messenger/exoplayer/util/UriUtil.java | 258 + .../messenger/exoplayer/util/Util.java | 914 + .../exoplayer/util/VerboseLogUtil.java | 81 + .../exoplayer/util/extensions/Buffer.java | 46 + .../exoplayer/util/extensions/Decoder.java | 63 + .../util/extensions/InputBuffer.java | 37 + .../util/extensions/OutputBuffer.java | 33 + .../util/extensions/SimpleDecoder.java | 282 + .../telegram/messenger/query/BotQuery.java | 14 +- .../telegram/messenger/query/DraftQuery.java | 315 + .../messenger/query/MessagesQuery.java | 55 +- .../messenger/query/MessagesSearchQuery.java | 94 +- .../telegram/messenger/query/SearchQuery.java | 410 + .../messenger/query/SharedMediaQuery.java | 12 +- .../messenger/query/StickersQuery.java | 16 +- .../support/widget/RecyclerView.java | 8 +- .../telegram/tgnet/ConnectionsManager.java | 5 +- .../org/telegram/tgnet/NativeByteBuffer.java | 12 +- .../main/java/org/telegram/tgnet/TLRPC.java | 1603 +- .../org/telegram/ui/ActionBar/ActionBar.java | 117 +- .../ui/ActionBar/ActionBarLayout.java | 133 +- .../telegram/ui/ActionBar/ActionBarMenu.java | 12 +- .../ui/ActionBar/ActionBarMenuItem.java | 99 +- .../telegram/ui/ActionBar/BaseFragment.java | 8 +- .../telegram/ui/ActionBar/BottomSheet.java | 1023 +- .../ui/ActionBar/DrawerLayoutContainer.java | 46 +- .../telegram/ui/ActionBar/SimpleTextView.java | 55 +- .../java/org/telegram/ui/ActionBar/Theme.java | 35 +- .../Adapters/BaseSearchAdapterRecycler.java | 4 +- .../ui/Adapters/ChatActivityAdapter.java | 396 - .../telegram/ui/Adapters/ContactsAdapter.java | 15 +- .../telegram/ui/Adapters/DialogsAdapter.java | 8 +- .../ui/Adapters/DialogsSearchAdapter.java | 215 +- .../telegram/ui/Adapters/MentionsAdapter.java | 208 +- .../telegram/ui/Adapters/StickersAdapter.java | 44 +- .../org/telegram/ui/AudioPlayerActivity.java | 3 +- .../org/telegram/ui/AudioSelectActivity.java | 5 +- .../org/telegram/ui/BlockedUsersActivity.java | 4 +- .../org/telegram/ui/CacheControlActivity.java | 22 +- .../org/telegram/ui/Cells/AboutLinkCell.java | 8 +- .../org/telegram/ui/Cells/ChatActionCell.java | 3 - .../org/telegram/ui/Cells/ChatBaseCell.java | 1234 - .../telegram/ui/Cells/ChatMessageCell.java | 2402 +- .../telegram/ui/Cells/ContextLinkCell.java | 23 +- .../org/telegram/ui/Cells/DialogCell.java | 272 +- .../telegram/ui/Cells/DrawerProfileCell.java | 1 + .../telegram/ui/Cells/GreySectionCell.java | 4 +- .../org/telegram/ui/Cells/HintDialogCell.java | 175 + .../org/telegram/ui/Cells/MentionCell.java | 9 +- .../ui/Cells/PhotoAttachCameraCell.java | 6 +- .../telegram/ui/Cells/PhotoEditToolCell.java | 4 +- .../ui/Cells/PhotoPickerAlbumsCell.java | 6 +- .../ui/Cells/PhotoPickerPhotoCell.java | 24 +- .../ui/Cells/PhotoPickerSearchCell.java | 3 +- .../telegram/ui/Cells/ProfileSearchCell.java | 6 +- .../telegram/ui/Cells/RadioButtonCell.java | 4 +- .../telegram/ui/Cells/ShareDialogCell.java | 9 +- .../ui/Cells/SharedPhotoVideoCell.java | 34 +- .../org/telegram/ui/Cells/StickerCell.java | 11 +- .../telegram/ui/Cells/StickerEmojiCell.java | 7 +- .../org/telegram/ui/Cells/StickerSetCell.java | 13 +- .../org/telegram/ui/Cells/TextBlockCell.java | 4 +- .../org/telegram/ui/Cells/TextCheckCell.java | 13 +- .../ui/Cells/TextDetailSettingsCell.java | 4 +- .../java/org/telegram/ui/Cells/UserCell.java | 10 +- .../org/telegram/ui/ChangePhoneActivity.java | 69 +- .../telegram/ui/ChannelCreateActivity.java | 79 +- .../org/telegram/ui/ChannelEditActivity.java | 37 +- .../telegram/ui/ChannelEditTypeActivity.java | 12 +- .../org/telegram/ui/ChannelUsersActivity.java | 5 +- .../java/org/telegram/ui/ChatActivity.java | 2384 +- .../telegram/ui/Components/AlertsCreator.java | 2 +- .../ui/Components/AnimatedFileDrawable.java | 25 +- .../ui/Components/ChatActivityEnterView.java | 575 +- .../ui/Components/ChatAttachAlert.java | 1351 + .../ui/Components/ChatAttachView.java | 611 - .../ui/Components/ChatAvatarContainer.java | 127 +- .../org/telegram/ui/Components/CheckBox.java | 6 +- .../ui/Components/CheckBoxSquare.java | 6 +- .../ui/Components/ClippingImageView.java | 11 +- .../org/telegram/ui/Components/EmojiView.java | 158 +- .../ui/Components/EmptyTextProgressView.java | 5 + .../Components/ExtendedGridLayoutManager.java | 225 + .../ui/Components/FlowLayoutManager.java | 529 - .../ui/Components/FrameLayoutFixed.java | 163 - .../ui/Components/NumberTextView.java | 11 +- .../telegram/ui/Components/PasscodeView.java | 274 +- .../ui/Components/PhotoFilterView.java | 37 +- .../PhotoViewerCaptionEnterView.java | 31 +- .../telegram/ui/Components/PlayerView.java | 44 +- .../ui/Components/ProgressCircleView.java | 28 + .../telegram/ui/Components/RadioButton.java | 21 +- .../ui/Components/RecyclerListView.java | 5 + .../ui/Components/SeekBarWaveform.java | 2 +- .../telegram/ui/Components/ShareAlert.java | 149 +- .../SizeNotifierFrameLayoutPhoto.java | 4 +- .../telegram/ui/Components/StickersAlert.java | 141 +- .../org/telegram/ui/Components/Switch.java | 16 +- .../ui/Components/TypingDotsDrawable.java | 20 +- .../ui/Components/URLSpanUserMention.java | 27 + .../telegram/ui/Components/VideoPlayer.java | 353 + .../ui/Components/VideoTimelineView.java | 7 +- .../ui/Components/WebFrameLayout.java | 22 +- .../org/telegram/ui/ContactsActivity.java | 18 +- .../telegram/ui/CountrySelectActivity.java | 15 +- .../java/org/telegram/ui/DialogsActivity.java | 109 +- .../telegram/ui/DocumentSelectActivity.java | 28 +- .../org/telegram/ui/GroupCreateActivity.java | 23 +- .../telegram/ui/GroupCreateFinalActivity.java | 3 +- .../org/telegram/ui/GroupInviteActivity.java | 12 +- .../java/org/telegram/ui/IntroActivity.java | 1 - .../java/org/telegram/ui/LaunchActivity.java | 144 +- .../java/org/telegram/ui/LoginActivity.java | 61 +- .../java/org/telegram/ui/MediaActivity.java | 33 +- .../ui/NotificationsSettingsActivity.java | 1 - .../org/telegram/ui/PasscodeActivity.java | 37 +- .../telegram/ui/PhotoAlbumPickerActivity.java | 5 - .../org/telegram/ui/PhotoPickerActivity.java | 17 +- .../java/org/telegram/ui/PhotoViewer.java | 1034 +- .../ui/PopupNotificationActivity.java | 49 +- .../org/telegram/ui/PrivacyUsersActivity.java | 5 +- .../java/org/telegram/ui/ProfileActivity.java | 251 +- .../ui/ProfileNotificationsActivity.java | 2 +- .../org/telegram/ui/SecretPhotoViewer.java | 4 +- .../org/telegram/ui/SettingsActivity.java | 169 +- .../org/telegram/ui/StickerPreviewViewer.java | 3 + .../org/telegram/ui/StickersActivity.java | 13 +- .../ui/TwoStepVerificationActivity.java | 35 +- .../org/telegram/ui/WallpapersActivity.java | 9 +- .../src/main/res/drawable-hdpi/book_bot.png | Bin 0 -> 3825 bytes .../main/res/drawable-hdpi/book_channel.png | Bin 0 -> 3110 bytes .../src/main/res/drawable-hdpi/book_group.png | Bin 0 -> 3563 bytes .../src/main/res/drawable-hdpi/book_logo.png | Bin 0 -> 1474 bytes .../src/main/res/drawable-hdpi/book_user.png | Bin 0 -> 3161 bytes .../main/res/drawable-hdpi/chat_badge.9.png | Bin 0 -> 1411 bytes .../res/drawable-hdpi/inline_video_pause.png | Bin 0 -> 991 bytes .../res/drawable-hdpi/inline_video_play.png | Bin 0 -> 1204 bytes .../main/res/drawable-hdpi/instant_camera.png | Bin 0 -> 2450 bytes .../src/main/res/drawable-hdpi/pagedown.png | Bin 2270 -> 2928 bytes .../src/main/res/drawable-hdpi/scroll_tip.png | Bin 0 -> 1139 bytes .../main/res/drawable-hdpi/search_down.png | Bin 1139 -> 1250 bytes .../src/main/res/drawable-hdpi/search_up.png | Bin 1123 -> 1217 bytes .../src/main/res/drawable-mdpi/boog_group.png | Bin 0 -> 2553 bytes .../src/main/res/drawable-mdpi/book_bot.png | Bin 0 -> 2826 bytes .../main/res/drawable-mdpi/book_channel.png | Bin 0 -> 2301 bytes .../src/main/res/drawable-mdpi/book_logo.png | Bin 0 -> 1279 bytes .../src/main/res/drawable-mdpi/book_user.png | Bin 0 -> 2303 bytes .../main/res/drawable-mdpi/chat_badge.9.png | Bin 0 -> 1246 bytes .../res/drawable-mdpi/inline_video_pause.png | Bin 0 -> 977 bytes .../res/drawable-mdpi/inline_video_play.png | Bin 0 -> 1043 bytes .../main/res/drawable-mdpi/instant_camera.png | Bin 0 -> 1639 bytes .../src/main/res/drawable-mdpi/pagedown.png | Bin 1960 -> 2096 bytes .../src/main/res/drawable-mdpi/scroll_tip.png | Bin 0 -> 1112 bytes .../main/res/drawable-mdpi/search_down.png | Bin 982 -> 1060 bytes .../src/main/res/drawable-mdpi/search_up.png | Bin 976 -> 1040 bytes .../src/main/res/drawable-xhdpi/book_bot.png | Bin 0 -> 4759 bytes .../main/res/drawable-xhdpi/book_channel.png | Bin 0 -> 3787 bytes .../main/res/drawable-xhdpi/book_group.png | Bin 0 -> 4390 bytes .../src/main/res/drawable-xhdpi/book_logo.png | Bin 0 -> 1635 bytes .../src/main/res/drawable-xhdpi/book_user.png | Bin 0 -> 3972 bytes .../main/res/drawable-xhdpi/chat_badge.9.png | Bin 0 -> 1627 bytes .../res/drawable-xhdpi/inline_video_pause.png | Bin 0 -> 1023 bytes .../res/drawable-xhdpi/inline_video_play.png | Bin 0 -> 1322 bytes .../res/drawable-xhdpi/instant_camera.png | Bin 0 -> 2883 bytes .../src/main/res/drawable-xhdpi/pagedown.png | Bin 3058 -> 3829 bytes .../main/res/drawable-xhdpi/scroll_tip.png | Bin 0 -> 1316 bytes .../main/res/drawable-xhdpi/search_down.png | Bin 1054 -> 1154 bytes .../src/main/res/drawable-xhdpi/search_up.png | Bin 1058 -> 1106 bytes .../src/main/res/drawable-xxhdpi/book_bot.png | Bin 0 -> 6717 bytes .../main/res/drawable-xxhdpi/book_channel.png | Bin 0 -> 5326 bytes .../main/res/drawable-xxhdpi/book_group.png | Bin 0 -> 6359 bytes .../main/res/drawable-xxhdpi/book_logo.png | Bin 0 -> 2056 bytes .../main/res/drawable-xxhdpi/book_user.png | Bin 0 -> 5470 bytes .../main/res/drawable-xxhdpi/chat_badge.9.png | Bin 0 -> 2070 bytes .../drawable-xxhdpi/inline_video_pause.png | Bin 0 -> 1056 bytes .../res/drawable-xxhdpi/inline_video_play.png | Bin 0 -> 1518 bytes .../res/drawable-xxhdpi/instant_camera.png | Bin 0 -> 4196 bytes .../src/main/res/drawable-xxhdpi/pagedown.png | Bin 3919 -> 5928 bytes .../main/res/drawable-xxhdpi/scroll_tip.png | Bin 0 -> 1531 bytes .../main/res/drawable-xxhdpi/search_down.png | Bin 1136 -> 1201 bytes .../main/res/drawable-xxhdpi/search_up.png | Bin 1125 -> 1151 bytes .../main/res/drawable-xxxhdpi/book_logo.png | Bin 0 -> 2434 bytes .../src/main/res/drawable/ic_player.png | Bin 0 -> 2047 bytes .../src/main/res/values-ar/strings.xml | 41 +- .../src/main/res/values-de/strings.xml | 34 +- .../src/main/res/values-es/strings.xml | 77 +- .../src/main/res/values-it/strings.xml | 105 +- .../src/main/res/values-ko/strings.xml | 41 +- .../src/main/res/values-nl/strings.xml | 69 +- .../src/main/res/values-pt-rBR/strings.xml | 69 +- .../src/main/res/values-pt-rPT/strings.xml | 69 +- TMessagesProj/src/main/res/values/strings.xml | 43 +- .../src/main/res/xml/automotive_app_desc.xml | 1 + 548 files changed, 90181 insertions(+), 26593 deletions(-) create mode 100755 TMessagesProj/jni/SqliteWrapper.cpp delete mode 100755 TMessagesProj/jni/sqlite.c delete mode 100755 TMessagesProj/jni/sqlite.h delete mode 100755 TMessagesProj/jni/sqlite_cursor.c delete mode 100755 TMessagesProj/jni/sqlite_database.c delete mode 100755 TMessagesProj/jni/sqlite_statement.c delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/Animation/Animator10.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorListenerAdapter10.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorSet10.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatKeyframeSet.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntKeyframeSet.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/Animation/Keyframe.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/Animation/KeyframeSet.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/Animation/ObjectAnimator10.java delete mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/Animation/Property.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/Animation/PropertyValuesHolder.java delete mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/Animation/ReflectiveProperty.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/Animation/ValueAnimator.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/Animation/View10.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorListenerAdapterProxy.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorSetProxy.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ObjectAnimatorProxy.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ViewProxy.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/AnimatorListenerAdapterProxy.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/DialogObject.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/NetworkAlarm.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/NotificationDismissReceiver.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/OpenChatReceiver.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/AspectRatioFrameLayout.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/BehindLiveWindowException.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/C.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/CodecCounters.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/CryptoInfo.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DecoderInfo.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DefaultLoadControl.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DummyTrackRenderer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlaybackException.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerImpl.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerImplInternal.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerLibraryInfo.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/FrameworkSampleSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/LoadControl.java rename TMessagesProj/src/main/java/org/telegram/messenger/{Animation/IntEvaluator.java => exoplayer/MediaClock.java} (61%) mode change 100644 => 100755 create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecAudioTrackRenderer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecSelector.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecTrackRenderer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecVideoTrackRenderer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaFormat.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaFormatHolder.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ParserException.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleHolder.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSourceTrackRenderer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SingleSampleSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/StandaloneMediaClock.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/TimeRange.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/TrackRenderer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/VideoFrameReleaseTimeHelper.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioCapabilities.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioCapabilitiesReceiver.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioTrack.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/BaseChunkSampleSourceEventListener.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/BaseMediaChunk.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Chunk.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkExtractorWrapper.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkOperationHolder.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkSampleSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ContainerMediaChunk.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/DataChunk.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Format.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/FormatEvaluator.java rename TMessagesProj/src/main/java/org/telegram/messenger/{Animation/FloatEvaluator.java => exoplayer/chunk/FormatWrapper.java} (60%) mode change 100644 => 100755 create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/InitializationChunk.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/MediaChunk.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/SingleSampleMediaChunk.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/VideoFormatSelectorUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/DrmInitData.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/DrmSessionManager.java rename TMessagesProj/src/main/java/org/telegram/messenger/{Animation/TypeEvaluator.java => exoplayer/drm/KeysExpiredException.java} (71%) mode change 100644 => 100755 create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/MediaDrmCallback.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/StreamingDrmSessionManager.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/UnsupportedDrmException.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ChunkIndex.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DefaultExtractorInput.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DefaultTrackOutput.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DummyTrackOutput.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/Extractor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorInput.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorOutput.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorSampleSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/GaplessInfo.java rename TMessagesProj/src/main/java/org/telegram/messenger/{Animation/NoSuchPropertyException.java => exoplayer/extractor/PositionHolder.java} (69%) create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/RollingSampleBuffer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/SeekMap.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/TrackOutput.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/AudioTagPayloadReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/FlvExtractor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/ScriptTagPayloadReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/TagPayloadReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/VideoTagPayloadReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/ConstantBitrateSeeker.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/Id3Util.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/Mp3Extractor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/VbriSeeker.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/XingSeeker.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Atom.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/AtomParsers.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/DefaultSampleValues.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/FixedSampleSizeRechunker.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/FragmentedMp4Extractor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Mp4Extractor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/PsshAtomUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Sniffer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Track.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackEncryptionBox.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackFragment.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackSampleTable.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/FlacReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggExtractor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggParser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggSeeker.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/StreamReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisBitArray.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/Ac3Reader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/AdtsExtractor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/AdtsReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/DtsReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/ElementaryStreamReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H262Reader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H264Reader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H265Reader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/Id3Reader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/MpegAudioReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/NalUnitTargetBuffer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/PsExtractor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/PtsTimestampAdjuster.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/SeiReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/TsExtractor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavExtractor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavHeader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavHeaderReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/DefaultEbmlReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/EbmlReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/EbmlReaderOutput.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/Sniffer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/VarintReader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/WebmExtractor.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/Aes128DataSource.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/DefaultHlsTrackSelector.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsChunkSource.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsExtractorWrapper.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsMasterPlaylist.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsMediaPlaylist.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsParserUtil.java rename TMessagesProj/src/main/java/org/telegram/messenger/{Animation/FloatProperty10.java => exoplayer/hls/HlsPlaylist.java} (57%) mode change 100755 => 100644 create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsPlaylistParser.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsSampleSource.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsTrackSelector.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/PtsTimestampAdjusterProvider.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/TsChunk.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/Variant.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/WebvttExtractor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/MetadataParser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/MetadataTrackRenderer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/ApicFrame.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/BinaryFrame.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/GeobFrame.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/Id3Frame.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/Id3Parser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/PrivFrame.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/TextInformationFrame.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/TxxxFrame.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/DefaultSmoothStreamingTrackSelector.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingManifest.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingTrackSelector.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/CaptionStyleCompat.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Cue.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/PlayableSubtitle.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Subtitle.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParserHelper.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextRenderer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextTrackRenderer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaption.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionCtrl.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionList.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionText.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/Eia608Parser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/Eia608TrackRenderer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/subrip/SubripParser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/subrip/SubripSubtitle.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlColorParser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlNode.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlParser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlRegion.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlRenderUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlStyle.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlSubtitle.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/tx3g/Tx3gParser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/tx3g/Tx3gSubtitle.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/Mp4WebvttParser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/Mp4WebvttSubtitle.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttCue.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttCueParser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttParser.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttParserUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttSubtitle.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Allocation.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Allocator.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/AssetDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/BandwidthMeter.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ByteArrayDataSink.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ByteArrayDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ContentDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSink.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSourceInputStream.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSpec.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultAllocator.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultBandwidthMeter.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultHttpDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultUriDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/FileDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/HttpDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Loader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/NetworkLock.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/PriorityDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/TeeDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/TransferListener.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UdpDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UriDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UriLoadable.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/Cache.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheDataSink.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheDataSource.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheEvictor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheSpan.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/LeastRecentlyUsedCacheEvictor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/NoOpCacheEvictor.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/SimpleCache.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Ac3Util.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Assertions.java rename TMessagesProj/src/main/java/org/telegram/messenger/{Animation/IntProperty.java => exoplayer/util/Clock.java} (57%) create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/CodecSpecificDataUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DebugTextViewHelper.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DtsUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacSeekTable.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacStreamInfo.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/LongArray.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ManifestFetcher.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/MimeTypes.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/MpegAudioHeader.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/NalUnitUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParsableBitArray.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParsableByteArray.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParserUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PlayerControl.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Predicate.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PriorityHandlerThread.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SlidingPercentile.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SystemClock.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/TraceUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/UriUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Util.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/VerboseLogUtil.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/Buffer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/Decoder.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/InputBuffer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/OutputBuffer.java create mode 100755 TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/SimpleDecoder.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/query/DraftQuery.java create mode 100644 TMessagesProj/src/main/java/org/telegram/messenger/query/SearchQuery.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/ui/Adapters/ChatActivityAdapter.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatBaseCell.java create mode 100644 TMessagesProj/src/main/java/org/telegram/ui/Cells/HintDialogCell.java create mode 100644 TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachView.java create mode 100644 TMessagesProj/src/main/java/org/telegram/ui/Components/ExtendedGridLayoutManager.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/ui/Components/FlowLayoutManager.java delete mode 100644 TMessagesProj/src/main/java/org/telegram/ui/Components/FrameLayoutFixed.java create mode 100644 TMessagesProj/src/main/java/org/telegram/ui/Components/ProgressCircleView.java create mode 100644 TMessagesProj/src/main/java/org/telegram/ui/Components/URLSpanUserMention.java create mode 100644 TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java create mode 100755 TMessagesProj/src/main/res/drawable-hdpi/book_bot.png create mode 100755 TMessagesProj/src/main/res/drawable-hdpi/book_channel.png create mode 100755 TMessagesProj/src/main/res/drawable-hdpi/book_group.png create mode 100755 TMessagesProj/src/main/res/drawable-hdpi/book_logo.png create mode 100755 TMessagesProj/src/main/res/drawable-hdpi/book_user.png create mode 100755 TMessagesProj/src/main/res/drawable-hdpi/chat_badge.9.png create mode 100644 TMessagesProj/src/main/res/drawable-hdpi/inline_video_pause.png create mode 100644 TMessagesProj/src/main/res/drawable-hdpi/inline_video_play.png create mode 100755 TMessagesProj/src/main/res/drawable-hdpi/instant_camera.png mode change 100644 => 100755 TMessagesProj/src/main/res/drawable-hdpi/pagedown.png create mode 100755 TMessagesProj/src/main/res/drawable-hdpi/scroll_tip.png mode change 100755 => 100644 TMessagesProj/src/main/res/drawable-hdpi/search_down.png mode change 100755 => 100644 TMessagesProj/src/main/res/drawable-hdpi/search_up.png create mode 100755 TMessagesProj/src/main/res/drawable-mdpi/boog_group.png create mode 100755 TMessagesProj/src/main/res/drawable-mdpi/book_bot.png create mode 100755 TMessagesProj/src/main/res/drawable-mdpi/book_channel.png create mode 100755 TMessagesProj/src/main/res/drawable-mdpi/book_logo.png create mode 100755 TMessagesProj/src/main/res/drawable-mdpi/book_user.png create mode 100755 TMessagesProj/src/main/res/drawable-mdpi/chat_badge.9.png create mode 100644 TMessagesProj/src/main/res/drawable-mdpi/inline_video_pause.png create mode 100644 TMessagesProj/src/main/res/drawable-mdpi/inline_video_play.png create mode 100755 TMessagesProj/src/main/res/drawable-mdpi/instant_camera.png mode change 100644 => 100755 TMessagesProj/src/main/res/drawable-mdpi/pagedown.png create mode 100755 TMessagesProj/src/main/res/drawable-mdpi/scroll_tip.png mode change 100755 => 100644 TMessagesProj/src/main/res/drawable-mdpi/search_down.png mode change 100755 => 100644 TMessagesProj/src/main/res/drawable-mdpi/search_up.png create mode 100755 TMessagesProj/src/main/res/drawable-xhdpi/book_bot.png create mode 100755 TMessagesProj/src/main/res/drawable-xhdpi/book_channel.png create mode 100755 TMessagesProj/src/main/res/drawable-xhdpi/book_group.png create mode 100755 TMessagesProj/src/main/res/drawable-xhdpi/book_logo.png create mode 100755 TMessagesProj/src/main/res/drawable-xhdpi/book_user.png create mode 100755 TMessagesProj/src/main/res/drawable-xhdpi/chat_badge.9.png create mode 100644 TMessagesProj/src/main/res/drawable-xhdpi/inline_video_pause.png create mode 100644 TMessagesProj/src/main/res/drawable-xhdpi/inline_video_play.png create mode 100755 TMessagesProj/src/main/res/drawable-xhdpi/instant_camera.png mode change 100644 => 100755 TMessagesProj/src/main/res/drawable-xhdpi/pagedown.png create mode 100755 TMessagesProj/src/main/res/drawable-xhdpi/scroll_tip.png mode change 100755 => 100644 TMessagesProj/src/main/res/drawable-xhdpi/search_down.png mode change 100755 => 100644 TMessagesProj/src/main/res/drawable-xhdpi/search_up.png create mode 100755 TMessagesProj/src/main/res/drawable-xxhdpi/book_bot.png create mode 100755 TMessagesProj/src/main/res/drawable-xxhdpi/book_channel.png create mode 100755 TMessagesProj/src/main/res/drawable-xxhdpi/book_group.png create mode 100755 TMessagesProj/src/main/res/drawable-xxhdpi/book_logo.png create mode 100755 TMessagesProj/src/main/res/drawable-xxhdpi/book_user.png create mode 100755 TMessagesProj/src/main/res/drawable-xxhdpi/chat_badge.9.png create mode 100644 TMessagesProj/src/main/res/drawable-xxhdpi/inline_video_pause.png create mode 100644 TMessagesProj/src/main/res/drawable-xxhdpi/inline_video_play.png create mode 100755 TMessagesProj/src/main/res/drawable-xxhdpi/instant_camera.png mode change 100644 => 100755 TMessagesProj/src/main/res/drawable-xxhdpi/pagedown.png create mode 100755 TMessagesProj/src/main/res/drawable-xxhdpi/scroll_tip.png mode change 100755 => 100644 TMessagesProj/src/main/res/drawable-xxhdpi/search_down.png mode change 100755 => 100644 TMessagesProj/src/main/res/drawable-xxhdpi/search_up.png create mode 100644 TMessagesProj/src/main/res/drawable-xxxhdpi/book_logo.png create mode 100644 TMessagesProj/src/main/res/drawable/ic_player.png diff --git a/.gitignore b/.gitignore index 86bf39ae3..cd7a643f0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ local.properties *.keystore TMessagesProj/libs/ +TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java diff --git a/TMessagesProj/build.gradle b/TMessagesProj/build.gradle index e18be3a6f..6ad05bc7b 100644 --- a/TMessagesProj/build.gradle +++ b/TMessagesProj/build.gradle @@ -24,8 +24,8 @@ tasks.withType(JavaCompile) { } dependencies { - compile 'com.android.support:support-v4:23.3.0' - compile 'com.googlecode.mp4parser:isoparser:1.0.+' + compile 'com.android.support:support-v4:23.4.0' + compile 'com.googlecode.mp4parser:isoparser:1.0.6' compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' compile fileTree(dir: "$buildDir/native-libs", include: 'native-libs.jar') } @@ -78,7 +78,7 @@ android { } } - defaultConfig.versionCode = 12 + defaultConfig.versionCode = 13 sourceSets.main { jniLibs.srcDir 'libs' @@ -127,9 +127,9 @@ android { } defaultConfig { - minSdkVersion 10 + minSdkVersion 14 targetSdkVersion 23 - versionName "0.1.2" + versionName "0.1.3" } } diff --git a/TMessagesProj/config/debug/AndroidManifest.xml b/TMessagesProj/config/debug/AndroidManifest.xml index d3a816c5f..f2488c9e7 100644 --- a/TMessagesProj/config/debug/AndroidManifest.xml +++ b/TMessagesProj/config/debug/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -18,7 +19,6 @@ android:name=".ApplicationLoader" android:hardwareAccelerated="@bool/useHardwareAcceleration" android:largeHeap="true"> - diff --git a/TMessagesProj/config/release/AndroidManifest.xml b/TMessagesProj/config/release/AndroidManifest.xml index 470ea3979..5f552a6fe 100644 --- a/TMessagesProj/config/release/AndroidManifest.xml +++ b/TMessagesProj/config/release/AndroidManifest.xml @@ -1,5 +1,6 @@ diff --git a/TMessagesProj/jni/Android.mk b/TMessagesProj/jni/Android.mk index 636a591d9..5ac98fe8c 100644 --- a/TMessagesProj/jni/Android.mk +++ b/TMessagesProj/jni/Android.mk @@ -997,7 +997,7 @@ include $(BUILD_STATIC_LIBRARY) include $(CLEAR_VARS) LOCAL_PRELINK_MODULE := false -LOCAL_MODULE := tmessages.21 +LOCAL_MODULE := tmessages.22 LOCAL_CFLAGS := -w -std=c11 -Os -DNULL=0 -DSOCKLEN_T=socklen_t -DLOCALE_NOT_USED -D_LARGEFILE_SOURCE=1 -D_FILE_OFFSET_BITS=64 LOCAL_CFLAGS += -Drestrict='' -D__EMX__ -DOPUS_BUILD -DFIXED_POINT -DUSE_ALLOCA -DHAVE_LRINT -DHAVE_LRINTF -fno-math-errno LOCAL_CFLAGS += -DANDROID_NDK -DDISABLE_IMPORTGL -fno-strict-aliasing -fprefetch-loop-arrays -DAVOID_TABLES -DANDROID_TILE_BASED_DECODE -DANDROID_ARMV6_IDCT -ffast-math -D__STDC_CONSTANT_MACROS @@ -1027,6 +1027,8 @@ else else ifeq ($(TARGET_ARCH_ABI),x86) + LOCAL_CPPFLAGS += -Dx86fix + LOCAL_CFLAGS += -Dx86fix LOCAL_ARM_MODE := arm LOCAL_SRC_FILE += \ ./libyuv/source/row_x86.asm @@ -1283,15 +1285,12 @@ endif LOCAL_SRC_FILES += \ ./jni.c \ -./sqlite_cursor.c \ -./sqlite_database.c \ -./sqlite_statement.c \ -./sqlite.c \ ./audio.c \ ./utils.c \ ./image.c \ ./video.c \ ./gifvideo.cpp \ +./SqliteWrapper.cpp \ ./TgNetWrapper.cpp \ ./NativeLoader.cpp diff --git a/TMessagesProj/jni/SqliteWrapper.cpp b/TMessagesProj/jni/SqliteWrapper.cpp new file mode 100755 index 000000000..67cf62315 --- /dev/null +++ b/TMessagesProj/jni/SqliteWrapper.cpp @@ -0,0 +1,241 @@ +#include "sqlite/sqlite3.h" +#include "tgnet/NativeByteBuffer.h" +#include "tgnet/BuffersStorage.h" + +void throw_sqlite3_exception(JNIEnv *env, sqlite3 *handle, int errcode) { + if (SQLITE_OK == errcode) { + errcode = sqlite3_errcode(handle); + } + const char *errmsg = sqlite3_errmsg(handle); + jclass exClass = env->FindClass("org/telegram/SQLite/SQLiteException"); + env->ThrowNew(exClass, errmsg); +} + +extern "C" { + +int Java_org_telegram_SQLite_SQLitePreparedStatement_step(JNIEnv *env, jobject object, int statementHandle) { + sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; + + int errcode = sqlite3_step(handle); + if (errcode == SQLITE_ROW) { + return 0; + } else if(errcode == SQLITE_DONE) { + return 1; + } else if(errcode == SQLITE_BUSY) { + return -1; + } + throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); +} + +int Java_org_telegram_SQLite_SQLitePreparedStatement_prepare(JNIEnv *env, jobject object, int sqliteHandle, jstring sql) { + sqlite3 *handle = (sqlite3 *) sqliteHandle; + + char const *sqlStr = env->GetStringUTFChars(sql, 0); + + sqlite3_stmt *stmt_handle; + + int errcode = sqlite3_prepare_v2(handle, sqlStr, -1, &stmt_handle, 0); + if (SQLITE_OK != errcode) { + throw_sqlite3_exception(env, handle, errcode); + } + + if (sqlStr != 0) { + env->ReleaseStringUTFChars(sql, sqlStr); + } + + return (int) stmt_handle; +} + +void Java_org_telegram_SQLite_SQLitePreparedStatement_reset(JNIEnv *env, jobject object, int statementHandle) { + sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; + + int errcode = sqlite3_reset(handle); + if (SQLITE_OK != errcode) { + throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); + } +} + +void Java_org_telegram_SQLite_SQLitePreparedStatement_finalize(JNIEnv *env, jobject object, int statementHandle) { + sqlite3_finalize((sqlite3_stmt *) statementHandle); +} + +void Java_org_telegram_SQLite_SQLitePreparedStatement_bindByteBuffer(JNIEnv *env, jobject object, int statementHandle, int index, jobject value, int length) { + sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; + void *buf = env->GetDirectBufferAddress(value); + + int errcode = sqlite3_bind_blob(handle, index, buf, length, SQLITE_STATIC); + if (SQLITE_OK != errcode) { + throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); + } +} + +void Java_org_telegram_SQLite_SQLitePreparedStatement_bindString(JNIEnv *env, jobject object, int statementHandle, int index, jstring value) { + sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; + + char const *valueStr = env->GetStringUTFChars(value, 0); + + int errcode = sqlite3_bind_text(handle, index, valueStr, -1, SQLITE_TRANSIENT); + if (SQLITE_OK != errcode) { + throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); + } + + if (valueStr != 0) { + env->ReleaseStringUTFChars(value, valueStr); + } +} + +void Java_org_telegram_SQLite_SQLitePreparedStatement_bindInt(JNIEnv *env, jobject object, int statementHandle, int index, int value) { + sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; + + int errcode = sqlite3_bind_int(handle, index, value); + if (SQLITE_OK != errcode) { + throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); + } +} + +void Java_org_telegram_SQLite_SQLitePreparedStatement_bindLong(JNIEnv *env, jobject object, int statementHandle, int index, long long value) { + sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; + + int errcode = sqlite3_bind_int64(handle, index, value); + if (SQLITE_OK != errcode) { + throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); + } +} + +void Java_org_telegram_SQLite_SQLitePreparedStatement_bindDouble(JNIEnv *env, jobject object, int statementHandle, int index, double value) { + sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; + + int errcode = sqlite3_bind_double(handle, index, value); + if (SQLITE_OK != errcode) { + throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); + } +} + +void Java_org_telegram_SQLite_SQLitePreparedStatement_bindNull(JNIEnv *env, jobject object, int statementHandle, int index) { + sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; + + int errcode = sqlite3_bind_null(handle, index); + if (SQLITE_OK != errcode) { + throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); + } +} + +void Java_org_telegram_SQLite_SQLiteDatabase_closedb(JNIEnv *env, jobject object, int sqliteHandle) { + sqlite3 *handle = (sqlite3 *)sqliteHandle; + int err = sqlite3_close(handle); + if (SQLITE_OK != err) { + throw_sqlite3_exception(env, handle, err); + } +} + +void Java_org_telegram_SQLite_SQLiteDatabase_beginTransaction(JNIEnv *env, jobject object, int sqliteHandle) { + sqlite3 *handle = (sqlite3 *)sqliteHandle; + sqlite3_exec(handle, "BEGIN", 0, 0, 0); +} + +void Java_org_telegram_SQLite_SQLiteDatabase_commitTransaction(JNIEnv *env, jobject object, int sqliteHandle) { + sqlite3 *handle = (sqlite3 *)sqliteHandle; + sqlite3_exec(handle, "COMMIT", 0, 0, 0); +} + +int Java_org_telegram_SQLite_SQLiteDatabase_opendb(JNIEnv *env, jobject object, jstring fileName, jstring tempDir) { + char const *fileNameStr = env->GetStringUTFChars(fileName, 0); + char const *tempDirStr = env->GetStringUTFChars(tempDir, 0); + + if (sqlite3_temp_directory != 0) { + sqlite3_free(sqlite3_temp_directory); + } + sqlite3_temp_directory = sqlite3_mprintf("%s", tempDirStr); + + sqlite3 *handle = 0; + int err = sqlite3_open(fileNameStr, &handle); + if (SQLITE_OK != err) { + throw_sqlite3_exception(env, handle, err); + } + if (fileNameStr != 0) { + env->ReleaseStringUTFChars(fileName, fileNameStr); + } + if (tempDirStr != 0) { + env->ReleaseStringUTFChars(tempDir, tempDirStr); + } + return (int)handle; +} + +int Java_org_telegram_SQLite_SQLiteCursor_columnType(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { + sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; + return sqlite3_column_type(handle, columnIndex); +} + +int Java_org_telegram_SQLite_SQLiteCursor_columnIsNull(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { + sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; + int valType = sqlite3_column_type(handle, columnIndex); + return SQLITE_NULL == valType; +} + +int Java_org_telegram_SQLite_SQLiteCursor_columnIntValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { + sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; + int valType = sqlite3_column_type(handle, columnIndex); + if (SQLITE_NULL == valType) { + return 0; + } + return sqlite3_column_int(handle, columnIndex); +} + +long long Java_org_telegram_SQLite_SQLiteCursor_columnLongValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { + sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; + int valType = sqlite3_column_type(handle, columnIndex); + if (SQLITE_NULL == valType) { + return 0; + } + return sqlite3_column_int64(handle, columnIndex); +} + +double Java_org_telegram_SQLite_SQLiteCursor_columnDoubleValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { + sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; + int valType = sqlite3_column_type(handle, columnIndex); + if (SQLITE_NULL == valType) { + return 0; + } + return sqlite3_column_double(handle, columnIndex); +} + +jstring Java_org_telegram_SQLite_SQLiteCursor_columnStringValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { + sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; + const char *str = (const char *) sqlite3_column_text(handle, columnIndex); + if (str != 0) { + return env->NewStringUTF(str); + } + return 0; +} + +jbyteArray Java_org_telegram_SQLite_SQLiteCursor_columnByteArrayValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { + sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; + const jbyte *buf = (const jbyte *) sqlite3_column_blob(handle, columnIndex); + int length = sqlite3_column_bytes(handle, columnIndex); + if (buf != nullptr && length > 0) { + jbyteArray result = env->NewByteArray(length); + env->SetByteArrayRegion(result, 0, length, buf); + return result; + } + return nullptr; +} + +int Java_org_telegram_SQLite_SQLiteCursor_columnByteBufferValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { + sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; + int length = sqlite3_column_bytes(handle, columnIndex); + if (length <= 0) { + return 0; + } + NativeByteBuffer *buffer = BuffersStorage::getInstance().getFreeBuffer(length); + if (buffer == nullptr) { + return 0; + } + const char *buf = (const char *) sqlite3_column_blob(handle, columnIndex); + if (buf == nullptr) { + return 0; + } + memcpy(buffer->bytes(), buf, length); + return (int) buffer; +} + +} diff --git a/TMessagesProj/jni/jni.c b/TMessagesProj/jni/jni.c index 711b91c90..1589a61e9 100644 --- a/TMessagesProj/jni/jni.c +++ b/TMessagesProj/jni/jni.c @@ -7,7 +7,6 @@ #include #include #include "utils.h" -#include "sqlite.h" #include "image.h" int registerNativeTgNetFunctions(JavaVM *vm, JNIEnv *env); @@ -21,10 +20,6 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { return -1; } - if (sqliteOnJNILoad(vm, reserved, env) == -1) { - return -1; - } - if (imageOnJNILoad(vm, reserved, env) == -1) { return -1; } diff --git a/TMessagesProj/jni/libyuv/include/libyuv/compare_row.h b/TMessagesProj/jni/libyuv/include/libyuv/compare_row.h index 4562da047..38a957b2c 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/compare_row.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/compare_row.h @@ -22,6 +22,12 @@ extern "C" { (defined(__i386__) && !defined(__SSE2__)) #define LIBYUV_DISABLE_X86 #endif +// MemorySanitizer does not support assembly code yet. http://crbug.com/344505 +#if defined(__has_feature) +#if __has_feature(memory_sanitizer) +#define LIBYUV_DISABLE_X86 +#endif +#endif // Visual C 2012 required for AVX2. #if defined(_M_IX86) && !defined(__clang__) && \ @@ -36,7 +42,8 @@ extern "C" { #endif // clang >= 3.4 #endif // __clang__ -#if defined(_M_IX86) && (defined(VISUALC_HAS_AVX2) || defined(CLANG_HAS_AVX2)) +#if !defined(LIBYUV_DISABLE_X86) && \ + defined(_M_IX86) && (defined(VISUALC_HAS_AVX2) || defined(CLANG_HAS_AVX2)) #define HAS_HASHDJB2_AVX2 #endif diff --git a/TMessagesProj/jni/libyuv/include/libyuv/convert.h b/TMessagesProj/jni/libyuv/include/libyuv/convert.h index a8d3fa07a..a2cdc5718 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/convert.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/convert.h @@ -12,10 +12,8 @@ #define INCLUDE_LIBYUV_CONVERT_H_ #include "libyuv/basic_types.h" -// TODO(fbarchard): Remove the following headers includes. -#include "libyuv/convert_from.h" -#include "libyuv/planar_functions.h" -#include "libyuv/rotate.h" + +#include "libyuv/rotate.h" // For enum RotationMode. #ifdef __cplusplus namespace libyuv { diff --git a/TMessagesProj/jni/libyuv/include/libyuv/convert_argb.h b/TMessagesProj/jni/libyuv/include/libyuv/convert_argb.h index ce4e3d075..079d273b1 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/convert_argb.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/convert_argb.h @@ -12,10 +12,8 @@ #define INCLUDE_LIBYUV_CONVERT_ARGB_H_ #include "libyuv/basic_types.h" -// TODO(fbarchard): Remove the following headers includes -#include "libyuv/convert_from.h" -#include "libyuv/planar_functions.h" -#include "libyuv/rotate.h" + +#include "libyuv/rotate.h" // For enum RotationMode. // TODO(fbarchard): This set of functions should exactly match convert.h // TODO(fbarchard): Add tests. Create random content of right size and convert diff --git a/TMessagesProj/jni/libyuv/include/libyuv/convert_from.h b/TMessagesProj/jni/libyuv/include/libyuv/convert_from.h index 9fd8d4de5..39e1578a0 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/convert_from.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/convert_from.h @@ -56,8 +56,6 @@ int I400Copy(const uint8* src_y, int src_stride_y, uint8* dst_y, int dst_stride_y, int width, int height); -// TODO(fbarchard): I420ToM420 - LIBYUV_API int I420ToNV12(const uint8* src_y, int src_stride_y, const uint8* src_u, int src_stride_u, diff --git a/TMessagesProj/jni/libyuv/include/libyuv/cpu_id.h b/TMessagesProj/jni/libyuv/include/libyuv/cpu_id.h index a83edd501..dfb7445e2 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/cpu_id.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/cpu_id.h @@ -41,7 +41,7 @@ static const int kCpuHasAVX3 = 0x2000; // These flags are only valid on MIPS processors. static const int kCpuHasMIPS = 0x10000; -static const int kCpuHasMIPS_DSPR2 = 0x20000; +static const int kCpuHasDSPR2 = 0x20000; // Internal function used to auto-init. LIBYUV_API @@ -62,7 +62,7 @@ static __inline int TestCpuFlag(int test_flag) { // For testing, allow CPU flags to be disabled. // ie MaskCpuFlags(~kCpuHasSSSE3) to disable SSSE3. // MaskCpuFlags(-1) to enable all cpu specific optimizations. -// MaskCpuFlags(0) to disable all cpu specific optimizations. +// MaskCpuFlags(1) to disable all cpu specific optimizations. LIBYUV_API void MaskCpuFlags(int enable_flags); diff --git a/TMessagesProj/jni/libyuv/include/libyuv/planar_functions.h b/TMessagesProj/jni/libyuv/include/libyuv/planar_functions.h index 9d30225d4..881b0c5c6 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/planar_functions.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/planar_functions.h @@ -288,6 +288,12 @@ int ARGBCopyAlpha(const uint8* src_argb, int src_stride_argb, uint8* dst_argb, int dst_stride_argb, int width, int height); +// Extract the alpha channel from ARGB. +LIBYUV_API +int ARGBExtractAlpha(const uint8* src_argb, int src_stride_argb, + uint8* dst_a, int dst_stride_a, + int width, int height); + // Copy Y channel to Alpha of ARGB. LIBYUV_API int ARGBCopyYToAlpha(const uint8* src_y, int src_stride_y, @@ -384,12 +390,6 @@ int ARGBUnattenuate(const uint8* src_argb, int src_stride_argb, uint8* dst_argb, int dst_stride_argb, int width, int height); -// Convert MJPG to ARGB. -LIBYUV_API -int MJPGToARGB(const uint8* sample, size_t sample_size, - uint8* argb, int argb_stride, - int w, int h, int dw, int dh); - // Internal function - do not call directly. // Computes table of cumulative sum for image where the value is the sum // of all values above and to the left of the entry. Used by ARGBBlur. @@ -453,6 +453,12 @@ int I420Interpolate(const uint8* src0_y, int src0_stride_y, (defined(__i386__) && !defined(__SSE2__)) #define LIBYUV_DISABLE_X86 #endif +// MemorySanitizer does not support assembly code yet. http://crbug.com/344505 +#if defined(__has_feature) +#if __has_feature(memory_sanitizer) +#define LIBYUV_DISABLE_X86 +#endif +#endif // The following are available on all x86 platforms: #if !defined(LIBYUV_DISABLE_X86) && \ (defined(_M_IX86) || defined(__x86_64__) || defined(__i386__)) diff --git a/TMessagesProj/jni/libyuv/include/libyuv/rotate_row.h b/TMessagesProj/jni/libyuv/include/libyuv/rotate_row.h index e3838295c..ebc487f9a 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/rotate_row.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/rotate_row.h @@ -22,7 +22,12 @@ extern "C" { (defined(__i386__) && !defined(__SSE2__)) #define LIBYUV_DISABLE_X86 #endif - +// MemorySanitizer does not support assembly code yet. http://crbug.com/344505 +#if defined(__has_feature) +#if __has_feature(memory_sanitizer) +#define LIBYUV_DISABLE_X86 +#endif +#endif // The following are available for Visual C and clangcl 32 bit: #if !defined(LIBYUV_DISABLE_X86) && defined(_M_IX86) #define HAS_TRANSPOSEWX8_SSSE3 @@ -51,8 +56,8 @@ extern "C" { #if !defined(LIBYUV_DISABLE_MIPS) && !defined(__native_client__) && \ defined(__mips__) && \ defined(__mips_dsp) && (__mips_dsp_rev >= 2) -#define HAS_TRANSPOSEWX8_MIPS_DSPR2 -#define HAS_TRANSPOSEUVWX8_MIPS_DSPR2 +#define HAS_TRANSPOSEWX8_DSPR2 +#define HAS_TRANSPOSEUVWX8_DSPR2 #endif // defined(__mips__) void TransposeWxH_C(const uint8* src, int src_stride, @@ -66,10 +71,10 @@ void TransposeWx8_SSSE3(const uint8* src, int src_stride, uint8* dst, int dst_stride, int width); void TransposeWx8_Fast_SSSE3(const uint8* src, int src_stride, uint8* dst, int dst_stride, int width); -void TransposeWx8_MIPS_DSPR2(const uint8* src, int src_stride, +void TransposeWx8_DSPR2(const uint8* src, int src_stride, + uint8* dst, int dst_stride, int width); +void TransposeWx8_Fast_DSPR2(const uint8* src, int src_stride, uint8* dst, int dst_stride, int width); -void TransposeWx8_Fast_MIPS_DSPR2(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); void TransposeWx8_Any_NEON(const uint8* src, int src_stride, uint8* dst, int dst_stride, int width); @@ -77,8 +82,8 @@ void TransposeWx8_Any_SSSE3(const uint8* src, int src_stride, uint8* dst, int dst_stride, int width); void TransposeWx8_Fast_Any_SSSE3(const uint8* src, int src_stride, uint8* dst, int dst_stride, int width); -void TransposeWx8_Any_MIPS_DSPR2(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width); +void TransposeWx8_Any_DSPR2(const uint8* src, int src_stride, + uint8* dst, int dst_stride, int width); void TransposeUVWxH_C(const uint8* src, int src_stride, uint8* dst_a, int dst_stride_a, @@ -94,9 +99,9 @@ void TransposeUVWx8_SSE2(const uint8* src, int src_stride, void TransposeUVWx8_NEON(const uint8* src, int src_stride, uint8* dst_a, int dst_stride_a, uint8* dst_b, int dst_stride_b, int width); -void TransposeUVWx8_MIPS_DSPR2(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, int width); +void TransposeUVWx8_DSPR2(const uint8* src, int src_stride, + uint8* dst_a, int dst_stride_a, + uint8* dst_b, int dst_stride_b, int width); void TransposeUVWx8_Any_SSE2(const uint8* src, int src_stride, uint8* dst_a, int dst_stride_a, @@ -104,9 +109,9 @@ void TransposeUVWx8_Any_SSE2(const uint8* src, int src_stride, void TransposeUVWx8_Any_NEON(const uint8* src, int src_stride, uint8* dst_a, int dst_stride_a, uint8* dst_b, int dst_stride_b, int width); -void TransposeUVWx8_Any_MIPS_DSPR2(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, int width); +void TransposeUVWx8_Any_DSPR2(const uint8* src, int src_stride, + uint8* dst_a, int dst_stride_a, + uint8* dst_b, int dst_stride_b, int width); #ifdef __cplusplus } // extern "C" diff --git a/TMessagesProj/jni/libyuv/include/libyuv/row.h b/TMessagesProj/jni/libyuv/include/libyuv/row.h index f7620eea1..8dae60166 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/row.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/row.h @@ -41,6 +41,12 @@ extern "C" { (defined(__i386__) && !defined(__SSE2__)) #define LIBYUV_DISABLE_X86 #endif +// MemorySanitizer does not support assembly code yet. http://crbug.com/344505 +#if defined(__has_feature) +#if __has_feature(memory_sanitizer) +#define LIBYUV_DISABLE_X86 +#endif +#endif // True if compiling for SSSE3 as a requirement. #if defined(__SSSE3__) || (defined(_M_IX86_FP) && (_M_IX86_FP >= 3)) #define LIBYUV_SSSE3_ONLY @@ -93,29 +99,18 @@ extern "C" { #define HAS_ARGBTORGB24ROW_SSSE3 #define HAS_ARGBTORGB565DITHERROW_SSE2 #define HAS_ARGBTORGB565ROW_SSE2 -#define HAS_ARGBTOUV422ROW_SSSE3 #define HAS_ARGBTOUV444ROW_SSSE3 #define HAS_ARGBTOUVJROW_SSSE3 #define HAS_ARGBTOUVROW_SSSE3 #define HAS_ARGBTOYJROW_SSSE3 #define HAS_ARGBTOYROW_SSSE3 +#define HAS_ARGBEXTRACTALPHAROW_SSE2 #define HAS_BGRATOUVROW_SSSE3 #define HAS_BGRATOYROW_SSSE3 #define HAS_COPYROW_ERMS #define HAS_COPYROW_SSE2 #define HAS_H422TOARGBROW_SSSE3 #define HAS_I400TOARGBROW_SSE2 -// The following functions fail on gcc/clang 32 bit with fpic and framepointer. -// caveat: clangcl uses row_win.cc which works. -#if defined(NDEBUG) || !(defined(_DEBUG) && defined(__i386__)) || \ - !defined(__i386__) || defined(_MSC_VER) -// TODO(fbarchard): fix build error on x86 debug -// https://code.google.com/p/libyuv/issues/detail?id=524 -#define HAS_I411TOARGBROW_SSSE3 -// TODO(fbarchard): fix build error on android_full_debug=1 -// https://code.google.com/p/libyuv/issues/detail?id=517 -#define HAS_I422ALPHATOARGBROW_SSSE3 -#endif #define HAS_I422TOARGB1555ROW_SSSE3 #define HAS_I422TOARGB4444ROW_SSSE3 #define HAS_I422TOARGBROW_SSSE3 @@ -129,7 +124,6 @@ extern "C" { #define HAS_J422TOARGBROW_SSSE3 #define HAS_MERGEUVROW_SSE2 #define HAS_MIRRORROW_SSSE3 -#define HAS_MIRRORROW_UV_SSSE3 #define HAS_MIRRORUVROW_SSSE3 #define HAS_NV12TOARGBROW_SSSE3 #define HAS_NV12TORGB565ROW_SSSE3 @@ -173,6 +167,7 @@ extern "C" { #define HAS_ARGBSHADEROW_SSE2 #define HAS_ARGBSUBTRACTROW_SSE2 #define HAS_ARGBUNATTENUATEROW_SSE2 +#define HAS_BLENDPLANEROW_SSSE3 #define HAS_COMPUTECUMULATIVESUMROW_SSE2 #define HAS_CUMULATIVESUMTOAVERAGEROW_SSE2 #define HAS_INTERPOLATEROW_SSSE3 @@ -182,7 +177,17 @@ extern "C" { #define HAS_SOBELXROW_SSE2 #define HAS_SOBELXYROW_SSE2 #define HAS_SOBELYROW_SSE2 -#define HAS_BLENDPLANEROW_SSSE3 + +// The following functions fail on gcc/clang 32 bit with fpic and framepointer. +// caveat: clangcl uses row_win.cc which works. +#if !defined(x86fix) +// TODO(fbarchard): fix build error on x86 debug +// https://code.google.com/p/libyuv/issues/detail?id=524 +#define HAS_I411TOARGBROW_SSSE3 +// TODO(fbarchard): fix build error on android_full_debug=1 +// https://code.google.com/p/libyuv/issues/detail?id=517 +#define HAS_I422ALPHATOARGBROW_SSSE3 +#endif #endif // The following are available on all x86 platforms, but @@ -196,26 +201,32 @@ extern "C" { #define HAS_ARGBPOLYNOMIALROW_AVX2 #define HAS_ARGBSHUFFLEROW_AVX2 #define HAS_ARGBTORGB565DITHERROW_AVX2 +#define HAS_ARGBTOUVJROW_AVX2 #define HAS_ARGBTOUVROW_AVX2 #define HAS_ARGBTOYJROW_AVX2 #define HAS_ARGBTOYROW_AVX2 #define HAS_COPYROW_AVX #define HAS_H422TOARGBROW_AVX2 #define HAS_I400TOARGBROW_AVX2 -#if !(defined(_DEBUG) && defined(__i386__)) +#if !defined(x86fix) // TODO(fbarchard): fix build error on android_full_debug=1 // https://code.google.com/p/libyuv/issues/detail?id=517 #define HAS_I422ALPHATOARGBROW_AVX2 #endif -#define HAS_I444TOARGBROW_AVX2 +#define HAS_I411TOARGBROW_AVX2 +#define HAS_I422TOARGB1555ROW_AVX2 +#define HAS_I422TOARGB4444ROW_AVX2 #define HAS_I422TOARGBROW_AVX2 #define HAS_I422TORGB24ROW_AVX2 +#define HAS_I422TORGB565ROW_AVX2 #define HAS_I422TORGBAROW_AVX2 +#define HAS_I444TOARGBROW_AVX2 #define HAS_INTERPOLATEROW_AVX2 #define HAS_J422TOARGBROW_AVX2 #define HAS_MERGEUVROW_AVX2 #define HAS_MIRRORROW_AVX2 #define HAS_NV12TOARGBROW_AVX2 +#define HAS_NV12TORGB565ROW_AVX2 #define HAS_NV21TOARGBROW_AVX2 #define HAS_SPLITUVROW_AVX2 #define HAS_UYVYTOARGBROW_AVX2 @@ -245,12 +256,7 @@ extern "C" { #define HAS_ARGBTOARGB1555ROW_AVX2 #define HAS_ARGBTOARGB4444ROW_AVX2 #define HAS_ARGBTORGB565ROW_AVX2 -#define HAS_I411TOARGBROW_AVX2 -#define HAS_I422TOARGB1555ROW_AVX2 -#define HAS_I422TOARGB4444ROW_AVX2 -#define HAS_I422TORGB565ROW_AVX2 #define HAS_J400TOARGBROW_AVX2 -#define HAS_NV12TORGB565ROW_AVX2 #define HAS_RGB565TOARGBROW_AVX2 #endif @@ -264,7 +270,6 @@ extern "C" { // The following are available on Neon platforms: #if !defined(LIBYUV_DISABLE_NEON) && \ (defined(__aarch64__) || defined(__ARM_NEON__) || defined(LIBYUV_NEON)) -#define HAS_I422ALPHATOARGBROW_NEON #define HAS_ABGRTOUVROW_NEON #define HAS_ABGRTOYROW_NEON #define HAS_ARGB1555TOARGBROW_NEON @@ -281,17 +286,18 @@ extern "C" { #define HAS_ARGBTORGB565DITHERROW_NEON #define HAS_ARGBTORGB565ROW_NEON #define HAS_ARGBTOUV411ROW_NEON -#define HAS_ARGBTOUV422ROW_NEON #define HAS_ARGBTOUV444ROW_NEON #define HAS_ARGBTOUVJROW_NEON #define HAS_ARGBTOUVROW_NEON #define HAS_ARGBTOYJROW_NEON #define HAS_ARGBTOYROW_NEON +#define HAS_ARGBEXTRACTALPHAROW_NEON #define HAS_BGRATOUVROW_NEON #define HAS_BGRATOYROW_NEON #define HAS_COPYROW_NEON #define HAS_I400TOARGBROW_NEON #define HAS_I411TOARGBROW_NEON +#define HAS_I422ALPHATOARGBROW_NEON #define HAS_I422TOARGB1555ROW_NEON #define HAS_I422TOARGB4444ROW_NEON #define HAS_I422TOARGBROW_NEON @@ -357,11 +363,11 @@ extern "C" { (_MIPS_SIM == _MIPS_SIM_ABI32) && (__mips_isa_rev < 6) #define HAS_COPYROW_MIPS #if defined(__mips_dsp) && (__mips_dsp_rev >= 2) -#define HAS_I422TOARGBROW_MIPS_DSPR2 -#define HAS_INTERPOLATEROW_MIPS_DSPR2 -#define HAS_MIRRORROW_MIPS_DSPR2 -#define HAS_MIRRORUVROW_MIPS_DSPR2 -#define HAS_SPLITUVROW_MIPS_DSPR2 +#define HAS_I422TOARGBROW_DSPR2 +#define HAS_INTERPOLATEROW_DSPR2 +#define HAS_MIRRORROW_DSPR2 +#define HAS_MIRRORUVROW_DSPR2 +#define HAS_SPLITUVROW_DSPR2 #endif #endif @@ -648,8 +654,6 @@ void ARGBToYRow_NEON(const uint8* src_argb, uint8* dst_y, int width); void ARGBToYJRow_NEON(const uint8* src_argb, uint8* dst_y, int width); void ARGBToUV444Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width); -void ARGBToUV422Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width); void ARGBToUV411Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width); void ARGBToUVRow_NEON(const uint8* src_argb, int src_stride_argb, @@ -712,8 +716,8 @@ void ARGB4444ToYRow_Any_NEON(const uint8* src_argb4444, uint8* dst_y, void ARGBToUVRow_AVX2(const uint8* src_argb, int src_stride_argb, uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVRow_Any_AVX2(const uint8* src_argb, int src_stride_argb, - uint8* dst_u, uint8* dst_v, int width); +void ARGBToUVJRow_AVX2(const uint8* src_argb, int src_stride_argb, + uint8* dst_u, uint8* dst_v, int width); void ARGBToUVRow_SSSE3(const uint8* src_argb, int src_stride_argb, uint8* dst_u, uint8* dst_v, int width); void ARGBToUVJRow_SSSE3(const uint8* src_argb, int src_stride_argb, @@ -724,6 +728,10 @@ void ABGRToUVRow_SSSE3(const uint8* src_abgr, int src_stride_abgr, uint8* dst_u, uint8* dst_v, int width); void RGBAToUVRow_SSSE3(const uint8* src_rgba, int src_stride_rgba, uint8* dst_u, uint8* dst_v, int width); +void ARGBToUVRow_Any_AVX2(const uint8* src_argb, int src_stride_argb, + uint8* dst_u, uint8* dst_v, int width); +void ARGBToUVJRow_Any_AVX2(const uint8* src_argb, int src_stride_argb, + uint8* dst_u, uint8* dst_v, int width); void ARGBToUVRow_Any_SSSE3(const uint8* src_argb, int src_stride_argb, uint8* dst_u, uint8* dst_v, int width); void ARGBToUVJRow_Any_SSSE3(const uint8* src_argb, int src_stride_argb, @@ -736,8 +744,6 @@ void RGBAToUVRow_Any_SSSE3(const uint8* src_rgba, int src_stride_rgba, uint8* dst_u, uint8* dst_v, int width); void ARGBToUV444Row_Any_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width); -void ARGBToUV422Row_Any_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width); void ARGBToUV411Row_Any_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width); void ARGBToUVRow_Any_NEON(const uint8* src_argb, int src_stride_argb, @@ -788,24 +794,15 @@ void ARGBToUV444Row_SSSE3(const uint8* src_argb, void ARGBToUV444Row_Any_SSSE3(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width); -void ARGBToUV422Row_SSSE3(const uint8* src_argb, - uint8* dst_u, uint8* dst_v, int width); -void ARGBToUV422Row_Any_SSSE3(const uint8* src_argb, - uint8* dst_u, uint8* dst_v, int width); - void ARGBToUV444Row_C(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width); -void ARGBToUV422Row_C(const uint8* src_argb, - uint8* dst_u, uint8* dst_v, int width); void ARGBToUV411Row_C(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width); -void ARGBToUVJ422Row_C(const uint8* src_argb, - uint8* dst_u, uint8* dst_v, int width); void MirrorRow_AVX2(const uint8* src, uint8* dst, int width); void MirrorRow_SSSE3(const uint8* src, uint8* dst, int width); void MirrorRow_NEON(const uint8* src, uint8* dst, int width); -void MirrorRow_MIPS_DSPR2(const uint8* src, uint8* dst, int width); +void MirrorRow_DSPR2(const uint8* src, uint8* dst, int width); void MirrorRow_C(const uint8* src, uint8* dst, int width); void MirrorRow_Any_AVX2(const uint8* src, uint8* dst, int width); void MirrorRow_Any_SSSE3(const uint8* src, uint8* dst, int width); @@ -816,10 +813,9 @@ void MirrorUVRow_SSSE3(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width); void MirrorUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width); -void MirrorUVRow_MIPS_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); -void MirrorUVRow_C(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); +void MirrorUVRow_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, + int width); +void MirrorUVRow_C(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width); void ARGBMirrorRow_AVX2(const uint8* src, uint8* dst, int width); void ARGBMirrorRow_SSE2(const uint8* src, uint8* dst, int width); @@ -836,16 +832,16 @@ void SplitUVRow_AVX2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width); void SplitUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width); -void SplitUVRow_MIPS_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); +void SplitUVRow_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, + int width); void SplitUVRow_Any_SSE2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width); void SplitUVRow_Any_AVX2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width); void SplitUVRow_Any_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width); -void SplitUVRow_Any_MIPS_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width); +void SplitUVRow_Any_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, + int width); void MergeUVRow_C(const uint8* src_u, const uint8* src_v, uint8* dst_uv, int width); @@ -882,6 +878,14 @@ void ARGBCopyAlphaRow_Any_SSE2(const uint8* src_argb, uint8* dst_argb, void ARGBCopyAlphaRow_Any_AVX2(const uint8* src_argb, uint8* dst_argb, int width); +void ARGBExtractAlphaRow_C(const uint8* src_argb, uint8* dst_a, int width); +void ARGBExtractAlphaRow_SSE2(const uint8* src_argb, uint8* dst_a, int width); +void ARGBExtractAlphaRow_NEON(const uint8* src_argb, uint8* dst_a, int width); +void ARGBExtractAlphaRow_Any_SSE2(const uint8* src_argb, uint8* dst_a, + int width); +void ARGBExtractAlphaRow_Any_NEON(const uint8* src_argb, uint8* dst_a, + int width); + void ARGBCopyYToAlphaRow_C(const uint8* src_y, uint8* dst_argb, int width); void ARGBCopyYToAlphaRow_SSE2(const uint8* src_y, uint8* dst_argb, int width); void ARGBCopyYToAlphaRow_AVX2(const uint8* src_y, uint8* dst_argb, int width); @@ -1625,18 +1629,18 @@ void UYVYToARGBRow_Any_NEON(const uint8* src_uyvy, uint8* dst_argb, const struct YuvConstants* yuvconstants, int width); -void I422ToARGBRow_MIPS_DSPR2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); -void I422ToARGBRow_MIPS_DSPR2(const uint8* src_y, - const uint8* src_u, - const uint8* src_v, - uint8* dst_argb, - const struct YuvConstants* yuvconstants, - int width); +void I422ToARGBRow_DSPR2(const uint8* src_y, + const uint8* src_u, + const uint8* src_v, + uint8* dst_argb, + const struct YuvConstants* yuvconstants, + int width); +void I422ToARGBRow_DSPR2(const uint8* src_y, + const uint8* src_u, + const uint8* src_v, + uint8* dst_argb, + const struct YuvConstants* yuvconstants, + int width); void YUY2ToYRow_AVX2(const uint8* src_yuy2, uint8* dst_y, int width); void YUY2ToUVRow_AVX2(const uint8* src_yuy2, int stride_yuy2, @@ -1846,9 +1850,9 @@ void InterpolateRow_AVX2(uint8* dst_ptr, const uint8* src_ptr, void InterpolateRow_NEON(uint8* dst_ptr, const uint8* src_ptr, ptrdiff_t src_stride_ptr, int width, int source_y_fraction); -void InterpolateRow_MIPS_DSPR2(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride_ptr, int width, - int source_y_fraction); +void InterpolateRow_DSPR2(uint8* dst_ptr, const uint8* src_ptr, + ptrdiff_t src_stride_ptr, int width, + int source_y_fraction); void InterpolateRow_Any_NEON(uint8* dst_ptr, const uint8* src_ptr, ptrdiff_t src_stride_ptr, int width, int source_y_fraction); @@ -1858,9 +1862,9 @@ void InterpolateRow_Any_SSSE3(uint8* dst_ptr, const uint8* src_ptr, void InterpolateRow_Any_AVX2(uint8* dst_ptr, const uint8* src_ptr, ptrdiff_t src_stride_ptr, int width, int source_y_fraction); -void InterpolateRow_Any_MIPS_DSPR2(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride_ptr, int width, - int source_y_fraction); +void InterpolateRow_Any_DSPR2(uint8* dst_ptr, const uint8* src_ptr, + ptrdiff_t src_stride_ptr, int width, + int source_y_fraction); void InterpolateRow_16_C(uint16* dst_ptr, const uint16* src_ptr, ptrdiff_t src_stride_ptr, diff --git a/TMessagesProj/jni/libyuv/include/libyuv/scale_row.h b/TMessagesProj/jni/libyuv/include/libyuv/scale_row.h index 45127bda3..df699e6c2 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/scale_row.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/scale_row.h @@ -23,6 +23,12 @@ extern "C" { (defined(__i386__) && !defined(__SSE2__)) #define LIBYUV_DISABLE_X86 #endif +// MemorySanitizer does not support assembly code yet. http://crbug.com/344505 +#if defined(__has_feature) +#if __has_feature(memory_sanitizer) +#define LIBYUV_DISABLE_X86 +#endif +#endif // GCC >= 4.7.0 required for AVX2. #if defined(__GNUC__) && (defined(__x86_64__) || defined(__i386__)) @@ -90,10 +96,10 @@ extern "C" { // The following are available on Mips platforms: #if !defined(LIBYUV_DISABLE_MIPS) && !defined(__native_client__) && \ defined(__mips__) && defined(__mips_dsp) && (__mips_dsp_rev >= 2) -#define HAS_SCALEROWDOWN2_MIPS_DSPR2 -#define HAS_SCALEROWDOWN4_MIPS_DSPR2 -#define HAS_SCALEROWDOWN34_MIPS_DSPR2 -#define HAS_SCALEROWDOWN38_MIPS_DSPR2 +#define HAS_SCALEROWDOWN2_DSPR2 +#define HAS_SCALEROWDOWN4_DSPR2 +#define HAS_SCALEROWDOWN34_DSPR2 +#define HAS_SCALEROWDOWN38_DSPR2 #endif // Scale ARGB vertically with bilinear interpolation. @@ -146,6 +152,8 @@ void ScaleRowDown2Linear_16_C(const uint16* src_ptr, ptrdiff_t src_stride, uint16* dst, int dst_width); void ScaleRowDown2Box_C(const uint8* src_ptr, ptrdiff_t src_stride, uint8* dst, int dst_width); +void ScaleRowDown2Box_Odd_C(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width); void ScaleRowDown2Box_16_C(const uint16* src_ptr, ptrdiff_t src_stride, uint16* dst, int dst_width); void ScaleRowDown4_C(const uint8* src_ptr, ptrdiff_t src_stride, @@ -269,13 +277,17 @@ void ScaleRowDown2_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, void ScaleRowDown2Linear_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, uint8* dst_ptr, int dst_width); void ScaleRowDown2Box_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); + uint8* dst_ptr, int dst_width); +void ScaleRowDown2Box_Odd_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst_ptr, int dst_width); void ScaleRowDown2_Any_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, uint8* dst_ptr, int dst_width); void ScaleRowDown2Linear_Any_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, uint8* dst_ptr, int dst_width); void ScaleRowDown2Box_Any_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); + uint8* dst_ptr, int dst_width); +void ScaleRowDown2Box_Odd_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst_ptr, int dst_width); void ScaleRowDown4_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, uint8* dst_ptr, int dst_width); void ScaleRowDown4Box_Any_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, @@ -431,6 +443,8 @@ void ScaleRowDown2Linear_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, uint8* dst, int dst_width); void ScaleRowDown2Box_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, uint8* dst, int dst_width); +void ScaleRowDown2Box_Odd_NEON(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width); void ScaleRowDown4_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, uint8* dst_ptr, int dst_width); void ScaleRowDown4Box_Any_NEON(const uint8* src_ptr, ptrdiff_t src_stride, @@ -460,28 +474,26 @@ void ScaleFilterCols_NEON(uint8* dst_ptr, const uint8* src_ptr, void ScaleFilterCols_Any_NEON(uint8* dst_ptr, const uint8* src_ptr, int dst_width, int x, int dx); - -void ScaleRowDown2_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown2Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown4_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown4Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown34_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown34_0_Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* d, int dst_width); -void ScaleRowDown34_1_Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* d, int dst_width); -void ScaleRowDown38_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width); -void ScaleRowDown38_2_Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); -void ScaleRowDown38_3_Box_MIPS_DSPR2(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width); +void ScaleRowDown2_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width); +void ScaleRowDown2Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width); +void ScaleRowDown4_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width); +void ScaleRowDown4Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width); +void ScaleRowDown34_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width); +void ScaleRowDown34_0_Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* d, int dst_width); +void ScaleRowDown34_1_Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* d, int dst_width); +void ScaleRowDown38_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width); +void ScaleRowDown38_2_Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst_ptr, int dst_width); +void ScaleRowDown38_3_Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst_ptr, int dst_width); #ifdef __cplusplus } // extern "C" diff --git a/TMessagesProj/jni/libyuv/include/libyuv/version.h b/TMessagesProj/jni/libyuv/include/libyuv/version.h index ab0f88d9e..3a6bbe3cb 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/version.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/version.h @@ -11,6 +11,6 @@ #ifndef INCLUDE_LIBYUV_VERSION_H_ // NOLINT #define INCLUDE_LIBYUV_VERSION_H_ -#define LIBYUV_VERSION 1561 +#define LIBYUV_VERSION 1597 #endif // INCLUDE_LIBYUV_VERSION_H_ NOLINT diff --git a/TMessagesProj/jni/libyuv/source/convert.cc b/TMessagesProj/jni/libyuv/source/convert.cc index 5dc279f8a..e332bc505 100644 --- a/TMessagesProj/jni/libyuv/source/convert.cc +++ b/TMessagesProj/jni/libyuv/source/convert.cc @@ -303,14 +303,14 @@ static int X420ToI420(const uint8* src_y, } } #endif -#if defined(HAS_SPLITUVROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && +#if defined(HAS_SPLITUVROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(src_uv, 4) && IS_ALIGNED(src_stride_uv, 4) && IS_ALIGNED(dst_u, 4) && IS_ALIGNED(dst_stride_u, 4) && IS_ALIGNED(dst_v, 4) && IS_ALIGNED(dst_stride_v, 4)) { - SplitUVRow = SplitUVRow_Any_MIPS_DSPR2; + SplitUVRow = SplitUVRow_Any_DSPR2; if (IS_ALIGNED(halfwidth, 16)) { - SplitUVRow = SplitUVRow_MIPS_DSPR2; + SplitUVRow = SplitUVRow_DSPR2; } } #endif diff --git a/TMessagesProj/jni/libyuv/source/convert_argb.cc b/TMessagesProj/jni/libyuv/source/convert_argb.cc index cf3d72289..fb9582d62 100644 --- a/TMessagesProj/jni/libyuv/source/convert_argb.cc +++ b/TMessagesProj/jni/libyuv/source/convert_argb.cc @@ -14,6 +14,7 @@ #ifdef HAVE_JPEG #include "libyuv/mjpeg_decoder.h" #endif +#include "libyuv/planar_functions.h" // For CopyPlane and ARGBShuffle. #include "libyuv/rotate_argb.h" #include "libyuv/row.h" #include "libyuv/video_common.h" @@ -92,13 +93,13 @@ static int I420ToARGBMatrix(const uint8* src_y, int src_stride_y, } } #endif -#if defined(HAS_I422TOARGBROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(width, 4) && +#if defined(HAS_I422TOARGBROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(width, 4) && IS_ALIGNED(src_y, 4) && IS_ALIGNED(src_stride_y, 4) && IS_ALIGNED(src_u, 2) && IS_ALIGNED(src_stride_u, 2) && IS_ALIGNED(src_v, 2) && IS_ALIGNED(src_stride_v, 2) && IS_ALIGNED(dst_argb, 4) && IS_ALIGNED(dst_stride_argb, 4)) { - I422ToARGBRow = I422ToARGBRow_MIPS_DSPR2; + I422ToARGBRow = I422ToARGBRow_DSPR2; } #endif @@ -262,13 +263,13 @@ static int I422ToARGBMatrix(const uint8* src_y, int src_stride_y, } } #endif -#if defined(HAS_I422TOARGBROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(width, 4) && +#if defined(HAS_I422TOARGBROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(width, 4) && IS_ALIGNED(src_y, 4) && IS_ALIGNED(src_stride_y, 4) && IS_ALIGNED(src_u, 2) && IS_ALIGNED(src_stride_u, 2) && IS_ALIGNED(src_v, 2) && IS_ALIGNED(src_stride_v, 2) && IS_ALIGNED(dst_argb, 4) && IS_ALIGNED(dst_stride_argb, 4)) { - I422ToARGBRow = I422ToARGBRow_MIPS_DSPR2; + I422ToARGBRow = I422ToARGBRow_DSPR2; } #endif @@ -607,13 +608,13 @@ static int I420AlphaToARGBMatrix(const uint8* src_y, int src_stride_y, } } #endif -#if defined(HAS_I422ALPHATOARGBROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(width, 4) && +#if defined(HAS_I422ALPHATOARGBROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(width, 4) && IS_ALIGNED(src_y, 4) && IS_ALIGNED(src_stride_y, 4) && IS_ALIGNED(src_u, 2) && IS_ALIGNED(src_stride_u, 2) && IS_ALIGNED(src_v, 2) && IS_ALIGNED(src_stride_v, 2) && IS_ALIGNED(dst_argb, 4) && IS_ALIGNED(dst_stride_argb, 4)) { - I422AlphaToARGBRow = I422AlphaToARGBRow_MIPS_DSPR2; + I422AlphaToARGBRow = I422AlphaToARGBRow_DSPR2; } #endif #if defined(HAS_ARGBATTENUATEROW_SSSE3) diff --git a/TMessagesProj/jni/libyuv/source/convert_from.cc b/TMessagesProj/jni/libyuv/source/convert_from.cc index 9c138d936..46abdebcd 100644 --- a/TMessagesProj/jni/libyuv/source/convert_from.cc +++ b/TMessagesProj/jni/libyuv/source/convert_from.cc @@ -445,7 +445,7 @@ int I420ToNV21(const uint8* src_y, int src_stride_y, return I420ToNV12(src_y, src_stride_y, src_v, src_stride_v, src_u, src_stride_u, - dst_y, src_stride_y, + dst_y, dst_stride_y, dst_vu, dst_stride_vu, width, height); } @@ -498,13 +498,13 @@ static int I420ToRGBAMatrix(const uint8* src_y, int src_stride_y, } } #endif -#if defined(HAS_I422TORGBAROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(width, 4) && +#if defined(HAS_I422TORGBAROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(width, 4) && IS_ALIGNED(src_y, 4) && IS_ALIGNED(src_stride_y, 4) && IS_ALIGNED(src_u, 2) && IS_ALIGNED(src_stride_u, 2) && IS_ALIGNED(src_v, 2) && IS_ALIGNED(src_stride_v, 2) && IS_ALIGNED(dst_rgba, 4) && IS_ALIGNED(dst_stride_rgba, 4)) { - I422ToRGBARow = I422ToRGBARow_MIPS_DSPR2; + I422ToRGBARow = I422ToRGBARow_DSPR2; } #endif @@ -888,12 +888,12 @@ int I420ToRGB565Dither(const uint8* src_y, int src_stride_y, } } #endif -#if defined(HAS_I422TOARGBROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(width, 4) && +#if defined(HAS_I422TOARGBROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(width, 4) && IS_ALIGNED(src_y, 4) && IS_ALIGNED(src_stride_y, 4) && IS_ALIGNED(src_u, 2) && IS_ALIGNED(src_stride_u, 2) && IS_ALIGNED(src_v, 2) && IS_ALIGNED(src_stride_v, 2)) { - I422ToARGBRow = I422ToARGBRow_MIPS_DSPR2; + I422ToARGBRow = I422ToARGBRow_DSPR2; } #endif #if defined(HAS_ARGBTORGB565DITHERROW_SSE2) @@ -1077,7 +1077,6 @@ int ConvertFromI420(const uint8* y, int y_stride, // Triplanar formats // TODO(fbarchard): halfstride instead of halfwidth case FOURCC_I420: - case FOURCC_YU12: case FOURCC_YV12: { int halfwidth = (width + 1) / 2; int halfheight = (height + 1) / 2; diff --git a/TMessagesProj/jni/libyuv/source/convert_from_argb.cc b/TMessagesProj/jni/libyuv/source/convert_from_argb.cc index 6796343c0..2a8682b7e 100644 --- a/TMessagesProj/jni/libyuv/source/convert_from_argb.cc +++ b/TMessagesProj/jni/libyuv/source/convert_from_argb.cc @@ -109,13 +109,16 @@ int ARGBToI422(const uint8* src_argb, int src_stride_argb, uint8* dst_v, int dst_stride_v, int width, int height) { int y; - void (*ARGBToUV422Row)(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width) = ARGBToUV422Row_C; + void (*ARGBToUVRow)(const uint8* src_argb0, int src_stride_argb, + uint8* dst_u, uint8* dst_v, int width) = ARGBToUVRow_C; void (*ARGBToYRow)(const uint8* src_argb, uint8* dst_y, int width) = ARGBToYRow_C; - if (!src_argb || !dst_y || !dst_u || !dst_v || width <= 0 || height == 0) { + if (!src_argb || + !dst_y || !dst_u || !dst_v || + width <= 0 || height == 0) { return -1; } + // Negative height means invert the image. if (height < 0) { height = -height; src_argb = src_argb + (height - 1) * src_stride_argb; @@ -130,34 +133,22 @@ int ARGBToI422(const uint8* src_argb, int src_stride_argb, height = 1; src_stride_argb = dst_stride_y = dst_stride_u = dst_stride_v = 0; } -#if defined(HAS_ARGBTOUV422ROW_SSSE3) - if (TestCpuFlag(kCpuHasSSSE3)) { - ARGBToUV422Row = ARGBToUV422Row_Any_SSSE3; - if (IS_ALIGNED(width, 16)) { - ARGBToUV422Row = ARGBToUV422Row_SSSE3; - } - } -#endif -#if defined(HAS_ARGBTOUV422ROW_NEON) - if (TestCpuFlag(kCpuHasNEON)) { - ARGBToUV422Row = ARGBToUV422Row_Any_NEON; - if (IS_ALIGNED(width, 16)) { - ARGBToUV422Row = ARGBToUV422Row_NEON; - } - } -#endif -#if defined(HAS_ARGBTOYROW_SSSE3) +#if defined(HAS_ARGBTOYROW_SSSE3) && defined(HAS_ARGBTOUVROW_SSSE3) if (TestCpuFlag(kCpuHasSSSE3)) { + ARGBToUVRow = ARGBToUVRow_Any_SSSE3; ARGBToYRow = ARGBToYRow_Any_SSSE3; if (IS_ALIGNED(width, 16)) { + ARGBToUVRow = ARGBToUVRow_SSSE3; ARGBToYRow = ARGBToYRow_SSSE3; } } #endif -#if defined(HAS_ARGBTOYROW_AVX2) +#if defined(HAS_ARGBTOYROW_AVX2) && defined(HAS_ARGBTOUVROW_AVX2) if (TestCpuFlag(kCpuHasAVX2)) { + ARGBToUVRow = ARGBToUVRow_Any_AVX2; ARGBToYRow = ARGBToYRow_Any_AVX2; if (IS_ALIGNED(width, 32)) { + ARGBToUVRow = ARGBToUVRow_AVX2; ARGBToYRow = ARGBToYRow_AVX2; } } @@ -170,9 +161,17 @@ int ARGBToI422(const uint8* src_argb, int src_stride_argb, } } #endif +#if defined(HAS_ARGBTOUVROW_NEON) + if (TestCpuFlag(kCpuHasNEON)) { + ARGBToUVRow = ARGBToUVRow_Any_NEON; + if (IS_ALIGNED(width, 16)) { + ARGBToUVRow = ARGBToUVRow_NEON; + } + } +#endif for (y = 0; y < height; ++y) { - ARGBToUV422Row(src_argb, dst_u, dst_v, width); + ARGBToUVRow(src_argb, 0, dst_u, dst_v, width); ARGBToYRow(src_argb, dst_y, width); src_argb += src_stride_argb; dst_y += dst_stride_y; @@ -478,8 +477,8 @@ int ARGBToYUY2(const uint8* src_argb, int src_stride_argb, uint8* dst_yuy2, int dst_stride_yuy2, int width, int height) { int y; - void (*ARGBToUV422Row)(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width) = ARGBToUV422Row_C; + void (*ARGBToUVRow)(const uint8* src_argb, int src_stride_argb, + uint8* dst_u, uint8* dst_v, int width) = ARGBToUVRow_C; void (*ARGBToYRow)(const uint8* src_argb, uint8* dst_y, int width) = ARGBToYRow_C; void (*I422ToYUY2Row)(const uint8* src_y, const uint8* src_u, @@ -502,34 +501,22 @@ int ARGBToYUY2(const uint8* src_argb, int src_stride_argb, height = 1; src_stride_argb = dst_stride_yuy2 = 0; } -#if defined(HAS_ARGBTOUV422ROW_SSSE3) - if (TestCpuFlag(kCpuHasSSSE3)) { - ARGBToUV422Row = ARGBToUV422Row_Any_SSSE3; - if (IS_ALIGNED(width, 16)) { - ARGBToUV422Row = ARGBToUV422Row_SSSE3; - } - } -#endif -#if defined(HAS_ARGBTOUV422ROW_NEON) - if (TestCpuFlag(kCpuHasNEON)) { - ARGBToUV422Row = ARGBToUV422Row_Any_NEON; - if (IS_ALIGNED(width, 16)) { - ARGBToUV422Row = ARGBToUV422Row_NEON; - } - } -#endif -#if defined(HAS_ARGBTOYROW_SSSE3) +#if defined(HAS_ARGBTOYROW_SSSE3) && defined(HAS_ARGBTOUVROW_SSSE3) if (TestCpuFlag(kCpuHasSSSE3)) { + ARGBToUVRow = ARGBToUVRow_Any_SSSE3; ARGBToYRow = ARGBToYRow_Any_SSSE3; if (IS_ALIGNED(width, 16)) { + ARGBToUVRow = ARGBToUVRow_SSSE3; ARGBToYRow = ARGBToYRow_SSSE3; } } #endif -#if defined(HAS_ARGBTOYROW_AVX2) +#if defined(HAS_ARGBTOYROW_AVX2) && defined(HAS_ARGBTOUVROW_AVX2) if (TestCpuFlag(kCpuHasAVX2)) { + ARGBToUVRow = ARGBToUVRow_Any_AVX2; ARGBToYRow = ARGBToYRow_Any_AVX2; if (IS_ALIGNED(width, 32)) { + ARGBToUVRow = ARGBToUVRow_AVX2; ARGBToYRow = ARGBToYRow_AVX2; } } @@ -542,7 +529,14 @@ int ARGBToYUY2(const uint8* src_argb, int src_stride_argb, } } #endif - +#if defined(HAS_ARGBTOUVROW_NEON) + if (TestCpuFlag(kCpuHasNEON)) { + ARGBToUVRow = ARGBToUVRow_Any_NEON; + if (IS_ALIGNED(width, 16)) { + ARGBToUVRow = ARGBToUVRow_NEON; + } + } +#endif #if defined(HAS_I422TOYUY2ROW_SSE2) if (TestCpuFlag(kCpuHasSSE2)) { I422ToYUY2Row = I422ToYUY2Row_Any_SSE2; @@ -567,7 +561,7 @@ int ARGBToYUY2(const uint8* src_argb, int src_stride_argb, uint8* row_v = row_u + ((width + 63) & ~63) / 2; for (y = 0; y < height; ++y) { - ARGBToUV422Row(src_argb, row_u, row_v, width); + ARGBToUVRow(src_argb, 0, row_u, row_v, width); ARGBToYRow(src_argb, row_y, width); I422ToYUY2Row(row_y, row_u, row_v, dst_yuy2, width); src_argb += src_stride_argb; @@ -585,8 +579,8 @@ int ARGBToUYVY(const uint8* src_argb, int src_stride_argb, uint8* dst_uyvy, int dst_stride_uyvy, int width, int height) { int y; - void (*ARGBToUV422Row)(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width) = ARGBToUV422Row_C; + void (*ARGBToUVRow)(const uint8* src_argb, int src_stride_argb, + uint8* dst_u, uint8* dst_v, int width) = ARGBToUVRow_C; void (*ARGBToYRow)(const uint8* src_argb, uint8* dst_y, int width) = ARGBToYRow_C; void (*I422ToUYVYRow)(const uint8* src_y, const uint8* src_u, @@ -609,34 +603,22 @@ int ARGBToUYVY(const uint8* src_argb, int src_stride_argb, height = 1; src_stride_argb = dst_stride_uyvy = 0; } -#if defined(HAS_ARGBTOUV422ROW_SSSE3) - if (TestCpuFlag(kCpuHasSSSE3)) { - ARGBToUV422Row = ARGBToUV422Row_Any_SSSE3; - if (IS_ALIGNED(width, 16)) { - ARGBToUV422Row = ARGBToUV422Row_SSSE3; - } - } -#endif -#if defined(HAS_ARGBTOUV422ROW_NEON) - if (TestCpuFlag(kCpuHasNEON)) { - ARGBToUV422Row = ARGBToUV422Row_Any_NEON; - if (IS_ALIGNED(width, 16)) { - ARGBToUV422Row = ARGBToUV422Row_NEON; - } - } -#endif -#if defined(HAS_ARGBTOYROW_SSSE3) +#if defined(HAS_ARGBTOYROW_SSSE3) && defined(HAS_ARGBTOUVROW_SSSE3) if (TestCpuFlag(kCpuHasSSSE3)) { + ARGBToUVRow = ARGBToUVRow_Any_SSSE3; ARGBToYRow = ARGBToYRow_Any_SSSE3; if (IS_ALIGNED(width, 16)) { + ARGBToUVRow = ARGBToUVRow_SSSE3; ARGBToYRow = ARGBToYRow_SSSE3; } } #endif -#if defined(HAS_ARGBTOYROW_AVX2) +#if defined(HAS_ARGBTOYROW_AVX2) && defined(HAS_ARGBTOUVROW_AVX2) if (TestCpuFlag(kCpuHasAVX2)) { + ARGBToUVRow = ARGBToUVRow_Any_AVX2; ARGBToYRow = ARGBToYRow_Any_AVX2; if (IS_ALIGNED(width, 32)) { + ARGBToUVRow = ARGBToUVRow_AVX2; ARGBToYRow = ARGBToYRow_AVX2; } } @@ -649,7 +631,14 @@ int ARGBToUYVY(const uint8* src_argb, int src_stride_argb, } } #endif - +#if defined(HAS_ARGBTOUVROW_NEON) + if (TestCpuFlag(kCpuHasNEON)) { + ARGBToUVRow = ARGBToUVRow_Any_NEON; + if (IS_ALIGNED(width, 16)) { + ARGBToUVRow = ARGBToUVRow_NEON; + } + } +#endif #if defined(HAS_I422TOUYVYROW_SSE2) if (TestCpuFlag(kCpuHasSSE2)) { I422ToUYVYRow = I422ToUYVYRow_Any_SSE2; @@ -674,7 +663,7 @@ int ARGBToUYVY(const uint8* src_argb, int src_stride_argb, uint8* row_v = row_u + ((width + 63) & ~63) / 2; for (y = 0; y < height; ++y) { - ARGBToUV422Row(src_argb, row_u, row_v, width); + ARGBToUVRow(src_argb, 0, row_u, row_v, width); ARGBToYRow(src_argb, row_y, width); I422ToUYVYRow(row_y, row_u, row_v, dst_uyvy, width); src_argb += src_stride_argb; @@ -1157,21 +1146,24 @@ int ARGBToJ420(const uint8* src_argb, int src_stride_argb, return 0; } -// ARGB little endian (bgra in memory) to J422 +// Convert ARGB to J422. (JPeg full range I422). LIBYUV_API int ARGBToJ422(const uint8* src_argb, int src_stride_argb, - uint8* dst_y, int dst_stride_y, + uint8* dst_yj, int dst_stride_yj, uint8* dst_u, int dst_stride_u, uint8* dst_v, int dst_stride_v, int width, int height) { int y; - void (*ARGBToUVJ422Row)(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width) = ARGBToUVJ422Row_C; - void (*ARGBToYJRow)(const uint8* src_argb, uint8* dst_y, int width) = + void (*ARGBToUVJRow)(const uint8* src_argb0, int src_stride_argb, + uint8* dst_u, uint8* dst_v, int width) = ARGBToUVJRow_C; + void (*ARGBToYJRow)(const uint8* src_argb, uint8* dst_yj, int width) = ARGBToYJRow_C; - if (!src_argb || !dst_y || !dst_u || !dst_v || width <= 0 || height == 0) { + if (!src_argb || + !dst_yj || !dst_u || !dst_v || + width <= 0 || height == 0) { return -1; } + // Negative height means invert the image. if (height < 0) { height = -height; src_argb = src_argb + (height - 1) * src_stride_argb; @@ -1179,34 +1171,19 @@ int ARGBToJ422(const uint8* src_argb, int src_stride_argb, } // Coalesce rows. if (src_stride_argb == width * 4 && - dst_stride_y == width && + dst_stride_yj == width && dst_stride_u * 2 == width && dst_stride_v * 2 == width) { width *= height; height = 1; - src_stride_argb = dst_stride_y = dst_stride_u = dst_stride_v = 0; + src_stride_argb = dst_stride_yj = dst_stride_u = dst_stride_v = 0; } -#if defined(HAS_ARGBTOUVJ422ROW_SSSE3) - if (TestCpuFlag(kCpuHasSSSE3)) { - ARGBToUVJ422Row = ARGBToUVJ422Row_Any_SSSE3; - if (IS_ALIGNED(width, 16)) { - ARGBToUVJ422Row = ARGBToUVJ422Row_SSSE3; - } - } -#endif -#if defined(HAS_ARGBTOUVJ422ROW_NEON) - if (TestCpuFlag(kCpuHasNEON)) { - ARGBToUVJ422Row = ARGBToUVJ422Row_Any_NEON; - if (IS_ALIGNED(width, 16)) { - ARGBToUVJ422Row = ARGBToUVJ422Row_NEON; - } - } -#endif - -#if defined(HAS_ARGBTOYJROW_SSSE3) +#if defined(HAS_ARGBTOYJROW_SSSE3) && defined(HAS_ARGBTOUVJROW_SSSE3) if (TestCpuFlag(kCpuHasSSSE3)) { + ARGBToUVJRow = ARGBToUVJRow_Any_SSSE3; ARGBToYJRow = ARGBToYJRow_Any_SSSE3; if (IS_ALIGNED(width, 16)) { + ARGBToUVJRow = ARGBToUVJRow_SSSE3; ARGBToYJRow = ARGBToYJRow_SSSE3; } } @@ -1227,12 +1204,20 @@ int ARGBToJ422(const uint8* src_argb, int src_stride_argb, } } #endif +#if defined(HAS_ARGBTOUVJROW_NEON) + if (TestCpuFlag(kCpuHasNEON)) { + ARGBToUVJRow = ARGBToUVJRow_Any_NEON; + if (IS_ALIGNED(width, 16)) { + ARGBToUVJRow = ARGBToUVJRow_NEON; + } + } +#endif for (y = 0; y < height; ++y) { - ARGBToUVJ422Row(src_argb, dst_u, dst_v, width); - ARGBToYJRow(src_argb, dst_y, width); + ARGBToUVJRow(src_argb, 0, dst_u, dst_v, width); + ARGBToYJRow(src_argb, dst_yj, width); src_argb += src_stride_argb; - dst_y += dst_stride_y; + dst_yj += dst_stride_yj; dst_u += dst_stride_u; dst_v += dst_stride_v; } diff --git a/TMessagesProj/jni/libyuv/source/convert_jpeg.cc b/TMessagesProj/jni/libyuv/source/convert_jpeg.cc index bcb980f7f..90f550a26 100644 --- a/TMessagesProj/jni/libyuv/source/convert_jpeg.cc +++ b/TMessagesProj/jni/libyuv/source/convert_jpeg.cc @@ -9,6 +9,7 @@ */ #include "libyuv/convert.h" +#include "libyuv/convert_argb.h" #ifdef HAVE_JPEG #include "libyuv/mjpeg_decoder.h" diff --git a/TMessagesProj/jni/libyuv/source/convert_to_argb.cc b/TMessagesProj/jni/libyuv/source/convert_to_argb.cc index af829fbd3..aecdc80fd 100644 --- a/TMessagesProj/jni/libyuv/source/convert_to_argb.cc +++ b/TMessagesProj/jni/libyuv/source/convert_to_argb.cc @@ -23,7 +23,7 @@ namespace libyuv { extern "C" { #endif -// Convert camera sample to I420 with cropping, rotation and vertical flip. +// Convert camera sample to ARGB with cropping, rotation and vertical flip. // src_width is used for source stride computation // src_height is used to compute location of planes, and indicate inversion // sample_size is measured in bytes and is the size of the frame. @@ -51,8 +51,8 @@ int ConvertToARGB(const uint8* sample, size_t sample_size, // also enable temporary buffer. LIBYUV_BOOL need_buf = (rotation && format != FOURCC_ARGB) || crop_argb == sample; - uint8* tmp_argb = crop_argb; - int tmp_argb_stride = argb_stride; + uint8* dest_argb = crop_argb; + int dest_argb_stride = argb_stride; uint8* rotate_buffer = NULL; int abs_crop_height = (crop_height < 0) ? -crop_height : crop_height; @@ -66,13 +66,13 @@ int ConvertToARGB(const uint8* sample, size_t sample_size, } if (need_buf) { - int argb_size = crop_width * abs_crop_height * 4; + int argb_size = crop_width * 4 * abs_crop_height; rotate_buffer = (uint8*)malloc(argb_size); if (!rotate_buffer) { return 1; // Out of memory runtime error. } crop_argb = rotate_buffer; - argb_stride = crop_width; + argb_stride = crop_width * 4; } switch (format) { @@ -176,7 +176,6 @@ int ConvertToARGB(const uint8* sample, size_t sample_size, break; // Triplanar formats case FOURCC_I420: - case FOURCC_YU12: case FOURCC_YV12: { const uint8* src_y = sample + (src_width * crop_y + crop_x); const uint8* src_u; @@ -291,7 +290,7 @@ int ConvertToARGB(const uint8* sample, size_t sample_size, if (need_buf) { if (!r) { r = ARGBRotate(crop_argb, argb_stride, - tmp_argb, tmp_argb_stride, + dest_argb, dest_argb_stride, crop_width, abs_crop_height, rotation); } free(rotate_buffer); diff --git a/TMessagesProj/jni/libyuv/source/convert_to_i420.cc b/TMessagesProj/jni/libyuv/source/convert_to_i420.cc index 5e75369b5..e5f307c44 100644 --- a/TMessagesProj/jni/libyuv/source/convert_to_i420.cc +++ b/TMessagesProj/jni/libyuv/source/convert_to_i420.cc @@ -39,12 +39,13 @@ int ConvertToI420(const uint8* sample, int aligned_src_width = (src_width + 1) & ~1; const uint8* src; const uint8* src_uv; - int abs_src_height = (src_height < 0) ? -src_height : src_height; - int inv_crop_height = (crop_height < 0) ? -crop_height : crop_height; + const int abs_src_height = (src_height < 0) ? -src_height : src_height; + // TODO(nisse): Why allow crop_height < 0? + const int abs_crop_height = (crop_height < 0) ? -crop_height : crop_height; int r = 0; LIBYUV_BOOL need_buf = (rotation && format != FOURCC_I420 && format != FOURCC_NV12 && format != FOURCC_NV21 && - format != FOURCC_YU12 && format != FOURCC_YV12) || y == sample; + format != FOURCC_YV12) || y == sample; uint8* tmp_y = y; uint8* tmp_u = u; uint8* tmp_v = v; @@ -52,16 +53,14 @@ int ConvertToI420(const uint8* sample, int tmp_u_stride = u_stride; int tmp_v_stride = v_stride; uint8* rotate_buffer = NULL; - int abs_crop_height = (crop_height < 0) ? -crop_height : crop_height; + const int inv_crop_height = + (src_height < 0) ? -abs_crop_height : abs_crop_height; if (!y || !u || !v || !sample || src_width <= 0 || crop_width <= 0 || src_height == 0 || crop_height == 0) { return -1; } - if (src_height < 0) { - inv_crop_height = -inv_crop_height; - } // One pass rotation is available for some formats. For the rest, convert // to I420 (with optional vertical flipping) into a temporary I420 buffer, @@ -214,7 +213,6 @@ int ConvertToI420(const uint8* sample, break; // Triplanar formats case FOURCC_I420: - case FOURCC_YU12: case FOURCC_YV12: { const uint8* src_y = sample + (src_width * crop_y + crop_x); const uint8* src_u; diff --git a/TMessagesProj/jni/libyuv/source/cpu_id.cc b/TMessagesProj/jni/libyuv/source/cpu_id.cc index ff7bdbd92..84927ebc3 100644 --- a/TMessagesProj/jni/libyuv/source/cpu_id.cc +++ b/TMessagesProj/jni/libyuv/source/cpu_id.cc @@ -10,12 +10,12 @@ #include "libyuv/cpu_id.h" -#if defined(_MSC_VER) && !defined(__clang__) +#if defined(_MSC_VER) #include // For __cpuidex() #endif #if !defined(__pnacl__) && !defined(__CLR_VER) && \ !defined(__native_client__) && (defined(_M_IX86) || defined(_M_X64)) && \ - defined(_MSC_VER) && !defined(__clang__) && (_MSC_FULL_VER >= 160040219) + defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 160040219) #include // For _xgetbv() #endif @@ -36,7 +36,8 @@ extern "C" { // For functions that use the stack and have runtime checks for overflow, // use SAFEBUFFERS to avoid additional check. -#if (defined(_MSC_VER) && !defined(__clang__)) && (_MSC_FULL_VER >= 160040219) +#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 160040219) && \ + !defined(__clang__) #define SAFEBUFFERS __declspec(safebuffers) #else #define SAFEBUFFERS @@ -48,9 +49,9 @@ extern "C" { !defined(__pnacl__) && !defined(__CLR_VER) LIBYUV_API void CpuId(uint32 info_eax, uint32 info_ecx, uint32* cpu_info) { -#if defined(_MSC_VER) && !defined(__clang__) +#if defined(_MSC_VER) // Visual C version uses intrinsic or inline x86 assembly. -#if (_MSC_FULL_VER >= 160040219) +#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 160040219) __cpuidex((int*)(cpu_info), info_eax, info_ecx); #elif defined(_M_IX86) __asm { @@ -71,7 +72,7 @@ void CpuId(uint32 info_eax, uint32 info_ecx, uint32* cpu_info) { } #endif // GCC version uses inline x86 assembly. -#else // defined(_MSC_VER) && !defined(__clang__) +#else // defined(_MSC_VER) uint32 info_ebx, info_edx; asm volatile ( #if defined( __i386__) && defined(__PIC__) @@ -89,7 +90,7 @@ void CpuId(uint32 info_eax, uint32 info_ecx, uint32* cpu_info) { cpu_info[1] = info_ebx; cpu_info[2] = info_ecx; cpu_info[3] = info_edx; -#endif // defined(_MSC_VER) && !defined(__clang__) +#endif // defined(_MSC_VER) } #else // (defined(_M_IX86) || defined(_M_X64) ... LIBYUV_API @@ -117,7 +118,7 @@ void CpuId(uint32 eax, uint32 ecx, uint32* cpu_info) { // X86 CPUs have xgetbv to detect OS saves high parts of ymm registers. int GetXCR0() { uint32 xcr0 = 0u; -#if (_MSC_FULL_VER >= 160040219) +#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 160040219) xcr0 = (uint32)(_xgetbv(0)); // VS2010 SP1 required. #elif defined(__i386__) || defined(__x86_64__) asm(".byte 0x0f, 0x01, 0xd0" : "=a" (xcr0) : "c" (0) : "%edx"); @@ -251,15 +252,11 @@ int InitCpuFlags(void) { #endif #if defined(__mips__) && defined(__linux__) #if defined(__mips_dspr2) - cpu_info |= kCpuHasMIPS_DSPR2; + cpu_info |= kCpuHasDSPR2; #endif cpu_info |= kCpuHasMIPS; - - if (getenv("LIBYUV_DISABLE_MIPS")) { - cpu_info &= ~kCpuHasMIPS; - } - if (getenv("LIBYUV_DISABLE_MIPS_DSPR2")) { - cpu_info &= ~kCpuHasMIPS_DSPR2; + if (getenv("LIBYUV_DISABLE_DSPR2")) { + cpu_info &= ~kCpuHasDSPR2; } #endif #if defined(__arm__) || defined(__aarch64__) diff --git a/TMessagesProj/jni/libyuv/source/planar_functions.cc b/TMessagesProj/jni/libyuv/source/planar_functions.cc index 536e1d528..237ab6831 100644 --- a/TMessagesProj/jni/libyuv/source/planar_functions.cc +++ b/TMessagesProj/jni/libyuv/source/planar_functions.cc @@ -255,11 +255,11 @@ void MirrorPlane(const uint8* src_y, int src_stride_y, } #endif // TODO(fbarchard): Mirror on mips handle unaligned memory. -#if defined(HAS_MIRRORROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && +#if defined(HAS_MIRRORROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(src_y, 4) && IS_ALIGNED(src_stride_y, 4) && IS_ALIGNED(dst_y, 4) && IS_ALIGNED(dst_stride_y, 4)) { - MirrorRow = MirrorRow_MIPS_DSPR2; + MirrorRow = MirrorRow_DSPR2; } #endif @@ -677,7 +677,7 @@ int I420Blend(const uint8* src_y0, int src_stride_y0, #if defined(HAS_BLENDPLANEROW_SSSE3) if (TestCpuFlag(kCpuHasSSSE3)) { - BlendPlaneRow = BlendPlaneRow_Any_SSSE3; + BlendPlaneRow = BlendPlaneRow_Any_SSSE3; if (IS_ALIGNED(halfwidth, 8)) { BlendPlaneRow = BlendPlaneRow_SSSE3; } @@ -685,33 +685,45 @@ int I420Blend(const uint8* src_y0, int src_stride_y0, #endif #if defined(HAS_BLENDPLANEROW_AVX2) if (TestCpuFlag(kCpuHasAVX2)) { - BlendPlaneRow = BlendPlaneRow_Any_AVX2; + BlendPlaneRow = BlendPlaneRow_Any_AVX2; if (IS_ALIGNED(halfwidth, 32)) { BlendPlaneRow = BlendPlaneRow_AVX2; } } #endif + if (!IS_ALIGNED(width, 2)) { + ScaleRowDown2 = ScaleRowDown2Box_Odd_C; + } #if defined(HAS_SCALEROWDOWN2_NEON) if (TestCpuFlag(kCpuHasNEON)) { - ScaleRowDown2 = ScaleRowDown2Box_Any_NEON; - if (IS_ALIGNED(halfwidth, 16)) { - ScaleRowDown2 = ScaleRowDown2Box_NEON; + ScaleRowDown2 = ScaleRowDown2Box_Odd_NEON; + if (IS_ALIGNED(width, 2)) { + ScaleRowDown2 = ScaleRowDown2Box_Any_NEON; + if (IS_ALIGNED(halfwidth, 16)) { + ScaleRowDown2 = ScaleRowDown2Box_NEON; + } } } #endif #if defined(HAS_SCALEROWDOWN2_SSSE3) if (TestCpuFlag(kCpuHasSSSE3)) { - ScaleRowDown2 = ScaleRowDown2Box_Any_SSSE3; - if (IS_ALIGNED(halfwidth, 16)) { - ScaleRowDown2 = ScaleRowDown2Box_SSSE3; + ScaleRowDown2 = ScaleRowDown2Box_Odd_SSSE3; + if (IS_ALIGNED(width, 2)) { + ScaleRowDown2 = ScaleRowDown2Box_Any_SSSE3; + if (IS_ALIGNED(halfwidth, 16)) { + ScaleRowDown2 = ScaleRowDown2Box_SSSE3; + } } } #endif #if defined(HAS_SCALEROWDOWN2_AVX2) if (TestCpuFlag(kCpuHasAVX2)) { - ScaleRowDown2 = ScaleRowDown2Box_Any_AVX2; - if (IS_ALIGNED(halfwidth, 32)) { - ScaleRowDown2 = ScaleRowDown2Box_AVX2; + ScaleRowDown2 = ScaleRowDown2Box_Odd_AVX2; + if (IS_ALIGNED(width, 2)) { + ScaleRowDown2 = ScaleRowDown2Box_Any_AVX2; + if (IS_ALIGNED(halfwidth, 32)) { + ScaleRowDown2 = ScaleRowDown2Box_AVX2; + } } } #endif @@ -974,13 +986,13 @@ static int I422ToRGBAMatrix(const uint8* src_y, int src_stride_y, } } #endif -#if defined(HAS_I422TORGBAROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(width, 4) && +#if defined(HAS_I422TORGBAROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(width, 4) && IS_ALIGNED(src_y, 4) && IS_ALIGNED(src_stride_y, 4) && IS_ALIGNED(src_u, 2) && IS_ALIGNED(src_stride_u, 2) && IS_ALIGNED(src_v, 2) && IS_ALIGNED(src_stride_v, 2) && IS_ALIGNED(dst_rgba, 4) && IS_ALIGNED(dst_stride_rgba, 4)) { - I422ToRGBARow = I422ToRGBARow_MIPS_DSPR2; + I422ToRGBARow = I422ToRGBARow_DSPR2; } #endif @@ -1894,19 +1906,18 @@ int InterpolatePlane(const uint8* src0, int src_stride0, } } #endif -#if defined(HAS_INTERPOLATEROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && +#if defined(HAS_INTERPOLATEROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(src0, 4) && IS_ALIGNED(src_stride0, 4) && IS_ALIGNED(src1, 4) && IS_ALIGNED(src_stride1, 4) && IS_ALIGNED(dst, 4) && IS_ALIGNED(dst_stride, 4) && IS_ALIGNED(width, 4)) { - InterpolateRow = InterpolateRow_MIPS_DSPR2; + InterpolateRow = InterpolateRow_DSPR2; } #endif for (y = 0; y < height; ++y) { - InterpolateRow(dst, src0, src1 - src0, - width, interpolation); + InterpolateRow(dst, src0, src1 - src0, width, interpolation); src0 += src_stride0; src1 += src_stride1; dst += dst_stride; @@ -2363,6 +2374,49 @@ int ARGBCopyAlpha(const uint8* src_argb, int src_stride_argb, return 0; } +// Extract just the alpha channel from ARGB. +LIBYUV_API +int ARGBExtractAlpha(const uint8* src_argb, int src_stride, + uint8* dst_a, int dst_stride, + int width, int height) { + if (!src_argb || !dst_a || width <= 0 || height == 0) { + return -1; + } + // Negative height means invert the image. + if (height < 0) { + height = -height; + src_argb += (height - 1) * src_stride; + src_stride = -src_stride; + } + // Coalesce rows. + if (src_stride == width * 4 && dst_stride == width) { + width *= height; + height = 1; + src_stride = dst_stride = 0; + } + void (*ARGBExtractAlphaRow)(const uint8 *src_argb, uint8 *dst_a, int width) = + ARGBExtractAlphaRow_C; +#if defined(HAS_ARGBEXTRACTALPHAROW_SSE2) + if (TestCpuFlag(kCpuHasSSE2)) { + ARGBExtractAlphaRow = IS_ALIGNED(width, 8) ? ARGBExtractAlphaRow_SSE2 + : ARGBExtractAlphaRow_Any_SSE2; + } +#endif +#if defined(HAS_ARGBEXTRACTALPHAROW_NEON) + if (TestCpuFlag(kCpuHasNEON)) { + ARGBExtractAlphaRow = IS_ALIGNED(width, 16) ? ARGBExtractAlphaRow_NEON + : ARGBExtractAlphaRow_Any_NEON; + } +#endif + + for (int y = 0; y < height; ++y) { + ARGBExtractAlphaRow(src_argb, dst_a, width); + src_argb += src_stride; + dst_a += dst_stride; + } + return 0; +} + // Copy a planar Y channel to the alpha channel of a destination ARGB image. LIBYUV_API int ARGBCopyYToAlpha(const uint8* src_y, int src_stride_y, @@ -2412,6 +2466,9 @@ int ARGBCopyYToAlpha(const uint8* src_y, int src_stride_y, return 0; } +// TODO(fbarchard): Consider if width is even Y channel can be split +// directly. A SplitUVRow_Odd function could copy the remaining chroma. + LIBYUV_API int YUY2ToNV12(const uint8* src_yuy2, int src_stride_yuy2, uint8* dst_y, int dst_stride_y, @@ -2486,22 +2543,24 @@ int YUY2ToNV12(const uint8* src_yuy2, int src_stride_yuy2, { int awidth = halfwidth * 2; - // 2 rows of uv - align_buffer_64(rows, awidth * 2); + // row of y and 2 rows of uv + align_buffer_64(rows, awidth * 3); for (y = 0; y < height - 1; y += 2) { // Split Y from UV. - SplitUVRow(src_yuy2, dst_y, rows, awidth); - SplitUVRow(src_yuy2 + src_stride_yuy2, dst_y + dst_stride_y, - rows + awidth, awidth); - InterpolateRow(dst_uv, rows, awidth, awidth, 128); + SplitUVRow(src_yuy2, rows, rows + awidth, awidth); + memcpy(dst_y, rows, width); + SplitUVRow(src_yuy2 + src_stride_yuy2, rows, rows + awidth * 2, awidth); + memcpy(dst_y + dst_stride_y, rows, width); + InterpolateRow(dst_uv, rows + awidth, awidth, awidth, 128); src_yuy2 += src_stride_yuy2 * 2; dst_y += dst_stride_y * 2; dst_uv += dst_stride_uv; } if (height & 1) { // Split Y from UV. - SplitUVRow(src_yuy2, dst_y, dst_uv, awidth); + SplitUVRow(src_yuy2, rows, dst_uv, awidth); + memcpy(dst_y, rows, width); } free_aligned_buffer_64(rows); } @@ -2582,22 +2641,24 @@ int UYVYToNV12(const uint8* src_uyvy, int src_stride_uyvy, { int awidth = halfwidth * 2; - // 2 rows of uv - align_buffer_64(rows, awidth * 2); + // row of y and 2 rows of uv + align_buffer_64(rows, awidth * 3); for (y = 0; y < height - 1; y += 2) { // Split Y from UV. - SplitUVRow(src_uyvy, rows, dst_y, awidth); - SplitUVRow(src_uyvy + src_stride_uyvy, rows + awidth, - dst_y + dst_stride_y, awidth); - InterpolateRow(dst_uv, rows, awidth, awidth, 128); + SplitUVRow(src_uyvy, rows + awidth, rows, awidth); + memcpy(dst_y, rows, width); + SplitUVRow(src_uyvy + src_stride_uyvy, rows + awidth * 2, rows, awidth); + memcpy(dst_y + dst_stride_y, rows, width); + InterpolateRow(dst_uv, rows + awidth, awidth, awidth, 128); src_uyvy += src_stride_uyvy * 2; dst_y += dst_stride_y * 2; dst_uv += dst_stride_uv; } if (height & 1) { // Split Y from UV. - SplitUVRow(src_uyvy, dst_uv, dst_y, awidth); + SplitUVRow(src_uyvy, dst_uv, rows, awidth); + memcpy(dst_y, rows, width); } free_aligned_buffer_64(rows); } diff --git a/TMessagesProj/jni/libyuv/source/rotate.cc b/TMessagesProj/jni/libyuv/source/rotate.cc index 31e04af9c..01ea5c407 100644 --- a/TMessagesProj/jni/libyuv/source/rotate.cc +++ b/TMessagesProj/jni/libyuv/source/rotate.cc @@ -49,13 +49,13 @@ void TransposePlane(const uint8* src, int src_stride, } } #endif -#if defined(HAS_TRANSPOSEWX8_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2)) { +#if defined(HAS_TRANSPOSEWX8_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2)) { if (IS_ALIGNED(width, 4) && IS_ALIGNED(src, 4) && IS_ALIGNED(src_stride, 4)) { - TransposeWx8 = TransposeWx8_Fast_MIPS_DSPR2; + TransposeWx8 = TransposeWx8_Fast_DSPR2; } else { - TransposeWx8 = TransposeWx8_MIPS_DSPR2; + TransposeWx8 = TransposeWx8_DSPR2; } } #endif @@ -134,11 +134,11 @@ void RotatePlane180(const uint8* src, int src_stride, } #endif // TODO(fbarchard): Mirror on mips handle unaligned memory. -#if defined(HAS_MIRRORROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && +#if defined(HAS_MIRRORROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(src, 4) && IS_ALIGNED(src_stride, 4) && IS_ALIGNED(dst, 4) && IS_ALIGNED(dst_stride, 4)) { - MirrorRow = MirrorRow_MIPS_DSPR2; + MirrorRow = MirrorRow_DSPR2; } #endif #if defined(HAS_COPYROW_SSE2) @@ -203,10 +203,10 @@ void TransposeUV(const uint8* src, int src_stride, } } #endif -#if defined(HAS_TRANSPOSEUVWX8_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(width, 2) && +#if defined(HAS_TRANSPOSEUVWX8_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(width, 2) && IS_ALIGNED(src, 4) && IS_ALIGNED(src_stride, 4)) { - TransposeUVWx8 = TransposeUVWx8_MIPS_DSPR2; + TransposeUVWx8 = TransposeUVWx8_DSPR2; } #endif @@ -267,22 +267,22 @@ void RotateUV180(const uint8* src, int src_stride, uint8* dst_b, int dst_stride_b, int width, int height) { int i; - void (*MirrorRowUV)(const uint8* src, uint8* dst_u, uint8* dst_v, int width) = + void (*MirrorUVRow)(const uint8* src, uint8* dst_u, uint8* dst_v, int width) = MirrorUVRow_C; #if defined(HAS_MIRRORUVROW_NEON) if (TestCpuFlag(kCpuHasNEON) && IS_ALIGNED(width, 8)) { - MirrorRowUV = MirrorUVRow_NEON; + MirrorUVRow = MirrorUVRow_NEON; } #endif -#if defined(HAS_MIRRORROW_UV_SSSE3) +#if defined(HAS_MIRRORUVROW_SSSE3) if (TestCpuFlag(kCpuHasSSSE3) && IS_ALIGNED(width, 16)) { - MirrorRowUV = MirrorUVRow_SSSE3; + MirrorUVRow = MirrorUVRow_SSSE3; } #endif -#if defined(HAS_MIRRORUVROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && +#if defined(HAS_MIRRORUVROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(src, 4) && IS_ALIGNED(src_stride, 4)) { - MirrorRowUV = MirrorUVRow_MIPS_DSPR2; + MirrorUVRow = MirrorUVRow_DSPR2; } #endif @@ -290,7 +290,7 @@ void RotateUV180(const uint8* src, int src_stride, dst_b += dst_stride_b * (height - 1); for (i = 0; i < height; ++i) { - MirrorRowUV(src, dst_a, dst_b, width); + MirrorUVRow(src, dst_a, dst_b, width); src += src_stride; dst_a -= dst_stride_a; dst_b -= dst_stride_b; diff --git a/TMessagesProj/jni/libyuv/source/rotate_any.cc b/TMessagesProj/jni/libyuv/source/rotate_any.cc index d12bad5dc..31a74c315 100644 --- a/TMessagesProj/jni/libyuv/source/rotate_any.cc +++ b/TMessagesProj/jni/libyuv/source/rotate_any.cc @@ -38,8 +38,8 @@ TANY(TransposeWx8_Any_SSSE3, TransposeWx8_SSSE3, 7) #ifdef HAS_TRANSPOSEWX8_FAST_SSSE3 TANY(TransposeWx8_Fast_Any_SSSE3, TransposeWx8_Fast_SSSE3, 15) #endif -#ifdef HAS_TRANSPOSEWX8_MIPS_DSPR2 -TANY(TransposeWx8_Any_MIPS_DSPR2, TransposeWx8_MIPS_DSPR2, 7) +#ifdef HAS_TRANSPOSEWX8_DSPR2 +TANY(TransposeWx8_Any_DSPR2, TransposeWx8_DSPR2, 7) #endif #undef TANY @@ -64,8 +64,8 @@ TUVANY(TransposeUVWx8_Any_NEON, TransposeUVWx8_NEON, 7) #ifdef HAS_TRANSPOSEUVWX8_SSE2 TUVANY(TransposeUVWx8_Any_SSE2, TransposeUVWx8_SSE2, 7) #endif -#ifdef HAS_TRANSPOSEUVWX8_MIPS_DSPR2 -TUVANY(TransposeUVWx8_Any_MIPS_DSPR2, TransposeUVWx8_MIPS_DSPR2, 7) +#ifdef HAS_TRANSPOSEUVWX8_DSPR2 +TUVANY(TransposeUVWx8_Any_DSPR2, TransposeUVWx8_DSPR2, 7) #endif #undef TUVANY diff --git a/TMessagesProj/jni/libyuv/source/rotate_mips.cc b/TMessagesProj/jni/libyuv/source/rotate_mips.cc index efe6bd909..1e8ce2519 100644 --- a/TMessagesProj/jni/libyuv/source/rotate_mips.cc +++ b/TMessagesProj/jni/libyuv/source/rotate_mips.cc @@ -22,8 +22,8 @@ extern "C" { defined(__mips_dsp) && (__mips_dsp_rev >= 2) && \ (_MIPS_SIM == _MIPS_SIM_ABI32) -void TransposeWx8_MIPS_DSPR2(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width) { +void TransposeWx8_DSPR2(const uint8* src, int src_stride, + uint8* dst, int dst_stride, int width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -106,8 +106,8 @@ void TransposeWx8_MIPS_DSPR2(const uint8* src, int src_stride, ); } -void TransposeWx8_Fast_MIPS_DSPR2(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width) { +void TransposeWx8_Fast_DSPR2(const uint8* src, int src_stride, + uint8* dst, int dst_stride, int width) { __asm__ __volatile__ ( ".set noat \n" ".set push \n" @@ -308,10 +308,10 @@ void TransposeWx8_Fast_MIPS_DSPR2(const uint8* src, int src_stride, ); } -void TransposeUVWx8_MIPS_DSPR2(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, - int width) { +void TransposeUVWx8_DSPR2(const uint8* src, int src_stride, + uint8* dst_a, int dst_stride_a, + uint8* dst_b, int dst_stride_b, + int width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" diff --git a/TMessagesProj/jni/libyuv/source/rotate_neon.cc b/TMessagesProj/jni/libyuv/source/rotate_neon.cc index 9e4ecd80d..1c22b472b 100644 --- a/TMessagesProj/jni/libyuv/source/rotate_neon.cc +++ b/TMessagesProj/jni/libyuv/source/rotate_neon.cc @@ -27,7 +27,7 @@ static uvec8 kVTbl4x4Transpose = void TransposeWx8_NEON(const uint8* src, int src_stride, uint8* dst, int dst_stride, int width) { - const uint8* src_temp = NULL; + const uint8* src_temp; asm volatile ( // loops are on blocks of 8. loop will stop when // counter gets to or below 0. starting the counter @@ -229,7 +229,7 @@ void TransposeWx8_NEON(const uint8* src, int src_stride, "4: \n" - : "+r"(src_temp), // %0 + : "=&r"(src_temp), // %0 "+r"(src), // %1 "+r"(src_stride), // %2 "+r"(dst), // %3 @@ -247,7 +247,7 @@ void TransposeUVWx8_NEON(const uint8* src, int src_stride, uint8* dst_a, int dst_stride_a, uint8* dst_b, int dst_stride_b, int width) { - const uint8* src_temp = NULL; + const uint8* src_temp; asm volatile ( // loops are on blocks of 8. loop will stop when // counter gets to or below 0. starting the counter @@ -512,7 +512,7 @@ void TransposeUVWx8_NEON(const uint8* src, int src_stride, "4: \n" - : "+r"(src_temp), // %0 + : "=&r"(src_temp), // %0 "+r"(src), // %1 "+r"(src_stride), // %2 "+r"(dst_a), // %3 diff --git a/TMessagesProj/jni/libyuv/source/rotate_neon64.cc b/TMessagesProj/jni/libyuv/source/rotate_neon64.cc index f52c082b3..1ab448f3a 100644 --- a/TMessagesProj/jni/libyuv/source/rotate_neon64.cc +++ b/TMessagesProj/jni/libyuv/source/rotate_neon64.cc @@ -26,7 +26,7 @@ static uvec8 kVTbl4x4Transpose = void TransposeWx8_NEON(const uint8* src, int src_stride, uint8* dst, int dst_stride, int width) { - const uint8* src_temp = NULL; + const uint8* src_temp; int64 width64 = (int64) width; // Work around clang 3.4 warning. asm volatile ( // loops are on blocks of 8. loop will stop when @@ -235,7 +235,7 @@ void TransposeWx8_NEON(const uint8* src, int src_stride, "4: \n" - : "+r"(src_temp), // %0 + : "=&r"(src_temp), // %0 "+r"(src), // %1 "+r"(dst), // %2 "+r"(width64) // %3 @@ -255,7 +255,7 @@ void TransposeUVWx8_NEON(const uint8* src, int src_stride, uint8* dst_a, int dst_stride_a, uint8* dst_b, int dst_stride_b, int width) { - const uint8* src_temp = NULL; + const uint8* src_temp; int64 width64 = (int64) width; // Work around clang 3.4 warning. asm volatile ( // loops are on blocks of 8. loop will stop when @@ -520,7 +520,7 @@ void TransposeUVWx8_NEON(const uint8* src, int src_stride, "4: \n" - : "+r"(src_temp), // %0 + : "=&r"(src_temp), // %0 "+r"(src), // %1 "+r"(dst_a), // %2 "+r"(dst_b), // %3 diff --git a/TMessagesProj/jni/libyuv/source/row_any.cc b/TMessagesProj/jni/libyuv/source/row_any.cc index 5e5f435a6..494164fd0 100644 --- a/TMessagesProj/jni/libyuv/source/row_any.cc +++ b/TMessagesProj/jni/libyuv/source/row_any.cc @@ -466,38 +466,15 @@ ANY11(ARGBUnattenuateRow_Any_AVX2, ARGBUnattenuateRow_AVX2, 0, 4, 4, 7) #ifdef HAS_ARGBATTENUATEROW_NEON ANY11(ARGBAttenuateRow_Any_NEON, ARGBAttenuateRow_NEON, 0, 4, 4, 7) #endif +#ifdef HAS_ARGBEXTRACTALPHAROW_SSE2 +ANY11(ARGBExtractAlphaRow_Any_SSE2, ARGBExtractAlphaRow_SSE2, 0, 4, 1, 7) +#endif +#ifdef HAS_ARGBEXTRACTALPHAROW_NEON +ANY11(ARGBExtractAlphaRow_Any_NEON, ARGBExtractAlphaRow_NEON, 0, 4, 1, 15) +#endif #undef ANY11 -// Any 1 to 1 with yuvconstants -#define ANY11C(NAMEANY, ANY_SIMD, UVSHIFT, SBPP, BPP, MASK) \ - void NAMEANY(const uint8* src_ptr, uint8* dst_ptr, \ - const struct YuvConstants* yuvconstants, int width) { \ - SIMD_ALIGNED(uint8 temp[128 * 2]); \ - memset(temp, 0, 128); /* for YUY2 and msan */ \ - int r = width & MASK; \ - int n = width & ~MASK; \ - if (n > 0) { \ - ANY_SIMD(src_ptr, dst_ptr, yuvconstants, n); \ - } \ - memcpy(temp, src_ptr + (n >> UVSHIFT) * SBPP, SS(r, UVSHIFT) * SBPP); \ - ANY_SIMD(temp, temp + 128, yuvconstants, MASK + 1); \ - memcpy(dst_ptr + n * BPP, temp + 128, r * BPP); \ - } -#if defined(HAS_YUY2TOARGBROW_SSSE3) -ANY11C(YUY2ToARGBRow_Any_SSSE3, YUY2ToARGBRow_SSSE3, 1, 4, 4, 15) -ANY11C(UYVYToARGBRow_Any_SSSE3, UYVYToARGBRow_SSSE3, 1, 4, 4, 15) -#endif -#if defined(HAS_YUY2TOARGBROW_AVX2) -ANY11C(YUY2ToARGBRow_Any_AVX2, YUY2ToARGBRow_AVX2, 1, 4, 4, 31) -ANY11C(UYVYToARGBRow_Any_AVX2, UYVYToARGBRow_AVX2, 1, 4, 4, 31) -#endif -#if defined(HAS_YUY2TOARGBROW_NEON) -ANY11C(YUY2ToARGBRow_Any_NEON, YUY2ToARGBRow_NEON, 1, 4, 4, 7) -ANY11C(UYVYToARGBRow_Any_NEON, UYVYToARGBRow_NEON, 1, 4, 4, 7) -#endif -#undef ANY11C - -// Any 1 to 1 blended. +// Any 1 to 1 blended. Destination is read, modify, write. #define ANY11B(NAMEANY, ANY_SIMD, UVSHIFT, SBPP, BPP, MASK) \ void NAMEANY(const uint8* src_ptr, uint8* dst_ptr, int width) { \ SIMD_ALIGNED(uint8 temp[128 * 2]); \ @@ -516,7 +493,7 @@ ANY11C(UYVYToARGBRow_Any_NEON, UYVYToARGBRow_NEON, 1, 4, 4, 7) #ifdef HAS_ARGBCOPYALPHAROW_AVX2 ANY11B(ARGBCopyAlphaRow_Any_AVX2, ARGBCopyAlphaRow_AVX2, 0, 4, 4, 15) #endif -#ifdef HAS_ARGBCOPYYTOALPHAROW_SSE2 +#ifdef HAS_ARGBCOPYALPHAROW_SSE2 ANY11B(ARGBCopyAlphaRow_Any_SSE2, ARGBCopyAlphaRow_SSE2, 0, 4, 4, 7) #endif #ifdef HAS_ARGBCOPYYTOALPHAROW_AVX2 @@ -569,6 +546,35 @@ ANY11P(ARGBShuffleRow_Any_NEON, ARGBShuffleRow_NEON, const uint8*, 4, 4, 3) #endif #undef ANY11P +// Any 1 to 1 with yuvconstants +#define ANY11C(NAMEANY, ANY_SIMD, UVSHIFT, SBPP, BPP, MASK) \ + void NAMEANY(const uint8* src_ptr, uint8* dst_ptr, \ + const struct YuvConstants* yuvconstants, int width) { \ + SIMD_ALIGNED(uint8 temp[128 * 2]); \ + memset(temp, 0, 128); /* for YUY2 and msan */ \ + int r = width & MASK; \ + int n = width & ~MASK; \ + if (n > 0) { \ + ANY_SIMD(src_ptr, dst_ptr, yuvconstants, n); \ + } \ + memcpy(temp, src_ptr + (n >> UVSHIFT) * SBPP, SS(r, UVSHIFT) * SBPP); \ + ANY_SIMD(temp, temp + 128, yuvconstants, MASK + 1); \ + memcpy(dst_ptr + n * BPP, temp + 128, r * BPP); \ + } +#if defined(HAS_YUY2TOARGBROW_SSSE3) +ANY11C(YUY2ToARGBRow_Any_SSSE3, YUY2ToARGBRow_SSSE3, 1, 4, 4, 15) +ANY11C(UYVYToARGBRow_Any_SSSE3, UYVYToARGBRow_SSSE3, 1, 4, 4, 15) +#endif +#if defined(HAS_YUY2TOARGBROW_AVX2) +ANY11C(YUY2ToARGBRow_Any_AVX2, YUY2ToARGBRow_AVX2, 1, 4, 4, 31) +ANY11C(UYVYToARGBRow_Any_AVX2, UYVYToARGBRow_AVX2, 1, 4, 4, 31) +#endif +#if defined(HAS_YUY2TOARGBROW_NEON) +ANY11C(YUY2ToARGBRow_Any_NEON, YUY2ToARGBRow_NEON, 1, 4, 4, 7) +ANY11C(UYVYToARGBRow_Any_NEON, UYVYToARGBRow_NEON, 1, 4, 4, 7) +#endif +#undef ANY11C + // Any 1 to 1 interpolate. Takes 2 rows of source via stride. #define ANY11T(NAMEANY, ANY_SIMD, SBPP, BPP, MASK) \ void NAMEANY(uint8* dst_ptr, const uint8* src_ptr, \ @@ -596,8 +602,8 @@ ANY11T(InterpolateRow_Any_SSSE3, InterpolateRow_SSSE3, 1, 1, 15) #ifdef HAS_INTERPOLATEROW_NEON ANY11T(InterpolateRow_Any_NEON, InterpolateRow_NEON, 1, 1, 15) #endif -#ifdef HAS_INTERPOLATEROW_MIPS_DSPR2 -ANY11T(InterpolateRow_Any_MIPS_DSPR2, InterpolateRow_MIPS_DSPR2, 1, 1, 3) +#ifdef HAS_INTERPOLATEROW_DSPR2 +ANY11T(InterpolateRow_Any_DSPR2, InterpolateRow_DSPR2, 1, 1, 3) #endif #undef ANY11T @@ -705,8 +711,8 @@ ANY12(SplitUVRow_Any_AVX2, SplitUVRow_AVX2, 0, 2, 0, 31) #ifdef HAS_SPLITUVROW_NEON ANY12(SplitUVRow_Any_NEON, SplitUVRow_NEON, 0, 2, 0, 15) #endif -#ifdef HAS_SPLITUVROW_MIPS_DSPR2 -ANY12(SplitUVRow_Any_MIPS_DSPR2, SplitUVRow_MIPS_DSPR2, 0, 2, 0, 15) +#ifdef HAS_SPLITUVROW_DSPR2 +ANY12(SplitUVRow_Any_DSPR2, SplitUVRow_DSPR2, 0, 2, 0, 15) #endif #ifdef HAS_ARGBTOUV444ROW_SSSE3 ANY12(ARGBToUV444Row_Any_SSSE3, ARGBToUV444Row_SSSE3, 0, 4, 0, 15) @@ -715,16 +721,12 @@ ANY12(ARGBToUV444Row_Any_SSSE3, ARGBToUV444Row_SSSE3, 0, 4, 0, 15) ANY12(YUY2ToUV422Row_Any_AVX2, YUY2ToUV422Row_AVX2, 1, 4, 1, 31) ANY12(UYVYToUV422Row_Any_AVX2, UYVYToUV422Row_AVX2, 1, 4, 1, 31) #endif -#ifdef HAS_ARGBTOUV422ROW_SSSE3 -ANY12(ARGBToUV422Row_Any_SSSE3, ARGBToUV422Row_SSSE3, 0, 4, 1, 15) -#endif #ifdef HAS_YUY2TOUV422ROW_SSE2 ANY12(YUY2ToUV422Row_Any_SSE2, YUY2ToUV422Row_SSE2, 1, 4, 1, 15) ANY12(UYVYToUV422Row_Any_SSE2, UYVYToUV422Row_SSE2, 1, 4, 1, 15) #endif #ifdef HAS_YUY2TOUV422ROW_NEON ANY12(ARGBToUV444Row_Any_NEON, ARGBToUV444Row_NEON, 0, 4, 0, 7) -ANY12(ARGBToUV422Row_Any_NEON, ARGBToUV422Row_NEON, 0, 4, 1, 15) ANY12(ARGBToUV411Row_Any_NEON, ARGBToUV411Row_NEON, 0, 4, 2, 31) ANY12(YUY2ToUV422Row_Any_NEON, YUY2ToUV422Row_NEON, 1, 4, 1, 15) ANY12(UYVYToUV422Row_Any_NEON, UYVYToUV422Row_NEON, 1, 4, 1, 15) @@ -760,6 +762,9 @@ ANY12(UYVYToUV422Row_Any_NEON, UYVYToUV422Row_NEON, 1, 4, 1, 15) #ifdef HAS_ARGBTOUVROW_AVX2 ANY12S(ARGBToUVRow_Any_AVX2, ARGBToUVRow_AVX2, 0, 4, 31) #endif +#ifdef HAS_ARGBTOUVJROW_AVX2 +ANY12S(ARGBToUVJRow_Any_AVX2, ARGBToUVJRow_AVX2, 0, 4, 31) +#endif #ifdef HAS_ARGBTOUVROW_SSSE3 ANY12S(ARGBToUVRow_Any_SSSE3, ARGBToUVRow_SSSE3, 0, 4, 15) ANY12S(ARGBToUVJRow_Any_SSSE3, ARGBToUVJRow_SSSE3, 0, 4, 15) diff --git a/TMessagesProj/jni/libyuv/source/row_common.cc b/TMessagesProj/jni/libyuv/source/row_common.cc index c820cdf1f..32d2f686f 100644 --- a/TMessagesProj/jni/libyuv/source/row_common.cc +++ b/TMessagesProj/jni/libyuv/source/row_common.cc @@ -433,28 +433,6 @@ void NAME ## ToUVJRow_C(const uint8* src_rgb0, int src_stride_rgb, \ MAKEROWYJ(ARGB, 2, 1, 0, 4) #undef MAKEROWYJ -void ARGBToUVJ422Row_C(const uint8* src_argb, - uint8* dst_u, uint8* dst_v, int width) { - int x; - for (x = 0; x < width - 1; x += 2) { - uint8 ab = (src_argb[0] + src_argb[4]) >> 1; - uint8 ag = (src_argb[1] + src_argb[5]) >> 1; - uint8 ar = (src_argb[2] + src_argb[6]) >> 1; - dst_u[0] = RGBToUJ(ar, ag, ab); - dst_v[0] = RGBToVJ(ar, ag, ab); - src_argb += 8; - dst_u += 1; - dst_v += 1; - } - if (width & 1) { - uint8 ab = src_argb[0]; - uint8 ag = src_argb[1]; - uint8 ar = src_argb[2]; - dst_u[0] = RGBToUJ(ar, ag, ab); - dst_v[0] = RGBToVJ(ar, ag, ab); - } -} - void RGB565ToYRow_C(const uint8* src_rgb565, uint8* dst_y, int width) { int x; for (x = 0; x < width; ++x) { @@ -658,28 +636,6 @@ void ARGBToUV444Row_C(const uint8* src_argb, } } -void ARGBToUV422Row_C(const uint8* src_argb, - uint8* dst_u, uint8* dst_v, int width) { - int x; - for (x = 0; x < width - 1; x += 2) { - uint8 ab = (src_argb[0] + src_argb[4]) >> 1; - uint8 ag = (src_argb[1] + src_argb[5]) >> 1; - uint8 ar = (src_argb[2] + src_argb[6]) >> 1; - dst_u[0] = RGBToU(ar, ag, ab); - dst_v[0] = RGBToV(ar, ag, ab); - src_argb += 8; - dst_u += 1; - dst_v += 1; - } - if (width & 1) { - uint8 ab = src_argb[0]; - uint8 ag = src_argb[1]; - uint8 ar = src_argb[2]; - dst_u[0] = RGBToU(ar, ag, ab); - dst_v[0] = RGBToV(ar, ag, ab); - } -} - void ARGBToUV411Row_C(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width) { int x; @@ -2191,7 +2147,7 @@ void ARGBAffineRow_C(const uint8* src_argb, int src_argb_stride, } // Blend 2 rows into 1. -static void HalfRow_C(const uint8* src_uv, int src_uv_stride, +static void HalfRow_C(const uint8* src_uv, ptrdiff_t src_uv_stride, uint8* dst_uv, int width) { int x; for (x = 0; x < width; ++x) { @@ -2199,7 +2155,7 @@ static void HalfRow_C(const uint8* src_uv, int src_uv_stride, } } -static void HalfRow_16_C(const uint16* src_uv, int src_uv_stride, +static void HalfRow_16_C(const uint16* src_uv, ptrdiff_t src_uv_stride, uint16* dst_uv, int width) { int x; for (x = 0; x < width; ++x) { @@ -2220,7 +2176,7 @@ void InterpolateRow_C(uint8* dst_ptr, const uint8* src_ptr, return; } if (y1_fraction == 128) { - HalfRow_C(src_ptr, (int)(src_stride), dst_ptr, width); + HalfRow_C(src_ptr, src_stride, dst_ptr, width); return; } for (x = 0; x < width - 1; x += 2) { @@ -2250,7 +2206,7 @@ void InterpolateRow_16_C(uint16* dst_ptr, const uint16* src_ptr, return; } if (source_y_fraction == 128) { - HalfRow_16_C(src_ptr, (int)(src_stride), dst_ptr, width); + HalfRow_16_C(src_ptr, src_stride, dst_ptr, width); return; } for (x = 0; x < width - 1; x += 2) { @@ -2425,6 +2381,19 @@ void ARGBCopyAlphaRow_C(const uint8* src, uint8* dst, int width) { } } +void ARGBExtractAlphaRow_C(const uint8* src_argb, uint8* dst_a, int width) { + int i; + for (i = 0; i < width - 1; i += 2) { + dst_a[0] = src_argb[3]; + dst_a[1] = src_argb[7]; + dst_a += 2; + src_argb += 8; + } + if (width & 1) { + dst_a[0] = src_argb[3]; + } +} + void ARGBCopyYToAlphaRow_C(const uint8* src, uint8* dst, int width) { int i; for (i = 0; i < width - 1; i += 2) { @@ -2539,7 +2508,11 @@ void I422ToRGB565Row_AVX2(const uint8* src_y, while (width > 0) { int twidth = width > MAXTWIDTH ? MAXTWIDTH : width; I422ToARGBRow_AVX2(src_y, src_u, src_v, row, yuvconstants, twidth); +#if defined(HAS_ARGBTORGB565ROW_AVX2) ARGBToRGB565Row_AVX2(row, dst_rgb565, twidth); +#else + ARGBToRGB565Row_SSE2(row, dst_rgb565, twidth); +#endif src_y += twidth; src_u += twidth / 2; src_v += twidth / 2; @@ -2561,7 +2534,11 @@ void I422ToARGB1555Row_AVX2(const uint8* src_y, while (width > 0) { int twidth = width > MAXTWIDTH ? MAXTWIDTH : width; I422ToARGBRow_AVX2(src_y, src_u, src_v, row, yuvconstants, twidth); +#if defined(HAS_ARGBTOARGB1555ROW_AVX2) ARGBToARGB1555Row_AVX2(row, dst_argb1555, twidth); +#else + ARGBToARGB1555Row_SSE2(row, dst_argb1555, twidth); +#endif src_y += twidth; src_u += twidth / 2; src_v += twidth / 2; @@ -2583,7 +2560,11 @@ void I422ToARGB4444Row_AVX2(const uint8* src_y, while (width > 0) { int twidth = width > MAXTWIDTH ? MAXTWIDTH : width; I422ToARGBRow_AVX2(src_y, src_u, src_v, row, yuvconstants, twidth); +#if defined(HAS_ARGBTOARGB4444ROW_AVX2) ARGBToARGB4444Row_AVX2(row, dst_argb4444, twidth); +#else + ARGBToARGB4444Row_SSE2(row, dst_argb4444, twidth); +#endif src_y += twidth; src_u += twidth / 2; src_v += twidth / 2; @@ -2627,7 +2608,11 @@ void NV12ToRGB565Row_AVX2(const uint8* src_y, while (width > 0) { int twidth = width > MAXTWIDTH ? MAXTWIDTH : width; NV12ToARGBRow_AVX2(src_y, src_uv, row, yuvconstants, twidth); +#if defined(HAS_ARGBTORGB565ROW_AVX2) ARGBToRGB565Row_AVX2(row, dst_rgb565, twidth); +#else + ARGBToRGB565Row_SSE2(row, dst_rgb565, twidth); +#endif src_y += twidth; src_uv += twidth; dst_rgb565 += twidth * 2; diff --git a/TMessagesProj/jni/libyuv/source/row_gcc.cc b/TMessagesProj/jni/libyuv/source/row_gcc.cc index 80b2a95aa..1ac7ef1aa 100644 --- a/TMessagesProj/jni/libyuv/source/row_gcc.cc +++ b/TMessagesProj/jni/libyuv/source/row_gcc.cc @@ -1023,6 +1023,67 @@ void ARGBToUVRow_AVX2(const uint8* src_argb0, int src_stride_argb, } #endif // HAS_ARGBTOUVROW_AVX2 +#ifdef HAS_ARGBTOUVJROW_AVX2 +void ARGBToUVJRow_AVX2(const uint8* src_argb0, int src_stride_argb, + uint8* dst_u, uint8* dst_v, int width) { + asm volatile ( + "vbroadcastf128 %5,%%ymm5 \n" + "vbroadcastf128 %6,%%ymm6 \n" + "vbroadcastf128 %7,%%ymm7 \n" + "sub %1,%2 \n" + LABELALIGN + "1: \n" + "vmovdqu " MEMACCESS(0) ",%%ymm0 \n" + "vmovdqu " MEMACCESS2(0x20,0) ",%%ymm1 \n" + "vmovdqu " MEMACCESS2(0x40,0) ",%%ymm2 \n" + "vmovdqu " MEMACCESS2(0x60,0) ",%%ymm3 \n" + VMEMOPREG(vpavgb,0x00,0,4,1,ymm0,ymm0) // vpavgb (%0,%4,1),%%ymm0,%%ymm0 + VMEMOPREG(vpavgb,0x20,0,4,1,ymm1,ymm1) + VMEMOPREG(vpavgb,0x40,0,4,1,ymm2,ymm2) + VMEMOPREG(vpavgb,0x60,0,4,1,ymm3,ymm3) + "lea " MEMLEA(0x80,0) ",%0 \n" + "vshufps $0x88,%%ymm1,%%ymm0,%%ymm4 \n" + "vshufps $0xdd,%%ymm1,%%ymm0,%%ymm0 \n" + "vpavgb %%ymm4,%%ymm0,%%ymm0 \n" + "vshufps $0x88,%%ymm3,%%ymm2,%%ymm4 \n" + "vshufps $0xdd,%%ymm3,%%ymm2,%%ymm2 \n" + "vpavgb %%ymm4,%%ymm2,%%ymm2 \n" + + "vpmaddubsw %%ymm7,%%ymm0,%%ymm1 \n" + "vpmaddubsw %%ymm7,%%ymm2,%%ymm3 \n" + "vpmaddubsw %%ymm6,%%ymm0,%%ymm0 \n" + "vpmaddubsw %%ymm6,%%ymm2,%%ymm2 \n" + "vphaddw %%ymm3,%%ymm1,%%ymm1 \n" + "vphaddw %%ymm2,%%ymm0,%%ymm0 \n" + "vpaddw %%ymm5,%%ymm0,%%ymm0 \n" + "vpaddw %%ymm5,%%ymm1,%%ymm1 \n" + "vpsraw $0x8,%%ymm1,%%ymm1 \n" + "vpsraw $0x8,%%ymm0,%%ymm0 \n" + "vpacksswb %%ymm0,%%ymm1,%%ymm0 \n" + "vpermq $0xd8,%%ymm0,%%ymm0 \n" + "vpshufb %8,%%ymm0,%%ymm0 \n" + + "vextractf128 $0x0,%%ymm0," MEMACCESS(1) " \n" + VEXTOPMEM(vextractf128,1,ymm0,0x0,1,2,1) // vextractf128 $1,%%ymm0,(%1,%2,1) + "lea " MEMLEA(0x10,1) ",%1 \n" + "sub $0x20,%3 \n" + "jg 1b \n" + "vzeroupper \n" + : "+r"(src_argb0), // %0 + "+r"(dst_u), // %1 + "+r"(dst_v), // %2 + "+rm"(width) // %3 + : "r"((intptr_t)(src_stride_argb)), // %4 + "m"(kAddUVJ128), // %5 + "m"(kARGBToVJ), // %6 + "m"(kARGBToUJ), // %7 + "m"(kShufARGBToUV_AVX) // %8 + : "memory", "cc", NACL_R14 + "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7" + ); +} +#endif // HAS_ARGBTOUVJROW_AVX2 + #ifdef HAS_ARGBTOUVJROW_SSSE3 void ARGBToUVJRow_SSSE3(const uint8* src_argb0, int src_stride_argb, uint8* dst_u, uint8* dst_v, int width) { @@ -1144,59 +1205,6 @@ void ARGBToUV444Row_SSSE3(const uint8* src_argb, uint8* dst_u, uint8* dst_v, } #endif // HAS_ARGBTOUV444ROW_SSSE3 -#ifdef HAS_ARGBTOUV422ROW_SSSE3 -void ARGBToUV422Row_SSSE3(const uint8* src_argb0, - uint8* dst_u, uint8* dst_v, int width) { - asm volatile ( - "movdqa %4,%%xmm3 \n" - "movdqa %5,%%xmm4 \n" - "movdqa %6,%%xmm5 \n" - "sub %1,%2 \n" - LABELALIGN - "1: \n" - "movdqu " MEMACCESS(0) ",%%xmm0 \n" - "movdqu " MEMACCESS2(0x10,0) ",%%xmm1 \n" - "movdqu " MEMACCESS2(0x20,0) ",%%xmm2 \n" - "movdqu " MEMACCESS2(0x30,0) ",%%xmm6 \n" - "lea " MEMLEA(0x40,0) ",%0 \n" - "movdqa %%xmm0,%%xmm7 \n" - "shufps $0x88,%%xmm1,%%xmm0 \n" - "shufps $0xdd,%%xmm1,%%xmm7 \n" - "pavgb %%xmm7,%%xmm0 \n" - "movdqa %%xmm2,%%xmm7 \n" - "shufps $0x88,%%xmm6,%%xmm2 \n" - "shufps $0xdd,%%xmm6,%%xmm7 \n" - "pavgb %%xmm7,%%xmm2 \n" - "movdqa %%xmm0,%%xmm1 \n" - "movdqa %%xmm2,%%xmm6 \n" - "pmaddubsw %%xmm4,%%xmm0 \n" - "pmaddubsw %%xmm4,%%xmm2 \n" - "pmaddubsw %%xmm3,%%xmm1 \n" - "pmaddubsw %%xmm3,%%xmm6 \n" - "phaddw %%xmm2,%%xmm0 \n" - "phaddw %%xmm6,%%xmm1 \n" - "psraw $0x8,%%xmm0 \n" - "psraw $0x8,%%xmm1 \n" - "packsswb %%xmm1,%%xmm0 \n" - "paddb %%xmm5,%%xmm0 \n" - "movlps %%xmm0," MEMACCESS(1) " \n" - MEMOPMEM(movhps,xmm0,0x00,1,2,1) // movhps %%xmm0,(%1,%2,1) - "lea " MEMLEA(0x8,1) ",%1 \n" - "sub $0x10,%3 \n" - "jg 1b \n" - : "+r"(src_argb0), // %0 - "+r"(dst_u), // %1 - "+r"(dst_v), // %2 - "+rm"(width) // %3 - : "m"(kARGBToV), // %4 - "m"(kARGBToU), // %5 - "m"(kAddUV128) // %6 - : "memory", "cc", NACL_R14 - "xmm0", "xmm1", "xmm2", "xmm6", "xmm7" - ); -} -#endif // HAS_ARGBTOUV422ROW_SSSE3 - void BGRAToYRow_SSSE3(const uint8* src_bgra, uint8* dst_y, int width) { asm volatile ( "movdqa %4,%%xmm5 \n" @@ -1484,7 +1492,7 @@ void RGBAToUVRow_SSSE3(const uint8* src_rgba0, int src_stride_rgba, #if defined(HAS_I422TOARGBROW_SSSE3) || defined(HAS_I422TOARGBROW_AVX2) -// Read 8 UV from 411 +// Read 8 UV from 444 #define READYUV444 \ "movq " MEMACCESS([u_buf]) ",%%xmm0 \n" \ MEMOPREG(movq, 0x00, [u_buf], [v_buf], 1, xmm1) \ @@ -1528,7 +1536,7 @@ void RGBAToUVRow_SSSE3(const uint8* src_rgba0, int src_stride_rgba, #define READYUV411_TEMP \ "movzwl " MEMACCESS([u_buf]) ",%[temp] \n" \ "movd %[temp],%%xmm0 \n" \ - MEMOPARG(movzwl,0x00,[u_buf],[v_buf],1,[temp]) " \n" \ + MEMOPARG(movzwl, 0x00, [u_buf], [v_buf], 1, [temp]) " \n" \ "movd %[temp],%%xmm1 \n" \ "lea " MEMLEA(0x2, [u_buf]) ",%[u_buf] \n" \ "punpcklbw %%xmm1,%%xmm0 \n" \ @@ -1803,7 +1811,7 @@ void OMITFP I411ToARGBRow_SSSE3(const uint8* y_buf, uint8* dst_argb, const struct YuvConstants* yuvconstants, int width) { - int temp = 0; + int temp; asm volatile ( YUVTORGB_SETUP(yuvconstants) "sub %[u_buf],%[v_buf] \n" @@ -1815,15 +1823,15 @@ void OMITFP I411ToARGBRow_SSSE3(const uint8* y_buf, STOREARGB "subl $0x8,%[width] \n" "jg 1b \n" - : [y_buf]"+r"(y_buf), // %[y_buf] - [u_buf]"+r"(u_buf), // %[u_buf] - [v_buf]"+r"(v_buf), // %[v_buf] + : [y_buf]"+r"(y_buf), // %[y_buf] + [u_buf]"+r"(u_buf), // %[u_buf] + [v_buf]"+r"(v_buf), // %[v_buf] [dst_argb]"+r"(dst_argb), // %[dst_argb] - [temp]"+r"(temp), // %[temp] + [temp]"=&r"(temp), // %[temp] #if defined(__i386__) && defined(__pic__) - [width]"+m"(width) // %[width] + [width]"+m"(width) // %[width] #else - [width]"+rm"(width) // %[width] + [width]"+rm"(width) // %[width] #endif : [yuvconstants]"r"(yuvconstants) // %[yuvconstants] : "memory", "cc", NACL_R14 YUVTORGB_REGS @@ -2005,6 +2013,20 @@ void OMITFP I422ToRGBARow_SSSE3(const uint8* y_buf, "vpermq $0xd8,%%ymm5,%%ymm5 \n" \ "lea " MEMLEA(0x10, [a_buf]) ",%[a_buf] \n" +// Read 4 UV from 411, upsample to 16 UV. +#define READYUV411_AVX2 \ + "vmovd " MEMACCESS([u_buf]) ",%%xmm0 \n" \ + MEMOPREG(vmovd, 0x00, [u_buf], [v_buf], 1, xmm1) \ + "lea " MEMLEA(0x4, [u_buf]) ",%[u_buf] \n" \ + "vpunpcklbw %%ymm1,%%ymm0,%%ymm0 \n" \ + "vpunpcklwd %%ymm0,%%ymm0,%%ymm0 \n" \ + "vpermq $0xd8,%%ymm0,%%ymm0 \n" \ + "vpunpckldq %%ymm0,%%ymm0,%%ymm0 \n" \ + "vmovdqu " MEMACCESS([y_buf]) ",%%xmm4 \n" \ + "vpermq $0xd8,%%ymm4,%%ymm4 \n" \ + "vpunpcklbw %%ymm4,%%ymm4,%%ymm4 \n" \ + "lea " MEMLEA(0x10, [y_buf]) ",%[y_buf] \n" + // Read 8 UV from NV12, upsample to 16 UV. #define READNV12_AVX2 \ "vmovdqu " MEMACCESS([uv_buf]) ",%%xmm0 \n" \ @@ -2071,7 +2093,7 @@ void OMITFP I422ToRGBARow_SSSE3(const uint8* y_buf, "vpackuswb %%ymm2,%%ymm2,%%ymm2 \n" #define YUVTORGB_REGS_AVX2 \ "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", -#else// Convert 16 pixels: 16 UV and 16 Y. +#else // Convert 16 pixels: 16 UV and 16 Y. #define YUVTORGB_SETUP_AVX2(yuvconstants) #define YUVTORGB_AVX2(yuvconstants) \ "vpmaddubsw " MEMACCESS2(64, [yuvconstants]) ",%%ymm0,%%ymm2 \n" \ @@ -2120,7 +2142,7 @@ void OMITFP I444ToARGBRow_AVX2(const uint8* y_buf, asm volatile ( YUVTORGB_SETUP_AVX2(yuvconstants) "sub %[u_buf],%[v_buf] \n" - "vpcmpeqb %%ymm5,%%ymm5,%%ymm5 \n" + "vpcmpeqb %%ymm5,%%ymm5,%%ymm5 \n" LABELALIGN "1: \n" READYUV444_AVX2 @@ -2141,6 +2163,39 @@ void OMITFP I444ToARGBRow_AVX2(const uint8* y_buf, } #endif // HAS_I444TOARGBROW_AVX2 +#ifdef HAS_I411TOARGBROW_AVX2 +// 16 pixels +// 4 UV values upsampled to 16 UV, mixed with 16 Y producing 16 ARGB (64 bytes). +void OMITFP I411ToARGBRow_AVX2(const uint8* y_buf, + const uint8* u_buf, + const uint8* v_buf, + uint8* dst_argb, + const struct YuvConstants* yuvconstants, + int width) { + asm volatile ( + YUVTORGB_SETUP_AVX2(yuvconstants) + "sub %[u_buf],%[v_buf] \n" + "vpcmpeqb %%ymm5,%%ymm5,%%ymm5 \n" + LABELALIGN + "1: \n" + READYUV411_AVX2 + YUVTORGB_AVX2(yuvconstants) + STOREARGB_AVX2 + "sub $0x10,%[width] \n" + "jg 1b \n" + "vzeroupper \n" + : [y_buf]"+r"(y_buf), // %[y_buf] + [u_buf]"+r"(u_buf), // %[u_buf] + [v_buf]"+r"(v_buf), // %[v_buf] + [dst_argb]"+r"(dst_argb), // %[dst_argb] + [width]"+rm"(width) // %[width] + : [yuvconstants]"r"(yuvconstants) // %[yuvconstants] + : "memory", "cc", NACL_R14 YUVTORGB_REGS_AVX2 + "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5" + ); +} +#endif // HAS_I411TOARGBROW_AVX2 + #if defined(HAS_I422TOARGBROW_AVX2) // 16 pixels // 8 UV values upsampled to 16 UV, mixed with 16 Y producing 16 ARGB (64 bytes). @@ -2153,7 +2208,7 @@ void OMITFP I422ToARGBRow_AVX2(const uint8* y_buf, asm volatile ( YUVTORGB_SETUP_AVX2(yuvconstants) "sub %[u_buf],%[v_buf] \n" - "vpcmpeqb %%ymm5,%%ymm5,%%ymm5 \n" + "vpcmpeqb %%ymm5,%%ymm5,%%ymm5 \n" LABELALIGN "1: \n" READYUV422_AVX2 @@ -2521,7 +2576,7 @@ void MirrorRow_AVX2(const uint8* src, uint8* dst, int width) { } #endif // HAS_MIRRORROW_AVX2 -#ifdef HAS_MIRRORROW_UV_SSSE3 +#ifdef HAS_MIRRORUVROW_SSSE3 // Shuffle table for reversing the bytes of UV channels. static uvec8 kShuffleMirrorUV = { 14u, 12u, 10u, 8u, 6u, 4u, 2u, 0u, 15u, 13u, 11u, 9u, 7u, 5u, 3u, 1u @@ -2552,7 +2607,7 @@ void MirrorUVRow_SSSE3(const uint8* src, uint8* dst_u, uint8* dst_v, "xmm0", "xmm1" ); } -#endif // HAS_MIRRORROW_UV_SSSE3 +#endif // HAS_MIRRORUVROW_SSSE3 #ifdef HAS_ARGBMIRRORROW_SSE2 @@ -2881,6 +2936,33 @@ void ARGBCopyAlphaRow_AVX2(const uint8* src, uint8* dst, int width) { } #endif // HAS_ARGBCOPYALPHAROW_AVX2 +#ifdef HAS_ARGBEXTRACTALPHAROW_SSE2 +// width in pixels +void ARGBExtractAlphaRow_SSE2(const uint8* src_argb, uint8* dst_a, int width) { + asm volatile ( + LABELALIGN + "1: \n" + "movdqu " MEMACCESS(0) ", %%xmm0 \n" + "movdqu " MEMACCESS2(0x10, 0) ", %%xmm1 \n" + "lea " MEMLEA(0x20, 0) ", %0 \n" + "psrld $0x18, %%xmm0 \n" + "psrld $0x18, %%xmm1 \n" + "packssdw %%xmm1, %%xmm0 \n" + "packuswb %%xmm0, %%xmm0 \n" + "movq %%xmm0," MEMACCESS(1) " \n" + "lea " MEMLEA(0x8, 1) ", %1 \n" + "sub $0x8, %2 \n" + "jg 1b \n" + : "+r"(src_argb), // %0 + "+r"(dst_a), // %1 + "+rm"(width) // %2 + : + : "memory", "cc" + , "xmm0", "xmm1" + ); +} +#endif // HAS_ARGBEXTRACTALPHAROW_SSE2 + #ifdef HAS_ARGBCOPYYTOALPHAROW_SSE2 // width in pixels void ARGBCopyYToAlphaRow_SSE2(const uint8* src, uint8* dst, int width) { @@ -2953,7 +3035,7 @@ void ARGBCopyYToAlphaRow_AVX2(const uint8* src, uint8* dst, int width) { #ifdef HAS_SETROW_X86 void SetRow_X86(uint8* dst, uint8 v8, int width) { size_t width_tmp = (size_t)(width >> 2); - const uint32 v32 = v8 * 0x01010101; // Duplicate byte to all bytes. + const uint32 v32 = v8 * 0x01010101u; // Duplicate byte to all bytes. asm volatile ( "rep stosl " MEMSTORESTRING(eax,0) " \n" : "+D"(dst), // %0 @@ -3514,7 +3596,7 @@ void BlendPlaneRow_SSSE3(const uint8* src0, const uint8* src1, "+r"(src1), // %1 "+r"(alpha), // %2 "+r"(dst), // %3 - "+r"(width) // %4 + "+rm"(width) // %4 :: "memory", "cc", "eax", "xmm0", "xmm1", "xmm2", "xmm5", "xmm6", "xmm7" ); } @@ -3571,7 +3653,7 @@ void BlendPlaneRow_AVX2(const uint8* src0, const uint8* src1, "+r"(src1), // %1 "+r"(alpha), // %2 "+r"(dst), // %3 - "+r"(width) // %4 + "+rm"(width) // %4 :: "memory", "cc", "eax", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7" ); @@ -3677,7 +3759,7 @@ void ARGBAttenuateRow_AVX2(const uint8* src_argb, uint8* dst_argb, int width) { // Unattenuate 4 pixels at a time. void ARGBUnattenuateRow_SSE2(const uint8* src_argb, uint8* dst_argb, int width) { - uintptr_t alpha = 0; + uintptr_t alpha; asm volatile ( // 4 pixel loop. LABELALIGN @@ -3708,10 +3790,10 @@ void ARGBUnattenuateRow_SSE2(const uint8* src_argb, uint8* dst_argb, "lea " MEMLEA(0x10,1) ",%1 \n" "sub $0x4,%2 \n" "jg 1b \n" - : "+r"(src_argb), // %0 - "+r"(dst_argb), // %1 - "+r"(width), // %2 - "+r"(alpha) // %3 + : "+r"(src_argb), // %0 + "+r"(dst_argb), // %1 + "+r"(width), // %2 + "=&r"(alpha) // %3 : "r"(fixed_invtbl8) // %4 : "memory", "cc", NACL_R14 "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5" @@ -3727,7 +3809,7 @@ static const uvec8 kUnattenShuffleAlpha_AVX2 = { // Unattenuate 8 pixels at a time. void ARGBUnattenuateRow_AVX2(const uint8* src_argb, uint8* dst_argb, int width) { - uintptr_t alpha = 0; + uintptr_t alpha; asm volatile ( "sub %0,%1 \n" "vbroadcastf128 %5,%%ymm5 \n" @@ -3776,10 +3858,10 @@ void ARGBUnattenuateRow_AVX2(const uint8* src_argb, uint8* dst_argb, "sub $0x8,%2 \n" "jg 1b \n" "vzeroupper \n" - : "+r"(src_argb), // %0 - "+r"(dst_argb), // %1 - "+r"(width), // %2 - "+r"(alpha) // %3 + : "+r"(src_argb), // %0 + "+r"(dst_argb), // %1 + "+r"(width), // %2 + "=&r"(alpha) // %3 : "r"(fixed_invtbl8), // %4 "m"(kUnattenShuffleAlpha_AVX2) // %5 : "memory", "cc", NACL_R14 @@ -4704,7 +4786,7 @@ LIBYUV_API void ARGBAffineRow_SSE2(const uint8* src_argb, int src_argb_stride, uint8* dst_argb, const float* src_dudv, int width) { intptr_t src_argb_stride_temp = src_argb_stride; - intptr_t temp = 0; + intptr_t temp; asm volatile ( "movq " MEMACCESS(3) ",%%xmm2 \n" "movq " MEMACCESS2(0x08,3) ",%%xmm7 \n" @@ -4776,7 +4858,7 @@ void ARGBAffineRow_SSE2(const uint8* src_argb, int src_argb_stride, "+r"(dst_argb), // %2 "+r"(src_dudv), // %3 "+rm"(width), // %4 - "+r"(temp) // %5 + "=&r"(temp) // %5 : : "memory", "cc", NACL_R14 "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7" @@ -4854,9 +4936,9 @@ void InterpolateRow_SSSE3(uint8* dst_ptr, const uint8* src_ptr, "jg 100b \n" "99: \n" - : "+r"(dst_ptr), // %0 - "+r"(src_ptr), // %1 - "+r"(dst_width), // %2 + : "+r"(dst_ptr), // %0 + "+r"(src_ptr), // %1 + "+rm"(dst_width), // %2 "+r"(source_y_fraction) // %3 : "r"((intptr_t)(src_stride)) // %4 : "memory", "cc", "eax", NACL_R14 @@ -4932,7 +5014,7 @@ void InterpolateRow_AVX2(uint8* dst_ptr, const uint8* src_ptr, "999: \n" : "+D"(dst_ptr), // %0 "+S"(src_ptr), // %1 - "+c"(dst_width), // %2 + "+cm"(dst_width), // %2 "+r"(source_y_fraction) // %3 : "r"((intptr_t)(src_stride)) // %4 : "memory", "cc", "eax", NACL_R14 @@ -5002,7 +5084,7 @@ void ARGBShuffleRow_AVX2(const uint8* src_argb, uint8* dst_argb, // For BGRAToARGB, ABGRToARGB, RGBAToARGB, and ARGBToRGBA. void ARGBShuffleRow_SSE2(const uint8* src_argb, uint8* dst_argb, const uint8* shuffler, int width) { - uintptr_t pixel_temp = 0u; + uintptr_t pixel_temp; asm volatile ( "pxor %%xmm5,%%xmm5 \n" "mov " MEMACCESS(4) ",%k2 \n" @@ -5107,11 +5189,11 @@ void ARGBShuffleRow_SSE2(const uint8* src_argb, uint8* dst_argb, "jg 3012b \n" "99: \n" - : "+r"(src_argb), // %0 - "+r"(dst_argb), // %1 - "+d"(pixel_temp), // %2 + : "+r"(src_argb), // %0 + "+r"(dst_argb), // %1 + "=&d"(pixel_temp), // %2 "+r"(width) // %3 - : "r"(shuffler) // %4 + : "r"(shuffler) // %4 : "memory", "cc", NACL_R14 "xmm0", "xmm1", "xmm5" ); @@ -5288,7 +5370,7 @@ void ARGBPolynomialRow_AVX2(const uint8* src_argb, // Tranform ARGB pixels with color table. void ARGBColorTableRow_X86(uint8* dst_argb, const uint8* table_argb, int width) { - uintptr_t pixel_temp = 0u; + uintptr_t pixel_temp; asm volatile ( // 1 pixel loop. LABELALIGN @@ -5308,10 +5390,10 @@ void ARGBColorTableRow_X86(uint8* dst_argb, const uint8* table_argb, "mov %b1," MEMACCESS2(-0x1,0) " \n" "dec %2 \n" "jg 1b \n" - : "+r"(dst_argb), // %0 - "+d"(pixel_temp), // %1 - "+r"(width) // %2 - : "r"(table_argb) // %3 + : "+r"(dst_argb), // %0 + "=&d"(pixel_temp), // %1 + "+r"(width) // %2 + : "r"(table_argb) // %3 : "memory", "cc"); } #endif // HAS_ARGBCOLORTABLEROW_X86 @@ -5319,7 +5401,7 @@ void ARGBColorTableRow_X86(uint8* dst_argb, const uint8* table_argb, #ifdef HAS_RGBCOLORTABLEROW_X86 // Tranform RGB pixels with color table. void RGBColorTableRow_X86(uint8* dst_argb, const uint8* table_argb, int width) { - uintptr_t pixel_temp = 0u; + uintptr_t pixel_temp; asm volatile ( // 1 pixel loop. LABELALIGN @@ -5336,10 +5418,10 @@ void RGBColorTableRow_X86(uint8* dst_argb, const uint8* table_argb, int width) { "mov %b1," MEMACCESS2(-0x2,0) " \n" "dec %2 \n" "jg 1b \n" - : "+r"(dst_argb), // %0 - "+d"(pixel_temp), // %1 - "+r"(width) // %2 - : "r"(table_argb) // %3 + : "+r"(dst_argb), // %0 + "=&d"(pixel_temp), // %1 + "+r"(width) // %2 + : "r"(table_argb) // %3 : "memory", "cc"); } #endif // HAS_RGBCOLORTABLEROW_X86 @@ -5349,8 +5431,8 @@ void RGBColorTableRow_X86(uint8* dst_argb, const uint8* table_argb, int width) { void ARGBLumaColorTableRow_SSSE3(const uint8* src_argb, uint8* dst_argb, int width, const uint8* luma, uint32 lumacoeff) { - uintptr_t pixel_temp = 0u; - uintptr_t table_temp = 0u; + uintptr_t pixel_temp; + uintptr_t table_temp; asm volatile ( "movd %6,%%xmm3 \n" "pshufd $0x0,%%xmm3,%%xmm3 \n" @@ -5432,13 +5514,13 @@ void ARGBLumaColorTableRow_SSSE3(const uint8* src_argb, uint8* dst_argb, "lea " MEMLEA(0x10,3) ",%3 \n" "sub $0x4,%4 \n" "jg 1b \n" - : "+d"(pixel_temp), // %0 - "+a"(table_temp), // %1 - "+r"(src_argb), // %2 - "+r"(dst_argb), // %3 - "+rm"(width) // %4 - : "r"(luma), // %5 - "rm"(lumacoeff) // %6 + : "=&d"(pixel_temp), // %0 + "=&a"(table_temp), // %1 + "+r"(src_argb), // %2 + "+r"(dst_argb), // %3 + "+rm"(width) // %4 + : "r"(luma), // %5 + "rm"(lumacoeff) // %6 : "memory", "cc", "xmm0", "xmm3", "xmm4", "xmm5" ); } diff --git a/TMessagesProj/jni/libyuv/source/row_mips.cc b/TMessagesProj/jni/libyuv/source/row_mips.cc index d12cf6ab7..285f0b5ad 100644 --- a/TMessagesProj/jni/libyuv/source/row_mips.cc +++ b/TMessagesProj/jni/libyuv/source/row_mips.cc @@ -375,13 +375,13 @@ void CopyRow_MIPS(const uint8* src, uint8* dst, int count) { } #endif // HAS_COPYROW_MIPS -// MIPS DSPR2 functions +// DSPR2 functions #if !defined(LIBYUV_DISABLE_MIPS) && defined(__mips_dsp) && \ (__mips_dsp_rev >= 2) && \ (_MIPS_SIM == _MIPS_SIM_ABI32) && (__mips_isa_rev < 6) -void SplitUVRow_MIPS_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width) { +void SplitUVRow_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, + int width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -446,7 +446,7 @@ void SplitUVRow_MIPS_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, ); } -void MirrorRow_MIPS_DSPR2(const uint8* src, uint8* dst, int width) { +void MirrorRow_DSPR2(const uint8* src, uint8* dst, int width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -496,10 +496,10 @@ void MirrorRow_MIPS_DSPR2(const uint8* src, uint8* dst, int width) { ); } -void MirrorUVRow_MIPS_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width) { - int x = 0; - int y = 0; +void MirrorUVRow_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, + int width) { + int x; + int y; __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -579,7 +579,7 @@ void MirrorUVRow_MIPS_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, [dst_u] "+r" (dst_u), [dst_v] "+r" (dst_v), [x] "=&r" (x), - [y] "+r" (y) + [y] "=&r" (y) : [width] "r" (width) : "t0", "t1", "t2", "t3", "t4", "t5", "t7", "t8", "t9" @@ -653,12 +653,12 @@ void MirrorUVRow_MIPS_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, "addu.ph $t1, $t1, $s5 \n" // TODO(fbarchard): accept yuv conversion constants. -void I422ToARGBRow_MIPS_DSPR2(const uint8* y_buf, - const uint8* u_buf, - const uint8* v_buf, - uint8* rgb_buf, - const struct YuvConstants* yuvconstants, - int width) { +void I422ToARGBRow_DSPR2(const uint8* y_buf, + const uint8* u_buf, + const uint8* v_buf, + uint8* rgb_buf, + const struct YuvConstants* yuvconstants, + int width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -716,9 +716,9 @@ void I422ToARGBRow_MIPS_DSPR2(const uint8* y_buf, } // Bilinear filter 8x2 -> 8x1 -void InterpolateRow_MIPS_DSPR2(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride, int dst_width, - int source_y_fraction) { +void InterpolateRow_DSPR2(uint8* dst_ptr, const uint8* src_ptr, + ptrdiff_t src_stride, int dst_width, + int source_y_fraction) { int y0_fraction = 256 - source_y_fraction; const uint8* src_ptr1 = src_ptr + src_stride; diff --git a/TMessagesProj/jni/libyuv/source/row_neon.cc b/TMessagesProj/jni/libyuv/source/row_neon.cc index 5b4ff3b5a..909df060c 100644 --- a/TMessagesProj/jni/libyuv/source/row_neon.cc +++ b/TMessagesProj/jni/libyuv/source/row_neon.cc @@ -317,16 +317,11 @@ void I422ToRGB24Row_NEON(const uint8* src_y, } #define ARGBTORGB565 \ - "vshr.u8 d20, d20, #3 \n" /* B */ \ - "vshr.u8 d21, d21, #2 \n" /* G */ \ - "vshr.u8 d22, d22, #3 \n" /* R */ \ - "vmovl.u8 q8, d20 \n" /* B */ \ - "vmovl.u8 q9, d21 \n" /* G */ \ - "vmovl.u8 q10, d22 \n" /* R */ \ - "vshl.u16 q9, q9, #5 \n" /* G */ \ - "vshl.u16 q10, q10, #11 \n" /* R */ \ - "vorr q0, q8, q9 \n" /* BG */ \ - "vorr q0, q0, q10 \n" /* BGR */ + "vshll.u8 q0, d22, #8 \n" /* R */ \ + "vshll.u8 q8, d21, #8 \n" /* G */ \ + "vshll.u8 q9, d20, #8 \n" /* B */ \ + "vsri.16 q0, q8, #5 \n" /* RG */ \ + "vsri.16 q0, q9, #11 \n" /* RGB */ void I422ToRGB565Row_NEON(const uint8* src_y, const uint8* src_u, @@ -359,19 +354,13 @@ void I422ToRGB565Row_NEON(const uint8* src_y, } #define ARGBTOARGB1555 \ - "vshr.u8 q10, q10, #3 \n" /* B */ \ - "vshr.u8 d22, d22, #3 \n" /* R */ \ - "vshr.u8 d23, d23, #7 \n" /* A */ \ - "vmovl.u8 q8, d20 \n" /* B */ \ - "vmovl.u8 q9, d21 \n" /* G */ \ - "vmovl.u8 q10, d22 \n" /* R */ \ - "vmovl.u8 q11, d23 \n" /* A */ \ - "vshl.u16 q9, q9, #5 \n" /* G */ \ - "vshl.u16 q10, q10, #10 \n" /* R */ \ - "vshl.u16 q11, q11, #15 \n" /* A */ \ - "vorr q0, q8, q9 \n" /* BG */ \ - "vorr q1, q10, q11 \n" /* RA */ \ - "vorr q0, q0, q1 \n" /* BGRA */ + "vshll.u8 q0, d23, #8 \n" /* A */ \ + "vshll.u8 q8, d22, #8 \n" /* R */ \ + "vshll.u8 q9, d21, #8 \n" /* G */ \ + "vshll.u8 q10, d20, #8 \n" /* B */ \ + "vsri.16 q0, q8, #1 \n" /* AR */ \ + "vsri.16 q0, q9, #6 \n" /* ARG */ \ + "vsri.16 q0, q10, #11 \n" /* ARGB */ void I422ToARGB1555Row_NEON(const uint8* src_y, const uint8* src_u, @@ -1309,6 +1298,24 @@ void ARGBToYRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { ); } +void ARGBExtractAlphaRow_NEON(const uint8* src_argb, uint8* dst_a, int width) { + asm volatile ( + "1: \n" + MEMACCESS(0) + "vld4.8 {d0, d2, d4, d6}, [%0]! \n" // load 8 ARGB pixels + "vld4.8 {d1, d3, d5, d7}, [%0]! \n" // load next 8 ARGB pixels + "subs %2, %2, #16 \n" // 16 processed per loop + MEMACCESS(1) + "vst1.8 {q3}, [%1]! \n" // store 16 A's. + "bgt 1b \n" + : "+r"(src_argb), // %0 + "+r"(dst_a), // %1 + "+r"(width) // %2 + : + : "cc", "memory", "q0", "q1", "q2", "q3" // Clobber List + ); +} + void ARGBToYJRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { asm volatile ( "vmov.u8 d24, #15 \n" // B * 0.11400 coefficient @@ -1374,55 +1381,6 @@ void ARGBToUV444Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, ); } -// 16x1 pixels -> 8x1. width is number of argb pixels. e.g. 16. -void ARGBToUV422Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width) { - asm volatile ( - "vmov.s16 q10, #112 / 2 \n" // UB / VR 0.875 coefficient - "vmov.s16 q11, #74 / 2 \n" // UG -0.5781 coefficient - "vmov.s16 q12, #38 / 2 \n" // UR -0.2969 coefficient - "vmov.s16 q13, #18 / 2 \n" // VB -0.1406 coefficient - "vmov.s16 q14, #94 / 2 \n" // VG -0.7344 coefficient - "vmov.u16 q15, #0x8080 \n" // 128.5 - "1: \n" - MEMACCESS(0) - "vld4.8 {d0, d2, d4, d6}, [%0]! \n" // load 8 ARGB pixels. - MEMACCESS(0) - "vld4.8 {d1, d3, d5, d7}, [%0]! \n" // load next 8 ARGB pixels. - - "vpaddl.u8 q0, q0 \n" // B 16 bytes -> 8 shorts. - "vpaddl.u8 q1, q1 \n" // G 16 bytes -> 8 shorts. - "vpaddl.u8 q2, q2 \n" // R 16 bytes -> 8 shorts. - - "subs %3, %3, #16 \n" // 16 processed per loop. - "vmul.s16 q8, q0, q10 \n" // B - "vmls.s16 q8, q1, q11 \n" // G - "vmls.s16 q8, q2, q12 \n" // R - "vadd.u16 q8, q8, q15 \n" // +128 -> unsigned - - "vmul.s16 q9, q2, q10 \n" // R - "vmls.s16 q9, q1, q14 \n" // G - "vmls.s16 q9, q0, q13 \n" // B - "vadd.u16 q9, q9, q15 \n" // +128 -> unsigned - - "vqshrn.u16 d0, q8, #8 \n" // 16 bit to 8 bit U - "vqshrn.u16 d1, q9, #8 \n" // 16 bit to 8 bit V - - MEMACCESS(1) - "vst1.8 {d0}, [%1]! \n" // store 8 pixels U. - MEMACCESS(2) - "vst1.8 {d1}, [%2]! \n" // store 8 pixels V. - "bgt 1b \n" - : "+r"(src_argb), // %0 - "+r"(dst_u), // %1 - "+r"(dst_v), // %2 - "+r"(width) // %3 - : - : "cc", "memory", "q0", "q1", "q2", "q3", - "q8", "q9", "q10", "q11", "q12", "q13", "q14", "q15" - ); -} - // 32x1 pixels -> 8x1. width is number of argb pixels. e.g. 32. void ARGBToUV411Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width) { @@ -2625,8 +2583,6 @@ void ARGBColorMatrixRow_NEON(const uint8* src_argb, uint8* dst_argb, ); } -// TODO(fbarchard): fix vqshrun in ARGBMultiplyRow_NEON and reenable. -#ifdef HAS_ARGBMULTIPLYROW_NEON // Multiply 2 rows of ARGB pixels together, 8 pixels at a time. void ARGBMultiplyRow_NEON(const uint8* src_argb0, const uint8* src_argb1, uint8* dst_argb, int width) { @@ -2658,7 +2614,6 @@ void ARGBMultiplyRow_NEON(const uint8* src_argb0, const uint8* src_argb1, : "cc", "memory", "q0", "q1", "q2", "q3" ); } -#endif // HAS_ARGBMULTIPLYROW_NEON // Add 2 rows of ARGB pixels together, 8 pixels at a time. void ARGBAddRow_NEON(const uint8* src_argb0, const uint8* src_argb1, diff --git a/TMessagesProj/jni/libyuv/source/row_neon64.cc b/TMessagesProj/jni/libyuv/source/row_neon64.cc index 6fe5a1080..6375d4f55 100644 --- a/TMessagesProj/jni/libyuv/source/row_neon64.cc +++ b/TMessagesProj/jni/libyuv/source/row_neon64.cc @@ -127,7 +127,6 @@ extern "C" { "sqshrun " #vG ".8b, " #vG ".8h, #6 \n" /* G */ \ "sqshrun " #vR ".8b, " #vR ".8h, #6 \n" /* R */ \ -#ifdef HAS_I444TOARGBROW_NEON void I444ToARGBRow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -157,9 +156,7 @@ void I444ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I444TOARGBROW_NEON -#ifdef HAS_I422TOARGBROW_NEON void I422ToARGBRow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -189,9 +186,7 @@ void I422ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TOARGBROW_NEON -#ifdef HAS_I422ALPHATOARGBROW_NEON void I422AlphaToARGBRow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -224,9 +219,7 @@ void I422AlphaToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422ALPHATOARGBROW_NEON -#ifdef HAS_I411TOARGBROW_NEON void I411ToARGBRow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -256,9 +249,7 @@ void I411ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I411TOARGBROW_NEON -#ifdef HAS_I422TORGBAROW_NEON void I422ToRGBARow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -288,9 +279,7 @@ void I422ToRGBARow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TORGBAROW_NEON -#ifdef HAS_I422TORGB24ROW_NEON void I422ToRGB24Row_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -319,16 +308,14 @@ void I422ToRGB24Row_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TORGB24ROW_NEON #define ARGBTORGB565 \ "shll v0.8h, v22.8b, #8 \n" /* R */ \ - "shll v20.8h, v20.8b, #8 \n" /* B */ \ "shll v21.8h, v21.8b, #8 \n" /* G */ \ + "shll v20.8h, v20.8b, #8 \n" /* B */ \ "sri v0.8h, v21.8h, #5 \n" /* RG */ \ "sri v0.8h, v20.8h, #11 \n" /* RGB */ -#ifdef HAS_I422TORGB565ROW_NEON void I422ToRGB565Row_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -358,18 +345,16 @@ void I422ToRGB565Row_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TORGB565ROW_NEON #define ARGBTOARGB1555 \ "shll v0.8h, v23.8b, #8 \n" /* A */ \ "shll v22.8h, v22.8b, #8 \n" /* R */ \ - "shll v20.8h, v20.8b, #8 \n" /* B */ \ "shll v21.8h, v21.8b, #8 \n" /* G */ \ + "shll v20.8h, v20.8b, #8 \n" /* B */ \ "sri v0.8h, v22.8h, #1 \n" /* AR */ \ "sri v0.8h, v21.8h, #6 \n" /* ARG */ \ "sri v0.8h, v20.8h, #11 \n" /* ARGB */ -#ifdef HAS_I422TOARGB1555ROW_NEON void I422ToARGB1555Row_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -400,7 +385,6 @@ void I422ToARGB1555Row_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TOARGB1555ROW_NEON #define ARGBTOARGB4444 \ /* Input v20.8b<=B, v21.8b<=G, v22.8b<=R, v23.8b<=A, v4.8b<=0x0f */ \ @@ -412,7 +396,6 @@ void I422ToARGB1555Row_NEON(const uint8* src_y, "orr v1.8b, v22.8b, v23.8b \n" /* RA */ \ "zip1 v0.16b, v0.16b, v1.16b \n" /* BGRA */ -#ifdef HAS_I422TOARGB4444ROW_NEON void I422ToARGB4444Row_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -444,13 +427,10 @@ void I422ToARGB4444Row_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TOARGB4444ROW_NEON -#ifdef HAS_I400TOARGBROW_NEON void I400ToARGBRow_NEON(const uint8* src_y, uint8* dst_argb, int width) { - int64 width64 = (int64)(width); asm volatile ( YUVTORGB_SETUP "movi v23.8b, #255 \n" @@ -463,7 +443,7 @@ void I400ToARGBRow_NEON(const uint8* src_y, "b.gt 1b \n" : "+r"(src_y), // %0 "+r"(dst_argb), // %1 - "+r"(width64) // %2 + "+r"(width) // %2 : [kUVToRB]"r"(&kYuvI601Constants.kUVToRB), [kUVToG]"r"(&kYuvI601Constants.kUVToG), [kUVBiasBGR]"r"(&kYuvI601Constants.kUVBiasBGR), @@ -472,9 +452,7 @@ void I400ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I400TOARGBROW_NEON -#ifdef HAS_J400TOARGBROW_NEON void J400ToARGBRow_NEON(const uint8* src_y, uint8* dst_argb, int width) { @@ -496,9 +474,7 @@ void J400ToARGBRow_NEON(const uint8* src_y, : "cc", "memory", "v20", "v21", "v22", "v23" ); } -#endif // HAS_J400TOARGBROW_NEON -#ifdef HAS_NV12TOARGBROW_NEON void NV12ToARGBRow_NEON(const uint8* src_y, const uint8* src_uv, uint8* dst_argb, @@ -526,9 +502,7 @@ void NV12ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_NV12TOARGBROW_NEON -#ifdef HAS_NV12TOARGBROW_NEON void NV21ToARGBRow_NEON(const uint8* src_y, const uint8* src_vu, uint8* dst_argb, @@ -556,9 +530,7 @@ void NV21ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_NV12TOARGBROW_NEON -#ifdef HAS_NV12TORGB565ROW_NEON void NV12ToRGB565Row_NEON(const uint8* src_y, const uint8* src_uv, uint8* dst_rgb565, @@ -586,14 +558,11 @@ void NV12ToRGB565Row_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_NV12TORGB565ROW_NEON -#ifdef HAS_YUY2TOARGBROW_NEON void YUY2ToARGBRow_NEON(const uint8* src_yuy2, uint8* dst_argb, const struct YuvConstants* yuvconstants, int width) { - int64 width64 = (int64)(width); asm volatile ( YUVTORGB_SETUP "movi v23.8b, #255 \n" @@ -606,7 +575,7 @@ void YUY2ToARGBRow_NEON(const uint8* src_yuy2, "b.gt 1b \n" : "+r"(src_yuy2), // %0 "+r"(dst_argb), // %1 - "+r"(width64) // %2 + "+r"(width) // %2 : [kUVToRB]"r"(&yuvconstants->kUVToRB), [kUVToG]"r"(&yuvconstants->kUVToG), [kUVBiasBGR]"r"(&yuvconstants->kUVBiasBGR), @@ -615,14 +584,11 @@ void YUY2ToARGBRow_NEON(const uint8* src_yuy2, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_YUY2TOARGBROW_NEON -#ifdef HAS_UYVYTOARGBROW_NEON void UYVYToARGBRow_NEON(const uint8* src_uyvy, uint8* dst_argb, const struct YuvConstants* yuvconstants, int width) { - int64 width64 = (int64)(width); asm volatile ( YUVTORGB_SETUP "movi v23.8b, #255 \n" @@ -635,7 +601,7 @@ void UYVYToARGBRow_NEON(const uint8* src_uyvy, "b.gt 1b \n" : "+r"(src_uyvy), // %0 "+r"(dst_argb), // %1 - "+r"(width64) // %2 + "+r"(width) // %2 : [kUVToRB]"r"(&yuvconstants->kUVToRB), [kUVToG]"r"(&yuvconstants->kUVToG), [kUVBiasBGR]"r"(&yuvconstants->kUVBiasBGR), @@ -644,10 +610,8 @@ void UYVYToARGBRow_NEON(const uint8* src_uyvy, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_UYVYTOARGBROW_NEON // Reads 16 pairs of UV and write even values to dst_u and odd to dst_v. -#ifdef HAS_SPLITUVROW_NEON void SplitUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width) { asm volatile ( @@ -668,10 +632,8 @@ void SplitUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, : "cc", "memory", "v0", "v1" // Clobber List ); } -#endif // HAS_SPLITUVROW_NEON // Reads 16 U's and V's and writes out 16 pairs of UV. -#ifdef HAS_MERGEUVROW_NEON void MergeUVRow_NEON(const uint8* src_u, const uint8* src_v, uint8* dst_uv, int width) { asm volatile ( @@ -693,10 +655,8 @@ void MergeUVRow_NEON(const uint8* src_u, const uint8* src_v, uint8* dst_uv, : "cc", "memory", "v0", "v1" // Clobber List ); } -#endif // HAS_MERGEUVROW_NEON // Copy multiple of 32. vld4.8 allow unaligned and is fastest on a15. -#ifdef HAS_COPYROW_NEON void CopyRow_NEON(const uint8* src, uint8* dst, int count) { asm volatile ( "1: \n" @@ -713,17 +673,16 @@ void CopyRow_NEON(const uint8* src, uint8* dst, int count) { : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_COPYROW_NEON // SetRow writes 'count' bytes using an 8 bit value repeated. void SetRow_NEON(uint8* dst, uint8 v8, int count) { asm volatile ( "dup v0.16b, %w2 \n" // duplicate 16 bytes "1: \n" - "subs %w1, %w1, #16 \n" // 16 bytes per loop + "subs %w1, %w1, #16 \n" // 16 bytes per loop MEMACCESS(0) "st1 {v0.16b}, [%0], #16 \n" // store - "b.gt 1b \n" + "b.gt 1b \n" : "+r"(dst), // %0 "+r"(count) // %1 : "r"(v8) // %2 @@ -735,10 +694,10 @@ void ARGBSetRow_NEON(uint8* dst, uint32 v32, int count) { asm volatile ( "dup v0.4s, %w2 \n" // duplicate 4 ints "1: \n" - "subs %w1, %w1, #4 \n" // 4 ints per loop + "subs %w1, %w1, #4 \n" // 4 ints per loop MEMACCESS(0) "st1 {v0.16b}, [%0], #16 \n" // store - "b.gt 1b \n" + "b.gt 1b \n" : "+r"(dst), // %0 "+r"(count) // %1 : "r"(v32) // %2 @@ -746,18 +705,15 @@ void ARGBSetRow_NEON(uint8* dst, uint32 v32, int count) { ); } -#ifdef HAS_MIRRORROW_NEON void MirrorRow_NEON(const uint8* src, uint8* dst, int width) { - int64 width64 = (int64) width; asm volatile ( // Start at end of source row. - "add %0, %0, %2 \n" + "add %0, %0, %w2, sxtw \n" "sub %0, %0, #16 \n" - "1: \n" MEMACCESS(0) "ld1 {v0.16b}, [%0], %3 \n" // src -= 16 - "subs %2, %2, #16 \n" // 16 pixels per loop. + "subs %w2, %w2, #16 \n" // 16 pixels per loop. "rev64 v0.16b, v0.16b \n" MEMACCESS(1) "st1 {v0.D}[1], [%1], #8 \n" // dst += 16 @@ -766,26 +722,22 @@ void MirrorRow_NEON(const uint8* src, uint8* dst, int width) { "b.gt 1b \n" : "+r"(src), // %0 "+r"(dst), // %1 - "+r"(width64) // %2 + "+r"(width) // %2 : "r"((ptrdiff_t)-16) // %3 : "cc", "memory", "v0" ); } -#endif // HAS_MIRRORROW_NEON -#ifdef HAS_MIRRORUVROW_NEON void MirrorUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width) { - int64 width64 = (int64) width; asm volatile ( // Start at end of source row. - "add %0, %0, %3, lsl #1 \n" + "add %0, %0, %w3, sxtw #1 \n" "sub %0, %0, #16 \n" - "1: \n" MEMACCESS(0) "ld2 {v0.8b, v1.8b}, [%0], %4 \n" // src -= 16 - "subs %3, %3, #8 \n" // 8 pixels per loop. + "subs %w3, %w3, #8 \n" // 8 pixels per loop. "rev64 v0.8b, v0.8b \n" "rev64 v1.8b, v1.8b \n" MEMACCESS(1) @@ -796,25 +748,21 @@ void MirrorUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, : "+r"(src_uv), // %0 "+r"(dst_u), // %1 "+r"(dst_v), // %2 - "+r"(width64) // %3 + "+r"(width) // %3 : "r"((ptrdiff_t)-16) // %4 : "cc", "memory", "v0", "v1" ); } -#endif // HAS_MIRRORUVROW_NEON -#ifdef HAS_ARGBMIRRORROW_NEON void ARGBMirrorRow_NEON(const uint8* src, uint8* dst, int width) { - int64 width64 = (int64) width; asm volatile ( - // Start at end of source row. - "add %0, %0, %2, lsl #2 \n" + // Start at end of source row. + "add %0, %0, %w2, sxtw #2 \n" "sub %0, %0, #16 \n" - "1: \n" MEMACCESS(0) "ld1 {v0.16b}, [%0], %3 \n" // src -= 16 - "subs %2, %2, #4 \n" // 4 pixels per loop. + "subs %w2, %w2, #4 \n" // 4 pixels per loop. "rev64 v0.4s, v0.4s \n" MEMACCESS(1) "st1 {v0.D}[1], [%1], #8 \n" // dst += 16 @@ -823,14 +771,12 @@ void ARGBMirrorRow_NEON(const uint8* src, uint8* dst, int width) { "b.gt 1b \n" : "+r"(src), // %0 "+r"(dst), // %1 - "+r"(width64) // %2 + "+r"(width) // %2 : "r"((ptrdiff_t)-16) // %3 : "cc", "memory", "v0" ); } -#endif // HAS_ARGBMIRRORROW_NEON -#ifdef HAS_RGB24TOARGBROW_NEON void RGB24ToARGBRow_NEON(const uint8* src_rgb24, uint8* dst_argb, int width) { asm volatile ( "movi v4.8b, #255 \n" // Alpha @@ -843,14 +789,12 @@ void RGB24ToARGBRow_NEON(const uint8* src_rgb24, uint8* dst_argb, int width) { "b.gt 1b \n" : "+r"(src_rgb24), // %0 "+r"(dst_argb), // %1 - "+r"(width) // %2 + "+r"(width) // %2 : : "cc", "memory", "v1", "v2", "v3", "v4" // Clobber List ); } -#endif // HAS_RGB24TOARGBROW_NEON -#ifdef HAS_RAWTOARGBROW_NEON void RAWToARGBRow_NEON(const uint8* src_raw, uint8* dst_argb, int width) { asm volatile ( "movi v5.8b, #255 \n" // Alpha @@ -865,12 +809,11 @@ void RAWToARGBRow_NEON(const uint8* src_raw, uint8* dst_argb, int width) { "b.gt 1b \n" : "+r"(src_raw), // %0 "+r"(dst_argb), // %1 - "+r"(width) // %2 + "+r"(width) // %2 : : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5" // Clobber List ); } -#endif // HAS_RAWTOARGBROW_NEON void RAWToRGB24Row_NEON(const uint8* src_raw, uint8* dst_rgb24, int width) { asm volatile ( @@ -904,7 +847,6 @@ void RAWToRGB24Row_NEON(const uint8* src_raw, uint8* dst_rgb24, int width) { "orr v0.16b, v0.16b, v2.16b \n" /* R,B */ \ "dup v2.2D, v0.D[1] \n" /* R */ -#ifdef HAS_RGB565TOARGBROW_NEON void RGB565ToARGBRow_NEON(const uint8* src_rgb565, uint8* dst_argb, int width) { asm volatile ( "movi v3.8b, #255 \n" // Alpha @@ -923,7 +865,6 @@ void RGB565ToARGBRow_NEON(const uint8* src_rgb565, uint8* dst_argb, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v6" // Clobber List ); } -#endif // HAS_RGB565TOARGBROW_NEON #define ARGB1555TOARGB \ "ushr v2.8h, v0.8h, #10 \n" /* R xxxRRRRR */ \ @@ -962,7 +903,6 @@ void RGB565ToARGBRow_NEON(const uint8* src_rgb565, uint8* dst_argb, int width) { "orr v2.16b, v1.16b, v3.16b \n" /* R */ \ "dup v1.2D, v0.D[1] \n" /* G */ \ -#ifdef HAS_ARGB1555TOARGBROW_NEON void ARGB1555ToARGBRow_NEON(const uint8* src_argb1555, uint8* dst_argb, int width) { asm volatile ( @@ -982,7 +922,6 @@ void ARGB1555ToARGBRow_NEON(const uint8* src_argb1555, uint8* dst_argb, : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_ARGB1555TOARGBROW_NEON #define ARGB4444TOARGB \ "shrn v1.8b, v0.8h, #8 \n" /* v1(l) AR */ \ @@ -996,7 +935,6 @@ void ARGB1555ToARGBRow_NEON(const uint8* src_argb1555, uint8* dst_argb, "dup v0.2D, v2.D[1] \n" \ "dup v1.2D, v3.D[1] \n" -#ifdef HAS_ARGB4444TOARGBROW_NEON void ARGB4444ToARGBRow_NEON(const uint8* src_argb4444, uint8* dst_argb, int width) { asm volatile ( @@ -1015,9 +953,7 @@ void ARGB4444ToARGBRow_NEON(const uint8* src_argb4444, uint8* dst_argb, : "cc", "memory", "v0", "v1", "v2", "v3", "v4" // Clobber List ); } -#endif // HAS_ARGB4444TOARGBROW_NEON -#ifdef HAS_ARGBTORGB24ROW_NEON void ARGBToRGB24Row_NEON(const uint8* src_argb, uint8* dst_rgb24, int width) { asm volatile ( "1: \n" @@ -1034,9 +970,7 @@ void ARGBToRGB24Row_NEON(const uint8* src_argb, uint8* dst_rgb24, int width) { : "cc", "memory", "v1", "v2", "v3", "v4" // Clobber List ); } -#endif // HAS_ARGBTORGB24ROW_NEON -#ifdef HAS_ARGBTORAWROW_NEON void ARGBToRAWRow_NEON(const uint8* src_argb, uint8* dst_raw, int width) { asm volatile ( "1: \n" @@ -1055,9 +989,7 @@ void ARGBToRAWRow_NEON(const uint8* src_argb, uint8* dst_raw, int width) { : "cc", "memory", "v1", "v2", "v3", "v4", "v5" // Clobber List ); } -#endif // HAS_ARGBTORAWROW_NEON -#ifdef HAS_YUY2TOYROW_NEON void YUY2ToYRow_NEON(const uint8* src_yuy2, uint8* dst_y, int width) { asm volatile ( "1: \n" @@ -1074,9 +1006,7 @@ void YUY2ToYRow_NEON(const uint8* src_yuy2, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1" // Clobber List ); } -#endif // HAS_YUY2TOYROW_NEON -#ifdef HAS_UYVYTOYROW_NEON void UYVYToYRow_NEON(const uint8* src_uyvy, uint8* dst_y, int width) { asm volatile ( "1: \n" @@ -1093,9 +1023,7 @@ void UYVYToYRow_NEON(const uint8* src_uyvy, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1" // Clobber List ); } -#endif // HAS_UYVYTOYROW_NEON -#ifdef HAS_YUY2TOUV422ROW_NEON void YUY2ToUV422Row_NEON(const uint8* src_yuy2, uint8* dst_u, uint8* dst_v, int width) { asm volatile ( @@ -1116,9 +1044,7 @@ void YUY2ToUV422Row_NEON(const uint8* src_yuy2, uint8* dst_u, uint8* dst_v, : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_YUY2TOUV422ROW_NEON -#ifdef HAS_UYVYTOUV422ROW_NEON void UYVYToUV422Row_NEON(const uint8* src_uyvy, uint8* dst_u, uint8* dst_v, int width) { asm volatile ( @@ -1139,9 +1065,7 @@ void UYVYToUV422Row_NEON(const uint8* src_uyvy, uint8* dst_u, uint8* dst_v, : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_UYVYTOUV422ROW_NEON -#ifdef HAS_YUY2TOUVROW_NEON void YUY2ToUVRow_NEON(const uint8* src_yuy2, int stride_yuy2, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_yuy2b = src_yuy2 + stride_yuy2; @@ -1169,9 +1093,7 @@ void YUY2ToUVRow_NEON(const uint8* src_yuy2, int stride_yuy2, "v5", "v6", "v7" // Clobber List ); } -#endif // HAS_YUY2TOUVROW_NEON -#ifdef HAS_UYVYTOUVROW_NEON void UYVYToUVRow_NEON(const uint8* src_uyvy, int stride_uyvy, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_uyvyb = src_uyvy + stride_uyvy; @@ -1199,10 +1121,8 @@ void UYVYToUVRow_NEON(const uint8* src_uyvy, int stride_uyvy, "v5", "v6", "v7" // Clobber List ); } -#endif // HAS_UYVYTOUVROW_NEON // For BGRAToARGB, ABGRToARGB, RGBAToARGB, and ARGBToRGBA. -#ifdef HAS_ARGBSHUFFLEROW_NEON void ARGBShuffleRow_NEON(const uint8* src_argb, uint8* dst_argb, const uint8* shuffler, int width) { asm volatile ( @@ -1223,9 +1143,7 @@ void ARGBShuffleRow_NEON(const uint8* src_argb, uint8* dst_argb, : "cc", "memory", "v0", "v1", "v2" // Clobber List ); } -#endif // HAS_ARGBSHUFFLEROW_NEON -#ifdef HAS_I422TOYUY2ROW_NEON void I422ToYUY2Row_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -1252,9 +1170,7 @@ void I422ToYUY2Row_NEON(const uint8* src_y, : "cc", "memory", "v0", "v1", "v2", "v3" ); } -#endif // HAS_I422TOYUY2ROW_NEON -#ifdef HAS_I422TOUYVYROW_NEON void I422ToUYVYRow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -1281,9 +1197,7 @@ void I422ToUYVYRow_NEON(const uint8* src_y, : "cc", "memory", "v0", "v1", "v2", "v3" ); } -#endif // HAS_I422TOUYVYROW_NEON -#ifdef HAS_ARGBTORGB565ROW_NEON void ARGBToRGB565Row_NEON(const uint8* src_argb, uint8* dst_rgb565, int width) { asm volatile ( "1: \n" @@ -1301,9 +1215,7 @@ void ARGBToRGB565Row_NEON(const uint8* src_argb, uint8* dst_rgb565, int width) { : "cc", "memory", "v0", "v20", "v21", "v22", "v23" ); } -#endif // HAS_ARGBTORGB565ROW_NEON -#ifdef HAS_ARGBTORGB565DITHERROW_NEON void ARGBToRGB565DitherRow_NEON(const uint8* src_argb, uint8* dst_rgb, const uint32 dither4, int width) { asm volatile ( @@ -1326,9 +1238,7 @@ void ARGBToRGB565DitherRow_NEON(const uint8* src_argb, uint8* dst_rgb, : "cc", "memory", "v0", "v1", "v20", "v21", "v22", "v23" ); } -#endif // HAS_ARGBTORGB565ROW_NEON -#ifdef HAS_ARGBTOARGB1555ROW_NEON void ARGBToARGB1555Row_NEON(const uint8* src_argb, uint8* dst_argb1555, int width) { asm volatile ( @@ -1347,9 +1257,7 @@ void ARGBToARGB1555Row_NEON(const uint8* src_argb, uint8* dst_argb1555, : "cc", "memory", "v0", "v20", "v21", "v22", "v23" ); } -#endif // HAS_ARGBTOARGB1555ROW_NEON -#ifdef HAS_ARGBTOARGB4444ROW_NEON void ARGBToARGB4444Row_NEON(const uint8* src_argb, uint8* dst_argb4444, int width) { asm volatile ( @@ -1369,9 +1277,7 @@ void ARGBToARGB4444Row_NEON(const uint8* src_argb, uint8* dst_argb4444, : "cc", "memory", "v0", "v1", "v4", "v20", "v21", "v22", "v23" ); } -#endif // HAS_ARGBTOARGB4444ROW_NEON -#ifdef HAS_ARGBTOYROW_NEON void ARGBToYRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #13 \n" // B * 0.1016 coefficient @@ -1397,9 +1303,24 @@ void ARGBToYRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGBTOYROW_NEON -#ifdef HAS_ARGBTOYJROW_NEON +void ARGBExtractAlphaRow_NEON(const uint8* src_argb, uint8* dst_a, int width) { + asm volatile ( + "1: \n" + MEMACCESS(0) + "ld4 {v0.16b,v1.16b,v2.16b,v3.16b}, [%0], #64 \n" // load row 16 pixels + "subs %w2, %w2, #16 \n" // 16 processed per loop + MEMACCESS(1) + "st1 {v3.16b}, [%1], #16 \n" // store 16 A's. + "b.gt 1b \n" + : "+r"(src_argb), // %0 + "+r"(dst_a), // %1 + "+r"(width) // %2 + : + : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List + ); +} + void ARGBToYJRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #15 \n" // B * 0.11400 coefficient @@ -1423,10 +1344,8 @@ void ARGBToYJRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6" ); } -#endif // HAS_ARGBTOYJROW_NEON // 8x1 pixels. -#ifdef HAS_ARGBTOUV444ROW_NEON void ARGBToUV444Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width) { asm volatile ( @@ -1467,7 +1386,6 @@ void ARGBToUV444Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, "v24", "v25", "v26", "v27", "v28", "v29" ); } -#endif // HAS_ARGBTOUV444ROW_NEON #define RGBTOUV_SETUP_REG \ "movi v20.8h, #56, lsl #0 \n" /* UB/VR coefficient (0.875) / 2 */ \ @@ -1477,52 +1395,7 @@ void ARGBToUV444Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, "movi v24.8h, #47, lsl #0 \n" /* VG coefficient (-0.7344) / 2 */ \ "movi v25.16b, #0x80 \n" /* 128.5 (0x8080 in 16-bit) */ -// 16x1 pixels -> 8x1. width is number of argb pixels. e.g. 16. -#ifdef HAS_ARGBTOUV422ROW_NEON -void ARGBToUV422Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, - int width) { - asm volatile ( - RGBTOUV_SETUP_REG - "1: \n" - MEMACCESS(0) - "ld4 {v0.16b,v1.16b,v2.16b,v3.16b}, [%0], #64 \n" // load 16 pixels. - - "uaddlp v0.8h, v0.16b \n" // B 16 bytes -> 8 shorts. - "uaddlp v1.8h, v1.16b \n" // G 16 bytes -> 8 shorts. - "uaddlp v2.8h, v2.16b \n" // R 16 bytes -> 8 shorts. - - "subs %w3, %w3, #16 \n" // 16 processed per loop. - "mul v3.8h, v0.8h, v20.8h \n" // B - "mls v3.8h, v1.8h, v21.8h \n" // G - "mls v3.8h, v2.8h, v22.8h \n" // R - "add v3.8h, v3.8h, v25.8h \n" // +128 -> unsigned - - "mul v4.8h, v2.8h, v20.8h \n" // R - "mls v4.8h, v1.8h, v24.8h \n" // G - "mls v4.8h, v0.8h, v23.8h \n" // B - "add v4.8h, v4.8h, v25.8h \n" // +128 -> unsigned - - "uqshrn v0.8b, v3.8h, #8 \n" // 16 bit to 8 bit U - "uqshrn v1.8b, v4.8h, #8 \n" // 16 bit to 8 bit V - - MEMACCESS(1) - "st1 {v0.8b}, [%1], #8 \n" // store 8 pixels U. - MEMACCESS(2) - "st1 {v1.8b}, [%2], #8 \n" // store 8 pixels V. - "b.gt 1b \n" - : "+r"(src_argb), // %0 - "+r"(dst_u), // %1 - "+r"(dst_v), // %2 - "+r"(width) // %3 - : - : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", - "v20", "v21", "v22", "v23", "v24", "v25" - ); -} -#endif // HAS_ARGBTOUV422ROW_NEON - // 32x1 pixels -> 8x1. width is number of argb pixels. e.g. 32. -#ifdef HAS_ARGBTOUV411ROW_NEON void ARGBToUV411Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width) { asm volatile ( @@ -1572,7 +1445,6 @@ void ARGBToUV411Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_ARGBTOUV411ROW_NEON // 16x2 pixels -> 8x1. width is number of argb pixels. e.g. 16. #define RGBTOUV(QB, QG, QR) \ @@ -1590,7 +1462,6 @@ void ARGBToUV411Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, // TODO(fbarchard): Consider vhadd vertical, then vpaddl horizontal, avoid shr. // TODO(fbarchard): consider ptrdiff_t for all strides. -#ifdef HAS_ARGBTOUVROW_NEON void ARGBToUVRow_NEON(const uint8* src_argb, int src_stride_argb, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_argb_1 = src_argb + src_stride_argb; @@ -1630,10 +1501,8 @@ void ARGBToUVRow_NEON(const uint8* src_argb, int src_stride_argb, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_ARGBTOUVROW_NEON // TODO(fbarchard): Subsample match C code. -#ifdef HAS_ARGBTOUVJROW_NEON void ARGBToUVJRow_NEON(const uint8* src_argb, int src_stride_argb, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_argb_1 = src_argb + src_stride_argb; @@ -1677,9 +1546,7 @@ void ARGBToUVJRow_NEON(const uint8* src_argb, int src_stride_argb, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_ARGBTOUVJROW_NEON -#ifdef HAS_BGRATOUVROW_NEON void BGRAToUVRow_NEON(const uint8* src_bgra, int src_stride_bgra, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_bgra_1 = src_bgra + src_stride_bgra; @@ -1718,9 +1585,7 @@ void BGRAToUVRow_NEON(const uint8* src_bgra, int src_stride_bgra, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_BGRATOUVROW_NEON -#ifdef HAS_ABGRTOUVROW_NEON void ABGRToUVRow_NEON(const uint8* src_abgr, int src_stride_abgr, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_abgr_1 = src_abgr + src_stride_abgr; @@ -1759,9 +1624,7 @@ void ABGRToUVRow_NEON(const uint8* src_abgr, int src_stride_abgr, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_ABGRTOUVROW_NEON -#ifdef HAS_RGBATOUVROW_NEON void RGBAToUVRow_NEON(const uint8* src_rgba, int src_stride_rgba, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_rgba_1 = src_rgba + src_stride_rgba; @@ -1800,9 +1663,7 @@ void RGBAToUVRow_NEON(const uint8* src_rgba, int src_stride_rgba, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_RGBATOUVROW_NEON -#ifdef HAS_RGB24TOUVROW_NEON void RGB24ToUVRow_NEON(const uint8* src_rgb24, int src_stride_rgb24, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_rgb24_1 = src_rgb24 + src_stride_rgb24; @@ -1841,9 +1702,7 @@ void RGB24ToUVRow_NEON(const uint8* src_rgb24, int src_stride_rgb24, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_RGB24TOUVROW_NEON -#ifdef HAS_RAWTOUVROW_NEON void RAWToUVRow_NEON(const uint8* src_raw, int src_stride_raw, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_raw_1 = src_raw + src_stride_raw; @@ -1882,10 +1741,8 @@ void RAWToUVRow_NEON(const uint8* src_raw, int src_stride_raw, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_RAWTOUVROW_NEON // 16x2 pixels -> 8x1. width is number of argb pixels. e.g. 16. -#ifdef HAS_RGB565TOUVROW_NEON void RGB565ToUVRow_NEON(const uint8* src_rgb565, int src_stride_rgb565, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_rgb565_1 = src_rgb565 + src_stride_rgb565; @@ -1958,10 +1815,8 @@ void RGB565ToUVRow_NEON(const uint8* src_rgb565, int src_stride_rgb565, "v25", "v26", "v27" ); } -#endif // HAS_RGB565TOUVROW_NEON // 16x2 pixels -> 8x1. width is number of argb pixels. e.g. 16. -#ifdef HAS_ARGB1555TOUVROW_NEON void ARGB1555ToUVRow_NEON(const uint8* src_argb1555, int src_stride_argb1555, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_argb1555_1 = src_argb1555 + src_stride_argb1555; @@ -2029,10 +1884,8 @@ void ARGB1555ToUVRow_NEON(const uint8* src_argb1555, int src_stride_argb1555, "v26", "v27", "v28" ); } -#endif // HAS_ARGB1555TOUVROW_NEON // 16x2 pixels -> 8x1. width is number of argb pixels. e.g. 16. -#ifdef HAS_ARGB4444TOUVROW_NEON void ARGB4444ToUVRow_NEON(const uint8* src_argb4444, int src_stride_argb4444, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_argb4444_1 = src_argb4444 + src_stride_argb4444; @@ -2101,9 +1954,7 @@ void ARGB4444ToUVRow_NEON(const uint8* src_argb4444, int src_stride_argb4444, ); } -#endif // HAS_ARGB4444TOUVROW_NEON -#ifdef HAS_RGB565TOYROW_NEON void RGB565ToYRow_NEON(const uint8* src_rgb565, uint8* dst_y, int width) { asm volatile ( "movi v24.8b, #13 \n" // B * 0.1016 coefficient @@ -2131,9 +1982,7 @@ void RGB565ToYRow_NEON(const uint8* src_rgb565, uint8* dst_y, int width) { "v24", "v25", "v26", "v27" ); } -#endif // HAS_RGB565TOYROW_NEON -#ifdef HAS_ARGB1555TOYROW_NEON void ARGB1555ToYRow_NEON(const uint8* src_argb1555, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #13 \n" // B * 0.1016 coefficient @@ -2160,9 +2009,7 @@ void ARGB1555ToYRow_NEON(const uint8* src_argb1555, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGB1555TOYROW_NEON -#ifdef HAS_ARGB4444TOYROW_NEON void ARGB4444ToYRow_NEON(const uint8* src_argb4444, uint8* dst_y, int width) { asm volatile ( "movi v24.8b, #13 \n" // B * 0.1016 coefficient @@ -2189,9 +2036,7 @@ void ARGB4444ToYRow_NEON(const uint8* src_argb4444, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v24", "v25", "v26", "v27" ); } -#endif // HAS_ARGB4444TOYROW_NEON -#ifdef HAS_BGRATOYROW_NEON void BGRAToYRow_NEON(const uint8* src_bgra, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #33 \n" // R * 0.2578 coefficient @@ -2217,9 +2062,7 @@ void BGRAToYRow_NEON(const uint8* src_bgra, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16" ); } -#endif // HAS_BGRATOYROW_NEON -#ifdef HAS_ABGRTOYROW_NEON void ABGRToYRow_NEON(const uint8* src_abgr, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #33 \n" // R * 0.2578 coefficient @@ -2245,9 +2088,7 @@ void ABGRToYRow_NEON(const uint8* src_abgr, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16" ); } -#endif // HAS_ABGRTOYROW_NEON -#ifdef HAS_RGBATOYROW_NEON void RGBAToYRow_NEON(const uint8* src_rgba, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #13 \n" // B * 0.1016 coefficient @@ -2273,9 +2114,7 @@ void RGBAToYRow_NEON(const uint8* src_rgba, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16" ); } -#endif // HAS_RGBATOYROW_NEON -#ifdef HAS_RGB24TOYROW_NEON void RGB24ToYRow_NEON(const uint8* src_rgb24, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #13 \n" // B * 0.1016 coefficient @@ -2301,9 +2140,7 @@ void RGB24ToYRow_NEON(const uint8* src_rgb24, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16" ); } -#endif // HAS_RGB24TOYROW_NEON -#ifdef HAS_RAWTOYROW_NEON void RAWToYRow_NEON(const uint8* src_raw, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #33 \n" // R * 0.2578 coefficient @@ -2329,10 +2166,8 @@ void RAWToYRow_NEON(const uint8* src_raw, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16" ); } -#endif // HAS_RAWTOYROW_NEON // Bilinear filter 16x2 -> 16x1 -#ifdef HAS_INTERPOLATEROW_NEON void InterpolateRow_NEON(uint8* dst_ptr, const uint8* src_ptr, ptrdiff_t src_stride, int dst_width, int source_y_fraction) { @@ -2398,10 +2233,8 @@ void InterpolateRow_NEON(uint8* dst_ptr, : "cc", "memory", "v0", "v1", "v3", "v4", "v5" ); } -#endif // HAS_INTERPOLATEROW_NEON // dr * (256 - sa) / 256 + sr = dr - dr * sa / 256 + sr -#ifdef HAS_ARGBBLENDROW_NEON void ARGBBlendRow_NEON(const uint8* src_argb0, const uint8* src_argb1, uint8* dst_argb, int width) { asm volatile ( @@ -2470,10 +2303,8 @@ void ARGBBlendRow_NEON(const uint8* src_argb0, const uint8* src_argb1, "v16", "v17", "v18" ); } -#endif // HAS_ARGBBLENDROW_NEON // Attenuate 8 pixels at a time. -#ifdef HAS_ARGBATTENUATEROW_NEON void ARGBAttenuateRow_NEON(const uint8* src_argb, uint8* dst_argb, int width) { asm volatile ( // Attenuate 8 pixels. @@ -2497,11 +2328,9 @@ void ARGBAttenuateRow_NEON(const uint8* src_argb, uint8* dst_argb, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6" ); } -#endif // HAS_ARGBATTENUATEROW_NEON // Quantize 8 ARGB pixels (32 bytes). // dst = (dst * scale >> 16) * interval_size + interval_offset; -#ifdef HAS_ARGBQUANTIZEROW_NEON void ARGBQuantizeRow_NEON(uint8* dst_argb, int scale, int interval_size, int interval_offset, int width) { asm volatile ( @@ -2541,12 +2370,10 @@ void ARGBQuantizeRow_NEON(uint8* dst_argb, int scale, int interval_size, : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6" ); } -#endif // HAS_ARGBQUANTIZEROW_NEON // Shade 8 pixels at a time by specified value. // NOTE vqrdmulh.s16 q10, q10, d0[0] must use a scaler register from 0 to 8. // Rounding in vqrdmulh does +1 to high if high bit of low s16 is set. -#ifdef HAS_ARGBSHADEROW_NEON void ARGBShadeRow_NEON(const uint8* src_argb, uint8* dst_argb, int width, uint32 value) { asm volatile ( @@ -2581,12 +2408,10 @@ void ARGBShadeRow_NEON(const uint8* src_argb, uint8* dst_argb, int width, : "cc", "memory", "v0", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGBSHADEROW_NEON // Convert 8 ARGB pixels (64 bytes) to 8 Gray ARGB pixels // Similar to ARGBToYJ but stores ARGB. // C code is (15 * b + 75 * g + 38 * r + 64) >> 7; -#ifdef HAS_ARGBGRAYROW_NEON void ARGBGrayRow_NEON(const uint8* src_argb, uint8* dst_argb, int width) { asm volatile ( "movi v24.8b, #15 \n" // B * 0.11400 coefficient @@ -2612,14 +2437,12 @@ void ARGBGrayRow_NEON(const uint8* src_argb, uint8* dst_argb, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v24", "v25", "v26" ); } -#endif // HAS_ARGBGRAYROW_NEON // Convert 8 ARGB pixels (32 bytes) to 8 Sepia ARGB pixels. // b = (r * 35 + g * 68 + b * 17) >> 7 // g = (r * 45 + g * 88 + b * 22) >> 7 // r = (r * 50 + g * 98 + b * 24) >> 7 -#ifdef HAS_ARGBSEPIAROW_NEON void ARGBSepiaRow_NEON(uint8* dst_argb, int width) { asm volatile ( "movi v20.8b, #17 \n" // BB coefficient @@ -2657,12 +2480,10 @@ void ARGBSepiaRow_NEON(uint8* dst_argb, int width) { "v20", "v21", "v22", "v24", "v25", "v26", "v28", "v29", "v30" ); } -#endif // HAS_ARGBSEPIAROW_NEON // Tranform 8 ARGB pixels (32 bytes) with color matrix. // TODO(fbarchard): Was same as Sepia except matrix is provided. This function // needs to saturate. Consider doing a non-saturating version. -#ifdef HAS_ARGBCOLORMATRIXROW_NEON void ARGBColorMatrixRow_NEON(const uint8* src_argb, uint8* dst_argb, const int8* matrix_argb, int width) { asm volatile ( @@ -2722,11 +2543,9 @@ void ARGBColorMatrixRow_NEON(const uint8* src_argb, uint8* dst_argb, "v18", "v19", "v22", "v23", "v24", "v25" ); } -#endif // HAS_ARGBCOLORMATRIXROW_NEON // TODO(fbarchard): fix vqshrun in ARGBMultiplyRow_NEON and reenable. // Multiply 2 rows of ARGB pixels together, 8 pixels at a time. -#ifdef HAS_ARGBMULTIPLYROW_NEON void ARGBMultiplyRow_NEON(const uint8* src_argb0, const uint8* src_argb1, uint8* dst_argb, int width) { asm volatile ( @@ -2757,10 +2576,8 @@ void ARGBMultiplyRow_NEON(const uint8* src_argb0, const uint8* src_argb1, : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGBMULTIPLYROW_NEON // Add 2 rows of ARGB pixels together, 8 pixels at a time. -#ifdef HAS_ARGBADDROW_NEON void ARGBAddRow_NEON(const uint8* src_argb0, const uint8* src_argb1, uint8* dst_argb, int width) { asm volatile ( @@ -2787,10 +2604,8 @@ void ARGBAddRow_NEON(const uint8* src_argb0, const uint8* src_argb1, : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGBADDROW_NEON // Subtract 2 rows of ARGB pixels, 8 pixels at a time. -#ifdef HAS_ARGBSUBTRACTROW_NEON void ARGBSubtractRow_NEON(const uint8* src_argb0, const uint8* src_argb1, uint8* dst_argb, int width) { asm volatile ( @@ -2817,14 +2632,12 @@ void ARGBSubtractRow_NEON(const uint8* src_argb0, const uint8* src_argb1, : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGBSUBTRACTROW_NEON // Adds Sobel X and Sobel Y and stores Sobel into ARGB. // A = 255 // R = Sobel // G = Sobel // B = Sobel -#ifdef HAS_SOBELROW_NEON void SobelRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, uint8* dst_argb, int width) { asm volatile ( @@ -2850,10 +2663,8 @@ void SobelRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, : "cc", "memory", "v0", "v1", "v2", "v3" ); } -#endif // HAS_SOBELROW_NEON // Adds Sobel X and Sobel Y and stores Sobel into plane. -#ifdef HAS_SOBELTOPLANEROW_NEON void SobelToPlaneRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, uint8* dst_y, int width) { asm volatile ( @@ -2876,14 +2687,12 @@ void SobelToPlaneRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, : "cc", "memory", "v0", "v1" ); } -#endif // HAS_SOBELTOPLANEROW_NEON // Mixes Sobel X, Sobel Y and Sobel into ARGB. // A = 255 // R = Sobel X // G = Sobel // B = Sobel Y -#ifdef HAS_SOBELXYROW_NEON void SobelXYRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, uint8* dst_argb, int width) { asm volatile ( @@ -2907,13 +2716,11 @@ void SobelXYRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, : "cc", "memory", "v0", "v1", "v2", "v3" ); } -#endif // HAS_SOBELXYROW_NEON // SobelX as a matrix is // -1 0 1 // -2 0 2 // -1 0 1 -#ifdef HAS_SOBELXROW_NEON void SobelXRow_NEON(const uint8* src_y0, const uint8* src_y1, const uint8* src_y2, uint8* dst_sobelx, int width) { asm volatile ( @@ -2952,13 +2759,11 @@ void SobelXRow_NEON(const uint8* src_y0, const uint8* src_y1, : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_SOBELXROW_NEON // SobelY as a matrix is // -1 -2 -1 // 0 0 0 // 1 2 1 -#ifdef HAS_SOBELYROW_NEON void SobelYRow_NEON(const uint8* src_y0, const uint8* src_y1, uint8* dst_sobely, int width) { asm volatile ( @@ -2996,7 +2801,6 @@ void SobelYRow_NEON(const uint8* src_y0, const uint8* src_y1, : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_SOBELYROW_NEON #endif // !defined(LIBYUV_DISABLE_NEON) && defined(__aarch64__) #ifdef __cplusplus diff --git a/TMessagesProj/jni/libyuv/source/row_win.cc b/TMessagesProj/jni/libyuv/source/row_win.cc index 5cb5d1e4f..cdb760603 100644 --- a/TMessagesProj/jni/libyuv/source/row_win.cc +++ b/TMessagesProj/jni/libyuv/source/row_win.cc @@ -1505,7 +1505,7 @@ void ARGBToUVJRow_SSSE3(const uint8* src_argb0, int src_stride_argb, pmaddubsw xmm3, xmm6 phaddw xmm0, xmm2 phaddw xmm1, xmm3 - paddw xmm0, xmm5 // +.5 rounding -> unsigned + paddw xmm0, xmm5 // +.5 rounding -> unsigned paddw xmm1, xmm5 psraw xmm0, 8 psraw xmm1, 8 @@ -1590,6 +1590,73 @@ void ARGBToUVRow_AVX2(const uint8* src_argb0, int src_stride_argb, } #endif // HAS_ARGBTOUVROW_AVX2 +#ifdef HAS_ARGBTOUVJROW_AVX2 +__declspec(naked) +void ARGBToUVJRow_AVX2(const uint8* src_argb0, int src_stride_argb, + uint8* dst_u, uint8* dst_v, int width) { + __asm { + push esi + push edi + mov eax, [esp + 8 + 4] // src_argb + mov esi, [esp + 8 + 8] // src_stride_argb + mov edx, [esp + 8 + 12] // dst_u + mov edi, [esp + 8 + 16] // dst_v + mov ecx, [esp + 8 + 20] // width + vbroadcastf128 ymm5, xmmword ptr kAddUV128 + vbroadcastf128 ymm6, xmmword ptr kARGBToV + vbroadcastf128 ymm7, xmmword ptr kARGBToU + sub edi, edx // stride from u to v + + convertloop: + /* step 1 - subsample 32x2 argb pixels to 16x1 */ + vmovdqu ymm0, [eax] + vmovdqu ymm1, [eax + 32] + vmovdqu ymm2, [eax + 64] + vmovdqu ymm3, [eax + 96] + vpavgb ymm0, ymm0, [eax + esi] + vpavgb ymm1, ymm1, [eax + esi + 32] + vpavgb ymm2, ymm2, [eax + esi + 64] + vpavgb ymm3, ymm3, [eax + esi + 96] + lea eax, [eax + 128] + vshufps ymm4, ymm0, ymm1, 0x88 + vshufps ymm0, ymm0, ymm1, 0xdd + vpavgb ymm0, ymm0, ymm4 // mutated by vshufps + vshufps ymm4, ymm2, ymm3, 0x88 + vshufps ymm2, ymm2, ymm3, 0xdd + vpavgb ymm2, ymm2, ymm4 // mutated by vshufps + + // step 2 - convert to U and V + // from here down is very similar to Y code except + // instead of 32 different pixels, its 16 pixels of U and 16 of V + vpmaddubsw ymm1, ymm0, ymm7 // U + vpmaddubsw ymm3, ymm2, ymm7 + vpmaddubsw ymm0, ymm0, ymm6 // V + vpmaddubsw ymm2, ymm2, ymm6 + vphaddw ymm1, ymm1, ymm3 // mutates + vphaddw ymm0, ymm0, ymm2 + vpaddw ymm1, ymm1, ymm5 // +.5 rounding -> unsigned + vpaddw ymm0, ymm0, ymm5 + vpsraw ymm1, ymm1, 8 + vpsraw ymm0, ymm0, 8 + vpacksswb ymm0, ymm1, ymm0 // mutates + vpermq ymm0, ymm0, 0xd8 // For vpacksswb + vpshufb ymm0, ymm0, ymmword ptr kShufARGBToUV_AVX // for vshufps/vphaddw + + // step 3 - store 16 U and 16 V values + vextractf128 [edx], ymm0, 0 // U + vextractf128 [edx + edi], ymm0, 1 // V + lea edx, [edx + 16] + sub ecx, 32 + jg convertloop + + pop edi + pop esi + vzeroupper + ret + } +} +#endif // HAS_ARGBTOUVJROW_AVX2 + __declspec(naked) void ARGBToUV444Row_SSSE3(const uint8* src_argb0, uint8* dst_u, uint8* dst_v, int width) { @@ -1647,64 +1714,6 @@ void ARGBToUV444Row_SSSE3(const uint8* src_argb0, } } -__declspec(naked) -void ARGBToUV422Row_SSSE3(const uint8* src_argb0, - uint8* dst_u, uint8* dst_v, int width) { - __asm { - push edi - mov eax, [esp + 4 + 4] // src_argb - mov edx, [esp + 4 + 8] // dst_u - mov edi, [esp + 4 + 12] // dst_v - mov ecx, [esp + 4 + 16] // width - movdqa xmm5, xmmword ptr kAddUV128 - movdqa xmm6, xmmword ptr kARGBToV - movdqa xmm7, xmmword ptr kARGBToU - sub edi, edx // stride from u to v - - convertloop: - /* step 1 - subsample 16x2 argb pixels to 8x1 */ - movdqu xmm0, [eax] - movdqu xmm1, [eax + 16] - movdqu xmm2, [eax + 32] - movdqu xmm3, [eax + 48] - lea eax, [eax + 64] - movdqa xmm4, xmm0 - shufps xmm0, xmm1, 0x88 - shufps xmm4, xmm1, 0xdd - pavgb xmm0, xmm4 - movdqa xmm4, xmm2 - shufps xmm2, xmm3, 0x88 - shufps xmm4, xmm3, 0xdd - pavgb xmm2, xmm4 - - // step 2 - convert to U and V - // from here down is very similar to Y code except - // instead of 16 different pixels, its 8 pixels of U and 8 of V - movdqa xmm1, xmm0 - movdqa xmm3, xmm2 - pmaddubsw xmm0, xmm7 // U - pmaddubsw xmm2, xmm7 - pmaddubsw xmm1, xmm6 // V - pmaddubsw xmm3, xmm6 - phaddw xmm0, xmm2 - phaddw xmm1, xmm3 - psraw xmm0, 8 - psraw xmm1, 8 - packsswb xmm0, xmm1 - paddb xmm0, xmm5 // -> unsigned - - // step 3 - store 8 U and 8 V values - movlps qword ptr [edx], xmm0 // U - movhps qword ptr [edx + edi], xmm0 // V - lea edx, [edx + 8] - sub ecx, 16 - jg convertloop - - pop edi - ret - } -} - __declspec(naked) void BGRAToUVRow_SSSE3(const uint8* src_argb0, int src_stride_argb, uint8* dst_u, uint8* dst_v, int width) { @@ -3154,7 +3163,7 @@ void MirrorRow_AVX2(const uint8* src, uint8* dst, int width) { } #endif // HAS_MIRRORROW_AVX2 -#ifdef HAS_MIRRORROW_UV_SSSE3 +#ifdef HAS_MIRRORUVROW_SSSE3 // Shuffle table for reversing the bytes of UV channels. static const uvec8 kShuffleMirrorUV = { 14u, 12u, 10u, 8u, 6u, 4u, 2u, 0u, 15u, 13u, 11u, 9u, 7u, 5u, 3u, 1u @@ -3187,7 +3196,7 @@ void MirrorUVRow_SSSE3(const uint8* src, uint8* dst_u, uint8* dst_v, ret } } -#endif // HAS_MIRRORROW_UV_SSSE3 +#endif // HAS_MIRRORUVROW_SSSE3 #ifdef HAS_ARGBMIRRORROW_SSE2 __declspec(naked) @@ -3523,6 +3532,33 @@ void ARGBCopyAlphaRow_AVX2(const uint8* src, uint8* dst, int width) { } #endif // HAS_ARGBCOPYALPHAROW_AVX2 +#ifdef HAS_ARGBEXTRACTALPHAROW_SSE2 +// width in pixels +__declspec(naked) +void ARGBExtractAlphaRow_SSE2(const uint8* src_argb, uint8* dst_a, int width) { + __asm { + mov eax, [esp + 4] // src_argb + mov edx, [esp + 8] // dst_a + mov ecx, [esp + 12] // width + + extractloop: + movdqu xmm0, [eax] + movdqu xmm1, [eax + 16] + lea eax, [eax + 32] + psrld xmm0, 24 + psrld xmm1, 24 + packssdw xmm0, xmm1 + packuswb xmm0, xmm0 + movq qword ptr [edx], xmm0 + lea edx, [edx + 8] + sub ecx, 8 + jg extractloop + + ret + } +} +#endif // HAS_ARGBEXTRACTALPHAROW_SSE2 + #ifdef HAS_ARGBCOPYYTOALPHAROW_SSE2 // width in pixels __declspec(naked) @@ -5239,6 +5275,7 @@ void SobelXYRow_SSE2(const uint8* src_sobelx, const uint8* src_sobely, // dst points to pixel to store result to. // count is number of averaged pixels to produce. // Does 4 pixels at a time. +// This function requires alignment on accumulation buffer pointers. void CumulativeSumToAverageRow_SSE2(const int32* topleft, const int32* botleft, int width, int area, uint8* dst, int count) { diff --git a/TMessagesProj/jni/libyuv/source/scale.cc b/TMessagesProj/jni/libyuv/source/scale.cc index 595314f35..36e3fe528 100644 --- a/TMessagesProj/jni/libyuv/source/scale.cc +++ b/TMessagesProj/jni/libyuv/source/scale.cc @@ -85,12 +85,12 @@ static void ScalePlaneDown2(int src_width, int src_height, } } #endif -#if defined(HAS_SCALEROWDOWN2_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(src_ptr, 4) && +#if defined(HAS_SCALEROWDOWN2_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(src_ptr, 4) && IS_ALIGNED(src_stride, 4) && IS_ALIGNED(row_stride, 4) && IS_ALIGNED(dst_ptr, 4) && IS_ALIGNED(dst_stride, 4)) { ScaleRowDown2 = filtering ? - ScaleRowDown2Box_MIPS_DSPR2 : ScaleRowDown2_MIPS_DSPR2; + ScaleRowDown2Box_DSPR2 : ScaleRowDown2_DSPR2; } #endif @@ -135,12 +135,12 @@ static void ScalePlaneDown2_16(int src_width, int src_height, ScaleRowDown2Box_16_SSE2); } #endif -#if defined(HAS_SCALEROWDOWN2_16_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(src_ptr, 4) && +#if defined(HAS_SCALEROWDOWN2_16_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(src_ptr, 4) && IS_ALIGNED(src_stride, 4) && IS_ALIGNED(row_stride, 4) && IS_ALIGNED(dst_ptr, 4) && IS_ALIGNED(dst_stride, 4)) { ScaleRowDown2 = filtering ? - ScaleRowDown2Box_16_MIPS_DSPR2 : ScaleRowDown2_16_MIPS_DSPR2; + ScaleRowDown2Box_16_DSPR2 : ScaleRowDown2_16_DSPR2; } #endif @@ -200,12 +200,12 @@ static void ScalePlaneDown4(int src_width, int src_height, } } #endif -#if defined(HAS_SCALEROWDOWN4_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(row_stride, 4) && +#if defined(HAS_SCALEROWDOWN4_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(row_stride, 4) && IS_ALIGNED(src_ptr, 4) && IS_ALIGNED(src_stride, 4) && IS_ALIGNED(dst_ptr, 4) && IS_ALIGNED(dst_stride, 4)) { ScaleRowDown4 = filtering ? - ScaleRowDown4Box_MIPS_DSPR2 : ScaleRowDown4_MIPS_DSPR2; + ScaleRowDown4Box_DSPR2 : ScaleRowDown4_DSPR2; } #endif @@ -245,12 +245,12 @@ static void ScalePlaneDown4_16(int src_width, int src_height, ScaleRowDown4_16_SSE2; } #endif -#if defined(HAS_SCALEROWDOWN4_16_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(row_stride, 4) && +#if defined(HAS_SCALEROWDOWN4_16_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(row_stride, 4) && IS_ALIGNED(src_ptr, 4) && IS_ALIGNED(src_stride, 4) && IS_ALIGNED(dst_ptr, 4) && IS_ALIGNED(dst_stride, 4)) { ScaleRowDown4 = filtering ? - ScaleRowDown4Box_16_MIPS_DSPR2 : ScaleRowDown4_16_MIPS_DSPR2; + ScaleRowDown4Box_16_DSPR2 : ScaleRowDown4_16_DSPR2; } #endif @@ -325,16 +325,16 @@ static void ScalePlaneDown34(int src_width, int src_height, } } #endif -#if defined(HAS_SCALEROWDOWN34_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && (dst_width % 24 == 0) && +#if defined(HAS_SCALEROWDOWN34_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && (dst_width % 24 == 0) && IS_ALIGNED(src_ptr, 4) && IS_ALIGNED(src_stride, 4) && IS_ALIGNED(dst_ptr, 4) && IS_ALIGNED(dst_stride, 4)) { if (!filtering) { - ScaleRowDown34_0 = ScaleRowDown34_MIPS_DSPR2; - ScaleRowDown34_1 = ScaleRowDown34_MIPS_DSPR2; + ScaleRowDown34_0 = ScaleRowDown34_DSPR2; + ScaleRowDown34_1 = ScaleRowDown34_DSPR2; } else { - ScaleRowDown34_0 = ScaleRowDown34_0_Box_MIPS_DSPR2; - ScaleRowDown34_1 = ScaleRowDown34_1_Box_MIPS_DSPR2; + ScaleRowDown34_0 = ScaleRowDown34_0_Box_DSPR2; + ScaleRowDown34_1 = ScaleRowDown34_1_Box_DSPR2; } } #endif @@ -404,16 +404,16 @@ static void ScalePlaneDown34_16(int src_width, int src_height, } } #endif -#if defined(HAS_SCALEROWDOWN34_16_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && (dst_width % 24 == 0) && +#if defined(HAS_SCALEROWDOWN34_16_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && (dst_width % 24 == 0) && IS_ALIGNED(src_ptr, 4) && IS_ALIGNED(src_stride, 4) && IS_ALIGNED(dst_ptr, 4) && IS_ALIGNED(dst_stride, 4)) { if (!filtering) { - ScaleRowDown34_0 = ScaleRowDown34_16_MIPS_DSPR2; - ScaleRowDown34_1 = ScaleRowDown34_16_MIPS_DSPR2; + ScaleRowDown34_0 = ScaleRowDown34_16_DSPR2; + ScaleRowDown34_1 = ScaleRowDown34_16_DSPR2; } else { - ScaleRowDown34_0 = ScaleRowDown34_0_Box_16_MIPS_DSPR2; - ScaleRowDown34_1 = ScaleRowDown34_1_Box_16_MIPS_DSPR2; + ScaleRowDown34_0 = ScaleRowDown34_0_Box_16_DSPR2; + ScaleRowDown34_1 = ScaleRowDown34_1_Box_16_DSPR2; } } #endif @@ -517,16 +517,16 @@ static void ScalePlaneDown38(int src_width, int src_height, } } #endif -#if defined(HAS_SCALEROWDOWN38_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && (dst_width % 12 == 0) && +#if defined(HAS_SCALEROWDOWN38_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && (dst_width % 12 == 0) && IS_ALIGNED(src_ptr, 4) && IS_ALIGNED(src_stride, 4) && IS_ALIGNED(dst_ptr, 4) && IS_ALIGNED(dst_stride, 4)) { if (!filtering) { - ScaleRowDown38_3 = ScaleRowDown38_MIPS_DSPR2; - ScaleRowDown38_2 = ScaleRowDown38_MIPS_DSPR2; + ScaleRowDown38_3 = ScaleRowDown38_DSPR2; + ScaleRowDown38_2 = ScaleRowDown38_DSPR2; } else { - ScaleRowDown38_3 = ScaleRowDown38_3_Box_MIPS_DSPR2; - ScaleRowDown38_2 = ScaleRowDown38_2_Box_MIPS_DSPR2; + ScaleRowDown38_3 = ScaleRowDown38_3_Box_DSPR2; + ScaleRowDown38_2 = ScaleRowDown38_2_Box_DSPR2; } } #endif @@ -595,16 +595,16 @@ static void ScalePlaneDown38_16(int src_width, int src_height, } } #endif -#if defined(HAS_SCALEROWDOWN38_16_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && (dst_width % 12 == 0) && +#if defined(HAS_SCALEROWDOWN38_16_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && (dst_width % 12 == 0) && IS_ALIGNED(src_ptr, 4) && IS_ALIGNED(src_stride, 4) && IS_ALIGNED(dst_ptr, 4) && IS_ALIGNED(dst_stride, 4)) { if (!filtering) { - ScaleRowDown38_3 = ScaleRowDown38_16_MIPS_DSPR2; - ScaleRowDown38_2 = ScaleRowDown38_16_MIPS_DSPR2; + ScaleRowDown38_3 = ScaleRowDown38_16_DSPR2; + ScaleRowDown38_2 = ScaleRowDown38_16_DSPR2; } else { - ScaleRowDown38_3 = ScaleRowDown38_3_Box_16_MIPS_DSPR2; - ScaleRowDown38_2 = ScaleRowDown38_2_Box_16_MIPS_DSPR2; + ScaleRowDown38_3 = ScaleRowDown38_3_Box_16_DSPR2; + ScaleRowDown38_2 = ScaleRowDown38_2_Box_16_DSPR2; } } #endif @@ -659,7 +659,6 @@ static void ScaleAddCols2_C(int dst_width, int boxheight, int x, int dx, int i; int scaletbl[2]; int minboxwidth = dx >> 16; - int* scaleptr = scaletbl - minboxwidth; int boxwidth; scaletbl[0] = 65536 / (MIN1(minboxwidth) * boxheight); scaletbl[1] = 65536 / (MIN1(minboxwidth + 1) * boxheight); @@ -667,7 +666,8 @@ static void ScaleAddCols2_C(int dst_width, int boxheight, int x, int dx, int ix = x >> 16; x += dx; boxwidth = MIN1((x >> 16) - ix); - *dst_ptr++ = SumPixels(boxwidth, src_ptr + ix) * scaleptr[boxwidth] >> 16; + *dst_ptr++ = SumPixels(boxwidth, src_ptr + ix) * + scaletbl[boxwidth - minboxwidth] >> 16; } } @@ -676,7 +676,6 @@ static void ScaleAddCols2_16_C(int dst_width, int boxheight, int x, int dx, int i; int scaletbl[2]; int minboxwidth = dx >> 16; - int* scaleptr = scaletbl - minboxwidth; int boxwidth; scaletbl[0] = 65536 / (MIN1(minboxwidth) * boxheight); scaletbl[1] = 65536 / (MIN1(minboxwidth + 1) * boxheight); @@ -684,8 +683,8 @@ static void ScaleAddCols2_16_C(int dst_width, int boxheight, int x, int dx, int ix = x >> 16; x += dx; boxwidth = MIN1((x >> 16) - ix); - *dst_ptr++ = - SumPixels_16(boxwidth, src_ptr + ix) * scaleptr[boxwidth] >> 16; + *dst_ptr++ = SumPixels_16(boxwidth, src_ptr + ix) * + scaletbl[boxwidth - minboxwidth] >> 16; } } @@ -899,11 +898,11 @@ void ScalePlaneBilinearDown(int src_width, int src_height, } } #endif -#if defined(HAS_INTERPOLATEROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2)) { - InterpolateRow = InterpolateRow_Any_MIPS_DSPR2; +#if defined(HAS_INTERPOLATEROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2)) { + InterpolateRow = InterpolateRow_Any_DSPR2; if (IS_ALIGNED(src_width, 4)) { - InterpolateRow = InterpolateRow_MIPS_DSPR2; + InterpolateRow = InterpolateRow_DSPR2; } } #endif @@ -1003,11 +1002,11 @@ void ScalePlaneBilinearDown_16(int src_width, int src_height, } } #endif -#if defined(HAS_INTERPOLATEROW_16_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2)) { - InterpolateRow = InterpolateRow_Any_16_MIPS_DSPR2; +#if defined(HAS_INTERPOLATEROW_16_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2)) { + InterpolateRow = InterpolateRow_Any_16_DSPR2; if (IS_ALIGNED(src_width, 4)) { - InterpolateRow = InterpolateRow_16_MIPS_DSPR2; + InterpolateRow = InterpolateRow_16_DSPR2; } } #endif @@ -1088,11 +1087,11 @@ void ScalePlaneBilinearUp(int src_width, int src_height, } } #endif -#if defined(HAS_INTERPOLATEROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2)) { - InterpolateRow = InterpolateRow_Any_MIPS_DSPR2; +#if defined(HAS_INTERPOLATEROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2)) { + InterpolateRow = InterpolateRow_Any_DSPR2; if (IS_ALIGNED(dst_width, 4)) { - InterpolateRow = InterpolateRow_MIPS_DSPR2; + InterpolateRow = InterpolateRow_DSPR2; } } #endif @@ -1227,11 +1226,11 @@ void ScalePlaneBilinearUp_16(int src_width, int src_height, } } #endif -#if defined(HAS_INTERPOLATEROW_16_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2)) { - InterpolateRow = InterpolateRow_Any_16_MIPS_DSPR2; +#if defined(HAS_INTERPOLATEROW_16_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2)) { + InterpolateRow = InterpolateRow_Any_16_DSPR2; if (IS_ALIGNED(dst_width, 4)) { - InterpolateRow = InterpolateRow_16_MIPS_DSPR2; + InterpolateRow = InterpolateRow_16_DSPR2; } } #endif diff --git a/TMessagesProj/jni/libyuv/source/scale_any.cc b/TMessagesProj/jni/libyuv/source/scale_any.cc index 1da4dacde..ed76a9e4c 100644 --- a/TMessagesProj/jni/libyuv/source/scale_any.cc +++ b/TMessagesProj/jni/libyuv/source/scale_any.cc @@ -55,12 +55,29 @@ CANY(ScaleARGBFilterCols_Any_NEON, ScaleARGBFilterCols_NEON, dst_ptr + n * BPP, r); \ } +// Fixed scale down for odd source width. Used by I420Blend subsampling. +// Since dst_width is (width + 1) / 2, this function scales one less pixel +// and copies the last pixel. +#define SDODD(NAMEANY, SCALEROWDOWN_SIMD, SCALEROWDOWN_C, FACTOR, BPP, MASK) \ + void NAMEANY(const uint8* src_ptr, ptrdiff_t src_stride, \ + uint8* dst_ptr, int dst_width) { \ + int r = (int)((unsigned int)(dst_width - 1) % (MASK + 1)); \ + int n = dst_width - r; \ + if (n > 0) { \ + SCALEROWDOWN_SIMD(src_ptr, src_stride, dst_ptr, n); \ + } \ + SCALEROWDOWN_C(src_ptr + (n * FACTOR) * BPP, src_stride, \ + dst_ptr + n * BPP, r); \ + } + #ifdef HAS_SCALEROWDOWN2_SSSE3 SDANY(ScaleRowDown2_Any_SSSE3, ScaleRowDown2_SSSE3, ScaleRowDown2_C, 2, 1, 15) SDANY(ScaleRowDown2Linear_Any_SSSE3, ScaleRowDown2Linear_SSSE3, ScaleRowDown2Linear_C, 2, 1, 15) SDANY(ScaleRowDown2Box_Any_SSSE3, ScaleRowDown2Box_SSSE3, ScaleRowDown2Box_C, 2, 1, 15) +SDODD(ScaleRowDown2Box_Odd_SSSE3, ScaleRowDown2Box_SSSE3, + ScaleRowDown2Box_Odd_C, 2, 1, 15) #endif #ifdef HAS_SCALEROWDOWN2_AVX2 SDANY(ScaleRowDown2_Any_AVX2, ScaleRowDown2_AVX2, ScaleRowDown2_C, 2, 1, 31) @@ -68,6 +85,8 @@ SDANY(ScaleRowDown2Linear_Any_AVX2, ScaleRowDown2Linear_AVX2, ScaleRowDown2Linear_C, 2, 1, 31) SDANY(ScaleRowDown2Box_Any_AVX2, ScaleRowDown2Box_AVX2, ScaleRowDown2Box_C, 2, 1, 31) +SDODD(ScaleRowDown2Box_Odd_AVX2, ScaleRowDown2Box_AVX2, ScaleRowDown2Box_Odd_C, + 2, 1, 31) #endif #ifdef HAS_SCALEROWDOWN2_NEON SDANY(ScaleRowDown2_Any_NEON, ScaleRowDown2_NEON, ScaleRowDown2_C, 2, 1, 15) @@ -75,6 +94,8 @@ SDANY(ScaleRowDown2Linear_Any_NEON, ScaleRowDown2Linear_NEON, ScaleRowDown2Linear_C, 2, 1, 15) SDANY(ScaleRowDown2Box_Any_NEON, ScaleRowDown2Box_NEON, ScaleRowDown2Box_C, 2, 1, 15) +SDODD(ScaleRowDown2Box_Odd_NEON, ScaleRowDown2Box_NEON, + ScaleRowDown2Box_Odd_C, 2, 1, 15) #endif #ifdef HAS_SCALEROWDOWN4_SSSE3 SDANY(ScaleRowDown4_Any_SSSE3, ScaleRowDown4_SSSE3, ScaleRowDown4_C, 4, 1, 7) diff --git a/TMessagesProj/jni/libyuv/source/scale_argb.cc b/TMessagesProj/jni/libyuv/source/scale_argb.cc index adddf9db5..17f51ae9b 100644 --- a/TMessagesProj/jni/libyuv/source/scale_argb.cc +++ b/TMessagesProj/jni/libyuv/source/scale_argb.cc @@ -234,12 +234,12 @@ static void ScaleARGBBilinearDown(int src_width, int src_height, } } #endif -#if defined(HAS_INTERPOLATEROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && +#if defined(HAS_INTERPOLATEROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(src_argb, 4) && IS_ALIGNED(src_stride, 4)) { - InterpolateRow = InterpolateRow_Any_MIPS_DSPR2; + InterpolateRow = InterpolateRow_Any_DSPR2; if (IS_ALIGNED(clip_src_width, 4)) { - InterpolateRow = InterpolateRow_MIPS_DSPR2; + InterpolateRow = InterpolateRow_DSPR2; } } #endif @@ -324,10 +324,10 @@ static void ScaleARGBBilinearUp(int src_width, int src_height, } } #endif -#if defined(HAS_INTERPOLATEROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && +#if defined(HAS_INTERPOLATEROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(dst_argb, 4) && IS_ALIGNED(dst_stride, 4)) { - InterpolateRow = InterpolateRow_MIPS_DSPR2; + InterpolateRow = InterpolateRow_DSPR2; } #endif if (src_width >= 32768) { @@ -465,13 +465,13 @@ static void ScaleYUVToARGBBilinearUp(int src_width, int src_height, } } #endif -#if defined(HAS_I422TOARGBROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && IS_ALIGNED(src_width, 4) && +#if defined(HAS_I422TOARGBROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(src_width, 4) && IS_ALIGNED(src_y, 4) && IS_ALIGNED(src_stride_y, 4) && IS_ALIGNED(src_u, 2) && IS_ALIGNED(src_stride_u, 2) && IS_ALIGNED(src_v, 2) && IS_ALIGNED(src_stride_v, 2) && IS_ALIGNED(dst_argb, 4) && IS_ALIGNED(dst_stride_argb, 4)) { - I422ToARGBRow = I422ToARGBRow_MIPS_DSPR2; + I422ToARGBRow = I422ToARGBRow_DSPR2; } #endif @@ -502,10 +502,10 @@ static void ScaleYUVToARGBBilinearUp(int src_width, int src_height, } } #endif -#if defined(HAS_INTERPOLATEROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && +#if defined(HAS_INTERPOLATEROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(dst_argb, 4) && IS_ALIGNED(dst_stride_argb, 4)) { - InterpolateRow = InterpolateRow_MIPS_DSPR2; + InterpolateRow = InterpolateRow_DSPR2; } #endif @@ -835,7 +835,6 @@ int YUVToARGBScaleClip(const uint8* src_y, int src_stride_y, int dst_width, int dst_height, int clip_x, int clip_y, int clip_width, int clip_height, enum FilterMode filtering) { - uint8* argb_buffer = (uint8*)malloc(src_width * src_height * 4); int r; I420ToARGB(src_y, src_stride_y, diff --git a/TMessagesProj/jni/libyuv/source/scale_common.cc b/TMessagesProj/jni/libyuv/source/scale_common.cc index f5c908d0b..d3992df2e 100644 --- a/TMessagesProj/jni/libyuv/source/scale_common.cc +++ b/TMessagesProj/jni/libyuv/source/scale_common.cc @@ -103,6 +103,28 @@ void ScaleRowDown2Box_C(const uint8* src_ptr, ptrdiff_t src_stride, } } +void ScaleRowDown2Box_Odd_C(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width) { + const uint8* s = src_ptr; + const uint8* t = src_ptr + src_stride; + int x; + dst_width -= 1; + for (x = 0; x < dst_width - 1; x += 2) { + dst[0] = (s[0] + s[1] + t[0] + t[1] + 2) >> 2; + dst[1] = (s[2] + s[3] + t[2] + t[3] + 2) >> 2; + dst += 2; + s += 4; + t += 4; + } + if (dst_width & 1) { + dst[0] = (s[0] + s[1] + t[0] + t[1] + 2) >> 2; + dst += 1; + s += 2; + t += 2; + } + dst[0] = (s[0] + t[0] + 1) >> 1; +} + void ScaleRowDown2Box_16_C(const uint16* src_ptr, ptrdiff_t src_stride, uint16* dst, int dst_width) { const uint16* s = src_ptr; @@ -900,13 +922,13 @@ void ScalePlaneVertical(int src_height, } } #endif -#if defined(HAS_INTERPOLATEROW_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && +#if defined(HAS_INTERPOLATEROW_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(src_argb, 4) && IS_ALIGNED(src_stride, 4) && IS_ALIGNED(dst_argb, 4) && IS_ALIGNED(dst_stride, 4)) { - InterpolateRow = InterpolateRow_Any_MIPS_DSPR2; + InterpolateRow = InterpolateRow_Any_DSPR2; if (IS_ALIGNED(dst_width_bytes, 4)) { - InterpolateRow = InterpolateRow_MIPS_DSPR2; + InterpolateRow = InterpolateRow_DSPR2; } } #endif @@ -974,13 +996,13 @@ void ScalePlaneVertical_16(int src_height, } } #endif -#if defined(HAS_INTERPOLATEROW_16_MIPS_DSPR2) - if (TestCpuFlag(kCpuHasMIPS_DSPR2) && +#if defined(HAS_INTERPOLATEROW_16_DSPR2) + if (TestCpuFlag(kCpuHasDSPR2) && IS_ALIGNED(src_argb, 4) && IS_ALIGNED(src_stride, 4) && IS_ALIGNED(dst_argb, 4) && IS_ALIGNED(dst_stride, 4)) { - InterpolateRow = InterpolateRow_Any_16_MIPS_DSPR2; + InterpolateRow = InterpolateRow_Any_16_DSPR2; if (IS_ALIGNED(dst_width_bytes, 4)) { - InterpolateRow = InterpolateRow_16_MIPS_DSPR2; + InterpolateRow = InterpolateRow_16_DSPR2; } } #endif diff --git a/TMessagesProj/jni/libyuv/source/scale_gcc.cc b/TMessagesProj/jni/libyuv/source/scale_gcc.cc index a1ae4e277..400f2fde9 100644 --- a/TMessagesProj/jni/libyuv/source/scale_gcc.cc +++ b/TMessagesProj/jni/libyuv/source/scale_gcc.cc @@ -316,7 +316,7 @@ void ScaleRowDown4_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, void ScaleRowDown4Box_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, uint8* dst_ptr, int dst_width) { - intptr_t stridex3 = 0; + intptr_t stridex3; asm volatile ( "pcmpeqb %%xmm4,%%xmm4 \n" "psrlw $0xf,%%xmm4 \n" @@ -361,7 +361,7 @@ void ScaleRowDown4Box_SSSE3(const uint8* src_ptr, ptrdiff_t src_stride, : "+r"(src_ptr), // %0 "+r"(dst_ptr), // %1 "+r"(dst_width), // %2 - "+r"(stridex3) // %3 + "=&r"(stridex3) // %3 : "r"((intptr_t)(src_stride)) // %4 : "memory", "cc", NACL_R14 "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5" @@ -824,7 +824,7 @@ void ScaleAddRow_AVX2(const uint8* src_ptr, uint16* dst_ptr, int src_width) { // Bilinear column filtering. SSSE3 version. void ScaleFilterCols_SSSE3(uint8* dst_ptr, const uint8* src_ptr, int dst_width, int x, int dx) { - intptr_t x0 = 0, x1 = 0, temp_pixel = 0; + intptr_t x0, x1, temp_pixel; asm volatile ( "movd %6,%%xmm2 \n" "movd %7,%%xmm3 \n" @@ -880,14 +880,14 @@ void ScaleFilterCols_SSSE3(uint8* dst_ptr, const uint8* src_ptr, "movd %%xmm0,%k2 \n" "mov %b2," MEMACCESS(0) " \n" "99: \n" - : "+r"(dst_ptr), // %0 - "+r"(src_ptr), // %1 - "+a"(temp_pixel), // %2 - "+r"(x0), // %3 - "+r"(x1), // %4 - "+rm"(dst_width) // %5 - : "rm"(x), // %6 - "rm"(dx) // %7 + : "+r"(dst_ptr), // %0 + "+r"(src_ptr), // %1 + "=&a"(temp_pixel), // %2 + "=&r"(x0), // %3 + "=&r"(x1), // %4 + "+rm"(dst_width) // %5 + : "rm"(x), // %6 + "rm"(dx) // %7 : "memory", "cc", NACL_R14 "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6" ); @@ -998,7 +998,7 @@ void ScaleARGBRowDown2Box_SSE2(const uint8* src_argb, void ScaleARGBRowDownEven_SSE2(const uint8* src_argb, ptrdiff_t src_stride, int src_stepx, uint8* dst_argb, int dst_width) { intptr_t src_stepx_x4 = (intptr_t)(src_stepx); - intptr_t src_stepx_x12 = 0; + intptr_t src_stepx_x12; asm volatile ( "lea " MEMLEA3(0x00,1,4) ",%1 \n" "lea " MEMLEA4(0x00,1,1,2) ",%4 \n" @@ -1016,11 +1016,11 @@ void ScaleARGBRowDownEven_SSE2(const uint8* src_argb, ptrdiff_t src_stride, "lea " MEMLEA(0x10,2) ",%2 \n" "sub $0x4,%3 \n" "jg 1b \n" - : "+r"(src_argb), // %0 - "+r"(src_stepx_x4), // %1 - "+r"(dst_argb), // %2 - "+r"(dst_width), // %3 - "+r"(src_stepx_x12) // %4 + : "+r"(src_argb), // %0 + "+r"(src_stepx_x4), // %1 + "+r"(dst_argb), // %2 + "+r"(dst_width), // %3 + "=&r"(src_stepx_x12) // %4 :: "memory", "cc", NACL_R14 "xmm0", "xmm1", "xmm2", "xmm3" ); @@ -1032,7 +1032,7 @@ void ScaleARGBRowDownEvenBox_SSE2(const uint8* src_argb, ptrdiff_t src_stride, int src_stepx, uint8* dst_argb, int dst_width) { intptr_t src_stepx_x4 = (intptr_t)(src_stepx); - intptr_t src_stepx_x12 = 0; + intptr_t src_stepx_x12; intptr_t row1 = (intptr_t)(src_stride); asm volatile ( "lea " MEMLEA3(0x00,1,4) ",%1 \n" @@ -1061,12 +1061,12 @@ void ScaleARGBRowDownEvenBox_SSE2(const uint8* src_argb, "lea " MEMLEA(0x10,2) ",%2 \n" "sub $0x4,%3 \n" "jg 1b \n" - : "+r"(src_argb), // %0 - "+r"(src_stepx_x4), // %1 - "+r"(dst_argb), // %2 - "+rm"(dst_width), // %3 - "+r"(src_stepx_x12), // %4 - "+r"(row1) // %5 + : "+r"(src_argb), // %0 + "+r"(src_stepx_x4), // %1 + "+r"(dst_argb), // %2 + "+rm"(dst_width), // %3 + "=&r"(src_stepx_x12), // %4 + "+r"(row1) // %5 :: "memory", "cc", NACL_R14 "xmm0", "xmm1", "xmm2", "xmm3" ); @@ -1074,7 +1074,7 @@ void ScaleARGBRowDownEvenBox_SSE2(const uint8* src_argb, void ScaleARGBCols_SSE2(uint8* dst_argb, const uint8* src_argb, int dst_width, int x, int dx) { - intptr_t x0 = 0, x1 = 0; + intptr_t x0, x1; asm volatile ( "movd %5,%%xmm2 \n" "movd %6,%%xmm3 \n" @@ -1127,8 +1127,8 @@ void ScaleARGBCols_SSE2(uint8* dst_argb, const uint8* src_argb, MEMOPREG(movd,0x00,3,0,4,xmm0) // movd (%3,%0,4),%%xmm0 "movd %%xmm0," MEMACCESS(2) " \n" "99: \n" - : "+a"(x0), // %0 - "+d"(x1), // %1 + : "=&a"(x0), // %0 + "=&d"(x1), // %1 "+r"(dst_argb), // %2 "+r"(src_argb), // %3 "+r"(dst_width) // %4 @@ -1179,7 +1179,7 @@ static uvec8 kShuffleFractions = { // Bilinear row filtering combines 4x2 -> 4x1. SSSE3 version void ScaleARGBFilterCols_SSSE3(uint8* dst_argb, const uint8* src_argb, int dst_width, int x, int dx) { - intptr_t x0 = 0, x1 = 0; + intptr_t x0, x1; asm volatile ( "movdqa %0,%%xmm4 \n" "movdqa %1,%%xmm5 \n" @@ -1242,8 +1242,8 @@ void ScaleARGBFilterCols_SSSE3(uint8* dst_argb, const uint8* src_argb, : "+r"(dst_argb), // %0 "+r"(src_argb), // %1 "+rm"(dst_width), // %2 - "+r"(x0), // %3 - "+r"(x1) // %4 + "=&r"(x0), // %3 + "=&r"(x1) // %4 : "rm"(x), // %5 "rm"(dx) // %6 : "memory", "cc", NACL_R14 diff --git a/TMessagesProj/jni/libyuv/source/scale_mips.cc b/TMessagesProj/jni/libyuv/source/scale_mips.cc index 2298a74b9..ae953073f 100644 --- a/TMessagesProj/jni/libyuv/source/scale_mips.cc +++ b/TMessagesProj/jni/libyuv/source/scale_mips.cc @@ -21,8 +21,8 @@ extern "C" { defined(__mips_dsp) && (__mips_dsp_rev >= 2) && \ (_MIPS_SIM == _MIPS_SIM_ABI32) -void ScaleRowDown2_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width) { +void ScaleRowDown2_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width) { __asm__ __volatile__( ".set push \n" ".set noreorder \n" @@ -77,8 +77,8 @@ void ScaleRowDown2_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, ); } -void ScaleRowDown2Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width) { +void ScaleRowDown2Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width) { const uint8* t = src_ptr + src_stride; __asm__ __volatile__ ( @@ -176,8 +176,8 @@ void ScaleRowDown2Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, ); } -void ScaleRowDown4_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width) { +void ScaleRowDown4_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -231,8 +231,8 @@ void ScaleRowDown4_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, ); } -void ScaleRowDown4Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width) { +void ScaleRowDown4Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width) { intptr_t stride = src_stride; const uint8* s1 = src_ptr + stride; const uint8* s2 = s1 + stride; @@ -310,8 +310,8 @@ void ScaleRowDown4Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, ); } -void ScaleRowDown34_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width) { +void ScaleRowDown34_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -356,8 +356,8 @@ void ScaleRowDown34_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, ); } -void ScaleRowDown34_0_Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* d, int dst_width) { +void ScaleRowDown34_0_Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* d, int dst_width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -412,8 +412,8 @@ void ScaleRowDown34_0_Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, ); } -void ScaleRowDown34_1_Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* d, int dst_width) { +void ScaleRowDown34_1_Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* d, int dst_width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -464,8 +464,8 @@ void ScaleRowDown34_1_Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, ); } -void ScaleRowDown38_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst, int dst_width) { +void ScaleRowDown38_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst, int dst_width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -510,8 +510,8 @@ void ScaleRowDown38_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, ); } -void ScaleRowDown38_2_Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width) { +void ScaleRowDown38_2_Box_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, + uint8* dst_ptr, int dst_width) { intptr_t stride = src_stride; const uint8* t = src_ptr + stride; const int c = 0x2AAA; @@ -563,9 +563,9 @@ void ScaleRowDown38_2_Box_MIPS_DSPR2(const uint8* src_ptr, ptrdiff_t src_stride, ); } -void ScaleRowDown38_3_Box_MIPS_DSPR2(const uint8* src_ptr, - ptrdiff_t src_stride, - uint8* dst_ptr, int dst_width) { +void ScaleRowDown38_3_Box_DSPR2(const uint8* src_ptr, + ptrdiff_t src_stride, + uint8* dst_ptr, int dst_width) { intptr_t stride = src_stride; const uint8* s1 = src_ptr + stride; stride += stride; diff --git a/TMessagesProj/jni/libyuv/source/scale_neon.cc b/TMessagesProj/jni/libyuv/source/scale_neon.cc index 10856cf84..95f3362a4 100644 --- a/TMessagesProj/jni/libyuv/source/scale_neon.cc +++ b/TMessagesProj/jni/libyuv/source/scale_neon.cc @@ -532,7 +532,7 @@ void ScaleRowDown38_2_Box_NEON(const uint8* src_ptr, void ScaleAddRows_NEON(const uint8* src_ptr, ptrdiff_t src_stride, uint16* dst_ptr, int src_width, int src_height) { - const uint8* src_tmp = NULL; + const uint8* src_tmp; asm volatile ( "1: \n" "mov %0, %1 \n" @@ -552,12 +552,12 @@ void ScaleAddRows_NEON(const uint8* src_ptr, ptrdiff_t src_stride, "add %1, %1, #16 \n" "subs %4, %4, #16 \n" // 16 processed per loop "bgt 1b \n" - : "+r"(src_tmp), // %0 - "+r"(src_ptr), // %1 - "+r"(dst_ptr), // %2 - "+r"(src_stride), // %3 - "+r"(src_width), // %4 - "+r"(src_height) // %5 + : "=&r"(src_tmp), // %0 + "+r"(src_ptr), // %1 + "+r"(dst_ptr), // %2 + "+r"(src_stride), // %3 + "+r"(src_width), // %4 + "+r"(src_height) // %5 : : "memory", "cc", "r12", "q0", "q1", "q2", "q3" // Clobber List ); @@ -909,7 +909,7 @@ void ScaleARGBRowDownEvenBox_NEON(const uint8* src_argb, ptrdiff_t src_stride, void ScaleARGBCols_NEON(uint8* dst_argb, const uint8* src_argb, int dst_width, int x, int dx) { - int tmp = 0; + int tmp; const uint8* src_tmp = src_argb; asm volatile ( "1: \n" @@ -926,13 +926,13 @@ void ScaleARGBCols_NEON(uint8* dst_argb, const uint8* src_argb, "vst1.32 {q0, q1}, [%0]! \n" // store pixels "subs %2, %2, #8 \n" // 8 processed per loop "bgt 1b \n" - : "+r"(dst_argb), // %0 - "+r"(src_argb), // %1 - "+r"(dst_width), // %2 - "+r"(x), // %3 - "+r"(dx), // %4 - "+r"(tmp), // %5 - "+r"(src_tmp) // %6 + : "+r"(dst_argb), // %0 + "+r"(src_argb), // %1 + "+r"(dst_width), // %2 + "+r"(x), // %3 + "+r"(dx), // %4 + "=&r"(tmp), // %5 + "+r"(src_tmp) // %6 : : "memory", "cc", "q0", "q1" ); diff --git a/TMessagesProj/jni/libyuv/source/scale_neon64.cc b/TMessagesProj/jni/libyuv/source/scale_neon64.cc index 1d5519357..3a62db5b8 100644 --- a/TMessagesProj/jni/libyuv/source/scale_neon64.cc +++ b/TMessagesProj/jni/libyuv/source/scale_neon64.cc @@ -547,7 +547,7 @@ void ScaleRowDown38_2_Box_NEON(const uint8* src_ptr, void ScaleAddRows_NEON(const uint8* src_ptr, ptrdiff_t src_stride, uint16* dst_ptr, int src_width, int src_height) { - const uint8* src_tmp = NULL; + const uint8* src_tmp; asm volatile ( "1: \n" "mov %0, %1 \n" @@ -567,12 +567,12 @@ void ScaleAddRows_NEON(const uint8* src_ptr, ptrdiff_t src_stride, "add %1, %1, #16 \n" "subs %w4, %w4, #16 \n" // 16 processed per loop "b.gt 1b \n" - : "+r"(src_tmp), // %0 - "+r"(src_ptr), // %1 - "+r"(dst_ptr), // %2 - "+r"(src_stride), // %3 - "+r"(src_width), // %4 - "+r"(src_height) // %5 + : "=&r"(src_tmp), // %0 + "+r"(src_ptr), // %1 + "+r"(dst_ptr), // %2 + "+r"(src_stride), // %3 + "+r"(src_width), // %4 + "+r"(src_height) // %5 : : "memory", "cc", "w12", "v0", "v1", "v2", "v3" // Clobber List ); @@ -931,7 +931,7 @@ void ScaleARGBCols_NEON(uint8* dst_argb, const uint8* src_argb, int64 dst_width64 = (int64) dst_width; // Work around ios 64 bit warning. int64 x64 = (int64) x; int64 dx64 = (int64) dx; - int64 tmp64 = 0; + int64 tmp64; asm volatile ( "1: \n" LOAD1_DATA32_LANE(v0, 0) @@ -947,13 +947,13 @@ void ScaleARGBCols_NEON(uint8* dst_argb, const uint8* src_argb, "st1 {v0.4s, v1.4s}, [%0], #32 \n" // store pixels "subs %w2, %w2, #8 \n" // 8 processed per loop "b.gt 1b \n" - : "+r"(dst_argb), // %0 - "+r"(src_argb), // %1 - "+r"(dst_width64), // %2 - "+r"(x64), // %3 - "+r"(dx64), // %4 - "+r"(tmp64), // %5 - "+r"(src_tmp) // %6 + : "+r"(dst_argb), // %0 + "+r"(src_argb), // %1 + "+r"(dst_width64), // %2 + "+r"(x64), // %3 + "+r"(dx64), // %4 + "=&r"(tmp64), // %5 + "+r"(src_tmp) // %6 : : "memory", "cc", "v0", "v1" ); diff --git a/TMessagesProj/jni/libyuv/source/scale_win.cc b/TMessagesProj/jni/libyuv/source/scale_win.cc index 5ab4fa0cc..21b1ed923 100644 --- a/TMessagesProj/jni/libyuv/source/scale_win.cc +++ b/TMessagesProj/jni/libyuv/source/scale_win.cc @@ -289,7 +289,7 @@ void ScaleRowDown2Box_AVX2(const uint8* src_ptr, ptrdiff_t src_stride, vpmaddubsw ymm3, ymm3, ymm4 vpaddw ymm0, ymm0, ymm2 // vertical add vpaddw ymm1, ymm1, ymm3 - vpsrlw ymm0, ymm0, 1 + vpsrlw ymm0, ymm0, 1 // (x + 2) / 4 = (x / 2 + 1) / 2 vpsrlw ymm1, ymm1, 1 vpavgw ymm0, ymm0, ymm5 // (x + 1) / 2 vpavgw ymm1, ymm1, ymm5 diff --git a/TMessagesProj/jni/libyuv/source/video_common.cc b/TMessagesProj/jni/libyuv/source/video_common.cc index 379a0669a..00fb71e18 100644 --- a/TMessagesProj/jni/libyuv/source/video_common.cc +++ b/TMessagesProj/jni/libyuv/source/video_common.cc @@ -25,6 +25,7 @@ struct FourCCAliasEntry { static const struct FourCCAliasEntry kFourCCAliases[] = { {FOURCC_IYUV, FOURCC_I420}, + {FOURCC_YU12, FOURCC_I420}, {FOURCC_YU16, FOURCC_I422}, {FOURCC_YU24, FOURCC_I444}, {FOURCC_YUYV, FOURCC_YUY2}, diff --git a/TMessagesProj/jni/sqlite.c b/TMessagesProj/jni/sqlite.c deleted file mode 100755 index 124b3df2f..000000000 --- a/TMessagesProj/jni/sqlite.c +++ /dev/null @@ -1,11 +0,0 @@ -#include "sqlite/sqlite3.h" -#include "sqlite.h" - -void throw_sqlite3_exception(JNIEnv *env, sqlite3 *handle, int errcode) { - if (SQLITE_OK == errcode) { - errcode = sqlite3_errcode(handle); - } - const char *errmsg = sqlite3_errmsg(handle); - jclass exClass = (*env)->FindClass(env, "org/telegram/SQLite/SQLiteException"); - (*env)->ThrowNew(env, exClass, errmsg); -} diff --git a/TMessagesProj/jni/sqlite.h b/TMessagesProj/jni/sqlite.h deleted file mode 100755 index af19fcb85..000000000 --- a/TMessagesProj/jni/sqlite.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef sqlite_h -#define sqlite_h - -#include -#include "sqlite/sqlite3.h" - -void throw_sqlite3_exception(JNIEnv* env, sqlite3 *handle, int errcode); -jint sqliteOnJNILoad(JavaVM *vm, void *reserved, JNIEnv *env); - -#endif diff --git a/TMessagesProj/jni/sqlite/sqlite3.c b/TMessagesProj/jni/sqlite/sqlite3.c index 486eefee4..e19867890 100644 --- a/TMessagesProj/jni/sqlite/sqlite3.c +++ b/TMessagesProj/jni/sqlite/sqlite3.c @@ -1,6 +1,6 @@ /****************************************************************************** ** This file is an amalgamation of many separate C source files from SQLite -** version 3.9.2. By combining all the individual C code files into this +** version 3.13.0. By combining all the individual C code files into this ** single large file, the entire code can be compiled as a single translation ** unit. This allows many compilers to do optimizations that would not be ** possible if the files were compiled separately. Performance improvements @@ -17,7 +17,6 @@ ** language. The code for the "sqlite3" command-line shell is also in a ** separate file. This file contains only code for the core SQLite library. */ -#define SQLITE_THREADSAFE 0 #define SQLITE_CORE 1 #define SQLITE_AMALGAMATION 1 #ifndef SQLITE_PRIVATE @@ -38,9 +37,44 @@ ** Internal interface definitions for SQLite. ** */ -#ifndef _SQLITEINT_H_ +#ifndef _SQLITEINT_H_ #define _SQLITEINT_H_ +/* Special Comments: +** +** Some comments have special meaning to the tools that measure test +** coverage: +** +** NO_TEST - The branches on this line are not +** measured by branch coverage. This is +** used on lines of code that actually +** implement parts of coverage testing. +** +** OPTIMIZATION-IF-TRUE - This branch is allowed to alway be false +** and the correct answer is still obtained, +** though perhaps more slowly. +** +** OPTIMIZATION-IF-FALSE - This branch is allowed to alway be true +** and the correct answer is still obtained, +** though perhaps more slowly. +** +** PREVENTS-HARMLESS-OVERREAD - This branch prevents a buffer overread +** that would be harmless and undetectable +** if it did occur. +** +** In all cases, the special comment must be enclosed in the usual +** slash-asterisk...asterisk-slash comment marks, with no spaces between the +** asterisks and the comment text. +*/ + +/* +** Make sure that rand_s() is available on Windows systems with MSVC 2005 +** or higher. +*/ +#if defined(_MSC_VER) && _MSC_VER>=1400 +# define _CRT_RAND_S +#endif + /* ** Include the header file used to customize the compiler options for MSVC. ** This should be done first so that it can successfully prevent spurious @@ -122,6 +156,9 @@ #else /* This is not VxWorks. */ #define OS_VXWORKS 0 +#define HAVE_FCHOWN 1 +#define HAVE_READLINK 1 +#define HAVE_LSTAT 1 #endif /* defined(_WRS_KERNEL) */ /************** End of vxworks.h *********************************************/ @@ -326,9 +363,9 @@ extern "C" { ** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite_version()] and [sqlite_source_id()]. */ -#define SQLITE_VERSION "3.9.2" -#define SQLITE_VERSION_NUMBER 3009002 -#define SQLITE_SOURCE_ID "2015-11-02 18:31:45 bda77dda9697c463c3d0704014d51627fceee328" +#define SQLITE_VERSION "3.13.0" +#define SQLITE_VERSION_NUMBER 3013000 +#define SQLITE_SOURCE_ID "2016-05-18 10:57:30 fc49f556e48970561d7ab6a2f24fdd7d9eb81ff2" /* ** CAPI3REF: Run-Time Library Version Numbers @@ -562,7 +599,7 @@ typedef int (*sqlite3_callback)(void*,int,char**, char**); ** from [sqlite3_malloc()] and passed back through the 5th parameter. ** To avoid memory leaks, the application should invoke [sqlite3_free()] ** on error message strings returned through the 5th parameter of -** of sqlite3_exec() after the error message string is no longer needed. +** sqlite3_exec() after the error message string is no longer needed. ** ^If the 5th parameter to sqlite3_exec() is not NULL and no errors ** occur, then sqlite3_exec() sets the pointer in its 5th parameter to ** NULL before returning. @@ -693,6 +730,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_exec( #define SQLITE_IOERR_GETTEMPPATH (SQLITE_IOERR | (25<<8)) #define SQLITE_IOERR_CONVPATH (SQLITE_IOERR | (26<<8)) #define SQLITE_IOERR_VNODE (SQLITE_IOERR | (27<<8)) +#define SQLITE_IOERR_AUTH (SQLITE_IOERR | (28<<8)) #define SQLITE_LOCKED_SHAREDCACHE (SQLITE_LOCKED | (1<<8)) #define SQLITE_BUSY_RECOVERY (SQLITE_BUSY | (1<<8)) #define SQLITE_BUSY_SNAPSHOT (SQLITE_BUSY | (2<<8)) @@ -1008,8 +1046,13 @@ struct sqlite3_io_methods { **
  • [[SQLITE_FCNTL_FILE_POINTER]] ** The [SQLITE_FCNTL_FILE_POINTER] opcode is used to obtain a pointer ** to the [sqlite3_file] object associated with a particular database -** connection. See the [sqlite3_file_control()] documentation for -** additional information. +** connection. See also [SQLITE_FCNTL_JOURNAL_POINTER]. +** +**
  • [[SQLITE_FCNTL_JOURNAL_POINTER]] +** The [SQLITE_FCNTL_JOURNAL_POINTER] opcode is used to obtain a pointer +** to the [sqlite3_file] object associated with the journal file (either +** the [rollback journal] or the [write-ahead log]) for a particular database +** connection. See also [SQLITE_FCNTL_FILE_POINTER]. ** **
  • [[SQLITE_FCNTL_SYNC_OMITTED]] ** No longer in use. @@ -1096,6 +1139,15 @@ struct sqlite3_io_methods { ** pointer in case this file-control is not implemented. This file-control ** is intended for diagnostic use only. ** +**
  • [[SQLITE_FCNTL_VFS_POINTER]] +** ^The [SQLITE_FCNTL_VFS_POINTER] opcode finds a pointer to the top-level +** [VFSes] currently in use. ^(The argument X in +** sqlite3_file_control(db,SQLITE_FCNTL_VFS_POINTER,X) must be +** of type "[sqlite3_vfs] **". This opcodes will set *X +** to a pointer to the top-level VFS.)^ +** ^When there are multiple VFS shims in the stack, this opcode finds the +** upper-most shim only. +** **
  • [[SQLITE_FCNTL_PRAGMA]] ** ^Whenever a [PRAGMA] statement is parsed, an [SQLITE_FCNTL_PRAGMA] ** file control is sent to the open [sqlite3_file] object corresponding @@ -1214,6 +1266,8 @@ struct sqlite3_io_methods { #define SQLITE_FCNTL_WAL_BLOCK 24 #define SQLITE_FCNTL_ZIPVFS 25 #define SQLITE_FCNTL_RBU 26 +#define SQLITE_FCNTL_VFS_POINTER 27 +#define SQLITE_FCNTL_JOURNAL_POINTER 28 /* deprecated names */ #define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE @@ -1426,7 +1480,7 @@ struct sqlite3_vfs { const char *(*xNextSystemCall)(sqlite3_vfs*, const char *zName); /* ** The methods above are in versions 1 through 3 of the sqlite_vfs object. - ** New fields may be appended in figure versions. The iVersion + ** New fields may be appended in future versions. The iVersion ** value will increment whenever this happens. */ }; @@ -1813,29 +1867,34 @@ struct sqlite3_mem_methods { ** ** ** [[SQLITE_CONFIG_PAGECACHE]]
    SQLITE_CONFIG_PAGECACHE
    -**
    ^The SQLITE_CONFIG_PAGECACHE option specifies a static memory buffer +**
    ^The SQLITE_CONFIG_PAGECACHE option specifies a memory pool ** that SQLite can use for the database page cache with the default page ** cache implementation. -** This configuration should not be used if an application-define page -** cache implementation is loaded using the [SQLITE_CONFIG_PCACHE2] -** configuration option. +** This configuration option is a no-op if an application-define page +** cache implementation is loaded using the [SQLITE_CONFIG_PCACHE2]. ** ^There are three arguments to SQLITE_CONFIG_PAGECACHE: A pointer to -** 8-byte aligned -** memory, the size of each page buffer (sz), and the number of pages (N). +** 8-byte aligned memory (pMem), the size of each page cache line (sz), +** and the number of cache lines (N). ** The sz argument should be the size of the largest database page ** (a power of two between 512 and 65536) plus some extra bytes for each ** page header. ^The number of extra bytes needed by the page header -** can be determined using the [SQLITE_CONFIG_PCACHE_HDRSZ] option -** to [sqlite3_config()]. +** can be determined using [SQLITE_CONFIG_PCACHE_HDRSZ]. ** ^It is harmless, apart from the wasted memory, -** for the sz parameter to be larger than necessary. The first -** argument should pointer to an 8-byte aligned block of memory that -** is at least sz*N bytes of memory, otherwise subsequent behavior is -** undefined. -** ^SQLite will use the memory provided by the first argument to satisfy its -** memory needs for the first N pages that it adds to cache. ^If additional -** page cache memory is needed beyond what is provided by this option, then -** SQLite goes to [sqlite3_malloc()] for the additional storage space.
    +** for the sz parameter to be larger than necessary. The pMem +** argument must be either a NULL pointer or a pointer to an 8-byte +** aligned block of memory of at least sz*N bytes, otherwise +** subsequent behavior is undefined. +** ^When pMem is not NULL, SQLite will strive to use the memory provided +** to satisfy page cache needs, falling back to [sqlite3_malloc()] if +** a page cache line is larger than sz bytes or if all of the pMem buffer +** is exhausted. +** ^If pMem is NULL and N is non-zero, then each database connection +** does an initial bulk allocation for page cache memory +** from [sqlite3_malloc()] sufficient for N cache lines if N is positive or +** of -1024*N bytes if N is negative, . ^If additional +** page cache memory is needed beyond what is provided by the initial +** allocation, then SQLite goes to [sqlite3_malloc()] separately for each +** additional cache line. ** ** [[SQLITE_CONFIG_HEAP]]
    SQLITE_CONFIG_HEAP
    **
    ^The SQLITE_CONFIG_HEAP option specifies a static memory buffer @@ -2013,6 +2072,20 @@ struct sqlite3_mem_methods { ** is enabled (using the [PRAGMA threads] command) and the amount of content ** to be sorted exceeds the page size times the minimum of the ** [PRAGMA cache_size] setting and this value. +** +** [[SQLITE_CONFIG_STMTJRNL_SPILL]] +**
    SQLITE_CONFIG_STMTJRNL_SPILL +**
    ^The SQLITE_CONFIG_STMTJRNL_SPILL option takes a single parameter which +** becomes the [statement journal] spill-to-disk threshold. +** [Statement journals] are held in memory until their size (in bytes) +** exceeds this threshold, at which point they are written to disk. +** Or if the threshold is -1, statement journals are always held +** exclusively in memory. +** Since many statement journals never become large, setting the spill +** threshold to a value such as 64KiB can greatly reduce the amount of +** I/O required to support statement rollback. +** The default value for this setting is controlled by the +** [SQLITE_STMTJRNL_SPILL] compile-time option. ** */ #define SQLITE_CONFIG_SINGLETHREAD 1 /* nil */ @@ -2040,6 +2113,7 @@ struct sqlite3_mem_methods { #define SQLITE_CONFIG_WIN32_HEAPSIZE 23 /* int nByte */ #define SQLITE_CONFIG_PCACHE_HDRSZ 24 /* int *psz */ #define SQLITE_CONFIG_PMASZ 25 /* unsigned int szPma */ +#define SQLITE_CONFIG_STMTJRNL_SPILL 26 /* int nByte */ /* ** CAPI3REF: Database Connection Configuration Options @@ -2097,11 +2171,43 @@ struct sqlite3_mem_methods { ** following this call. The second parameter may be a NULL pointer, in ** which case the trigger setting is not reported back.
    ** +**
    SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER
    +**
    ^This option is used to enable or disable the two-argument +** version of the [fts3_tokenizer()] function which is part of the +** [FTS3] full-text search engine extension. +** There should be two additional arguments. +** The first argument is an integer which is 0 to disable fts3_tokenizer() or +** positive to enable fts3_tokenizer() or negative to leave the setting +** unchanged. +** The second parameter is a pointer to an integer into which +** is written 0 or 1 to indicate whether fts3_tokenizer is disabled or enabled +** following this call. The second parameter may be a NULL pointer, in +** which case the new setting is not reported back.
    +** +**
    SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION
    +**
    ^This option is used to enable or disable the [sqlite3_load_extension()] +** interface independently of the [load_extension()] SQL function. +** The [sqlite3_enable_load_extension()] API enables or disables both the +** C-API [sqlite3_load_extension()] and the SQL function [load_extension()]. +** There should be two additional arguments. +** When the first argument to this interface is 1, then only the C-API is +** enabled and the SQL function remains disabled. If the first argment to +** this interface is 0, then both the C-API and the SQL function are disabled. +** If the first argument is -1, then no changes are made to state of either the +** C-API or the SQL function. +** The second parameter is a pointer to an integer into which +** is written 0 or 1 to indicate whether [sqlite3_load_extension()] interface +** is disabled or enabled following this call. The second parameter may +** be a NULL pointer, in which case the new setting is not reported back. +**
    +** ** */ -#define SQLITE_DBCONFIG_LOOKASIDE 1001 /* void* int int */ -#define SQLITE_DBCONFIG_ENABLE_FKEY 1002 /* int int* */ -#define SQLITE_DBCONFIG_ENABLE_TRIGGER 1003 /* int int* */ +#define SQLITE_DBCONFIG_LOOKASIDE 1001 /* void* int int */ +#define SQLITE_DBCONFIG_ENABLE_FKEY 1002 /* int int* */ +#define SQLITE_DBCONFIG_ENABLE_TRIGGER 1003 /* int int* */ +#define SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER 1004 /* int int* */ +#define SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION 1005 /* int int* */ /* @@ -4604,8 +4710,8 @@ SQLITE_API unsigned int SQLITE_STDCALL sqlite3_value_subtype(sqlite3_value*); ** previously obtained from [sqlite3_value_dup()]. ^If V is a NULL pointer ** then sqlite3_value_free(V) is a harmless no-op. */ -SQLITE_API SQLITE_EXPERIMENTAL sqlite3_value *SQLITE_STDCALL sqlite3_value_dup(const sqlite3_value*); -SQLITE_API SQLITE_EXPERIMENTAL void SQLITE_STDCALL sqlite3_value_free(sqlite3_value*); +SQLITE_API sqlite3_value *SQLITE_STDCALL sqlite3_value_dup(const sqlite3_value*); +SQLITE_API void SQLITE_STDCALL sqlite3_value_free(sqlite3_value*); /* ** CAPI3REF: Obtain Aggregate Function Context @@ -5351,7 +5457,7 @@ SQLITE_API void *SQLITE_STDCALL sqlite3_rollback_hook(sqlite3*, void(*)(void *), ** ^The sqlite3_update_hook() interface registers a callback function ** with the [database connection] identified by the first argument ** to be invoked whenever a row is updated, inserted or deleted in -** a rowid table. +** a [rowid table]. ** ^Any callback set by a previous call to this function ** for the same database connection is overridden. ** @@ -5390,8 +5496,8 @@ SQLITE_API void *SQLITE_STDCALL sqlite3_rollback_hook(sqlite3*, void(*)(void *), ** on the same [database connection] D, or NULL for ** the first call on D. ** -** See also the [sqlite3_commit_hook()] and [sqlite3_rollback_hook()] -** interfaces. +** See also the [sqlite3_commit_hook()], [sqlite3_rollback_hook()], +** and [sqlite3_preupdate_hook()] interfaces. */ SQLITE_API void *SQLITE_STDCALL sqlite3_update_hook( sqlite3*, @@ -5638,9 +5744,18 @@ SQLITE_API int SQLITE_STDCALL sqlite3_table_column_metadata( ** should free this memory by calling [sqlite3_free()]. ** ** ^Extension loading must be enabled using -** [sqlite3_enable_load_extension()] prior to calling this API, +** [sqlite3_enable_load_extension()] or +** [sqlite3_db_config](db,[SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION],1,NULL) +** prior to calling this API, ** otherwise an error will be returned. ** +** Security warning: It is recommended that the +** [SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION] method be used to enable only this +** interface. The use of the [sqlite3_enable_load_extension()] interface +** should be avoided. This will keep the SQL function [load_extension()] +** disabled and prevent SQL injections from giving attackers +** access to extension loading capabilities. +** ** See also the [load_extension() SQL function]. */ SQLITE_API int SQLITE_STDCALL sqlite3_load_extension( @@ -5663,6 +5778,17 @@ SQLITE_API int SQLITE_STDCALL sqlite3_load_extension( ** ^Call the sqlite3_enable_load_extension() routine with onoff==1 ** to turn extension loading on and call it with onoff==0 to turn ** it back off again. +** +** ^This interface enables or disables both the C-API +** [sqlite3_load_extension()] and the SQL function [load_extension()]. +** Use [sqlite3_db_config](db,[SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION],..) +** to enable or disable only the C-API. +** +** Security warning: It is recommended that extension loading +** be disabled using the [SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION] method +** rather than this interface, so the [load_extension()] SQL function +** remains disabled. This will prevent SQL injections from giving attackers +** access to extension loading capabilities. */ SQLITE_API int SQLITE_STDCALL sqlite3_enable_load_extension(sqlite3 *db, int onoff); @@ -5824,6 +5950,17 @@ struct sqlite3_module { ** ^Information about the ORDER BY clause is stored in aOrderBy[]. ** ^Each term of aOrderBy records a column of the ORDER BY clause. ** +** The colUsed field indicates which columns of the virtual table may be +** required by the current scan. Virtual table columns are numbered from +** zero in the order in which they appear within the CREATE TABLE statement +** passed to sqlite3_declare_vtab(). For the first 63 columns (columns 0-62), +** the corresponding bit is set within the colUsed mask if the column may be +** required by SQLite. If the table has at least 64 columns and any column +** to the right of the first 63 is required, then bit 63 of colUsed is also +** set. In other words, column iCol may be required if the expression +** (colUsed & ((sqlite3_uint64)1 << (iCol>=63 ? 63 : iCol))) evaluates to +** non-zero. +** ** The [xBestIndex] method must fill aConstraintUsage[] with information ** about what parameters to pass to xFilter. ^If argvIndex>0 then ** the right-hand side of the corresponding aConstraint[] is evaluated @@ -5879,7 +6016,7 @@ struct sqlite3_index_info { /* Inputs */ int nConstraint; /* Number of entries in aConstraint */ struct sqlite3_index_constraint { - int iColumn; /* Column on left-hand side of constraint */ + int iColumn; /* Column constrained. -1 for ROWID */ unsigned char op; /* Constraint operator */ unsigned char usable; /* True if this constraint is usable */ int iTermOffset; /* Used internally - xBestIndex should ignore */ @@ -5903,6 +6040,8 @@ struct sqlite3_index_info { sqlite3_int64 estimatedRows; /* Estimated number of rows returned */ /* Fields below are only available in SQLite 3.9.0 and later */ int idxFlags; /* Mask of SQLITE_INDEX_SCAN_* flags */ + /* Fields below are only available in SQLite 3.10.0 and later */ + sqlite3_uint64 colUsed; /* Input: Mask of columns used by statement */ }; /* @@ -5918,12 +6057,15 @@ struct sqlite3_index_info { ** an operator that is part of a constraint term in the wHERE clause of ** a query that uses a [virtual table]. */ -#define SQLITE_INDEX_CONSTRAINT_EQ 2 -#define SQLITE_INDEX_CONSTRAINT_GT 4 -#define SQLITE_INDEX_CONSTRAINT_LE 8 -#define SQLITE_INDEX_CONSTRAINT_LT 16 -#define SQLITE_INDEX_CONSTRAINT_GE 32 -#define SQLITE_INDEX_CONSTRAINT_MATCH 64 +#define SQLITE_INDEX_CONSTRAINT_EQ 2 +#define SQLITE_INDEX_CONSTRAINT_GT 4 +#define SQLITE_INDEX_CONSTRAINT_LE 8 +#define SQLITE_INDEX_CONSTRAINT_LT 16 +#define SQLITE_INDEX_CONSTRAINT_GE 32 +#define SQLITE_INDEX_CONSTRAINT_MATCH 64 +#define SQLITE_INDEX_CONSTRAINT_LIKE 65 +#define SQLITE_INDEX_CONSTRAINT_GLOB 66 +#define SQLITE_INDEX_CONSTRAINT_REGEXP 67 /* ** CAPI3REF: Register A Virtual Table Implementation @@ -6787,7 +6929,8 @@ SQLITE_API int SQLITE_STDCALL sqlite3_status64( ** The value written into the *pCurrent parameter is undefined.)^ ** ** [[SQLITE_STATUS_PARSER_STACK]] ^(
    SQLITE_STATUS_PARSER_STACK
    -**
    This parameter records the deepest parser stack. It is only +**
    The *pHighwater parameter records the deepest parser stack. +** The *pCurrent value is undefined. The *pHighwater value is only ** meaningful if SQLite is compiled with [YYTRACKMAXSTACKDEPTH].
    )^ ** ** @@ -7284,7 +7427,7 @@ typedef struct sqlite3_backup sqlite3_backup; ** must be different or else sqlite3_backup_init(D,N,S,M) will fail with ** an error. ** -** ^A call to sqlite3_backup_init() will fail, returning SQLITE_ERROR, if +** ^A call to sqlite3_backup_init() will fail, returning NULL, if ** there is already a read or read-write transaction open on the ** destination database. ** @@ -7573,18 +7716,43 @@ SQLITE_API int SQLITE_STDCALL sqlite3_strnicmp(const char *, const char *, int); /* ** CAPI3REF: String Globbing * -** ^The [sqlite3_strglob(P,X)] interface returns zero if string X matches -** the glob pattern P, and it returns non-zero if string X does not match -** the glob pattern P. ^The definition of glob pattern matching used in +** ^The [sqlite3_strglob(P,X)] interface returns zero if and only if +** string X matches the [GLOB] pattern P. +** ^The definition of [GLOB] pattern matching used in ** [sqlite3_strglob(P,X)] is the same as for the "X GLOB P" operator in the -** SQL dialect used by SQLite. ^The sqlite3_strglob(P,X) function is case -** sensitive. +** SQL dialect understood by SQLite. ^The [sqlite3_strglob(P,X)] function +** is case sensitive. ** ** Note that this routine returns zero on a match and non-zero if the strings ** do not match, the same as [sqlite3_stricmp()] and [sqlite3_strnicmp()]. +** +** See also: [sqlite3_strlike()]. */ SQLITE_API int SQLITE_STDCALL sqlite3_strglob(const char *zGlob, const char *zStr); +/* +** CAPI3REF: String LIKE Matching +* +** ^The [sqlite3_strlike(P,X,E)] interface returns zero if and only if +** string X matches the [LIKE] pattern P with escape character E. +** ^The definition of [LIKE] pattern matching used in +** [sqlite3_strlike(P,X,E)] is the same as for the "X LIKE P ESCAPE E" +** operator in the SQL dialect understood by SQLite. ^For "X LIKE P" without +** the ESCAPE clause, set the E parameter of [sqlite3_strlike(P,X,E)] to 0. +** ^As with the LIKE operator, the [sqlite3_strlike(P,X,E)] function is case +** insensitive - equivalent upper and lower case ASCII characters match +** one another. +** +** ^The [sqlite3_strlike(P,X,E)] function matches Unicode characters, though +** only ASCII characters are case folded. +** +** Note that this routine returns zero on a match and non-zero if the strings +** do not match, the same as [sqlite3_stricmp()] and [sqlite3_strnicmp()]. +** +** See also: [sqlite3_strglob()]. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_strlike(const char *zGlob, const char *zStr, unsigned int cEsc); + /* ** CAPI3REF: Error Logging Interface ** @@ -7640,7 +7808,7 @@ SQLITE_API void SQLITE_CDECL sqlite3_log(int iErrCode, const char *zFormat, ...) ** previously registered write-ahead log callback. ^Note that the ** [sqlite3_wal_autocheckpoint()] interface and the ** [wal_autocheckpoint pragma] both invoke [sqlite3_wal_hook()] and will -** those overwrite any prior [sqlite3_wal_hook()] settings. +** overwrite any prior [sqlite3_wal_hook()] settings. */ SQLITE_API void *SQLITE_STDCALL sqlite3_wal_hook( sqlite3*, @@ -8005,6 +8173,277 @@ SQLITE_API int SQLITE_STDCALL sqlite3_stmt_scanstatus( */ SQLITE_API void SQLITE_STDCALL sqlite3_stmt_scanstatus_reset(sqlite3_stmt*); +/* +** CAPI3REF: Flush caches to disk mid-transaction +** +** ^If a write-transaction is open on [database connection] D when the +** [sqlite3_db_cacheflush(D)] interface invoked, any dirty +** pages in the pager-cache that are not currently in use are written out +** to disk. A dirty page may be in use if a database cursor created by an +** active SQL statement is reading from it, or if it is page 1 of a database +** file (page 1 is always "in use"). ^The [sqlite3_db_cacheflush(D)] +** interface flushes caches for all schemas - "main", "temp", and +** any [attached] databases. +** +** ^If this function needs to obtain extra database locks before dirty pages +** can be flushed to disk, it does so. ^If those locks cannot be obtained +** immediately and there is a busy-handler callback configured, it is invoked +** in the usual manner. ^If the required lock still cannot be obtained, then +** the database is skipped and an attempt made to flush any dirty pages +** belonging to the next (if any) database. ^If any databases are skipped +** because locks cannot be obtained, but no other error occurs, this +** function returns SQLITE_BUSY. +** +** ^If any other error occurs while flushing dirty pages to disk (for +** example an IO error or out-of-memory condition), then processing is +** abandoned and an SQLite [error code] is returned to the caller immediately. +** +** ^Otherwise, if no error occurs, [sqlite3_db_cacheflush()] returns SQLITE_OK. +** +** ^This function does not set the database handle error code or message +** returned by the [sqlite3_errcode()] and [sqlite3_errmsg()] functions. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_db_cacheflush(sqlite3*); + +/* +** CAPI3REF: The pre-update hook. +** +** ^These interfaces are only available if SQLite is compiled using the +** [SQLITE_ENABLE_PREUPDATE_HOOK] compile-time option. +** +** ^The [sqlite3_preupdate_hook()] interface registers a callback function +** that is invoked prior to each [INSERT], [UPDATE], and [DELETE] operation +** on a [rowid table]. +** ^At most one preupdate hook may be registered at a time on a single +** [database connection]; each call to [sqlite3_preupdate_hook()] overrides +** the previous setting. +** ^The preupdate hook is disabled by invoking [sqlite3_preupdate_hook()] +** with a NULL pointer as the second parameter. +** ^The third parameter to [sqlite3_preupdate_hook()] is passed through as +** the first parameter to callbacks. +** +** ^The preupdate hook only fires for changes to [rowid tables]; the preupdate +** hook is not invoked for changes to [virtual tables] or [WITHOUT ROWID] +** tables. +** +** ^The second parameter to the preupdate callback is a pointer to +** the [database connection] that registered the preupdate hook. +** ^The third parameter to the preupdate callback is one of the constants +** [SQLITE_INSERT], [SQLITE_DELETE], or [SQLITE_UPDATE] to indentify the +** kind of update operation that is about to occur. +** ^(The fourth parameter to the preupdate callback is the name of the +** database within the database connection that is being modified. This +** will be "main" for the main database or "temp" for TEMP tables or +** the name given after the AS keyword in the [ATTACH] statement for attached +** databases.)^ +** ^The fifth parameter to the preupdate callback is the name of the +** table that is being modified. +** ^The sixth parameter to the preupdate callback is the initial [rowid] of the +** row being changes for SQLITE_UPDATE and SQLITE_DELETE changes and is +** undefined for SQLITE_INSERT changes. +** ^The seventh parameter to the preupdate callback is the final [rowid] of +** the row being changed for SQLITE_UPDATE and SQLITE_INSERT changes and is +** undefined for SQLITE_DELETE changes. +** +** The [sqlite3_preupdate_old()], [sqlite3_preupdate_new()], +** [sqlite3_preupdate_count()], and [sqlite3_preupdate_depth()] interfaces +** provide additional information about a preupdate event. These routines +** may only be called from within a preupdate callback. Invoking any of +** these routines from outside of a preupdate callback or with a +** [database connection] pointer that is different from the one supplied +** to the preupdate callback results in undefined and probably undesirable +** behavior. +** +** ^The [sqlite3_preupdate_count(D)] interface returns the number of columns +** in the row that is being inserted, updated, or deleted. +** +** ^The [sqlite3_preupdate_old(D,N,P)] interface writes into P a pointer to +** a [protected sqlite3_value] that contains the value of the Nth column of +** the table row before it is updated. The N parameter must be between 0 +** and one less than the number of columns or the behavior will be +** undefined. This must only be used within SQLITE_UPDATE and SQLITE_DELETE +** preupdate callbacks; if it is used by an SQLITE_INSERT callback then the +** behavior is undefined. The [sqlite3_value] that P points to +** will be destroyed when the preupdate callback returns. +** +** ^The [sqlite3_preupdate_new(D,N,P)] interface writes into P a pointer to +** a [protected sqlite3_value] that contains the value of the Nth column of +** the table row after it is updated. The N parameter must be between 0 +** and one less than the number of columns or the behavior will be +** undefined. This must only be used within SQLITE_INSERT and SQLITE_UPDATE +** preupdate callbacks; if it is used by an SQLITE_DELETE callback then the +** behavior is undefined. The [sqlite3_value] that P points to +** will be destroyed when the preupdate callback returns. +** +** ^The [sqlite3_preupdate_depth(D)] interface returns 0 if the preupdate +** callback was invoked as a result of a direct insert, update, or delete +** operation; or 1 for inserts, updates, or deletes invoked by top-level +** triggers; or 2 for changes resulting from triggers called by top-level +** triggers; and so forth. +** +** See also: [sqlite3_update_hook()] +*/ +SQLITE_API SQLITE_EXPERIMENTAL void *SQLITE_STDCALL sqlite3_preupdate_hook( + sqlite3 *db, + void(*xPreUpdate)( + void *pCtx, /* Copy of third arg to preupdate_hook() */ + sqlite3 *db, /* Database handle */ + int op, /* SQLITE_UPDATE, DELETE or INSERT */ + char const *zDb, /* Database name */ + char const *zName, /* Table name */ + sqlite3_int64 iKey1, /* Rowid of row about to be deleted/updated */ + sqlite3_int64 iKey2 /* New rowid value (for a rowid UPDATE) */ + ), + void* +); +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_preupdate_old(sqlite3 *, int, sqlite3_value **); +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_preupdate_count(sqlite3 *); +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_preupdate_depth(sqlite3 *); +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_preupdate_new(sqlite3 *, int, sqlite3_value **); + +/* +** CAPI3REF: Low-level system error code +** +** ^Attempt to return the underlying operating system error code or error +** number that caused the most recent I/O error or failure to open a file. +** The return value is OS-dependent. For example, on unix systems, after +** [sqlite3_open_v2()] returns [SQLITE_CANTOPEN], this interface could be +** called to get back the underlying "errno" that caused the problem, such +** as ENOSPC, EAUTH, EISDIR, and so forth. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_system_errno(sqlite3*); + +/* +** CAPI3REF: Database Snapshot +** KEYWORDS: {snapshot} +** EXPERIMENTAL +** +** An instance of the snapshot object records the state of a [WAL mode] +** database for some specific point in history. +** +** In [WAL mode], multiple [database connections] that are open on the +** same database file can each be reading a different historical version +** of the database file. When a [database connection] begins a read +** transaction, that connection sees an unchanging copy of the database +** as it existed for the point in time when the transaction first started. +** Subsequent changes to the database from other connections are not seen +** by the reader until a new read transaction is started. +** +** The sqlite3_snapshot object records state information about an historical +** version of the database file so that it is possible to later open a new read +** transaction that sees that historical version of the database rather than +** the most recent version. +** +** The constructor for this object is [sqlite3_snapshot_get()]. The +** [sqlite3_snapshot_open()] method causes a fresh read transaction to refer +** to an historical snapshot (if possible). The destructor for +** sqlite3_snapshot objects is [sqlite3_snapshot_free()]. +*/ +typedef struct sqlite3_snapshot sqlite3_snapshot; + +/* +** CAPI3REF: Record A Database Snapshot +** EXPERIMENTAL +** +** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a +** new [sqlite3_snapshot] object that records the current state of +** schema S in database connection D. ^On success, the +** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly +** created [sqlite3_snapshot] object into *P and returns SQLITE_OK. +** ^If schema S of [database connection] D is not a [WAL mode] database +** that is in a read transaction, then [sqlite3_snapshot_get(D,S,P)] +** leaves the *P value unchanged and returns an appropriate [error code]. +** +** The [sqlite3_snapshot] object returned from a successful call to +** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()] +** to avoid a memory leak. +** +** The [sqlite3_snapshot_get()] interface is only available when the +** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +*/ +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_snapshot_get( + sqlite3 *db, + const char *zSchema, + sqlite3_snapshot **ppSnapshot +); + +/* +** CAPI3REF: Start a read transaction on an historical snapshot +** EXPERIMENTAL +** +** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a +** read transaction for schema S of +** [database connection] D such that the read transaction +** refers to historical [snapshot] P, rather than the most +** recent change to the database. +** ^The [sqlite3_snapshot_open()] interface returns SQLITE_OK on success +** or an appropriate [error code] if it fails. +** +** ^In order to succeed, a call to [sqlite3_snapshot_open(D,S,P)] must be +** the first operation following the [BEGIN] that takes the schema S +** out of [autocommit mode]. +** ^In other words, schema S must not currently be in +** a transaction for [sqlite3_snapshot_open(D,S,P)] to work, but the +** database connection D must be out of [autocommit mode]. +** ^A [snapshot] will fail to open if it has been overwritten by a +** [checkpoint]. +** ^(A call to [sqlite3_snapshot_open(D,S,P)] will fail if the +** database connection D does not know that the database file for +** schema S is in [WAL mode]. A database connection might not know +** that the database file is in [WAL mode] if there has been no prior +** I/O on that database connection, or if the database entered [WAL mode] +** after the most recent I/O on the database connection.)^ +** (Hint: Run "[PRAGMA application_id]" against a newly opened +** database connection in order to make it ready to use snapshots.) +** +** The [sqlite3_snapshot_open()] interface is only available when the +** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +*/ +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_snapshot_open( + sqlite3 *db, + const char *zSchema, + sqlite3_snapshot *pSnapshot +); + +/* +** CAPI3REF: Destroy a snapshot +** EXPERIMENTAL +** +** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P. +** The application must eventually free every [sqlite3_snapshot] object +** using this routine to avoid a memory leak. +** +** The [sqlite3_snapshot_free()] interface is only available when the +** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +*/ +SQLITE_API SQLITE_EXPERIMENTAL void SQLITE_STDCALL sqlite3_snapshot_free(sqlite3_snapshot*); + +/* +** CAPI3REF: Compare the ages of two snapshot handles. +** EXPERIMENTAL +** +** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages +** of two valid snapshot handles. +** +** If the two snapshot handles are not associated with the same database +** file, the result of the comparison is undefined. +** +** Additionally, the result of the comparison is only valid if both of the +** snapshot handles were obtained by calling sqlite3_snapshot_get() since the +** last time the wal file was deleted. The wal file is deleted when the +** database is changed back to rollback mode or when the number of database +** clients drops to zero. If either snapshot handle was obtained before the +** wal file was last deleted, the value returned by this function +** is undefined. +** +** Otherwise, this API returns a negative value if P1 refers to an older +** snapshot than P2, zero if the two handles refer to the same database +** snapshot, and a positive value if P1 is a newer snapshot than P2. +*/ +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_snapshot_cmp( + sqlite3_snapshot *p1, + sqlite3_snapshot *p2 +); /* ** Undo the hack that converts floating point types to integer for @@ -8019,6 +8458,7 @@ SQLITE_API void SQLITE_STDCALL sqlite3_stmt_scanstatus_reset(sqlite3_stmt*); #endif #endif /* _SQLITE3_H_ */ +/******** Begin file sqlite3rtree.h *********/ /* ** 2010 August 30 ** @@ -8136,6 +8576,1287 @@ struct sqlite3_rtree_query_info { #endif /* ifndef _SQLITE3RTREE_H_ */ +/******** End of sqlite3rtree.h *********/ +/******** Begin file sqlite3session.h *********/ + +#if !defined(__SQLITESESSION_H_) && defined(SQLITE_ENABLE_SESSION) +#define __SQLITESESSION_H_ 1 + +/* +** Make sure we can call this stuff from C++. +*/ +#if 0 +extern "C" { +#endif + + +/* +** CAPI3REF: Session Object Handle +*/ +typedef struct sqlite3_session sqlite3_session; + +/* +** CAPI3REF: Changeset Iterator Handle +*/ +typedef struct sqlite3_changeset_iter sqlite3_changeset_iter; + +/* +** CAPI3REF: Create A New Session Object +** +** Create a new session object attached to database handle db. If successful, +** a pointer to the new object is written to *ppSession and SQLITE_OK is +** returned. If an error occurs, *ppSession is set to NULL and an SQLite +** error code (e.g. SQLITE_NOMEM) is returned. +** +** It is possible to create multiple session objects attached to a single +** database handle. +** +** Session objects created using this function should be deleted using the +** [sqlite3session_delete()] function before the database handle that they +** are attached to is itself closed. If the database handle is closed before +** the session object is deleted, then the results of calling any session +** module function, including [sqlite3session_delete()] on the session object +** are undefined. +** +** Because the session module uses the [sqlite3_preupdate_hook()] API, it +** is not possible for an application to register a pre-update hook on a +** database handle that has one or more session objects attached. Nor is +** it possible to create a session object attached to a database handle for +** which a pre-update hook is already defined. The results of attempting +** either of these things are undefined. +** +** The session object will be used to create changesets for tables in +** database zDb, where zDb is either "main", or "temp", or the name of an +** attached database. It is not an error if database zDb is not attached +** to the database when the session object is created. +*/ +int sqlite3session_create( + sqlite3 *db, /* Database handle */ + const char *zDb, /* Name of db (e.g. "main") */ + sqlite3_session **ppSession /* OUT: New session object */ +); + +/* +** CAPI3REF: Delete A Session Object +** +** Delete a session object previously allocated using +** [sqlite3session_create()]. Once a session object has been deleted, the +** results of attempting to use pSession with any other session module +** function are undefined. +** +** Session objects must be deleted before the database handle to which they +** are attached is closed. Refer to the documentation for +** [sqlite3session_create()] for details. +*/ +void sqlite3session_delete(sqlite3_session *pSession); + + +/* +** CAPI3REF: Enable Or Disable A Session Object +** +** Enable or disable the recording of changes by a session object. When +** enabled, a session object records changes made to the database. When +** disabled - it does not. A newly created session object is enabled. +** Refer to the documentation for [sqlite3session_changeset()] for further +** details regarding how enabling and disabling a session object affects +** the eventual changesets. +** +** Passing zero to this function disables the session. Passing a value +** greater than zero enables it. Passing a value less than zero is a +** no-op, and may be used to query the current state of the session. +** +** The return value indicates the final state of the session object: 0 if +** the session is disabled, or 1 if it is enabled. +*/ +int sqlite3session_enable(sqlite3_session *pSession, int bEnable); + +/* +** CAPI3REF: Set Or Clear the Indirect Change Flag +** +** Each change recorded by a session object is marked as either direct or +** indirect. A change is marked as indirect if either: +** +**
      +**
    • The session object "indirect" flag is set when the change is +** made, or +**
    • The change is made by an SQL trigger or foreign key action +** instead of directly as a result of a users SQL statement. +**
    +** +** If a single row is affected by more than one operation within a session, +** then the change is considered indirect if all operations meet the criteria +** for an indirect change above, or direct otherwise. +** +** This function is used to set, clear or query the session object indirect +** flag. If the second argument passed to this function is zero, then the +** indirect flag is cleared. If it is greater than zero, the indirect flag +** is set. Passing a value less than zero does not modify the current value +** of the indirect flag, and may be used to query the current state of the +** indirect flag for the specified session object. +** +** The return value indicates the final state of the indirect flag: 0 if +** it is clear, or 1 if it is set. +*/ +int sqlite3session_indirect(sqlite3_session *pSession, int bIndirect); + +/* +** CAPI3REF: Attach A Table To A Session Object +** +** If argument zTab is not NULL, then it is the name of a table to attach +** to the session object passed as the first argument. All subsequent changes +** made to the table while the session object is enabled will be recorded. See +** documentation for [sqlite3session_changeset()] for further details. +** +** Or, if argument zTab is NULL, then changes are recorded for all tables +** in the database. If additional tables are added to the database (by +** executing "CREATE TABLE" statements) after this call is made, changes for +** the new tables are also recorded. +** +** Changes can only be recorded for tables that have a PRIMARY KEY explicitly +** defined as part of their CREATE TABLE statement. It does not matter if the +** PRIMARY KEY is an "INTEGER PRIMARY KEY" (rowid alias) or not. The PRIMARY +** KEY may consist of a single column, or may be a composite key. +** +** It is not an error if the named table does not exist in the database. Nor +** is it an error if the named table does not have a PRIMARY KEY. However, +** no changes will be recorded in either of these scenarios. +** +** Changes are not recorded for individual rows that have NULL values stored +** in one or more of their PRIMARY KEY columns. +** +** SQLITE_OK is returned if the call completes without error. Or, if an error +** occurs, an SQLite error code (e.g. SQLITE_NOMEM) is returned. +*/ +int sqlite3session_attach( + sqlite3_session *pSession, /* Session object */ + const char *zTab /* Table name */ +); + +/* +** CAPI3REF: Set a table filter on a Session Object. +** +** The second argument (xFilter) is the "filter callback". For changes to rows +** in tables that are not attached to the Session oject, the filter is called +** to determine whether changes to the table's rows should be tracked or not. +** If xFilter returns 0, changes is not tracked. Note that once a table is +** attached, xFilter will not be called again. +*/ +void sqlite3session_table_filter( + sqlite3_session *pSession, /* Session object */ + int(*xFilter)( + void *pCtx, /* Copy of third arg to _filter_table() */ + const char *zTab /* Table name */ + ), + void *pCtx /* First argument passed to xFilter */ +); + +/* +** CAPI3REF: Generate A Changeset From A Session Object +** +** Obtain a changeset containing changes to the tables attached to the +** session object passed as the first argument. If successful, +** set *ppChangeset to point to a buffer containing the changeset +** and *pnChangeset to the size of the changeset in bytes before returning +** SQLITE_OK. If an error occurs, set both *ppChangeset and *pnChangeset to +** zero and return an SQLite error code. +** +** A changeset consists of zero or more INSERT, UPDATE and/or DELETE changes, +** each representing a change to a single row of an attached table. An INSERT +** change contains the values of each field of a new database row. A DELETE +** contains the original values of each field of a deleted database row. An +** UPDATE change contains the original values of each field of an updated +** database row along with the updated values for each updated non-primary-key +** column. It is not possible for an UPDATE change to represent a change that +** modifies the values of primary key columns. If such a change is made, it +** is represented in a changeset as a DELETE followed by an INSERT. +** +** Changes are not recorded for rows that have NULL values stored in one or +** more of their PRIMARY KEY columns. If such a row is inserted or deleted, +** no corresponding change is present in the changesets returned by this +** function. If an existing row with one or more NULL values stored in +** PRIMARY KEY columns is updated so that all PRIMARY KEY columns are non-NULL, +** only an INSERT is appears in the changeset. Similarly, if an existing row +** with non-NULL PRIMARY KEY values is updated so that one or more of its +** PRIMARY KEY columns are set to NULL, the resulting changeset contains a +** DELETE change only. +** +** The contents of a changeset may be traversed using an iterator created +** using the [sqlite3changeset_start()] API. A changeset may be applied to +** a database with a compatible schema using the [sqlite3changeset_apply()] +** API. +** +** Within a changeset generated by this function, all changes related to a +** single table are grouped together. In other words, when iterating through +** a changeset or when applying a changeset to a database, all changes related +** to a single table are processed before moving on to the next table. Tables +** are sorted in the same order in which they were attached (or auto-attached) +** to the sqlite3_session object. The order in which the changes related to +** a single table are stored is undefined. +** +** Following a successful call to this function, it is the responsibility of +** the caller to eventually free the buffer that *ppChangeset points to using +** [sqlite3_free()]. +** +**

    Changeset Generation

    +** +** Once a table has been attached to a session object, the session object +** records the primary key values of all new rows inserted into the table. +** It also records the original primary key and other column values of any +** deleted or updated rows. For each unique primary key value, data is only +** recorded once - the first time a row with said primary key is inserted, +** updated or deleted in the lifetime of the session. +** +** There is one exception to the previous paragraph: when a row is inserted, +** updated or deleted, if one or more of its primary key columns contain a +** NULL value, no record of the change is made. +** +** The session object therefore accumulates two types of records - those +** that consist of primary key values only (created when the user inserts +** a new record) and those that consist of the primary key values and the +** original values of other table columns (created when the users deletes +** or updates a record). +** +** When this function is called, the requested changeset is created using +** both the accumulated records and the current contents of the database +** file. Specifically: +** +**
      +**
    • For each record generated by an insert, the database is queried +** for a row with a matching primary key. If one is found, an INSERT +** change is added to the changeset. If no such row is found, no change +** is added to the changeset. +** +**
    • For each record generated by an update or delete, the database is +** queried for a row with a matching primary key. If such a row is +** found and one or more of the non-primary key fields have been +** modified from their original values, an UPDATE change is added to +** the changeset. Or, if no such row is found in the table, a DELETE +** change is added to the changeset. If there is a row with a matching +** primary key in the database, but all fields contain their original +** values, no change is added to the changeset. +**
    +** +** This means, amongst other things, that if a row is inserted and then later +** deleted while a session object is active, neither the insert nor the delete +** will be present in the changeset. Or if a row is deleted and then later a +** row with the same primary key values inserted while a session object is +** active, the resulting changeset will contain an UPDATE change instead of +** a DELETE and an INSERT. +** +** When a session object is disabled (see the [sqlite3session_enable()] API), +** it does not accumulate records when rows are inserted, updated or deleted. +** This may appear to have some counter-intuitive effects if a single row +** is written to more than once during a session. For example, if a row +** is inserted while a session object is enabled, then later deleted while +** the same session object is disabled, no INSERT record will appear in the +** changeset, even though the delete took place while the session was disabled. +** Or, if one field of a row is updated while a session is disabled, and +** another field of the same row is updated while the session is enabled, the +** resulting changeset will contain an UPDATE change that updates both fields. +*/ +int sqlite3session_changeset( + sqlite3_session *pSession, /* Session object */ + int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + void **ppChangeset /* OUT: Buffer containing changeset */ +); + +/* +** CAPI3REF: Load The Difference Between Tables Into A Session +** +** If it is not already attached to the session object passed as the first +** argument, this function attaches table zTbl in the same manner as the +** [sqlite3session_attach()] function. If zTbl does not exist, or if it +** does not have a primary key, this function is a no-op (but does not return +** an error). +** +** Argument zFromDb must be the name of a database ("main", "temp" etc.) +** attached to the same database handle as the session object that contains +** a table compatible with the table attached to the session by this function. +** A table is considered compatible if it: +** +**
      +**
    • Has the same name, +**
    • Has the same set of columns declared in the same order, and +**
    • Has the same PRIMARY KEY definition. +**
    +** +** If the tables are not compatible, SQLITE_SCHEMA is returned. If the tables +** are compatible but do not have any PRIMARY KEY columns, it is not an error +** but no changes are added to the session object. As with other session +** APIs, tables without PRIMARY KEYs are simply ignored. +** +** This function adds a set of changes to the session object that could be +** used to update the table in database zFrom (call this the "from-table") +** so that its content is the same as the table attached to the session +** object (call this the "to-table"). Specifically: +** +**
      +**
    • For each row (primary key) that exists in the to-table but not in +** the from-table, an INSERT record is added to the session object. +** +**
    • For each row (primary key) that exists in the to-table but not in +** the from-table, a DELETE record is added to the session object. +** +**
    • For each row (primary key) that exists in both tables, but features +** different in each, an UPDATE record is added to the session. +**
    +** +** To clarify, if this function is called and then a changeset constructed +** using [sqlite3session_changeset()], then after applying that changeset to +** database zFrom the contents of the two compatible tables would be +** identical. +** +** It an error if database zFrom does not exist or does not contain the +** required compatible table. +** +** If the operation successful, SQLITE_OK is returned. Otherwise, an SQLite +** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg +** may be set to point to a buffer containing an English language error +** message. It is the responsibility of the caller to free this buffer using +** sqlite3_free(). +*/ +int sqlite3session_diff( + sqlite3_session *pSession, + const char *zFromDb, + const char *zTbl, + char **pzErrMsg +); + + +/* +** CAPI3REF: Generate A Patchset From A Session Object +** +** The differences between a patchset and a changeset are that: +** +**
      +**
    • DELETE records consist of the primary key fields only. The +** original values of other fields are omitted. +**
    • The original values of any modified fields are omitted from +** UPDATE records. +**
    +** +** A patchset blob may be used with up to date versions of all +** sqlite3changeset_xxx API functions except for sqlite3changeset_invert(), +** which returns SQLITE_CORRUPT if it is passed a patchset. Similarly, +** attempting to use a patchset blob with old versions of the +** sqlite3changeset_xxx APIs also provokes an SQLITE_CORRUPT error. +** +** Because the non-primary key "old.*" fields are omitted, no +** SQLITE_CHANGESET_DATA conflicts can be detected or reported if a patchset +** is passed to the sqlite3changeset_apply() API. Other conflict types work +** in the same way as for changesets. +** +** Changes within a patchset are ordered in the same way as for changesets +** generated by the sqlite3session_changeset() function (i.e. all changes for +** a single table are grouped together, tables appear in the order in which +** they were attached to the session object). +*/ +int sqlite3session_patchset( + sqlite3_session *pSession, /* Session object */ + int *pnPatchset, /* OUT: Size of buffer at *ppChangeset */ + void **ppPatchset /* OUT: Buffer containing changeset */ +); + +/* +** CAPI3REF: Test if a changeset has recorded any changes. +** +** Return non-zero if no changes to attached tables have been recorded by +** the session object passed as the first argument. Otherwise, if one or +** more changes have been recorded, return zero. +** +** Even if this function returns zero, it is possible that calling +** [sqlite3session_changeset()] on the session handle may still return a +** changeset that contains no changes. This can happen when a row in +** an attached table is modified and then later on the original values +** are restored. However, if this function returns non-zero, then it is +** guaranteed that a call to sqlite3session_changeset() will return a +** changeset containing zero changes. +*/ +int sqlite3session_isempty(sqlite3_session *pSession); + +/* +** CAPI3REF: Create An Iterator To Traverse A Changeset +** +** Create an iterator used to iterate through the contents of a changeset. +** If successful, *pp is set to point to the iterator handle and SQLITE_OK +** is returned. Otherwise, if an error occurs, *pp is set to zero and an +** SQLite error code is returned. +** +** The following functions can be used to advance and query a changeset +** iterator created by this function: +** +**
      +**
    • [sqlite3changeset_next()] +**
    • [sqlite3changeset_op()] +**
    • [sqlite3changeset_new()] +**
    • [sqlite3changeset_old()] +**
    +** +** It is the responsibility of the caller to eventually destroy the iterator +** by passing it to [sqlite3changeset_finalize()]. The buffer containing the +** changeset (pChangeset) must remain valid until after the iterator is +** destroyed. +** +** Assuming the changeset blob was created by one of the +** [sqlite3session_changeset()], [sqlite3changeset_concat()] or +** [sqlite3changeset_invert()] functions, all changes within the changeset +** that apply to a single table are grouped together. This means that when +** an application iterates through a changeset using an iterator created by +** this function, all changes that relate to a single table are visted +** consecutively. There is no chance that the iterator will visit a change +** the applies to table X, then one for table Y, and then later on visit +** another change for table X. +*/ +int sqlite3changeset_start( + sqlite3_changeset_iter **pp, /* OUT: New changeset iterator handle */ + int nChangeset, /* Size of changeset blob in bytes */ + void *pChangeset /* Pointer to blob containing changeset */ +); + + +/* +** CAPI3REF: Advance A Changeset Iterator +** +** This function may only be used with iterators created by function +** [sqlite3changeset_start()]. If it is called on an iterator passed to +** a conflict-handler callback by [sqlite3changeset_apply()], SQLITE_MISUSE +** is returned and the call has no effect. +** +** Immediately after an iterator is created by sqlite3changeset_start(), it +** does not point to any change in the changeset. Assuming the changeset +** is not empty, the first call to this function advances the iterator to +** point to the first change in the changeset. Each subsequent call advances +** the iterator to point to the next change in the changeset (if any). If +** no error occurs and the iterator points to a valid change after a call +** to sqlite3changeset_next() has advanced it, SQLITE_ROW is returned. +** Otherwise, if all changes in the changeset have already been visited, +** SQLITE_DONE is returned. +** +** If an error occurs, an SQLite error code is returned. Possible error +** codes include SQLITE_CORRUPT (if the changeset buffer is corrupt) or +** SQLITE_NOMEM. +*/ +int sqlite3changeset_next(sqlite3_changeset_iter *pIter); + +/* +** CAPI3REF: Obtain The Current Operation From A Changeset Iterator +** +** The pIter argument passed to this function may either be an iterator +** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator +** created by [sqlite3changeset_start()]. In the latter case, the most recent +** call to [sqlite3changeset_next()] must have returned [SQLITE_ROW]. If this +** is not the case, this function returns [SQLITE_MISUSE]. +** +** If argument pzTab is not NULL, then *pzTab is set to point to a +** nul-terminated utf-8 encoded string containing the name of the table +** affected by the current change. The buffer remains valid until either +** sqlite3changeset_next() is called on the iterator or until the +** conflict-handler function returns. If pnCol is not NULL, then *pnCol is +** set to the number of columns in the table affected by the change. If +** pbIncorrect is not NULL, then *pbIndirect is set to true (1) if the change +** is an indirect change, or false (0) otherwise. See the documentation for +** [sqlite3session_indirect()] for a description of direct and indirect +** changes. Finally, if pOp is not NULL, then *pOp is set to one of +** [SQLITE_INSERT], [SQLITE_DELETE] or [SQLITE_UPDATE], depending on the +** type of change that the iterator currently points to. +** +** If no error occurs, SQLITE_OK is returned. If an error does occur, an +** SQLite error code is returned. The values of the output variables may not +** be trusted in this case. +*/ +int sqlite3changeset_op( + sqlite3_changeset_iter *pIter, /* Iterator object */ + const char **pzTab, /* OUT: Pointer to table name */ + int *pnCol, /* OUT: Number of columns in table */ + int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + int *pbIndirect /* OUT: True for an 'indirect' change */ +); + +/* +** CAPI3REF: Obtain The Primary Key Definition Of A Table +** +** For each modified table, a changeset includes the following: +** +**
      +**
    • The number of columns in the table, and +**
    • Which of those columns make up the tables PRIMARY KEY. +**
    +** +** This function is used to find which columns comprise the PRIMARY KEY of +** the table modified by the change that iterator pIter currently points to. +** If successful, *pabPK is set to point to an array of nCol entries, where +** nCol is the number of columns in the table. Elements of *pabPK are set to +** 0x01 if the corresponding column is part of the tables primary key, or +** 0x00 if it is not. +** +** If argumet pnCol is not NULL, then *pnCol is set to the number of columns +** in the table. +** +** If this function is called when the iterator does not point to a valid +** entry, SQLITE_MISUSE is returned and the output variables zeroed. Otherwise, +** SQLITE_OK is returned and the output variables populated as described +** above. +*/ +int sqlite3changeset_pk( + sqlite3_changeset_iter *pIter, /* Iterator object */ + unsigned char **pabPK, /* OUT: Array of boolean - true for PK cols */ + int *pnCol /* OUT: Number of entries in output array */ +); + +/* +** CAPI3REF: Obtain old.* Values From A Changeset Iterator +** +** The pIter argument passed to this function may either be an iterator +** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator +** created by [sqlite3changeset_start()]. In the latter case, the most recent +** call to [sqlite3changeset_next()] must have returned SQLITE_ROW. +** Furthermore, it may only be called if the type of change that the iterator +** currently points to is either [SQLITE_DELETE] or [SQLITE_UPDATE]. Otherwise, +** this function returns [SQLITE_MISUSE] and sets *ppValue to NULL. +** +** Argument iVal must be greater than or equal to 0, and less than the number +** of columns in the table affected by the current change. Otherwise, +** [SQLITE_RANGE] is returned and *ppValue is set to NULL. +** +** If successful, this function sets *ppValue to point to a protected +** sqlite3_value object containing the iVal'th value from the vector of +** original row values stored as part of the UPDATE or DELETE change and +** returns SQLITE_OK. The name of the function comes from the fact that this +** is similar to the "old.*" columns available to update or delete triggers. +** +** If some other error occurs (e.g. an OOM condition), an SQLite error code +** is returned and *ppValue is set to NULL. +*/ +int sqlite3changeset_old( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Column number */ + sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */ +); + +/* +** CAPI3REF: Obtain new.* Values From A Changeset Iterator +** +** The pIter argument passed to this function may either be an iterator +** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator +** created by [sqlite3changeset_start()]. In the latter case, the most recent +** call to [sqlite3changeset_next()] must have returned SQLITE_ROW. +** Furthermore, it may only be called if the type of change that the iterator +** currently points to is either [SQLITE_UPDATE] or [SQLITE_INSERT]. Otherwise, +** this function returns [SQLITE_MISUSE] and sets *ppValue to NULL. +** +** Argument iVal must be greater than or equal to 0, and less than the number +** of columns in the table affected by the current change. Otherwise, +** [SQLITE_RANGE] is returned and *ppValue is set to NULL. +** +** If successful, this function sets *ppValue to point to a protected +** sqlite3_value object containing the iVal'th value from the vector of +** new row values stored as part of the UPDATE or INSERT change and +** returns SQLITE_OK. If the change is an UPDATE and does not include +** a new value for the requested column, *ppValue is set to NULL and +** SQLITE_OK returned. The name of the function comes from the fact that +** this is similar to the "new.*" columns available to update or delete +** triggers. +** +** If some other error occurs (e.g. an OOM condition), an SQLite error code +** is returned and *ppValue is set to NULL. +*/ +int sqlite3changeset_new( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Column number */ + sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */ +); + +/* +** CAPI3REF: Obtain Conflicting Row Values From A Changeset Iterator +** +** This function should only be used with iterator objects passed to a +** conflict-handler callback by [sqlite3changeset_apply()] with either +** [SQLITE_CHANGESET_DATA] or [SQLITE_CHANGESET_CONFLICT]. If this function +** is called on any other iterator, [SQLITE_MISUSE] is returned and *ppValue +** is set to NULL. +** +** Argument iVal must be greater than or equal to 0, and less than the number +** of columns in the table affected by the current change. Otherwise, +** [SQLITE_RANGE] is returned and *ppValue is set to NULL. +** +** If successful, this function sets *ppValue to point to a protected +** sqlite3_value object containing the iVal'th value from the +** "conflicting row" associated with the current conflict-handler callback +** and returns SQLITE_OK. +** +** If some other error occurs (e.g. an OOM condition), an SQLite error code +** is returned and *ppValue is set to NULL. +*/ +int sqlite3changeset_conflict( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Column number */ + sqlite3_value **ppValue /* OUT: Value from conflicting row */ +); + +/* +** CAPI3REF: Determine The Number Of Foreign Key Constraint Violations +** +** This function may only be called with an iterator passed to an +** SQLITE_CHANGESET_FOREIGN_KEY conflict handler callback. In this case +** it sets the output variable to the total number of known foreign key +** violations in the destination database and returns SQLITE_OK. +** +** In all other cases this function returns SQLITE_MISUSE. +*/ +int sqlite3changeset_fk_conflicts( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int *pnOut /* OUT: Number of FK violations */ +); + + +/* +** CAPI3REF: Finalize A Changeset Iterator +** +** This function is used to finalize an iterator allocated with +** [sqlite3changeset_start()]. +** +** This function should only be called on iterators created using the +** [sqlite3changeset_start()] function. If an application calls this +** function with an iterator passed to a conflict-handler by +** [sqlite3changeset_apply()], [SQLITE_MISUSE] is immediately returned and the +** call has no effect. +** +** If an error was encountered within a call to an sqlite3changeset_xxx() +** function (for example an [SQLITE_CORRUPT] in [sqlite3changeset_next()] or an +** [SQLITE_NOMEM] in [sqlite3changeset_new()]) then an error code corresponding +** to that error is returned by this function. Otherwise, SQLITE_OK is +** returned. This is to allow the following pattern (pseudo-code): +** +** sqlite3changeset_start(); +** while( SQLITE_ROW==sqlite3changeset_next() ){ +** // Do something with change. +** } +** rc = sqlite3changeset_finalize(); +** if( rc!=SQLITE_OK ){ +** // An error has occurred +** } +*/ +int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter); + +/* +** CAPI3REF: Invert A Changeset +** +** This function is used to "invert" a changeset object. Applying an inverted +** changeset to a database reverses the effects of applying the uninverted +** changeset. Specifically: +** +**
      +**
    • Each DELETE change is changed to an INSERT, and +**
    • Each INSERT change is changed to a DELETE, and +**
    • For each UPDATE change, the old.* and new.* values are exchanged. +**
    +** +** This function does not change the order in which changes appear within +** the changeset. It merely reverses the sense of each individual change. +** +** If successful, a pointer to a buffer containing the inverted changeset +** is stored in *ppOut, the size of the same buffer is stored in *pnOut, and +** SQLITE_OK is returned. If an error occurs, both *pnOut and *ppOut are +** zeroed and an SQLite error code returned. +** +** It is the responsibility of the caller to eventually call sqlite3_free() +** on the *ppOut pointer to free the buffer allocation following a successful +** call to this function. +** +** WARNING/TODO: This function currently assumes that the input is a valid +** changeset. If it is not, the results are undefined. +*/ +int sqlite3changeset_invert( + int nIn, const void *pIn, /* Input changeset */ + int *pnOut, void **ppOut /* OUT: Inverse of input */ +); + +/* +** CAPI3REF: Concatenate Two Changeset Objects +** +** This function is used to concatenate two changesets, A and B, into a +** single changeset. The result is a changeset equivalent to applying +** changeset A followed by changeset B. +** +** This function combines the two input changesets using an +** sqlite3_changegroup object. Calling it produces similar results as the +** following code fragment: +** +** sqlite3_changegroup *pGrp; +** rc = sqlite3_changegroup_new(&pGrp); +** if( rc==SQLITE_OK ) rc = sqlite3changegroup_add(pGrp, nA, pA); +** if( rc==SQLITE_OK ) rc = sqlite3changegroup_add(pGrp, nB, pB); +** if( rc==SQLITE_OK ){ +** rc = sqlite3changegroup_output(pGrp, pnOut, ppOut); +** }else{ +** *ppOut = 0; +** *pnOut = 0; +** } +** +** Refer to the sqlite3_changegroup documentation below for details. +*/ +int sqlite3changeset_concat( + int nA, /* Number of bytes in buffer pA */ + void *pA, /* Pointer to buffer containing changeset A */ + int nB, /* Number of bytes in buffer pB */ + void *pB, /* Pointer to buffer containing changeset B */ + int *pnOut, /* OUT: Number of bytes in output changeset */ + void **ppOut /* OUT: Buffer containing output changeset */ +); + + +/* +** Changegroup handle. +*/ +typedef struct sqlite3_changegroup sqlite3_changegroup; + +/* +** CAPI3REF: Combine two or more changesets into a single changeset. +** +** An sqlite3_changegroup object is used to combine two or more changesets +** (or patchsets) into a single changeset (or patchset). A single changegroup +** object may combine changesets or patchsets, but not both. The output is +** always in the same format as the input. +** +** If successful, this function returns SQLITE_OK and populates (*pp) with +** a pointer to a new sqlite3_changegroup object before returning. The caller +** should eventually free the returned object using a call to +** sqlite3changegroup_delete(). If an error occurs, an SQLite error code +** (i.e. SQLITE_NOMEM) is returned and *pp is set to NULL. +** +** The usual usage pattern for an sqlite3_changegroup object is as follows: +** +**
      +**
    • It is created using a call to sqlite3changegroup_new(). +** +**
    • Zero or more changesets (or patchsets) are added to the object +** by calling sqlite3changegroup_add(). +** +**
    • The result of combining all input changesets together is obtained +** by the application via a call to sqlite3changegroup_output(). +** +**
    • The object is deleted using a call to sqlite3changegroup_delete(). +**
    +** +** Any number of calls to add() and output() may be made between the calls to +** new() and delete(), and in any order. +** +** As well as the regular sqlite3changegroup_add() and +** sqlite3changegroup_output() functions, also available are the streaming +** versions sqlite3changegroup_add_strm() and sqlite3changegroup_output_strm(). +*/ +int sqlite3changegroup_new(sqlite3_changegroup **pp); + +/* +** Add all changes within the changeset (or patchset) in buffer pData (size +** nData bytes) to the changegroup. +** +** If the buffer contains a patchset, then all prior calls to this function +** on the same changegroup object must also have specified patchsets. Or, if +** the buffer contains a changeset, so must have the earlier calls to this +** function. Otherwise, SQLITE_ERROR is returned and no changes are added +** to the changegroup. +** +** Rows within the changeset and changegroup are identified by the values in +** their PRIMARY KEY columns. A change in the changeset is considered to +** apply to the same row as a change already present in the changegroup if +** the two rows have the same primary key. +** +** Changes to rows that that do not already appear in the changegroup are +** simply copied into it. Or, if both the new changeset and the changegroup +** contain changes that apply to a single row, the final contents of the +** changegroup depends on the type of each change, as follows: +** +** +** +** +**
    Existing Change New Change Output Change +**
    INSERT INSERT +** The new change is ignored. This case does not occur if the new +** changeset was recorded immediately after the changesets already +** added to the changegroup. +**
    INSERT UPDATE +** The INSERT change remains in the changegroup. The values in the +** INSERT change are modified as if the row was inserted by the +** existing change and then updated according to the new change. +**
    INSERT DELETE +** The existing INSERT is removed from the changegroup. The DELETE is +** not added. +**
    UPDATE INSERT +** The new change is ignored. This case does not occur if the new +** changeset was recorded immediately after the changesets already +** added to the changegroup. +**
    UPDATE UPDATE +** The existing UPDATE remains within the changegroup. It is amended +** so that the accompanying values are as if the row was updated once +** by the existing change and then again by the new change. +**
    UPDATE DELETE +** The existing UPDATE is replaced by the new DELETE within the +** changegroup. +**
    DELETE INSERT +** If one or more of the column values in the row inserted by the +** new change differ from those in the row deleted by the existing +** change, the existing DELETE is replaced by an UPDATE within the +** changegroup. Otherwise, if the inserted row is exactly the same +** as the deleted row, the existing DELETE is simply discarded. +**
    DELETE UPDATE +** The new change is ignored. This case does not occur if the new +** changeset was recorded immediately after the changesets already +** added to the changegroup. +**
    DELETE DELETE +** The new change is ignored. This case does not occur if the new +** changeset was recorded immediately after the changesets already +** added to the changegroup. +**
    +** +** If the new changeset contains changes to a table that is already present +** in the changegroup, then the number of columns and the position of the +** primary key columns for the table must be consistent. If this is not the +** case, this function fails with SQLITE_SCHEMA. If the input changeset +** appears to be corrupt and the corruption is detected, SQLITE_CORRUPT is +** returned. Or, if an out-of-memory condition occurs during processing, this +** function returns SQLITE_NOMEM. In all cases, if an error occurs the +** final contents of the changegroup is undefined. +** +** If no error occurs, SQLITE_OK is returned. +*/ +int sqlite3changegroup_add(sqlite3_changegroup*, int nData, void *pData); + +/* +** Obtain a buffer containing a changeset (or patchset) representing the +** current contents of the changegroup. If the inputs to the changegroup +** were themselves changesets, the output is a changeset. Or, if the +** inputs were patchsets, the output is also a patchset. +** +** As with the output of the sqlite3session_changeset() and +** sqlite3session_patchset() functions, all changes related to a single +** table are grouped together in the output of this function. Tables appear +** in the same order as for the very first changeset added to the changegroup. +** If the second or subsequent changesets added to the changegroup contain +** changes for tables that do not appear in the first changeset, they are +** appended onto the end of the output changeset, again in the order in +** which they are first encountered. +** +** If an error occurs, an SQLite error code is returned and the output +** variables (*pnData) and (*ppData) are set to 0. Otherwise, SQLITE_OK +** is returned and the output variables are set to the size of and a +** pointer to the output buffer, respectively. In this case it is the +** responsibility of the caller to eventually free the buffer using a +** call to sqlite3_free(). +*/ +int sqlite3changegroup_output( + sqlite3_changegroup*, + int *pnData, /* OUT: Size of output buffer in bytes */ + void **ppData /* OUT: Pointer to output buffer */ +); + +/* +** Delete a changegroup object. +*/ +void sqlite3changegroup_delete(sqlite3_changegroup*); + +/* +** CAPI3REF: Apply A Changeset To A Database +** +** Apply a changeset to a database. This function attempts to update the +** "main" database attached to handle db with the changes found in the +** changeset passed via the second and third arguments. +** +** The fourth argument (xFilter) passed to this function is the "filter +** callback". If it is not NULL, then for each table affected by at least one +** change in the changeset, the filter callback is invoked with +** the table name as the second argument, and a copy of the context pointer +** passed as the sixth argument to this function as the first. If the "filter +** callback" returns zero, then no attempt is made to apply any changes to +** the table. Otherwise, if the return value is non-zero or the xFilter +** argument to this function is NULL, all changes related to the table are +** attempted. +** +** For each table that is not excluded by the filter callback, this function +** tests that the target database contains a compatible table. A table is +** considered compatible if all of the following are true: +** +**
      +**
    • The table has the same name as the name recorded in the +** changeset, and +**
    • The table has the same number of columns as recorded in the +** changeset, and +**
    • The table has primary key columns in the same position as +** recorded in the changeset. +**
    +** +** If there is no compatible table, it is not an error, but none of the +** changes associated with the table are applied. A warning message is issued +** via the sqlite3_log() mechanism with the error code SQLITE_SCHEMA. At most +** one such warning is issued for each table in the changeset. +** +** For each change for which there is a compatible table, an attempt is made +** to modify the table contents according to the UPDATE, INSERT or DELETE +** change. If a change cannot be applied cleanly, the conflict handler +** function passed as the fifth argument to sqlite3changeset_apply() may be +** invoked. A description of exactly when the conflict handler is invoked for +** each type of change is below. +** +** Unlike the xFilter argument, xConflict may not be passed NULL. The results +** of passing anything other than a valid function pointer as the xConflict +** argument are undefined. +** +** Each time the conflict handler function is invoked, it must return one +** of [SQLITE_CHANGESET_OMIT], [SQLITE_CHANGESET_ABORT] or +** [SQLITE_CHANGESET_REPLACE]. SQLITE_CHANGESET_REPLACE may only be returned +** if the second argument passed to the conflict handler is either +** SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT. If the conflict-handler +** returns an illegal value, any changes already made are rolled back and +** the call to sqlite3changeset_apply() returns SQLITE_MISUSE. Different +** actions are taken by sqlite3changeset_apply() depending on the value +** returned by each invocation of the conflict-handler function. Refer to +** the documentation for the three +** [SQLITE_CHANGESET_OMIT|available return values] for details. +** +**
    +**
    DELETE Changes
    +** For each DELETE change, this function checks if the target database +** contains a row with the same primary key value (or values) as the +** original row values stored in the changeset. If it does, and the values +** stored in all non-primary key columns also match the values stored in +** the changeset the row is deleted from the target database. +** +** If a row with matching primary key values is found, but one or more of +** the non-primary key fields contains a value different from the original +** row value stored in the changeset, the conflict-handler function is +** invoked with [SQLITE_CHANGESET_DATA] as the second argument. +** +** If no row with matching primary key values is found in the database, +** the conflict-handler function is invoked with [SQLITE_CHANGESET_NOTFOUND] +** passed as the second argument. +** +** If the DELETE operation is attempted, but SQLite returns SQLITE_CONSTRAINT +** (which can only happen if a foreign key constraint is violated), the +** conflict-handler function is invoked with [SQLITE_CHANGESET_CONSTRAINT] +** passed as the second argument. This includes the case where the DELETE +** operation is attempted because an earlier call to the conflict handler +** function returned [SQLITE_CHANGESET_REPLACE]. +** +**
    INSERT Changes
    +** For each INSERT change, an attempt is made to insert the new row into +** the database. +** +** If the attempt to insert the row fails because the database already +** contains a row with the same primary key values, the conflict handler +** function is invoked with the second argument set to +** [SQLITE_CHANGESET_CONFLICT]. +** +** If the attempt to insert the row fails because of some other constraint +** violation (e.g. NOT NULL or UNIQUE), the conflict handler function is +** invoked with the second argument set to [SQLITE_CHANGESET_CONSTRAINT]. +** This includes the case where the INSERT operation is re-attempted because +** an earlier call to the conflict handler function returned +** [SQLITE_CHANGESET_REPLACE]. +** +**
    UPDATE Changes
    +** For each UPDATE change, this function checks if the target database +** contains a row with the same primary key value (or values) as the +** original row values stored in the changeset. If it does, and the values +** stored in all non-primary key columns also match the values stored in +** the changeset the row is updated within the target database. +** +** If a row with matching primary key values is found, but one or more of +** the non-primary key fields contains a value different from an original +** row value stored in the changeset, the conflict-handler function is +** invoked with [SQLITE_CHANGESET_DATA] as the second argument. Since +** UPDATE changes only contain values for non-primary key fields that are +** to be modified, only those fields need to match the original values to +** avoid the SQLITE_CHANGESET_DATA conflict-handler callback. +** +** If no row with matching primary key values is found in the database, +** the conflict-handler function is invoked with [SQLITE_CHANGESET_NOTFOUND] +** passed as the second argument. +** +** If the UPDATE operation is attempted, but SQLite returns +** SQLITE_CONSTRAINT, the conflict-handler function is invoked with +** [SQLITE_CHANGESET_CONSTRAINT] passed as the second argument. +** This includes the case where the UPDATE operation is attempted after +** an earlier call to the conflict handler function returned +** [SQLITE_CHANGESET_REPLACE]. +**
    +** +** It is safe to execute SQL statements, including those that write to the +** table that the callback related to, from within the xConflict callback. +** This can be used to further customize the applications conflict +** resolution strategy. +** +** All changes made by this function are enclosed in a savepoint transaction. +** If any other error (aside from a constraint failure when attempting to +** write to the target database) occurs, then the savepoint transaction is +** rolled back, restoring the target database to its original state, and an +** SQLite error code returned. +*/ +int sqlite3changeset_apply( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int nChangeset, /* Size of changeset in bytes */ + void *pChangeset, /* Changeset blob */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +); + +/* +** CAPI3REF: Constants Passed To The Conflict Handler +** +** Values that may be passed as the second argument to a conflict-handler. +** +**
    +**
    SQLITE_CHANGESET_DATA
    +** The conflict handler is invoked with CHANGESET_DATA as the second argument +** when processing a DELETE or UPDATE change if a row with the required +** PRIMARY KEY fields is present in the database, but one or more other +** (non primary-key) fields modified by the update do not contain the +** expected "before" values. +** +** The conflicting row, in this case, is the database row with the matching +** primary key. +** +**
    SQLITE_CHANGESET_NOTFOUND
    +** The conflict handler is invoked with CHANGESET_NOTFOUND as the second +** argument when processing a DELETE or UPDATE change if a row with the +** required PRIMARY KEY fields is not present in the database. +** +** There is no conflicting row in this case. The results of invoking the +** sqlite3changeset_conflict() API are undefined. +** +**
    SQLITE_CHANGESET_CONFLICT
    +** CHANGESET_CONFLICT is passed as the second argument to the conflict +** handler while processing an INSERT change if the operation would result +** in duplicate primary key values. +** +** The conflicting row in this case is the database row with the matching +** primary key. +** +**
    SQLITE_CHANGESET_FOREIGN_KEY
    +** If foreign key handling is enabled, and applying a changeset leaves the +** database in a state containing foreign key violations, the conflict +** handler is invoked with CHANGESET_FOREIGN_KEY as the second argument +** exactly once before the changeset is committed. If the conflict handler +** returns CHANGESET_OMIT, the changes, including those that caused the +** foreign key constraint violation, are committed. Or, if it returns +** CHANGESET_ABORT, the changeset is rolled back. +** +** No current or conflicting row information is provided. The only function +** it is possible to call on the supplied sqlite3_changeset_iter handle +** is sqlite3changeset_fk_conflicts(). +** +**
    SQLITE_CHANGESET_CONSTRAINT
    +** If any other constraint violation occurs while applying a change (i.e. +** a UNIQUE, CHECK or NOT NULL constraint), the conflict handler is +** invoked with CHANGESET_CONSTRAINT as the second argument. +** +** There is no conflicting row in this case. The results of invoking the +** sqlite3changeset_conflict() API are undefined. +** +**
    +*/ +#define SQLITE_CHANGESET_DATA 1 +#define SQLITE_CHANGESET_NOTFOUND 2 +#define SQLITE_CHANGESET_CONFLICT 3 +#define SQLITE_CHANGESET_CONSTRAINT 4 +#define SQLITE_CHANGESET_FOREIGN_KEY 5 + +/* +** CAPI3REF: Constants Returned By The Conflict Handler +** +** A conflict handler callback must return one of the following three values. +** +**
    +**
    SQLITE_CHANGESET_OMIT
    +** If a conflict handler returns this value no special action is taken. The +** change that caused the conflict is not applied. The session module +** continues to the next change in the changeset. +** +**
    SQLITE_CHANGESET_REPLACE
    +** This value may only be returned if the second argument to the conflict +** handler was SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT. If this +** is not the case, any changes applied so far are rolled back and the +** call to sqlite3changeset_apply() returns SQLITE_MISUSE. +** +** If CHANGESET_REPLACE is returned by an SQLITE_CHANGESET_DATA conflict +** handler, then the conflicting row is either updated or deleted, depending +** on the type of change. +** +** If CHANGESET_REPLACE is returned by an SQLITE_CHANGESET_CONFLICT conflict +** handler, then the conflicting row is removed from the database and a +** second attempt to apply the change is made. If this second attempt fails, +** the original row is restored to the database before continuing. +** +**
    SQLITE_CHANGESET_ABORT
    +** If this value is returned, any changes applied so far are rolled back +** and the call to sqlite3changeset_apply() returns SQLITE_ABORT. +**
    +*/ +#define SQLITE_CHANGESET_OMIT 0 +#define SQLITE_CHANGESET_REPLACE 1 +#define SQLITE_CHANGESET_ABORT 2 + +/* +** CAPI3REF: Streaming Versions of API functions. +** +** The six streaming API xxx_strm() functions serve similar purposes to the +** corresponding non-streaming API functions: +** +** +** +**
    Streaming functionNon-streaming equivalent
    sqlite3changeset_apply_str[sqlite3changeset_apply] +**
    sqlite3changeset_concat_str[sqlite3changeset_concat] +**
    sqlite3changeset_invert_str[sqlite3changeset_invert] +**
    sqlite3changeset_start_str[sqlite3changeset_start] +**
    sqlite3session_changeset_str[sqlite3session_changeset] +**
    sqlite3session_patchset_str[sqlite3session_patchset] +**
    +** +** Non-streaming functions that accept changesets (or patchsets) as input +** require that the entire changeset be stored in a single buffer in memory. +** Similarly, those that return a changeset or patchset do so by returning +** a pointer to a single large buffer allocated using sqlite3_malloc(). +** Normally this is convenient. However, if an application running in a +** low-memory environment is required to handle very large changesets, the +** large contiguous memory allocations required can become onerous. +** +** In order to avoid this problem, instead of a single large buffer, input +** is passed to a streaming API functions by way of a callback function that +** the sessions module invokes to incrementally request input data as it is +** required. In all cases, a pair of API function parameters such as +** +**
    +**        int nChangeset,
    +**        void *pChangeset,
    +**  
    +** +** Is replaced by: +** +**
    +**        int (*xInput)(void *pIn, void *pData, int *pnData),
    +**        void *pIn,
    +**  
    +** +** Each time the xInput callback is invoked by the sessions module, the first +** argument passed is a copy of the supplied pIn context pointer. The second +** argument, pData, points to a buffer (*pnData) bytes in size. Assuming no +** error occurs the xInput method should copy up to (*pnData) bytes of data +** into the buffer and set (*pnData) to the actual number of bytes copied +** before returning SQLITE_OK. If the input is completely exhausted, (*pnData) +** should be set to zero to indicate this. Or, if an error occurs, an SQLite +** error code should be returned. In all cases, if an xInput callback returns +** an error, all processing is abandoned and the streaming API function +** returns a copy of the error code to the caller. +** +** In the case of sqlite3changeset_start_strm(), the xInput callback may be +** invoked by the sessions module at any point during the lifetime of the +** iterator. If such an xInput callback returns an error, the iterator enters +** an error state, whereby all subsequent calls to iterator functions +** immediately fail with the same error code as returned by xInput. +** +** Similarly, streaming API functions that return changesets (or patchsets) +** return them in chunks by way of a callback function instead of via a +** pointer to a single large buffer. In this case, a pair of parameters such +** as: +** +**
    +**        int *pnChangeset,
    +**        void **ppChangeset,
    +**  
    +** +** Is replaced by: +** +**
    +**        int (*xOutput)(void *pOut, const void *pData, int nData),
    +**        void *pOut
    +**  
    +** +** The xOutput callback is invoked zero or more times to return data to +** the application. The first parameter passed to each call is a copy of the +** pOut pointer supplied by the application. The second parameter, pData, +** points to a buffer nData bytes in size containing the chunk of output +** data being returned. If the xOutput callback successfully processes the +** supplied data, it should return SQLITE_OK to indicate success. Otherwise, +** it should return some other SQLite error code. In this case processing +** is immediately abandoned and the streaming API function returns a copy +** of the xOutput error code to the application. +** +** The sessions module never invokes an xOutput callback with the third +** parameter set to a value less than or equal to zero. Other than this, +** no guarantees are made as to the size of the chunks of data returned. +*/ +int sqlite3changeset_apply_strm( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int (*xInput)(void *pIn, void *pData, int *pnData), /* Input function */ + void *pIn, /* First arg for xInput */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +); +int sqlite3changeset_concat_strm( + int (*xInputA)(void *pIn, void *pData, int *pnData), + void *pInA, + int (*xInputB)(void *pIn, void *pData, int *pnData), + void *pInB, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); +int sqlite3changeset_invert_strm( + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); +int sqlite3changeset_start_strm( + sqlite3_changeset_iter **pp, + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn +); +int sqlite3session_changeset_strm( + sqlite3_session *pSession, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); +int sqlite3session_patchset_strm( + sqlite3_session *pSession, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); +int sqlite3changegroup_add_strm(sqlite3_changegroup*, + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn +); +int sqlite3changegroup_output_strm(sqlite3_changegroup*, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); + + +/* +** Make sure we can call this stuff from C++. +*/ +#if 0 +} +#endif + +#endif /* !defined(__SQLITESESSION_H_) && defined(SQLITE_ENABLE_SESSION) */ + +/******** End of sqlite3session.h *********/ +/******** Begin file fts5.h *********/ /* ** 2014 May 31 ** @@ -8221,6 +9942,9 @@ struct Fts5PhraseIter { ** an OOM condition or IO error), an appropriate SQLite error code is ** returned. ** +** This function may be quite inefficient if used with an FTS5 table +** created with the "columnsize=0" option. +** ** xColumnText: ** This function attempts to retrieve the text of column iCol of the ** current document. If successful, (*pz) is set to point to a buffer @@ -8241,15 +9965,29 @@ struct Fts5PhraseIter { ** the query within the current row. Return SQLITE_OK if successful, or ** an error code (i.e. SQLITE_NOMEM) if an error occurs. ** +** This API can be quite slow if used with an FTS5 table created with the +** "detail=none" or "detail=column" option. If the FTS5 table is created +** with either "detail=none" or "detail=column" and "content=" option +** (i.e. if it is a contentless table), then this API always returns 0. +** ** xInst: ** Query for the details of phrase match iIdx within the current row. ** Phrase matches are numbered starting from zero, so the iIdx argument ** should be greater than or equal to zero and smaller than the value ** output by xInstCount(). ** +** Usually, output parameter *piPhrase is set to the phrase number, *piCol +** to the column in which it occurs and *piOff the token offset of the +** first token of the phrase. The exception is if the table was created +** with the offsets=0 option specified. In this case *piOff is always +** set to -1. +** ** Returns SQLITE_OK if successful, or an error code (i.e. SQLITE_NOMEM) ** if an error occurs. ** +** This API can be quite slow if used with an FTS5 table created with the +** "detail=none" or "detail=column" option. +** ** xRowid: ** Returns the rowid of the current row. ** @@ -8263,11 +10001,13 @@ struct Fts5PhraseIter { ** ... FROM ftstable WHERE ftstable MATCH $p ORDER BY rowid ** ** with $p set to a phrase equivalent to the phrase iPhrase of the -** current query is executed. For each row visited, the callback function -** passed as the fourth argument is invoked. The context and API objects -** passed to the callback function may be used to access the properties of -** each matched row. Invoking Api.xUserData() returns a copy of the pointer -** passed as the third argument to pUserData. +** current query is executed. Any column filter that applies to +** phrase iPhrase of the current query is included in $p. For each +** row visited, the callback function passed as the fourth argument +** is invoked. The context and API objects passed to the callback +** function may be used to access the properties of each matched row. +** Invoking Api.xUserData() returns a copy of the pointer passed as +** the third argument to pUserData. ** ** If the callback function returns any value other than SQLITE_OK, the ** query is abandoned and the xQueryPhrase function returns immediately. @@ -8333,7 +10073,7 @@ struct Fts5PhraseIter { ** Fts5PhraseIter iter; ** int iCol, iOff; ** for(pApi->xPhraseFirst(pFts, iPhrase, &iter, &iCol, &iOff); -** iOff>=0; +** iCol>=0; ** pApi->xPhraseNext(pFts, &iter, &iCol, &iOff) ** ){ ** // An instance of phrase iPhrase at offset iOff of column iCol @@ -8341,13 +10081,51 @@ struct Fts5PhraseIter { ** ** The Fts5PhraseIter structure is defined above. Applications should not ** modify this structure directly - it should only be used as shown above -** with the xPhraseFirst() and xPhraseNext() API methods. +** with the xPhraseFirst() and xPhraseNext() API methods (and by +** xPhraseFirstColumn() and xPhraseNextColumn() as illustrated below). +** +** This API can be quite slow if used with an FTS5 table created with the +** "detail=none" or "detail=column" option. If the FTS5 table is created +** with either "detail=none" or "detail=column" and "content=" option +** (i.e. if it is a contentless table), then this API always iterates +** through an empty set (all calls to xPhraseFirst() set iCol to -1). ** ** xPhraseNext() ** See xPhraseFirst above. +** +** xPhraseFirstColumn() +** This function and xPhraseNextColumn() are similar to the xPhraseFirst() +** and xPhraseNext() APIs described above. The difference is that instead +** of iterating through all instances of a phrase in the current row, these +** APIs are used to iterate through the set of columns in the current row +** that contain one or more instances of a specified phrase. For example: +** +** Fts5PhraseIter iter; +** int iCol; +** for(pApi->xPhraseFirstColumn(pFts, iPhrase, &iter, &iCol); +** iCol>=0; +** pApi->xPhraseNextColumn(pFts, &iter, &iCol) +** ){ +** // Column iCol contains at least one instance of phrase iPhrase +** } +** +** This API can be quite slow if used with an FTS5 table created with the +** "detail=none" option. If the FTS5 table is created with either +** "detail=none" "content=" option (i.e. if it is a contentless table), +** then this API always iterates through an empty set (all calls to +** xPhraseFirstColumn() set iCol to -1). +** +** The information accessed using this API and its companion +** xPhraseFirstColumn() may also be obtained using xPhraseFirst/xPhraseNext +** (or xInst/xInstCount). The chief advantage of this API is that it is +** significantly more efficient than those alternatives when used with +** "detail=column" tables. +** +** xPhraseNextColumn() +** See xPhraseFirstColumn above. */ struct Fts5ExtensionApi { - int iVersion; /* Currently always set to 1 */ + int iVersion; /* Currently always set to 3 */ void *(*xUserData)(Fts5Context*); @@ -8377,8 +10155,11 @@ struct Fts5ExtensionApi { int (*xSetAuxdata)(Fts5Context*, void *pAux, void(*xDelete)(void*)); void *(*xGetAuxdata)(Fts5Context*, int bClear); - void (*xPhraseFirst)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*, int*); + int (*xPhraseFirst)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*, int*); void (*xPhraseNext)(Fts5Context*, Fts5PhraseIter*, int *piCol, int *piOff); + + int (*xPhraseFirstColumn)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*); + void (*xPhraseNextColumn)(Fts5Context*, Fts5PhraseIter*, int *piCol); }; /* @@ -8656,6 +10437,7 @@ struct fts5_api { #endif /* _FTS5_H */ +/******** End of fts5.h *********/ /************** End of sqlite3.h *********************************************/ /************** Continuing where we left off in sqliteInt.h ******************/ @@ -8773,13 +10555,13 @@ struct fts5_api { ** The suggested maximum number of in-memory pages to use for ** the main database table and for temporary tables. ** -** IMPLEMENTATION-OF: R-31093-59126 The default suggested cache size -** is 2000 pages. +** IMPLEMENTATION-OF: R-30185-15359 The default suggested cache size is -2000, +** which means the cache size is limited to 2048000 bytes of memory. ** IMPLEMENTATION-OF: R-48205-43578 The default suggested cache size can be ** altered using the SQLITE_DEFAULT_CACHE_SIZE compile-time options. */ #ifndef SQLITE_DEFAULT_CACHE_SIZE -# define SQLITE_DEFAULT_CACHE_SIZE 2000 +# define SQLITE_DEFAULT_CACHE_SIZE -2000 #endif /* @@ -8792,8 +10574,9 @@ struct fts5_api { /* ** The maximum number of attached databases. This must be between 0 -** and 62. The upper bound on 62 is because a 64-bit integer bitmap -** is used internally to track attached databases. +** and 125. The upper bound of 125 is because the attached databases are +** counted using a signed 8-bit integer which has a maximum value of 127 +** and we have to allow 2 extra counts for the "main" and "temp" databases. */ #ifndef SQLITE_MAX_ATTACHED # define SQLITE_MAX_ATTACHED 10 @@ -8828,7 +10611,7 @@ struct fts5_api { ** The default size of a database page. */ #ifndef SQLITE_DEFAULT_PAGE_SIZE -# define SQLITE_DEFAULT_PAGE_SIZE 1024 +# define SQLITE_DEFAULT_PAGE_SIZE 4096 #endif #if SQLITE_DEFAULT_PAGE_SIZE>SQLITE_MAX_PAGE_SIZE # undef SQLITE_DEFAULT_PAGE_SIZE @@ -8909,7 +10692,7 @@ struct fts5_api { ** to the next, so we have developed the following set of #if statements ** to generate appropriate macros for a wide range of compilers. ** -** The correct "ANSI" way to do this is to use the intptr_t type. +** The correct "ANSI" way to do this is to use the intptr_t type. ** Unfortunately, that typedef is not available on all compilers, or ** if it is available, it requires an #include of specific headers ** that vary from one machine to the next. @@ -9061,7 +10844,7 @@ struct fts5_api { ** is set. Thus NDEBUG becomes an opt-in rather than an opt-out ** feature. */ -#if !defined(NDEBUG) && !defined(SQLITE_DEBUG) +#if !defined(NDEBUG) && !defined(SQLITE_DEBUG) # define NDEBUG 1 #endif #if defined(NDEBUG) && defined(SQLITE_DEBUG) @@ -9076,7 +10859,7 @@ struct fts5_api { #endif /* -** The testcase() macro is used to aid in coverage testing. When +** The testcase() macro is used to aid in coverage testing. When ** doing coverage testing, the condition inside the argument to ** testcase() must be evaluated both true and false in order to ** get full branch coverage. The testcase() macro is inserted @@ -9122,7 +10905,7 @@ SQLITE_PRIVATE void sqlite3Coverage(int); #endif /* -** The ALWAYS and NEVER macros surround boolean expressions which +** The ALWAYS and NEVER macros surround boolean expressions which ** are intended to always be true or false, respectively. Such ** expressions could be omitted from the code completely. But they ** are included in a few cases in order to enhance the resilience @@ -9136,7 +10919,7 @@ SQLITE_PRIVATE void sqlite3Coverage(int); ** be true and false so that the unreachable code they specify will ** not be counted as untested code. */ -#if defined(SQLITE_COVERAGE_TEST) +#if defined(SQLITE_COVERAGE_TEST) || defined(SQLITE_MUTATION_TEST) # define ALWAYS(X) (1) # define NEVER(X) (0) #elif !defined(NDEBUG) @@ -9147,6 +10930,21 @@ SQLITE_PRIVATE void sqlite3Coverage(int); # define NEVER(X) (X) #endif +/* +** Some malloc failures are only possible if SQLITE_TEST_REALLOC_STRESS is +** defined. We need to defend against those failures when testing with +** SQLITE_TEST_REALLOC_STRESS, but we don't want the unreachable branches +** during a normal build. The following macro can be used to disable tests +** that are always false except when SQLITE_TEST_REALLOC_STRESS is set. +*/ +#if defined(SQLITE_TEST_REALLOC_STRESS) +# define ONLY_IF_REALLOC_STRESS(X) (X) +#elif !defined(NDEBUG) +# define ONLY_IF_REALLOC_STRESS(X) ((X)?(assert(0),1):0) +#else +# define ONLY_IF_REALLOC_STRESS(X) (0) +#endif + /* ** Declarations used for tracing the operating system interfaces. */ @@ -9173,6 +10971,13 @@ SQLITE_PRIVATE void sqlite3Coverage(int); # undef SQLITE_NEED_ERR_NAME #endif +/* +** SQLITE_ENABLE_EXPLAIN_COMMENTS is incompatible with SQLITE_OMIT_EXPLAIN +*/ +#ifdef SQLITE_OMIT_EXPLAIN +# undef SQLITE_ENABLE_EXPLAIN_COMMENTS +#endif + /* ** Return true (non-zero) if the input is an integer that is too large ** to fit in 32-bits. This macro is used inside of various testcase() @@ -9319,76 +11124,76 @@ SQLITE_PRIVATE void sqlite3HashClear(Hash*); #define TK_AS 24 #define TK_WITHOUT 25 #define TK_COMMA 26 -#define TK_ID 27 -#define TK_INDEXED 28 -#define TK_ABORT 29 -#define TK_ACTION 30 -#define TK_AFTER 31 -#define TK_ANALYZE 32 -#define TK_ASC 33 -#define TK_ATTACH 34 -#define TK_BEFORE 35 -#define TK_BY 36 -#define TK_CASCADE 37 -#define TK_CAST 38 -#define TK_COLUMNKW 39 -#define TK_CONFLICT 40 -#define TK_DATABASE 41 -#define TK_DESC 42 -#define TK_DETACH 43 -#define TK_EACH 44 -#define TK_FAIL 45 -#define TK_FOR 46 -#define TK_IGNORE 47 -#define TK_INITIALLY 48 -#define TK_INSTEAD 49 -#define TK_LIKE_KW 50 -#define TK_MATCH 51 -#define TK_NO 52 -#define TK_KEY 53 -#define TK_OF 54 -#define TK_OFFSET 55 -#define TK_PRAGMA 56 -#define TK_RAISE 57 -#define TK_RECURSIVE 58 -#define TK_REPLACE 59 -#define TK_RESTRICT 60 -#define TK_ROW 61 -#define TK_TRIGGER 62 -#define TK_VACUUM 63 -#define TK_VIEW 64 -#define TK_VIRTUAL 65 -#define TK_WITH 66 -#define TK_REINDEX 67 -#define TK_RENAME 68 -#define TK_CTIME_KW 69 -#define TK_ANY 70 -#define TK_OR 71 -#define TK_AND 72 -#define TK_IS 73 -#define TK_BETWEEN 74 -#define TK_IN 75 -#define TK_ISNULL 76 -#define TK_NOTNULL 77 -#define TK_NE 78 -#define TK_EQ 79 -#define TK_GT 80 -#define TK_LE 81 -#define TK_LT 82 -#define TK_GE 83 -#define TK_ESCAPE 84 -#define TK_BITAND 85 -#define TK_BITOR 86 -#define TK_LSHIFT 87 -#define TK_RSHIFT 88 -#define TK_PLUS 89 -#define TK_MINUS 90 -#define TK_STAR 91 -#define TK_SLASH 92 -#define TK_REM 93 -#define TK_CONCAT 94 -#define TK_COLLATE 95 -#define TK_BITNOT 96 +#define TK_OR 27 +#define TK_AND 28 +#define TK_IS 29 +#define TK_MATCH 30 +#define TK_LIKE_KW 31 +#define TK_BETWEEN 32 +#define TK_IN 33 +#define TK_ISNULL 34 +#define TK_NOTNULL 35 +#define TK_NE 36 +#define TK_EQ 37 +#define TK_GT 38 +#define TK_LE 39 +#define TK_LT 40 +#define TK_GE 41 +#define TK_ESCAPE 42 +#define TK_BITAND 43 +#define TK_BITOR 44 +#define TK_LSHIFT 45 +#define TK_RSHIFT 46 +#define TK_PLUS 47 +#define TK_MINUS 48 +#define TK_STAR 49 +#define TK_SLASH 50 +#define TK_REM 51 +#define TK_CONCAT 52 +#define TK_COLLATE 53 +#define TK_BITNOT 54 +#define TK_ID 55 +#define TK_INDEXED 56 +#define TK_ABORT 57 +#define TK_ACTION 58 +#define TK_AFTER 59 +#define TK_ANALYZE 60 +#define TK_ASC 61 +#define TK_ATTACH 62 +#define TK_BEFORE 63 +#define TK_BY 64 +#define TK_CASCADE 65 +#define TK_CAST 66 +#define TK_COLUMNKW 67 +#define TK_CONFLICT 68 +#define TK_DATABASE 69 +#define TK_DESC 70 +#define TK_DETACH 71 +#define TK_EACH 72 +#define TK_FAIL 73 +#define TK_FOR 74 +#define TK_IGNORE 75 +#define TK_INITIALLY 76 +#define TK_INSTEAD 77 +#define TK_NO 78 +#define TK_KEY 79 +#define TK_OF 80 +#define TK_OFFSET 81 +#define TK_PRAGMA 82 +#define TK_RAISE 83 +#define TK_RECURSIVE 84 +#define TK_REPLACE 85 +#define TK_RESTRICT 86 +#define TK_ROW 87 +#define TK_TRIGGER 88 +#define TK_VACUUM 89 +#define TK_VIEW 90 +#define TK_VIRTUAL 91 +#define TK_WITH 92 +#define TK_REINDEX 93 +#define TK_RENAME 94 +#define TK_CTIME_KW 95 +#define TK_ANY 96 #define TK_STRING 97 #define TK_JOIN_KW 98 #define TK_CONSTRAINT 99 @@ -9442,16 +11247,25 @@ SQLITE_PRIVATE void sqlite3HashClear(Hash*); #define TK_TO_REAL 147 #define TK_ISNOT 148 #define TK_END_OF_FILE 149 -#define TK_ILLEGAL 150 -#define TK_SPACE 151 -#define TK_UNCLOSED_STRING 152 -#define TK_FUNCTION 153 -#define TK_COLUMN 154 -#define TK_AGG_FUNCTION 155 -#define TK_AGG_COLUMN 156 -#define TK_UMINUS 157 -#define TK_UPLUS 158 -#define TK_REGISTER 159 +#define TK_UNCLOSED_STRING 150 +#define TK_FUNCTION 151 +#define TK_COLUMN 152 +#define TK_AGG_FUNCTION 153 +#define TK_AGG_COLUMN 154 +#define TK_UMINUS 155 +#define TK_UPLUS 156 +#define TK_REGISTER 157 +#define TK_ASTERISK 158 +#define TK_SPAN 159 +#define TK_SPACE 160 +#define TK_ILLEGAL 161 + +/* The token codes above must all fit in 8 bits */ +#define TKFLG_MASK 0xff + +/* Flags that can be added to a token code when it is not +** being stored in a u8: */ +#define TKFLG_DONTFOLD 0x100 /* Omit constant folding optimizations */ /************** End of parse.h ***********************************************/ /************** Continuing where we left off in sqliteInt.h ******************/ @@ -9483,7 +11297,7 @@ SQLITE_PRIVATE void sqlite3HashClear(Hash*); /* ** OMIT_TEMPDB is set to 1 if SQLITE_OMIT_TEMPDB is defined, or 0 -** afterward. Having this macro allows us to cause the C compiler +** afterward. Having this macro allows us to cause the C compiler ** to omit code used by TEMP tables without messy #ifndef statements. */ #ifdef SQLITE_OMIT_TEMPDB @@ -9522,7 +11336,7 @@ SQLITE_PRIVATE void sqlite3HashClear(Hash*); /* ** If no value has been provided for SQLITE_MAX_WORKER_THREADS, or if -** SQLITE_TEMP_STORE is set to 3 (never use temporary files), set it +** SQLITE_TEMP_STORE is set to 3 (never use temporary files), set it ** to zero. */ #if SQLITE_TEMP_STORE==3 || SQLITE_THREADSAFE==0 @@ -9550,7 +11364,6 @@ SQLITE_PRIVATE void sqlite3HashClear(Hash*); # define SQLITE_DEFAULT_PCACHE_INITSZ 100 #endif - /* ** GCC does not define the offsetof() macro so we'll have to do it ** ourselves. @@ -9562,8 +11375,12 @@ SQLITE_PRIVATE void sqlite3HashClear(Hash*); /* ** Macros to compute minimum and maximum of two numbers. */ -#define MIN(A,B) ((A)<(B)?(A):(B)) -#define MAX(A,B) ((A)>(B)?(A):(B)) +#ifndef MIN +# define MIN(A,B) ((A)<(B)?(A):(B)) +#endif +#ifndef MAX +# define MAX(A,B) ((A)>(B)?(A):(B)) +#endif /* ** Swap two objects of type TYPE. @@ -9671,7 +11488,7 @@ typedef INT8_TYPE i8; /* 1-byte signed integer */ ** 4 -> 20 1000 -> 99 1048576 -> 200 ** 10 -> 33 1024 -> 100 4294967296 -> 320 ** -** The LogEst can be negative to indicate fractional values. +** The LogEst can be negative to indicate fractional values. ** Examples: ** ** 0.5 -> -10 0.1 -> -33 0.0625 -> -40 @@ -9692,6 +11509,27 @@ typedef INT16_TYPE LogEst; # endif #endif +/* The uptr type is an unsigned integer large enough to hold a pointer +*/ +#if defined(HAVE_STDINT_H) + typedef uintptr_t uptr; +#elif SQLITE_PTRSIZE==4 + typedef u32 uptr; +#else + typedef u64 uptr; +#endif + +/* +** The SQLITE_WITHIN(P,S,E) macro checks to see if pointer P points to +** something between S (inclusive) and E (exclusive). +** +** In other words, S is a buffer and E is a pointer to the first byte after +** the end of buffer S. This macro returns true if P points to something +** contained within the buffer S. +*/ +#define SQLITE_WITHIN(P,S,E) (((uptr)(P)>=(uptr)(S))&&((uptr)(P)<(uptr)(E))) + + /* ** Macros to determine whether the machine is big or little endian, ** and whether or not that determination is run-time or compile-time. @@ -9701,11 +11539,6 @@ typedef INT16_TYPE LogEst; ** -DSQLITE_RUNTIME_BYTEORDER=1 is set, then byte-order is determined ** at run-time. */ -#ifdef SQLITE_AMALGAMATION -SQLITE_PRIVATE const int sqlite3one = 1; -#else -SQLITE_PRIVATE const int sqlite3one; -#endif #if (defined(i386) || defined(__i386__) || defined(_M_IX86) || \ defined(__x86_64) || defined(__x86_64__) || defined(_M_X64) || \ defined(_M_AMD64) || defined(_M_ARM) || defined(__x86) || \ @@ -9723,6 +11556,11 @@ SQLITE_PRIVATE const int sqlite3one; # define SQLITE_UTF16NATIVE SQLITE_UTF16BE #endif #if !defined(SQLITE_BYTEORDER) +# ifdef SQLITE_AMALGAMATION + const int sqlite3one = 1; +# else + extern const int sqlite3one; +# endif # define SQLITE_BYTEORDER 0 /* 0 means "unknown at compile-time" */ # define SQLITE_BIGENDIAN (*(char *)(&sqlite3one)==0) # define SQLITE_LITTLEENDIAN (*(char *)(&sqlite3one)==1) @@ -9737,7 +11575,7 @@ SQLITE_PRIVATE const int sqlite3one; #define LARGEST_INT64 (0xffffffff|(((i64)0x7fffffff)<<32)) #define SMALLEST_INT64 (((i64)-1) - LARGEST_INT64) -/* +/* ** Round up a number to the next larger multiple of 8. This is used ** to force 8-byte alignment on 64-bit architectures. */ @@ -9776,10 +11614,6 @@ SQLITE_PRIVATE const int sqlite3one; */ #ifdef __APPLE__ # include -# if TARGET_OS_IPHONE -# undef SQLITE_MAX_MMAP_SIZE -# define SQLITE_MAX_MMAP_SIZE 0 -# endif #endif #ifndef SQLITE_MAX_MMAP_SIZE # if defined(__linux__) \ @@ -9835,7 +11669,7 @@ SQLITE_PRIVATE const int sqlite3one; /* ** An instance of the following structure is used to store the busy-handler -** callback for a given sqlite handle. +** callback for a given sqlite handle. ** ** The sqlite.busyHandler member of the sqlite struct contains the busy ** callback for the database handle. Each pager opened via the sqlite @@ -9880,9 +11714,9 @@ struct BusyHandler { /* ** The following value as a destructor means to use sqlite3DbFree(). -** The sqlite3DbFree() routine requires two parameters instead of the -** one parameter that destructors normally want. So we have to introduce -** this magic value that the code knows to handle differently. Any +** The sqlite3DbFree() routine requires two parameters instead of the +** one parameter that destructors normally want. So we have to introduce +** this magic value that the code knows to handle differently. Any ** pointer will work here as long as it is distinct from SQLITE_STATIC ** and SQLITE_TRANSIENT. */ @@ -9909,16 +11743,16 @@ struct BusyHandler { SQLITE_API int SQLITE_STDCALL sqlite3_wsd_init(int N, int J); SQLITE_API void *SQLITE_STDCALL sqlite3_wsd_find(void *K, int L); #else - #define SQLITE_WSD + #define SQLITE_WSD #define GLOBAL(t,v) v #define sqlite3GlobalConfig sqlite3Config #endif /* ** The following macros are used to suppress compiler warnings and to -** make it clear to human readers when a function parameter is deliberately +** make it clear to human readers when a function parameter is deliberately ** left unused within the body of a function. This usually happens when -** a function is called via a function pointer. For example the +** a function is called via a function pointer. For example the ** implementation of an SQL aggregate step callback may not use the ** parameter indicating the number of arguments passed to the aggregate, ** if it knows that this is enforced elsewhere. @@ -9961,6 +11795,7 @@ typedef struct LookasideSlot LookasideSlot; typedef struct Module Module; typedef struct NameContext NameContext; typedef struct Parse Parse; +typedef struct PreUpdate PreUpdate; typedef struct PrintfArguments PrintfArguments; typedef struct RowSet RowSet; typedef struct Savepoint Savepoint; @@ -9984,7 +11819,7 @@ typedef struct WhereInfo WhereInfo; typedef struct With With; /* -** Defer sourcing vdbe.h and btree.h until after the "u8" and +** Defer sourcing vdbe.h and btree.h until after the "u8" and ** "BusyHandler" typedefs. vdbe.h also requires a few of the opaque ** pointer types (i.e. FuncDef) defined above. */ @@ -10055,11 +11890,11 @@ SQLITE_PRIVATE int sqlite3BtreeOpen( SQLITE_PRIVATE int sqlite3BtreeClose(Btree*); SQLITE_PRIVATE int sqlite3BtreeSetCacheSize(Btree*,int); +SQLITE_PRIVATE int sqlite3BtreeSetSpillSize(Btree*,int); #if SQLITE_MAX_MMAP_SIZE>0 SQLITE_PRIVATE int sqlite3BtreeSetMmapLimit(Btree*,sqlite3_int64); #endif SQLITE_PRIVATE int sqlite3BtreeSetPagerFlags(Btree*,unsigned); -SQLITE_PRIVATE int sqlite3BtreeSyncDisabled(Btree*); SQLITE_PRIVATE int sqlite3BtreeSetPageSize(Btree *p, int nPagesize, int nReserve, int eFix); SQLITE_PRIVATE int sqlite3BtreeGetPageSize(Btree*); SQLITE_PRIVATE int sqlite3BtreeMaxPageCount(Btree*,int); @@ -10142,8 +11977,37 @@ SQLITE_PRIVATE int sqlite3BtreeNewDb(Btree *p); #define BTREE_DATA_VERSION 15 /* A virtual meta-value */ /* -** Values that may be OR'd together to form the second argument of an -** sqlite3BtreeCursorHints() call. +** Kinds of hints that can be passed into the sqlite3BtreeCursorHint() +** interface. +** +** BTREE_HINT_RANGE (arguments: Expr*, Mem*) +** +** The first argument is an Expr* (which is guaranteed to be constant for +** the lifetime of the cursor) that defines constraints on which rows +** might be fetched with this cursor. The Expr* tree may contain +** TK_REGISTER nodes that refer to values stored in the array of registers +** passed as the second parameter. In other words, if Expr.op==TK_REGISTER +** then the value of the node is the value in Mem[pExpr.iTable]. Any +** TK_COLUMN node in the expression tree refers to the Expr.iColumn-th +** column of the b-tree of the cursor. The Expr tree will not contain +** any function calls nor subqueries nor references to b-trees other than +** the cursor being hinted. +** +** The design of the _RANGE hint is aid b-tree implementations that try +** to prefetch content from remote machines - to provide those +** implementations with limits on what needs to be prefetched and thereby +** reduce network bandwidth. +** +** Note that BTREE_HINT_FLAGS with BTREE_BULKLOAD is the only hint used by +** standard SQLite. The other hints are provided for extentions that use +** the SQLite parser and code generator but substitute their own storage +** engine. +*/ +#define BTREE_HINT_RANGE 0 /* Range constraints on queries */ + +/* +** Values that may be OR'd together to form the argument to the +** BTREE_HINT_FLAGS hint for sqlite3BtreeCursorHint(): ** ** The BTREE_BULKLOAD flag is set on index cursors when the index is going ** to be filled with content that is already in sorted order. @@ -10157,6 +12021,32 @@ SQLITE_PRIVATE int sqlite3BtreeNewDb(Btree *p); #define BTREE_BULKLOAD 0x00000001 /* Used to full index in sorted order */ #define BTREE_SEEK_EQ 0x00000002 /* EQ seeks only - no range seeks */ +/* +** Flags passed as the third argument to sqlite3BtreeCursor(). +** +** For read-only cursors the wrFlag argument is always zero. For read-write +** cursors it may be set to either (BTREE_WRCSR|BTREE_FORDELETE) or just +** (BTREE_WRCSR). If the BTREE_FORDELETE bit is set, then the cursor will +** only be used by SQLite for the following: +** +** * to seek to and then delete specific entries, and/or +** +** * to read values that will be used to create keys that other +** BTREE_FORDELETE cursors will seek to and delete. +** +** The BTREE_FORDELETE flag is an optimization hint. It is not used by +** by this, the native b-tree engine of SQLite, but it is available to +** alternative storage engines that might be substituted in place of this +** b-tree system. For alternative storage engines in which a delete of +** the main table row automatically deletes corresponding index rows, +** the FORDELETE flag hint allows those alternative storage engines to +** skip a lot of work. Namely: FORDELETE cursors may treat all SEEK +** and DELETE operations as no-ops, and any READ operation against a +** FORDELETE cursor may return a null row: 0x01 0x00. +*/ +#define BTREE_WRCSR 0x00000004 /* read-write cursor */ +#define BTREE_FORDELETE 0x00000008 /* Cursor is for seek/delete only */ + SQLITE_PRIVATE int sqlite3BtreeCursor( Btree*, /* BTree containing table to open */ int iTable, /* Index of root page */ @@ -10166,6 +12056,10 @@ SQLITE_PRIVATE int sqlite3BtreeCursor( ); SQLITE_PRIVATE int sqlite3BtreeCursorSize(void); SQLITE_PRIVATE void sqlite3BtreeCursorZero(BtCursor*); +SQLITE_PRIVATE void sqlite3BtreeCursorHintFlags(BtCursor*, unsigned); +#ifdef SQLITE_ENABLE_CURSOR_HINTS +SQLITE_PRIVATE void sqlite3BtreeCursorHint(BtCursor*, int, ...); +#endif SQLITE_PRIVATE int sqlite3BtreeCloseCursor(BtCursor*); SQLITE_PRIVATE int sqlite3BtreeMovetoUnpacked( @@ -10177,7 +12071,12 @@ SQLITE_PRIVATE int sqlite3BtreeMovetoUnpacked( ); SQLITE_PRIVATE int sqlite3BtreeCursorHasMoved(BtCursor*); SQLITE_PRIVATE int sqlite3BtreeCursorRestore(BtCursor*, int*); -SQLITE_PRIVATE int sqlite3BtreeDelete(BtCursor*, int); +SQLITE_PRIVATE int sqlite3BtreeDelete(BtCursor*, u8 flags); + +/* Allowed flags for the 2nd argument to sqlite3BtreeDelete() */ +#define BTREE_SAVEPOSITION 0x02 /* Leave cursor pointing at NEXT or PREV */ +#define BTREE_AUXDELETE 0x04 /* not the primary delete operation */ + SQLITE_PRIVATE int sqlite3BtreeInsert(BtCursor*, const void *pKey, i64 nKey, const void *pData, int nData, int nZero, int bias, int seekResult); @@ -10200,10 +12099,7 @@ SQLITE_PRIVATE int sqlite3BtreePutData(BtCursor*, u32 offset, u32 amt, void*); SQLITE_PRIVATE void sqlite3BtreeIncrblobCursor(BtCursor *); SQLITE_PRIVATE void sqlite3BtreeClearCursor(BtCursor *); SQLITE_PRIVATE int sqlite3BtreeSetVersion(Btree *pBt, int iVersion); -SQLITE_PRIVATE void sqlite3BtreeCursorHints(BtCursor *, unsigned int mask); -#ifdef SQLITE_DEBUG SQLITE_PRIVATE int sqlite3BtreeCursorHasHint(BtCursor*, unsigned int mask); -#endif SQLITE_PRIVATE int sqlite3BtreeIsReadonly(Btree *pBt); SQLITE_PRIVATE int sqlite3HeaderSizeBtree(void); @@ -10232,15 +12128,17 @@ SQLITE_PRIVATE int sqlite3BtreeCheckpoint(Btree*, int, int *, int *); #ifndef SQLITE_OMIT_SHARED_CACHE SQLITE_PRIVATE void sqlite3BtreeEnter(Btree*); SQLITE_PRIVATE void sqlite3BtreeEnterAll(sqlite3*); +SQLITE_PRIVATE int sqlite3BtreeSharable(Btree*); +SQLITE_PRIVATE void sqlite3BtreeEnterCursor(BtCursor*); #else # define sqlite3BtreeEnter(X) # define sqlite3BtreeEnterAll(X) +# define sqlite3BtreeSharable(X) 0 +# define sqlite3BtreeEnterCursor(X) #endif #if !defined(SQLITE_OMIT_SHARED_CACHE) && SQLITE_THREADSAFE -SQLITE_PRIVATE int sqlite3BtreeSharable(Btree*); SQLITE_PRIVATE void sqlite3BtreeLeave(Btree*); -SQLITE_PRIVATE void sqlite3BtreeEnterCursor(BtCursor*); SQLITE_PRIVATE void sqlite3BtreeLeaveCursor(BtCursor*); SQLITE_PRIVATE void sqlite3BtreeLeaveAll(sqlite3*); #ifndef NDEBUG @@ -10251,9 +12149,7 @@ SQLITE_PRIVATE int sqlite3SchemaMutexHeld(sqlite3*,int,Schema*); #endif #else -# define sqlite3BtreeSharable(X) 0 # define sqlite3BtreeLeave(X) -# define sqlite3BtreeEnterCursor(X) # define sqlite3BtreeLeaveCursor(X) # define sqlite3BtreeLeaveAll(X) @@ -10312,7 +12208,7 @@ typedef struct SubProgram SubProgram; struct VdbeOp { u8 opcode; /* What operation to perform */ signed char p4type; /* One of the P4_xxx constants for p4 */ - u8 opflags; /* Mask of the OPFLG_* flags in opcodes.h */ + u8 notUsed1; u8 p5; /* Fifth parameter is an unsigned character */ int p1; /* First operand */ int p2; /* Second parameter (often the jump destination) */ @@ -10331,6 +12227,10 @@ struct VdbeOp { KeyInfo *pKeyInfo; /* Used when p4type is P4_KEYINFO */ int *ai; /* Used when p4type is P4_INTARRAY */ SubProgram *pProgram; /* Used when p4type is P4_SUBPROGRAM */ + Table *pTab; /* Used when p4type is P4_TABLE */ +#ifdef SQLITE_ENABLE_CURSOR_HINTS + Expr *pExpr; /* Used when p4type is P4_EXPR */ +#endif int (*xAdvance)(BtCursor *, int *); } p4; #ifdef SQLITE_ENABLE_EXPLAIN_COMMENTS @@ -10381,6 +12281,7 @@ typedef struct VdbeOpList VdbeOpList; #define P4_COLLSEQ (-4) /* P4 is a pointer to a CollSeq structure */ #define P4_FUNCDEF (-5) /* P4 is a pointer to a FuncDef structure */ #define P4_KEYINFO (-6) /* P4 is a pointer to a KeyInfo structure */ +#define P4_EXPR (-7) /* P4 is a pointer to an Expr tree */ #define P4_MEM (-8) /* P4 is a pointer to a Mem* structure */ #define P4_TRANSIENT 0 /* P4 is a pointer to a transient string */ #define P4_VTAB (-10) /* P4 is a pointer to an sqlite3_vtab structure */ @@ -10391,7 +12292,8 @@ typedef struct VdbeOpList VdbeOpList; #define P4_INTARRAY (-15) /* P4 is a vector of 32-bit integers */ #define P4_SUBPROGRAM (-18) /* P4 is a pointer to a SubProgram structure */ #define P4_ADVANCE (-19) /* P4 is a pointer to BtreeNext() or BtreePrev() */ -#define P4_FUNCCTX (-20) /* P4 is a pointer to an sqlite3_context object */ +#define P4_TABLE (-20) /* P4 is a pointer to a Table structure */ +#define P4_FUNCCTX (-21) /* P4 is a pointer to an sqlite3_context object */ /* Error message codes for OP_Halt */ #define P5_ConstraintNotNull 1 @@ -10433,202 +12335,210 @@ typedef struct VdbeOpList VdbeOpList; /************** Include opcodes.h in the middle of vdbe.h ********************/ /************** Begin file opcodes.h *****************************************/ /* Automatically generated. Do not edit */ -/* See the mkopcodeh.awk script for details */ -#define OP_Savepoint 1 -#define OP_AutoCommit 2 -#define OP_Transaction 3 -#define OP_SorterNext 4 -#define OP_PrevIfOpen 5 -#define OP_NextIfOpen 6 -#define OP_Prev 7 -#define OP_Next 8 -#define OP_Checkpoint 9 -#define OP_JournalMode 10 -#define OP_Vacuum 11 -#define OP_VFilter 12 /* synopsis: iplan=r[P3] zplan='P4' */ -#define OP_VUpdate 13 /* synopsis: data=r[P3@P2] */ -#define OP_Goto 14 -#define OP_Gosub 15 -#define OP_Return 16 -#define OP_InitCoroutine 17 -#define OP_EndCoroutine 18 +/* See the tool/mkopcodeh.tcl script for details */ +#define OP_Savepoint 0 +#define OP_AutoCommit 1 +#define OP_Transaction 2 +#define OP_SorterNext 3 +#define OP_PrevIfOpen 4 +#define OP_NextIfOpen 5 +#define OP_Prev 6 +#define OP_Next 7 +#define OP_Checkpoint 8 +#define OP_JournalMode 9 +#define OP_Vacuum 10 +#define OP_VFilter 11 /* synopsis: iplan=r[P3] zplan='P4' */ +#define OP_VUpdate 12 /* synopsis: data=r[P3@P2] */ +#define OP_Goto 13 +#define OP_Gosub 14 +#define OP_InitCoroutine 15 +#define OP_Yield 16 +#define OP_MustBeInt 17 +#define OP_Jump 18 #define OP_Not 19 /* same as TK_NOT, synopsis: r[P2]= !r[P1] */ -#define OP_Yield 20 -#define OP_HaltIfNull 21 /* synopsis: if r[P3]=null halt */ -#define OP_Halt 22 -#define OP_Integer 23 /* synopsis: r[P2]=P1 */ -#define OP_Int64 24 /* synopsis: r[P2]=P4 */ -#define OP_String 25 /* synopsis: r[P2]='P4' (len=P1) */ -#define OP_Null 26 /* synopsis: r[P2..P3]=NULL */ -#define OP_SoftNull 27 /* synopsis: r[P1]=NULL */ -#define OP_Blob 28 /* synopsis: r[P2]=P4 (len=P1) */ -#define OP_Variable 29 /* synopsis: r[P2]=parameter(P1,P4) */ -#define OP_Move 30 /* synopsis: r[P2@P3]=r[P1@P3] */ -#define OP_Copy 31 /* synopsis: r[P2@P3+1]=r[P1@P3+1] */ -#define OP_SCopy 32 /* synopsis: r[P2]=r[P1] */ -#define OP_ResultRow 33 /* synopsis: output=r[P1@P2] */ -#define OP_CollSeq 34 -#define OP_Function0 35 /* synopsis: r[P3]=func(r[P2@P5]) */ -#define OP_Function 36 /* synopsis: r[P3]=func(r[P2@P5]) */ -#define OP_AddImm 37 /* synopsis: r[P1]=r[P1]+P2 */ -#define OP_MustBeInt 38 -#define OP_RealAffinity 39 -#define OP_Cast 40 /* synopsis: affinity(r[P1]) */ -#define OP_Permutation 41 -#define OP_Compare 42 /* synopsis: r[P1@P3] <-> r[P2@P3] */ -#define OP_Jump 43 -#define OP_Once 44 -#define OP_If 45 -#define OP_IfNot 46 -#define OP_Column 47 /* synopsis: r[P3]=PX */ -#define OP_Affinity 48 /* synopsis: affinity(r[P1@P2]) */ -#define OP_MakeRecord 49 /* synopsis: r[P3]=mkrec(r[P1@P2]) */ -#define OP_Count 50 /* synopsis: r[P2]=count() */ -#define OP_ReadCookie 51 -#define OP_SetCookie 52 -#define OP_ReopenIdx 53 /* synopsis: root=P2 iDb=P3 */ -#define OP_OpenRead 54 /* synopsis: root=P2 iDb=P3 */ -#define OP_OpenWrite 55 /* synopsis: root=P2 iDb=P3 */ -#define OP_OpenAutoindex 56 /* synopsis: nColumn=P2 */ -#define OP_OpenEphemeral 57 /* synopsis: nColumn=P2 */ -#define OP_SorterOpen 58 -#define OP_SequenceTest 59 /* synopsis: if( cursor[P1].ctr++ ) pc = P2 */ -#define OP_OpenPseudo 60 /* synopsis: P3 columns in r[P2] */ -#define OP_Close 61 -#define OP_ColumnsUsed 62 -#define OP_SeekLT 63 /* synopsis: key=r[P3@P4] */ -#define OP_SeekLE 64 /* synopsis: key=r[P3@P4] */ -#define OP_SeekGE 65 /* synopsis: key=r[P3@P4] */ -#define OP_SeekGT 66 /* synopsis: key=r[P3@P4] */ -#define OP_Seek 67 /* synopsis: intkey=r[P2] */ -#define OP_NoConflict 68 /* synopsis: key=r[P3@P4] */ -#define OP_NotFound 69 /* synopsis: key=r[P3@P4] */ -#define OP_Found 70 /* synopsis: key=r[P3@P4] */ -#define OP_Or 71 /* same as TK_OR, synopsis: r[P3]=(r[P1] || r[P2]) */ -#define OP_And 72 /* same as TK_AND, synopsis: r[P3]=(r[P1] && r[P2]) */ -#define OP_NotExists 73 /* synopsis: intkey=r[P3] */ -#define OP_Sequence 74 /* synopsis: r[P2]=cursor[P1].ctr++ */ -#define OP_NewRowid 75 /* synopsis: r[P2]=rowid */ -#define OP_IsNull 76 /* same as TK_ISNULL, synopsis: if r[P1]==NULL goto P2 */ -#define OP_NotNull 77 /* same as TK_NOTNULL, synopsis: if r[P1]!=NULL goto P2 */ -#define OP_Ne 78 /* same as TK_NE, synopsis: if r[P1]!=r[P3] goto P2 */ -#define OP_Eq 79 /* same as TK_EQ, synopsis: if r[P1]==r[P3] goto P2 */ -#define OP_Gt 80 /* same as TK_GT, synopsis: if r[P1]>r[P3] goto P2 */ -#define OP_Le 81 /* same as TK_LE, synopsis: if r[P1]<=r[P3] goto P2 */ -#define OP_Lt 82 /* same as TK_LT, synopsis: if r[P1]=r[P3] goto P2 */ -#define OP_Insert 84 /* synopsis: intkey=r[P3] data=r[P2] */ -#define OP_BitAnd 85 /* same as TK_BITAND, synopsis: r[P3]=r[P1]&r[P2] */ -#define OP_BitOr 86 /* same as TK_BITOR, synopsis: r[P3]=r[P1]|r[P2] */ -#define OP_ShiftLeft 87 /* same as TK_LSHIFT, synopsis: r[P3]=r[P2]<>r[P1] */ -#define OP_Add 89 /* same as TK_PLUS, synopsis: r[P3]=r[P1]+r[P2] */ -#define OP_Subtract 90 /* same as TK_MINUS, synopsis: r[P3]=r[P2]-r[P1] */ -#define OP_Multiply 91 /* same as TK_STAR, synopsis: r[P3]=r[P1]*r[P2] */ -#define OP_Divide 92 /* same as TK_SLASH, synopsis: r[P3]=r[P2]/r[P1] */ -#define OP_Remainder 93 /* same as TK_REM, synopsis: r[P3]=r[P2]%r[P1] */ -#define OP_Concat 94 /* same as TK_CONCAT, synopsis: r[P3]=r[P2]+r[P1] */ -#define OP_InsertInt 95 /* synopsis: intkey=P3 data=r[P2] */ -#define OP_BitNot 96 /* same as TK_BITNOT, synopsis: r[P1]= ~r[P1] */ +#define OP_Once 20 +#define OP_If 21 +#define OP_IfNot 22 +#define OP_SeekLT 23 /* synopsis: key=r[P3@P4] */ +#define OP_SeekLE 24 /* synopsis: key=r[P3@P4] */ +#define OP_SeekGE 25 /* synopsis: key=r[P3@P4] */ +#define OP_SeekGT 26 /* synopsis: key=r[P3@P4] */ +#define OP_Or 27 /* same as TK_OR, synopsis: r[P3]=(r[P1] || r[P2]) */ +#define OP_And 28 /* same as TK_AND, synopsis: r[P3]=(r[P1] && r[P2]) */ +#define OP_NoConflict 29 /* synopsis: key=r[P3@P4] */ +#define OP_NotFound 30 /* synopsis: key=r[P3@P4] */ +#define OP_Found 31 /* synopsis: key=r[P3@P4] */ +#define OP_NotExists 32 /* synopsis: intkey=r[P3] */ +#define OP_Last 33 +#define OP_IsNull 34 /* same as TK_ISNULL, synopsis: if r[P1]==NULL goto P2 */ +#define OP_NotNull 35 /* same as TK_NOTNULL, synopsis: if r[P1]!=NULL goto P2 */ +#define OP_Ne 36 /* same as TK_NE, synopsis: if r[P1]!=r[P3] goto P2 */ +#define OP_Eq 37 /* same as TK_EQ, synopsis: if r[P1]==r[P3] goto P2 */ +#define OP_Gt 38 /* same as TK_GT, synopsis: if r[P1]>r[P3] goto P2 */ +#define OP_Le 39 /* same as TK_LE, synopsis: if r[P1]<=r[P3] goto P2 */ +#define OP_Lt 40 /* same as TK_LT, synopsis: if r[P1]=r[P3] goto P2 */ +#define OP_SorterSort 42 +#define OP_BitAnd 43 /* same as TK_BITAND, synopsis: r[P3]=r[P1]&r[P2] */ +#define OP_BitOr 44 /* same as TK_BITOR, synopsis: r[P3]=r[P1]|r[P2] */ +#define OP_ShiftLeft 45 /* same as TK_LSHIFT, synopsis: r[P3]=r[P2]<>r[P1] */ +#define OP_Add 47 /* same as TK_PLUS, synopsis: r[P3]=r[P1]+r[P2] */ +#define OP_Subtract 48 /* same as TK_MINUS, synopsis: r[P3]=r[P2]-r[P1] */ +#define OP_Multiply 49 /* same as TK_STAR, synopsis: r[P3]=r[P1]*r[P2] */ +#define OP_Divide 50 /* same as TK_SLASH, synopsis: r[P3]=r[P2]/r[P1] */ +#define OP_Remainder 51 /* same as TK_REM, synopsis: r[P3]=r[P2]%r[P1] */ +#define OP_Concat 52 /* same as TK_CONCAT, synopsis: r[P3]=r[P2]+r[P1] */ +#define OP_Sort 53 +#define OP_BitNot 54 /* same as TK_BITNOT, synopsis: r[P1]= ~r[P1] */ +#define OP_Rewind 55 +#define OP_IdxLE 56 /* synopsis: key=r[P3@P4] */ +#define OP_IdxGT 57 /* synopsis: key=r[P3@P4] */ +#define OP_IdxLT 58 /* synopsis: key=r[P3@P4] */ +#define OP_IdxGE 59 /* synopsis: key=r[P3@P4] */ +#define OP_RowSetRead 60 /* synopsis: r[P3]=rowset(P1) */ +#define OP_RowSetTest 61 /* synopsis: if r[P3] in rowset(P1) goto P2 */ +#define OP_Program 62 +#define OP_FkIfZero 63 /* synopsis: if fkctr[P1]==0 goto P2 */ +#define OP_IfPos 64 /* synopsis: if r[P1]>0 then r[P1]-=P3, goto P2 */ +#define OP_IfNotZero 65 /* synopsis: if r[P1]!=0 then r[P1]-=P3, goto P2 */ +#define OP_DecrJumpZero 66 /* synopsis: if (--r[P1])==0 goto P2 */ +#define OP_IncrVacuum 67 +#define OP_VNext 68 +#define OP_Init 69 /* synopsis: Start at P2 */ +#define OP_Return 70 +#define OP_EndCoroutine 71 +#define OP_HaltIfNull 72 /* synopsis: if r[P3]=null halt */ +#define OP_Halt 73 +#define OP_Integer 74 /* synopsis: r[P2]=P1 */ +#define OP_Int64 75 /* synopsis: r[P2]=P4 */ +#define OP_String 76 /* synopsis: r[P2]='P4' (len=P1) */ +#define OP_Null 77 /* synopsis: r[P2..P3]=NULL */ +#define OP_SoftNull 78 /* synopsis: r[P1]=NULL */ +#define OP_Blob 79 /* synopsis: r[P2]=P4 (len=P1) */ +#define OP_Variable 80 /* synopsis: r[P2]=parameter(P1,P4) */ +#define OP_Move 81 /* synopsis: r[P2@P3]=r[P1@P3] */ +#define OP_Copy 82 /* synopsis: r[P2@P3+1]=r[P1@P3+1] */ +#define OP_SCopy 83 /* synopsis: r[P2]=r[P1] */ +#define OP_IntCopy 84 /* synopsis: r[P2]=r[P1] */ +#define OP_ResultRow 85 /* synopsis: output=r[P1@P2] */ +#define OP_CollSeq 86 +#define OP_Function0 87 /* synopsis: r[P3]=func(r[P2@P5]) */ +#define OP_Function 88 /* synopsis: r[P3]=func(r[P2@P5]) */ +#define OP_AddImm 89 /* synopsis: r[P1]=r[P1]+P2 */ +#define OP_RealAffinity 90 +#define OP_Cast 91 /* synopsis: affinity(r[P1]) */ +#define OP_Permutation 92 +#define OP_Compare 93 /* synopsis: r[P1@P3] <-> r[P2@P3] */ +#define OP_Column 94 /* synopsis: r[P3]=PX */ +#define OP_Affinity 95 /* synopsis: affinity(r[P1@P2]) */ +#define OP_MakeRecord 96 /* synopsis: r[P3]=mkrec(r[P1@P2]) */ #define OP_String8 97 /* same as TK_STRING, synopsis: r[P2]='P4' */ -#define OP_Delete 98 -#define OP_ResetCount 99 -#define OP_SorterCompare 100 /* synopsis: if key(P1)!=trim(r[P3],P4) goto P2 */ -#define OP_SorterData 101 /* synopsis: r[P2]=data */ -#define OP_RowKey 102 /* synopsis: r[P2]=key */ -#define OP_RowData 103 /* synopsis: r[P2]=data */ -#define OP_Rowid 104 /* synopsis: r[P2]=rowid */ -#define OP_NullRow 105 -#define OP_Last 106 -#define OP_SorterSort 107 -#define OP_Sort 108 -#define OP_Rewind 109 -#define OP_SorterInsert 110 -#define OP_IdxInsert 111 /* synopsis: key=r[P2] */ -#define OP_IdxDelete 112 /* synopsis: key=r[P2@P3] */ -#define OP_IdxRowid 113 /* synopsis: r[P2]=rowid */ -#define OP_IdxLE 114 /* synopsis: key=r[P3@P4] */ -#define OP_IdxGT 115 /* synopsis: key=r[P3@P4] */ -#define OP_IdxLT 116 /* synopsis: key=r[P3@P4] */ -#define OP_IdxGE 117 /* synopsis: key=r[P3@P4] */ -#define OP_Destroy 118 -#define OP_Clear 119 -#define OP_ResetSorter 120 -#define OP_CreateIndex 121 /* synopsis: r[P2]=root iDb=P1 */ -#define OP_CreateTable 122 /* synopsis: r[P2]=root iDb=P1 */ -#define OP_ParseSchema 123 -#define OP_LoadAnalysis 124 -#define OP_DropTable 125 -#define OP_DropIndex 126 -#define OP_DropTrigger 127 -#define OP_IntegrityCk 128 -#define OP_RowSetAdd 129 /* synopsis: rowset(P1)=r[P2] */ -#define OP_RowSetRead 130 /* synopsis: r[P3]=rowset(P1) */ -#define OP_RowSetTest 131 /* synopsis: if r[P3] in rowset(P1) goto P2 */ -#define OP_Program 132 +#define OP_Count 98 /* synopsis: r[P2]=count() */ +#define OP_ReadCookie 99 +#define OP_SetCookie 100 +#define OP_ReopenIdx 101 /* synopsis: root=P2 iDb=P3 */ +#define OP_OpenRead 102 /* synopsis: root=P2 iDb=P3 */ +#define OP_OpenWrite 103 /* synopsis: root=P2 iDb=P3 */ +#define OP_OpenAutoindex 104 /* synopsis: nColumn=P2 */ +#define OP_OpenEphemeral 105 /* synopsis: nColumn=P2 */ +#define OP_SorterOpen 106 +#define OP_SequenceTest 107 /* synopsis: if( cursor[P1].ctr++ ) pc = P2 */ +#define OP_OpenPseudo 108 /* synopsis: P3 columns in r[P2] */ +#define OP_Close 109 +#define OP_ColumnsUsed 110 +#define OP_Sequence 111 /* synopsis: r[P2]=cursor[P1].ctr++ */ +#define OP_NewRowid 112 /* synopsis: r[P2]=rowid */ +#define OP_Insert 113 /* synopsis: intkey=r[P3] data=r[P2] */ +#define OP_InsertInt 114 /* synopsis: intkey=P3 data=r[P2] */ +#define OP_Delete 115 +#define OP_ResetCount 116 +#define OP_SorterCompare 117 /* synopsis: if key(P1)!=trim(r[P3],P4) goto P2 */ +#define OP_SorterData 118 /* synopsis: r[P2]=data */ +#define OP_RowKey 119 /* synopsis: r[P2]=key */ +#define OP_RowData 120 /* synopsis: r[P2]=data */ +#define OP_Rowid 121 /* synopsis: r[P2]=rowid */ +#define OP_NullRow 122 +#define OP_SorterInsert 123 +#define OP_IdxInsert 124 /* synopsis: key=r[P2] */ +#define OP_IdxDelete 125 /* synopsis: key=r[P2@P3] */ +#define OP_Seek 126 /* synopsis: Move P3 to P1.rowid */ +#define OP_IdxRowid 127 /* synopsis: r[P2]=rowid */ +#define OP_Destroy 128 +#define OP_Clear 129 +#define OP_ResetSorter 130 +#define OP_CreateIndex 131 /* synopsis: r[P2]=root iDb=P1 */ +#define OP_CreateTable 132 /* synopsis: r[P2]=root iDb=P1 */ #define OP_Real 133 /* same as TK_FLOAT, synopsis: r[P2]=P4 */ -#define OP_Param 134 -#define OP_FkCounter 135 /* synopsis: fkctr[P1]+=P2 */ -#define OP_FkIfZero 136 /* synopsis: if fkctr[P1]==0 goto P2 */ -#define OP_MemMax 137 /* synopsis: r[P1]=max(r[P1],r[P2]) */ -#define OP_IfPos 138 /* synopsis: if r[P1]>0 then r[P1]-=P3, goto P2 */ -#define OP_SetIfNotPos 139 /* synopsis: if r[P1]<=0 then r[P2]=P3 */ -#define OP_IfNotZero 140 /* synopsis: if r[P1]!=0 then r[P1]-=P3, goto P2 */ -#define OP_DecrJumpZero 141 /* synopsis: if (--r[P1])==0 goto P2 */ -#define OP_JumpZeroIncr 142 /* synopsis: if (r[P1]++)==0 ) goto P2 */ -#define OP_AggStep0 143 /* synopsis: accum=r[P3] step(r[P2@P5]) */ -#define OP_AggStep 144 /* synopsis: accum=r[P3] step(r[P2@P5]) */ -#define OP_AggFinal 145 /* synopsis: accum=r[P1] N=P2 */ -#define OP_IncrVacuum 146 -#define OP_Expire 147 -#define OP_TableLock 148 /* synopsis: iDb=P1 root=P2 write=P3 */ -#define OP_VBegin 149 -#define OP_VCreate 150 -#define OP_VDestroy 151 -#define OP_VOpen 152 -#define OP_VColumn 153 /* synopsis: r[P3]=vcolumn(P2) */ -#define OP_VNext 154 +#define OP_ParseSchema 134 +#define OP_LoadAnalysis 135 +#define OP_DropTable 136 +#define OP_DropIndex 137 +#define OP_DropTrigger 138 +#define OP_IntegrityCk 139 +#define OP_RowSetAdd 140 /* synopsis: rowset(P1)=r[P2] */ +#define OP_Param 141 +#define OP_FkCounter 142 /* synopsis: fkctr[P1]+=P2 */ +#define OP_MemMax 143 /* synopsis: r[P1]=max(r[P1],r[P2]) */ +#define OP_OffsetLimit 144 /* synopsis: if r[P1]>0 then r[P2]=r[P1]+max(0,r[P3]) else r[P2]=(-1) */ +#define OP_AggStep0 145 /* synopsis: accum=r[P3] step(r[P2@P5]) */ +#define OP_AggStep 146 /* synopsis: accum=r[P3] step(r[P2@P5]) */ +#define OP_AggFinal 147 /* synopsis: accum=r[P1] N=P2 */ +#define OP_Expire 148 +#define OP_TableLock 149 /* synopsis: iDb=P1 root=P2 write=P3 */ +#define OP_VBegin 150 +#define OP_VCreate 151 +#define OP_VDestroy 152 +#define OP_VOpen 153 +#define OP_VColumn 154 /* synopsis: r[P3]=vcolumn(P2) */ #define OP_VRename 155 #define OP_Pagecount 156 #define OP_MaxPgcnt 157 -#define OP_Init 158 /* synopsis: Start at P2 */ +#define OP_CursorHint 158 #define OP_Noop 159 #define OP_Explain 160 - /* Properties such as "out2" or "jump" that are specified in ** comments following the "case" for each opcode in the vdbe.c ** are encoded into bitvectors as follows: */ -#define OPFLG_JUMP 0x0001 /* jump: P2 holds jmp target */ -#define OPFLG_IN1 0x0002 /* in1: P1 is an input */ -#define OPFLG_IN2 0x0004 /* in2: P2 is an input */ -#define OPFLG_IN3 0x0008 /* in3: P3 is an input */ -#define OPFLG_OUT2 0x0010 /* out2: P2 is an output */ -#define OPFLG_OUT3 0x0020 /* out3: P3 is an output */ +#define OPFLG_JUMP 0x01 /* jump: P2 holds jmp target */ +#define OPFLG_IN1 0x02 /* in1: P1 is an input */ +#define OPFLG_IN2 0x04 /* in2: P2 is an input */ +#define OPFLG_IN3 0x08 /* in3: P3 is an input */ +#define OPFLG_OUT2 0x10 /* out2: P2 is an output */ +#define OPFLG_OUT3 0x20 /* out3: P3 is an output */ #define OPFLG_INITIALIZER {\ -/* 0 */ 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01,\ -/* 8 */ 0x01, 0x00, 0x10, 0x00, 0x01, 0x00, 0x01, 0x01,\ -/* 16 */ 0x02, 0x01, 0x02, 0x12, 0x03, 0x08, 0x00, 0x10,\ -/* 24 */ 0x10, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, 0x00,\ -/* 32 */ 0x10, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x02,\ -/* 40 */ 0x02, 0x00, 0x00, 0x01, 0x01, 0x03, 0x03, 0x00,\ -/* 48 */ 0x00, 0x00, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00,\ -/* 56 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,\ -/* 64 */ 0x09, 0x09, 0x09, 0x04, 0x09, 0x09, 0x09, 0x26,\ -/* 72 */ 0x26, 0x09, 0x10, 0x10, 0x03, 0x03, 0x0b, 0x0b,\ -/* 80 */ 0x0b, 0x0b, 0x0b, 0x0b, 0x00, 0x26, 0x26, 0x26,\ -/* 88 */ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x00,\ -/* 96 */ 0x12, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\ -/* 104 */ 0x10, 0x00, 0x01, 0x01, 0x01, 0x01, 0x04, 0x04,\ -/* 112 */ 0x00, 0x10, 0x01, 0x01, 0x01, 0x01, 0x10, 0x00,\ -/* 120 */ 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,\ -/* 128 */ 0x00, 0x06, 0x23, 0x0b, 0x01, 0x10, 0x10, 0x00,\ -/* 136 */ 0x01, 0x04, 0x03, 0x06, 0x03, 0x03, 0x03, 0x00,\ -/* 144 */ 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,\ -/* 152 */ 0x00, 0x00, 0x01, 0x00, 0x10, 0x10, 0x01, 0x00,\ +/* 0 */ 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01,\ +/* 8 */ 0x00, 0x10, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01,\ +/* 16 */ 0x03, 0x03, 0x01, 0x12, 0x01, 0x03, 0x03, 0x09,\ +/* 24 */ 0x09, 0x09, 0x09, 0x26, 0x26, 0x09, 0x09, 0x09,\ +/* 32 */ 0x09, 0x01, 0x03, 0x03, 0x0b, 0x0b, 0x0b, 0x0b,\ +/* 40 */ 0x0b, 0x0b, 0x01, 0x26, 0x26, 0x26, 0x26, 0x26,\ +/* 48 */ 0x26, 0x26, 0x26, 0x26, 0x26, 0x01, 0x12, 0x01,\ +/* 56 */ 0x01, 0x01, 0x01, 0x01, 0x23, 0x0b, 0x01, 0x01,\ +/* 64 */ 0x03, 0x03, 0x03, 0x01, 0x01, 0x01, 0x02, 0x02,\ +/* 72 */ 0x08, 0x00, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10,\ +/* 80 */ 0x10, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00,\ +/* 88 */ 0x00, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00,\ +/* 96 */ 0x00, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,\ +/* 104 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,\ +/* 112 */ 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\ +/* 120 */ 0x00, 0x10, 0x00, 0x04, 0x04, 0x00, 0x00, 0x10,\ +/* 128 */ 0x10, 0x00, 0x00, 0x10, 0x10, 0x10, 0x00, 0x00,\ +/* 136 */ 0x00, 0x00, 0x00, 0x00, 0x06, 0x10, 0x00, 0x04,\ +/* 144 */ 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\ +/* 152 */ 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00,\ /* 160 */ 0x00,} +/* The sqlite3P2Values() routine is able to run faster if it knows +** the value of the largest JUMP opcode. The smaller the maximum +** JUMP opcode the better, so the mkopcodeh.tcl script that +** generated this include file strives to group all JUMP opcodes +** together near the beginning of the list. +*/ +#define SQLITE_MX_JUMP_OPCODE 69 /* Maximum JUMP opcode */ + /************** End of opcodes.h *********************************************/ /************** Continuing where we left off in vdbe.h ***********************/ @@ -10647,7 +12557,13 @@ SQLITE_PRIVATE int sqlite3VdbeAddOp3(Vdbe*,int,int,int,int); SQLITE_PRIVATE int sqlite3VdbeAddOp4(Vdbe*,int,int,int,int,const char *zP4,int); SQLITE_PRIVATE int sqlite3VdbeAddOp4Dup8(Vdbe*,int,int,int,int,const u8*,int); SQLITE_PRIVATE int sqlite3VdbeAddOp4Int(Vdbe*,int,int,int,int,int); -SQLITE_PRIVATE int sqlite3VdbeAddOpList(Vdbe*, int nOp, VdbeOpList const *aOp, int iLineno); +SQLITE_PRIVATE void sqlite3VdbeEndCoroutine(Vdbe*,int); +#if defined(SQLITE_DEBUG) && !defined(SQLITE_TEST_REALLOC_STRESS) +SQLITE_PRIVATE void sqlite3VdbeVerifyNoMallocRequired(Vdbe *p, int N); +#else +# define sqlite3VdbeVerifyNoMallocRequired(A,B) +#endif +SQLITE_PRIVATE VdbeOp *sqlite3VdbeAddOpList(Vdbe*, int nOp, VdbeOpList const *aOp, int iLineno); SQLITE_PRIVATE void sqlite3VdbeAddParseSchemaOp(Vdbe*,int,char*); SQLITE_PRIVATE void sqlite3VdbeChangeOpcode(Vdbe*, u32 addr, u8); SQLITE_PRIVATE void sqlite3VdbeChangeP1(Vdbe*, u32 addr, int P1); @@ -10655,7 +12571,7 @@ SQLITE_PRIVATE void sqlite3VdbeChangeP2(Vdbe*, u32 addr, int P2); SQLITE_PRIVATE void sqlite3VdbeChangeP3(Vdbe*, u32 addr, int P3); SQLITE_PRIVATE void sqlite3VdbeChangeP5(Vdbe*, u8 P5); SQLITE_PRIVATE void sqlite3VdbeJumpHere(Vdbe*, int addr); -SQLITE_PRIVATE void sqlite3VdbeChangeToNoop(Vdbe*, int addr); +SQLITE_PRIVATE int sqlite3VdbeChangeToNoop(Vdbe*, int addr); SQLITE_PRIVATE int sqlite3VdbeDeletePriorOpcode(Vdbe*, u8 op); SQLITE_PRIVATE void sqlite3VdbeChangeP4(Vdbe*, int addr, const char *zP4, int N); SQLITE_PRIVATE void sqlite3VdbeSetP4KeyInfo(Parse*, Index*); @@ -10663,6 +12579,7 @@ SQLITE_PRIVATE void sqlite3VdbeUsesBtree(Vdbe*, int); SQLITE_PRIVATE VdbeOp *sqlite3VdbeGetOp(Vdbe*, int); SQLITE_PRIVATE int sqlite3VdbeMakeLabel(Vdbe*); SQLITE_PRIVATE void sqlite3VdbeRunOnlyOnce(Vdbe*); +SQLITE_PRIVATE void sqlite3VdbeReusable(Vdbe*); SQLITE_PRIVATE void sqlite3VdbeDelete(Vdbe*); SQLITE_PRIVATE void sqlite3VdbeClearObject(sqlite3*,Vdbe*); SQLITE_PRIVATE void sqlite3VdbeMakeReady(Vdbe*,Parse*); @@ -10843,7 +12760,11 @@ typedef struct PgHdr DbPage; #define PAGER_LOCKINGMODE_EXCLUSIVE 1 /* -** Numeric constants that encode the journalmode. +** Numeric constants that encode the journalmode. +** +** The numeric values encoded here (other than PAGER_JOURNALMODE_QUERY) +** are exposed in the API via the "PRAGMA journal_mode" command and +** therefore cannot be changed without a compatibility break. */ #define PAGER_JOURNALMODE_QUERY (-1) /* Query the value of journalmode */ #define PAGER_JOURNALMODE_DELETE 0 /* Commit by deleting journal file */ @@ -10854,22 +12775,28 @@ typedef struct PgHdr DbPage; #define PAGER_JOURNALMODE_WAL 5 /* Use write-ahead logging */ /* -** Flags that make up the mask passed to sqlite3PagerAcquire(). +** Flags that make up the mask passed to sqlite3PagerGet(). */ #define PAGER_GET_NOCONTENT 0x01 /* Do not load data from disk */ #define PAGER_GET_READONLY 0x02 /* Read-only page is acceptable */ /* ** Flags for sqlite3PagerSetFlags() +** +** Value constraints (enforced via assert()): +** PAGER_FULLFSYNC == SQLITE_FullFSync +** PAGER_CKPT_FULLFSYNC == SQLITE_CkptFullFSync +** PAGER_CACHE_SPILL == SQLITE_CacheSpill */ #define PAGER_SYNCHRONOUS_OFF 0x01 /* PRAGMA synchronous=OFF */ #define PAGER_SYNCHRONOUS_NORMAL 0x02 /* PRAGMA synchronous=NORMAL */ #define PAGER_SYNCHRONOUS_FULL 0x03 /* PRAGMA synchronous=FULL */ -#define PAGER_SYNCHRONOUS_MASK 0x03 /* Mask for three values above */ -#define PAGER_FULLFSYNC 0x04 /* PRAGMA fullfsync=ON */ -#define PAGER_CKPT_FULLFSYNC 0x08 /* PRAGMA checkpoint_fullfsync=ON */ -#define PAGER_CACHESPILL 0x10 /* PRAGMA cache_spill=ON */ -#define PAGER_FLAGS_MASK 0x1c /* All above except SYNCHRONOUS */ +#define PAGER_SYNCHRONOUS_EXTRA 0x04 /* PRAGMA synchronous=EXTRA */ +#define PAGER_SYNCHRONOUS_MASK 0x07 /* Mask for four values above */ +#define PAGER_FULLFSYNC 0x08 /* PRAGMA fullfsync=ON */ +#define PAGER_CKPT_FULLFSYNC 0x10 /* PRAGMA checkpoint_fullfsync=ON */ +#define PAGER_CACHESPILL 0x20 /* PRAGMA cache_spill=ON */ +#define PAGER_FLAGS_MASK 0x38 /* All above except SYNCHRONOUS */ /* ** The remainder of this file contains the declarations of the functions @@ -10898,6 +12825,7 @@ SQLITE_PRIVATE void sqlite3PagerAlignReserve(Pager*,Pager*); #endif SQLITE_PRIVATE int sqlite3PagerMaxPageCount(Pager*, int); SQLITE_PRIVATE void sqlite3PagerSetCachesize(Pager*, int); +SQLITE_PRIVATE int sqlite3PagerSetSpillsize(Pager*, int); SQLITE_PRIVATE void sqlite3PagerSetMmapLimit(Pager *, sqlite3_int64); SQLITE_PRIVATE void sqlite3PagerShrink(Pager*); SQLITE_PRIVATE void sqlite3PagerSetFlags(Pager*,unsigned); @@ -10907,10 +12835,10 @@ SQLITE_PRIVATE int sqlite3PagerGetJournalMode(Pager*); SQLITE_PRIVATE int sqlite3PagerOkToChangeJournalMode(Pager*); SQLITE_PRIVATE i64 sqlite3PagerJournalSizeLimit(Pager *, i64); SQLITE_PRIVATE sqlite3_backup **sqlite3PagerBackupPtr(Pager*); +SQLITE_PRIVATE int sqlite3PagerFlush(Pager*); /* Functions used to obtain and release page references. */ -SQLITE_PRIVATE int sqlite3PagerAcquire(Pager *pPager, Pgno pgno, DbPage **ppPage, int clrFlag); -#define sqlite3PagerGet(A,B,C) sqlite3PagerAcquire(A,B,C,0) +SQLITE_PRIVATE int sqlite3PagerGet(Pager *pPager, Pgno pgno, DbPage **ppPage, int clrFlag); SQLITE_PRIVATE DbPage *sqlite3PagerLookup(Pager *pPager, Pgno pgno); SQLITE_PRIVATE void sqlite3PagerRef(DbPage*); SQLITE_PRIVATE void sqlite3PagerUnref(DbPage*); @@ -10942,6 +12870,10 @@ SQLITE_PRIVATE int sqlite3PagerWalSupported(Pager *pPager); SQLITE_PRIVATE int sqlite3PagerWalCallback(Pager *pPager); SQLITE_PRIVATE int sqlite3PagerOpenWal(Pager *pPager, int *pisOpen); SQLITE_PRIVATE int sqlite3PagerCloseWal(Pager *pPager); +# ifdef SQLITE_ENABLE_SNAPSHOT +SQLITE_PRIVATE int sqlite3PagerSnapshotGet(Pager *pPager, sqlite3_snapshot **ppSnapshot); +SQLITE_PRIVATE int sqlite3PagerSnapshotOpen(Pager *pPager, sqlite3_snapshot *pSnapshot); +# endif #endif #ifdef SQLITE_ENABLE_ZIPVFS @@ -10956,10 +12888,10 @@ SQLITE_PRIVATE int sqlite3PagerRefcount(Pager*); #endif SQLITE_PRIVATE int sqlite3PagerMemUsed(Pager*); SQLITE_PRIVATE const char *sqlite3PagerFilename(Pager*, int); -SQLITE_PRIVATE const sqlite3_vfs *sqlite3PagerVfs(Pager*); +SQLITE_PRIVATE sqlite3_vfs *sqlite3PagerVfs(Pager*); SQLITE_PRIVATE sqlite3_file *sqlite3PagerFile(Pager*); +SQLITE_PRIVATE sqlite3_file *sqlite3PagerJrnlFile(Pager*); SQLITE_PRIVATE const char *sqlite3PagerJournalname(Pager*); -SQLITE_PRIVATE int sqlite3PagerNosync(Pager*); SQLITE_PRIVATE void *sqlite3PagerTempSpace(Pager*); SQLITE_PRIVATE int sqlite3PagerIsMemdb(Pager*); SQLITE_PRIVATE void sqlite3PagerCacheStat(Pager *, int, int, int *); @@ -11024,7 +12956,7 @@ struct PgHdr { sqlite3_pcache_page *pPage; /* Pcache object page handle */ void *pData; /* Page data */ void *pExtra; /* Extra content */ - PgHdr *pDirty; /* Transient list of dirty pages */ + PgHdr *pDirty; /* Transient list of dirty sorted by pgno */ Pager *pPager; /* The pager this page is part of */ Pgno pgno; /* Page number for this page */ #ifdef SQLITE_CHECK_PAGES @@ -11049,9 +12981,10 @@ struct PgHdr { #define PGHDR_WRITEABLE 0x004 /* Journaled and ready to modify */ #define PGHDR_NEED_SYNC 0x008 /* Fsync the rollback journal before ** writing this page to the database */ -#define PGHDR_NEED_READ 0x010 /* Content is unread */ -#define PGHDR_DONT_WRITE 0x020 /* Do not write content to disk */ -#define PGHDR_MMAP 0x040 /* This is an mmap page object */ +#define PGHDR_DONT_WRITE 0x010 /* Do not write content to disk */ +#define PGHDR_MMAP 0x020 /* This is an mmap page object */ + +#define PGHDR_WAL_APPEND 0x040 /* Appended to wal file */ /* Initialize and shutdown the page cache subsystem */ SQLITE_PRIVATE int sqlite3PcacheInitialize(void); @@ -11095,6 +13028,7 @@ SQLITE_PRIVATE void sqlite3PcacheDrop(PgHdr*); /* Remove page from cache SQLITE_PRIVATE void sqlite3PcacheMakeDirty(PgHdr*); /* Make sure page is marked dirty */ SQLITE_PRIVATE void sqlite3PcacheMakeClean(PgHdr*); /* Mark a single page as clean */ SQLITE_PRIVATE void sqlite3PcacheCleanAll(PCache*); /* Mark all dirty list pages as clean */ +SQLITE_PRIVATE void sqlite3PcacheClearWritable(PCache*); /* Change a page number. Used by incr-vacuum. */ SQLITE_PRIVATE void sqlite3PcacheMove(PgHdr*, Pgno); @@ -11133,6 +13067,11 @@ SQLITE_PRIVATE int sqlite3PcachePagecount(PCache*); SQLITE_PRIVATE void sqlite3PcacheIterateDirty(PCache *pCache, void (*xIter)(PgHdr *)); #endif +#if defined(SQLITE_DEBUG) +/* Check invariants on a PgHdr object */ +SQLITE_PRIVATE int sqlite3PcachePageSanity(PgHdr*); +#endif + /* Set and get the suggested cache-size for the specified pager-cache. ** ** If no global maximum is configured, then the system attempts to limit @@ -11144,6 +13083,13 @@ SQLITE_PRIVATE void sqlite3PcacheSetCachesize(PCache *, int); SQLITE_PRIVATE int sqlite3PcacheGetCachesize(PCache *); #endif +/* Set or get the suggested spill-size for the specified pager-cache. +** +** The spill-size is the minimum number of pages in cache before the cache +** will attempt to spill dirty pages by calling xStress. +*/ +SQLITE_PRIVATE int sqlite3PcacheSetSpillsize(PCache *, int); + /* Free up as much memory as possible from the page cache */ SQLITE_PRIVATE void sqlite3PcacheShrink(PCache*); @@ -11162,11 +13108,13 @@ SQLITE_PRIVATE void sqlite3PCacheSetDefault(void); SQLITE_PRIVATE int sqlite3HeaderSizePcache(void); SQLITE_PRIVATE int sqlite3HeaderSizePcache1(void); +/* Number of dirty pages as a percentage of the configured cache size */ +SQLITE_PRIVATE int sqlite3PCachePercentDirty(PCache*); + #endif /* _PCACHE_H_ */ /************** End of pcache.h **********************************************/ /************** Continuing where we left off in sqliteInt.h ******************/ - /************** Include os.h in the middle of sqliteInt.h ********************/ /************** Begin file os.h **********************************************/ /* @@ -11392,7 +13340,7 @@ SQLITE_PRIVATE int sqlite3OsInit(void); /* ** Functions for accessing sqlite3_file methods */ -SQLITE_PRIVATE int sqlite3OsClose(sqlite3_file*); +SQLITE_PRIVATE void sqlite3OsClose(sqlite3_file*); SQLITE_PRIVATE int sqlite3OsRead(sqlite3_file*, void*, int amt, i64 offset); SQLITE_PRIVATE int sqlite3OsWrite(sqlite3_file*, const void*, int amt, i64 offset); SQLITE_PRIVATE int sqlite3OsTruncate(sqlite3_file*, i64 size); @@ -11429,6 +13377,7 @@ SQLITE_PRIVATE void sqlite3OsDlClose(sqlite3_vfs *, void *); #endif /* SQLITE_OMIT_LOAD_EXTENSION */ SQLITE_PRIVATE int sqlite3OsRandomness(sqlite3_vfs *, int, char *); SQLITE_PRIVATE int sqlite3OsSleep(sqlite3_vfs *, int); +SQLITE_PRIVATE int sqlite3OsGetLastError(sqlite3_vfs*); SQLITE_PRIVATE int sqlite3OsCurrentTimeInt64(sqlite3_vfs *, sqlite3_int64*); /* @@ -11436,7 +13385,7 @@ SQLITE_PRIVATE int sqlite3OsCurrentTimeInt64(sqlite3_vfs *, sqlite3_int64*); ** sqlite3_malloc() to obtain space for the file-handle structure. */ SQLITE_PRIVATE int sqlite3OsOpenMalloc(sqlite3_vfs *, const char *, sqlite3_file **, int,int*); -SQLITE_PRIVATE int sqlite3OsCloseFree(sqlite3_file *); +SQLITE_PRIVATE void sqlite3OsCloseFree(sqlite3_file *); #endif /* _SQLITE_OS_H_ */ @@ -11518,6 +13467,36 @@ SQLITE_PRIVATE int sqlite3OsCloseFree(sqlite3_file *); /************** End of mutex.h ***********************************************/ /************** Continuing where we left off in sqliteInt.h ******************/ +/* The SQLITE_EXTRA_DURABLE compile-time option used to set the default +** synchronous setting to EXTRA. It is no longer supported. +*/ +#ifdef SQLITE_EXTRA_DURABLE +# warning Use SQLITE_DEFAULT_SYNCHRONOUS=3 instead of SQLITE_EXTRA_DURABLE +# define SQLITE_DEFAULT_SYNCHRONOUS 3 +#endif + +/* +** Default synchronous levels. +** +** Note that (for historcal reasons) the PAGER_SYNCHRONOUS_* macros differ +** from the SQLITE_DEFAULT_SYNCHRONOUS value by 1. +** +** PAGER_SYNCHRONOUS DEFAULT_SYNCHRONOUS +** OFF 1 0 +** NORMAL 2 1 +** FULL 3 2 +** EXTRA 4 3 +** +** The "PRAGMA synchronous" statement also uses the zero-based numbers. +** In other words, the zero-based numbers are used for all external interfaces +** and the one-based values are used internally. +*/ +#ifndef SQLITE_DEFAULT_SYNCHRONOUS +# define SQLITE_DEFAULT_SYNCHRONOUS (PAGER_SYNCHRONOUS_FULL-1) +#endif +#ifndef SQLITE_DEFAULT_WAL_SYNCHRONOUS +# define SQLITE_DEFAULT_WAL_SYNCHRONOUS SQLITE_DEFAULT_SYNCHRONOUS +#endif /* ** Each database file to be accessed by the system is an instance @@ -11530,6 +13509,7 @@ struct Db { char *zName; /* Name of this database */ Btree *pBt; /* The B*Tree structure for this database file */ u8 safety_level; /* How aggressive at syncing data to disk */ + u8 bSyncSet; /* True if "PRAGMA synchronous=N" has been run */ Schema *pSchema; /* Pointer to database schema (possibly shared) */ }; @@ -11540,7 +13520,7 @@ struct Db { ** the Schema for the TEMP databaes (sqlite3.aDb[1]) which is free-standing. ** In shared cache mode, a single Schema object can be shared by multiple ** Btrees that refer to the same underlying BtShared object. -** +** ** Schema objects are automatically deallocated when the last Btree that ** references them is destroyed. The TEMP Schema is manually freed by ** sqlite3_close(). @@ -11565,7 +13545,7 @@ struct Schema { }; /* -** These macros can be used to test, set, or clear bits in the +** These macros can be used to test, set, or clear bits in the ** Db.pSchema->flags field. */ #define DbHasProperty(D,I,P) (((D)->aDb[I].pSchema->schemaFlags&(P))==(P)) @@ -11614,8 +13594,8 @@ struct Schema { ** lookaside allocations are not used to construct the schema objects. */ struct Lookaside { + u32 bDisable; /* Only operate the lookaside when zero */ u16 sz; /* Size of each buffer in bytes */ - u8 bEnabled; /* False to disable new lookaside allocations */ u8 bMalloced; /* True if pStart obtained from sqlite3_malloc() */ int nOut; /* Number of buffers currently checked out */ int mxOut; /* Highwater mark for nOut */ @@ -11629,13 +13609,15 @@ struct LookasideSlot { }; /* -** A hash table for function definitions. +** A hash table for built-in function definitions. (Application-defined +** functions use a regular table table from hash.h.) ** ** Hash each FuncDef structure into one of the FuncDefHash.a[] slots. -** Collisions are on the FuncDef.pHash chain. +** Collisions are on the FuncDef.u.pHash chain. */ +#define SQLITE_FUNC_HASH_SZ 23 struct FuncDefHash { - FuncDef *a[23]; /* Hash table for functions */ + FuncDef *a[SQLITE_FUNC_HASH_SZ]; /* Hash table for functions */ }; #ifdef SQLITE_USER_AUTHENTICATION @@ -11693,11 +13675,13 @@ struct sqlite3 { unsigned int openFlags; /* Flags passed to sqlite3_vfs.xOpen() */ int errCode; /* Most recent error code (SQLITE_*) */ int errMask; /* & result codes with this before returning */ + int iSysErrno; /* Errno value from last system error */ u16 dbOptFlags; /* Flags to enable/disable optimizations */ u8 enc; /* Text encoding */ u8 autoCommit; /* The auto-commit flag. */ u8 temp_store; /* 1: file 2: memory 0: default */ u8 mallocFailed; /* True if we have seen a malloc failure */ + u8 bBenignMalloc; /* Do not require OOMs if true */ u8 dfltLockMode; /* Default locking-mode for attached dbs */ signed char nextAutovac; /* Autovac setting after VACUUM if >=0 */ u8 suppressErr; /* Do not issue error messages if true */ @@ -11727,12 +13711,19 @@ struct sqlite3 { void *pTraceArg; /* Argument to the trace function */ void (*xProfile)(void*,const char*,u64); /* Profiling function */ void *pProfileArg; /* Argument to profile function */ - void *pCommitArg; /* Argument to xCommitCallback() */ + void *pCommitArg; /* Argument to xCommitCallback() */ int (*xCommitCallback)(void*); /* Invoked at every commit. */ - void *pRollbackArg; /* Argument to xRollbackCallback() */ + void *pRollbackArg; /* Argument to xRollbackCallback() */ void (*xRollbackCallback)(void*); /* Invoked at every commit. */ void *pUpdateArg; void (*xUpdateCallback)(void*,int, const char*,const char*,sqlite_int64); +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + void *pPreUpdateArg; /* First argument to xPreUpdateCallback */ + void (*xPreUpdateCallback)( /* Registered using sqlite3_preupdate_hook() */ + void*,sqlite3*,int,char const*,char const*,sqlite3_int64,sqlite3_int64 + ); + PreUpdate *pPreUpdate; /* Context for active pre-update callback */ +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ #ifndef SQLITE_OMIT_WAL int (*xWalCallback)(void *, sqlite3 *, const char *, int); void *pWalArg; @@ -11762,7 +13753,7 @@ struct sqlite3 { VTable **aVTrans; /* Virtual tables with open transactions */ VTable *pDisconnect; /* Disconnect these in next sqlite3_prepare() */ #endif - FuncDefHash aFunc; /* Hash table of connection functions */ + Hash aFunc; /* Hash table of connection functions */ Hash aCollSeq; /* All collating sequences */ BusyHandler busyHandler; /* Busy callback */ Db aDbStatic[2]; /* Static space for the 2 default backends */ @@ -11774,8 +13765,8 @@ struct sqlite3 { i64 nDeferredImmCons; /* Net deferred immediate constraints */ int *pnBytesFreed; /* If not NULL, increment this in DbFree() */ #ifdef SQLITE_ENABLE_UNLOCK_NOTIFY - /* The following variables are all protected by the STATIC_MASTER - ** mutex, not by sqlite3.mutex. They are used by code in notify.c. + /* The following variables are all protected by the STATIC_MASTER + ** mutex, not by sqlite3.mutex. They are used by code in notify.c. ** ** When X.pUnlockConnection==Y, that means that X is waiting for Y to ** unlock so that it can proceed. @@ -11803,13 +13794,18 @@ struct sqlite3 { /* ** Possible values for the sqlite3.flags. +** +** Value constraints (enforced via assert()): +** SQLITE_FullFSync == PAGER_FULLFSYNC +** SQLITE_CkptFullFSync == PAGER_CKPT_FULLFSYNC +** SQLITE_CacheSpill == PAGER_CACHE_SPILL */ #define SQLITE_VdbeTrace 0x00000001 /* True to trace VDBE execution */ #define SQLITE_InternChanges 0x00000002 /* Uncommitted Hash table changes */ -#define SQLITE_FullFSync 0x00000004 /* Use full fsync on the backend */ -#define SQLITE_CkptFullFSync 0x00000008 /* Use full fsync for checkpoint */ -#define SQLITE_CacheSpill 0x00000010 /* OK to spill pager cache */ -#define SQLITE_FullColNames 0x00000020 /* Show full column names on SELECT */ +#define SQLITE_FullColNames 0x00000004 /* Show full column names on SELECT */ +#define SQLITE_FullFSync 0x00000008 /* Use full fsync on the backend */ +#define SQLITE_CkptFullFSync 0x00000010 /* Use full fsync for checkpoint */ +#define SQLITE_CacheSpill 0x00000020 /* OK to spill pager cache */ #define SQLITE_ShortColNames 0x00000040 /* Show short columns names */ #define SQLITE_CountRows 0x00000080 /* Count rows changed by INSERT, */ /* DELETE, or UPDATE and return */ @@ -11830,12 +13826,14 @@ struct sqlite3 { #define SQLITE_AutoIndex 0x00100000 /* Enable automatic indexes */ #define SQLITE_PreferBuiltin 0x00200000 /* Preference to built-in funcs */ #define SQLITE_LoadExtension 0x00400000 /* Enable load_extension */ -#define SQLITE_EnableTrigger 0x00800000 /* True to enable triggers */ -#define SQLITE_DeferFKs 0x01000000 /* Defer all FK constraints */ -#define SQLITE_QueryOnly 0x02000000 /* Disable database changes */ -#define SQLITE_VdbeEQP 0x04000000 /* Debug EXPLAIN QUERY PLAN */ -#define SQLITE_Vacuum 0x08000000 /* Currently in a VACUUM */ -#define SQLITE_CellSizeCk 0x10000000 /* Check btree cell sizes on load */ +#define SQLITE_LoadExtFunc 0x00800000 /* Enable load_extension() SQL func */ +#define SQLITE_EnableTrigger 0x01000000 /* True to enable triggers */ +#define SQLITE_DeferFKs 0x02000000 /* Defer all FK constraints */ +#define SQLITE_QueryOnly 0x04000000 /* Disable database changes */ +#define SQLITE_VdbeEQP 0x08000000 /* Debug EXPLAIN QUERY PLAN */ +#define SQLITE_Vacuum 0x10000000 /* Currently in a VACUUM */ +#define SQLITE_CellSizeCk 0x20000000 /* Check btree cell sizes on load */ +#define SQLITE_Fts3Tokenizer 0x40000000 /* Enable fts3_tokenizer(2) */ /* @@ -11855,6 +13853,7 @@ struct sqlite3 { #define SQLITE_Transitive 0x0200 /* Transitive constraints */ #define SQLITE_OmitNoopJoin 0x0400 /* Omit unused tables in joins */ #define SQLITE_Stat34 0x0800 /* Use STAT3 or STAT4 data */ +#define SQLITE_CursorHints 0x2000 /* Add OP_CursorHint opcodes */ #define SQLITE_AllOpts 0xffff /* All optimizations */ /* @@ -11888,28 +13887,33 @@ struct sqlite3 { /* ** Each SQL function is defined by an instance of the following -** structure. A pointer to this structure is stored in the sqlite.aFunc -** hash table. When multiple functions have the same name, the hash table -** points to a linked list of these structures. +** structure. For global built-in functions (ex: substr(), max(), count()) +** a pointer to this structure is held in the sqlite3BuiltinFunctions object. +** For per-connection application-defined functions, a pointer to this +** structure is held in the db->aHash hash table. +** +** The u.pHash field is used by the global built-ins. The u.pDestructor +** field is used by per-connection app-def functions. */ struct FuncDef { - i16 nArg; /* Number of arguments. -1 means unlimited */ + i8 nArg; /* Number of arguments. -1 means unlimited */ u16 funcFlags; /* Some combination of SQLITE_FUNC_* */ void *pUserData; /* User data parameter */ FuncDef *pNext; /* Next function with same name */ - void (*xFunc)(sqlite3_context*,int,sqlite3_value**); /* Regular function */ - void (*xStep)(sqlite3_context*,int,sqlite3_value**); /* Aggregate step */ - void (*xFinalize)(sqlite3_context*); /* Aggregate finalizer */ - char *zName; /* SQL name of the function. */ - FuncDef *pHash; /* Next with a different name but the same hash */ - FuncDestructor *pDestructor; /* Reference counted destructor function */ + void (*xSFunc)(sqlite3_context*,int,sqlite3_value**); /* func or agg-step */ + void (*xFinalize)(sqlite3_context*); /* Agg finalizer */ + const char *zName; /* SQL name of the function. */ + union { + FuncDef *pHash; /* Next with a different name but the same hash */ + FuncDestructor *pDestructor; /* Reference counted destructor function */ + } u; }; /* ** This structure encapsulates a user-function destructor callback (as ** configured using create_function_v2()) and a reference counter. When ** create_function_v2() is called to create a function with a destructor, -** a single object of this type is allocated. FuncDestructor.nRef is set to +** a single object of this type is allocated. FuncDestructor.nRef is set to ** the number of FuncDef objects created (either 1 or 3, depending on whether ** or not the specified encoding is SQLITE_ANY). The FuncDef.pDestructor ** member of each of the new FuncDef objects is set to point to the allocated @@ -11927,8 +13931,16 @@ struct FuncDestructor { /* ** Possible values for FuncDef.flags. Note that the _LENGTH and _TYPEOF -** values must correspond to OPFLAG_LENGTHARG and OPFLAG_TYPEOFARG. There +** values must correspond to OPFLAG_LENGTHARG and OPFLAG_TYPEOFARG. And +** SQLITE_FUNC_CONSTANT must be the same as SQLITE_DETERMINISTIC. There ** are assert() statements in the code to verify this. +** +** Value constraints (enforced via assert()): +** SQLITE_FUNC_MINMAX == NC_MinMaxAgg == SF_MinMaxAgg +** SQLITE_FUNC_LENGTH == OPFLAG_LENGTHARG +** SQLITE_FUNC_TYPEOF == OPFLAG_TYPEOFARG +** SQLITE_FUNC_CONSTANT == SQLITE_DETERMINISTIC from the API +** SQLITE_FUNC_ENCMASK depends on SQLITE_UTF* macros in the API */ #define SQLITE_FUNC_ENCMASK 0x0003 /* SQLITE_UTF8, SQLITE_UTF16BE or UTF16LE */ #define SQLITE_FUNC_LIKE 0x0004 /* Candidate for the LIKE optimization */ @@ -11950,10 +13962,10 @@ struct FuncDestructor { ** used to create the initializers for the FuncDef structures. ** ** FUNCTION(zName, nArg, iArg, bNC, xFunc) -** Used to create a scalar function definition of a function zName +** Used to create a scalar function definition of a function zName ** implemented by C function xFunc that accepts nArg arguments. The ** value passed as iArg is cast to a (void*) and made available -** as the user-data (sqlite3_user_data()) for the function. If +** as the user-data (sqlite3_user_data()) for the function. If ** argument bNC is true, then the SQLITE_FUNC_NEEDCOLL flag is set. ** ** VFUNCTION(zName, nArg, iArg, bNC, xFunc) @@ -11972,8 +13984,8 @@ struct FuncDestructor { ** FUNCTION(). ** ** LIKEFUNC(zName, nArg, pArg, flags) -** Used to create a scalar function definition of a function zName -** that accepts nArg arguments and is implemented by a call to C +** Used to create a scalar function definition of a function zName +** that accepts nArg arguments and is implemented by a call to C ** function likeFunc. Argument pArg is cast to a (void *) and made ** available as the function user-data (sqlite3_user_data()). The ** FuncDef.flags variable is set to the value passed as the flags @@ -11981,28 +13993,28 @@ struct FuncDestructor { */ #define FUNCTION(zName, nArg, iArg, bNC, xFunc) \ {nArg, SQLITE_FUNC_CONSTANT|SQLITE_UTF8|(bNC*SQLITE_FUNC_NEEDCOLL), \ - SQLITE_INT_TO_PTR(iArg), 0, xFunc, 0, 0, #zName, 0, 0} + SQLITE_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0} } #define VFUNCTION(zName, nArg, iArg, bNC, xFunc) \ {nArg, SQLITE_UTF8|(bNC*SQLITE_FUNC_NEEDCOLL), \ - SQLITE_INT_TO_PTR(iArg), 0, xFunc, 0, 0, #zName, 0, 0} + SQLITE_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0} } #define DFUNCTION(zName, nArg, iArg, bNC, xFunc) \ {nArg, SQLITE_FUNC_SLOCHNG|SQLITE_UTF8|(bNC*SQLITE_FUNC_NEEDCOLL), \ - SQLITE_INT_TO_PTR(iArg), 0, xFunc, 0, 0, #zName, 0, 0} + SQLITE_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0} } #define FUNCTION2(zName, nArg, iArg, bNC, xFunc, extraFlags) \ {nArg,SQLITE_FUNC_CONSTANT|SQLITE_UTF8|(bNC*SQLITE_FUNC_NEEDCOLL)|extraFlags,\ - SQLITE_INT_TO_PTR(iArg), 0, xFunc, 0, 0, #zName, 0, 0} + SQLITE_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0} } #define STR_FUNCTION(zName, nArg, pArg, bNC, xFunc) \ {nArg, SQLITE_FUNC_SLOCHNG|SQLITE_UTF8|(bNC*SQLITE_FUNC_NEEDCOLL), \ - pArg, 0, xFunc, 0, 0, #zName, 0, 0} + pArg, 0, xFunc, 0, #zName, } #define LIKEFUNC(zName, nArg, arg, flags) \ {nArg, SQLITE_FUNC_CONSTANT|SQLITE_UTF8|flags, \ - (void *)arg, 0, likeFunc, 0, 0, #zName, 0, 0} + (void *)arg, 0, likeFunc, 0, #zName, {0} } #define AGGREGATE(zName, nArg, arg, nc, xStep, xFinal) \ {nArg, SQLITE_UTF8|(nc*SQLITE_FUNC_NEEDCOLL), \ - SQLITE_INT_TO_PTR(arg), 0, 0, xStep,xFinal,#zName,0,0} + SQLITE_INT_TO_PTR(arg), 0, xStep,xFinal,#zName, {0}} #define AGGREGATE2(zName, nArg, arg, nc, xStep, xFinal, extraFlags) \ {nArg, SQLITE_UTF8|(nc*SQLITE_FUNC_NEEDCOLL)|extraFlags, \ - SQLITE_INT_TO_PTR(arg), 0, 0, xStep,xFinal,#zName,0,0} + SQLITE_INT_TO_PTR(arg), 0, xStep,xFinal,#zName, {0}} /* ** All current savepoints are stored in a linked list starting at @@ -12044,14 +14056,12 @@ struct Module { ** of this structure. */ struct Column { - char *zName; /* Name of this column */ + char *zName; /* Name of this column, \000, then the type */ Expr *pDflt; /* Default value of this column */ - char *zDflt; /* Original text of the default value */ - char *zType; /* Data type for this column */ char *zColl; /* Collating sequence. If NULL, use the default */ u8 notNull; /* An OE_ code for handling a NOT NULL constraint */ char affinity; /* One of the SQLITE_AFF_... values */ - u8 szEst; /* Estimated size of this column. INT==1 */ + u8 szEst; /* Estimated size of value in this column. sizeof(INT)==1 */ u8 colFlags; /* Boolean properties. See COLFLAG_ defines below */ }; @@ -12059,6 +14069,7 @@ struct Column { */ #define COLFLAG_PRIMKEY 0x0001 /* Column is part of the primary key */ #define COLFLAG_HIDDEN 0x0002 /* A hidden column in a virtual table */ +#define COLFLAG_HASTYPE 0x0004 /* Type name follows column name */ /* ** A "Collating Sequence" is defined by an instance of the following @@ -12089,7 +14100,7 @@ struct CollSeq { ** ** These used to have mnemonic name like 'i' for SQLITE_AFF_INTEGER and ** 't' for SQLITE_AFF_TEXT. But we can save a little space and improve -** the speed a little by numbering the values consecutively. +** the speed a little by numbering the values consecutively. ** ** But rather than start with 0 or 1, we begin with 'A'. That way, ** when multiple affinity types are concatenated into a string and @@ -12108,7 +14119,7 @@ struct CollSeq { /* ** The SQLITE_AFF_MASK values masks off the significant bits of an -** affinity value. +** affinity value. */ #define SQLITE_AFF_MASK 0x47 @@ -12128,20 +14139,20 @@ struct CollSeq { /* ** An object of this type is created for each virtual table present in -** the database schema. +** the database schema. ** ** If the database schema is shared, then there is one instance of this ** structure for each database connection (sqlite3*) that uses the shared ** schema. This is because each database connection requires its own unique -** instance of the sqlite3_vtab* handle used to access the virtual table -** implementation. sqlite3_vtab* handles can not be shared between -** database connections, even when the rest of the in-memory database +** instance of the sqlite3_vtab* handle used to access the virtual table +** implementation. sqlite3_vtab* handles can not be shared between +** database connections, even when the rest of the in-memory database ** schema is shared, as the implementation often stores the database ** connection handle passed to it via the xConnect() or xCreate() method ** during initialization internally. This database connection handle may -** then be used by the virtual table implementation to access real tables -** within the database. So that they appear as part of the callers -** transaction, these accesses need to be made via the same database +** then be used by the virtual table implementation to access real tables +** within the database. So that they appear as part of the callers +** transaction, these accesses need to be made via the same database ** connection as that used to execute SQL operations on the virtual table. ** ** All VTable objects that correspond to a single table in a shared @@ -12153,19 +14164,19 @@ struct CollSeq { ** sqlite3_vtab* handle in the compiled query. ** ** When an in-memory Table object is deleted (for example when the -** schema is being reloaded for some reason), the VTable objects are not -** deleted and the sqlite3_vtab* handles are not xDisconnect()ed +** schema is being reloaded for some reason), the VTable objects are not +** deleted and the sqlite3_vtab* handles are not xDisconnect()ed ** immediately. Instead, they are moved from the Table.pVTable list to ** another linked list headed by the sqlite3.pDisconnect member of the -** corresponding sqlite3 structure. They are then deleted/xDisconnected +** corresponding sqlite3 structure. They are then deleted/xDisconnected ** next time a statement is prepared using said sqlite3*. This is done ** to avoid deadlock issues involving multiple sqlite3.mutex mutexes. ** Refer to comments above function sqlite3VtabUnlockList() for an ** explanation as to why it is safe to add an entry to an sqlite3.pDisconnect ** list without holding the corresponding sqlite3.mutex mutex. ** -** The memory for objects of this type is always allocated by -** sqlite3DbMalloc(), using the connection handle stored in VTable.db as +** The memory for objects of this type is always allocated by +** sqlite3DbMalloc(), using the connection handle stored in VTable.db as ** the first argument. */ struct VTable { @@ -12218,7 +14229,7 @@ struct Table { /* ** Allowed values for Table.tabFlags. ** -** TF_OOOHidden applies to virtual tables that have hidden columns that are +** TF_OOOHidden applies to tables or view that have hidden columns that are ** followed by non-hidden columns. Example: "CREATE VIRTUAL TABLE x USING ** vtab1(a HIDDEN, b);". Since "b" is a non-hidden column but "a" is hidden, ** the TF_OOOHidden attribute would apply in this case. Such tables require @@ -12241,12 +14252,28 @@ struct Table { */ #ifndef SQLITE_OMIT_VIRTUALTABLE # define IsVirtual(X) (((X)->tabFlags & TF_Virtual)!=0) -# define IsHiddenColumn(X) (((X)->colFlags & COLFLAG_HIDDEN)!=0) #else # define IsVirtual(X) 0 -# define IsHiddenColumn(X) 0 #endif +/* +** Macros to determine if a column is hidden. IsOrdinaryHiddenColumn() +** only works for non-virtual tables (ordinary tables and views) and is +** always false unless SQLITE_ENABLE_HIDDEN_COLUMNS is defined. The +** IsHiddenColumn() macro is general purpose. +*/ +#if defined(SQLITE_ENABLE_HIDDEN_COLUMNS) +# define IsHiddenColumn(X) (((X)->colFlags & COLFLAG_HIDDEN)!=0) +# define IsOrdinaryHiddenColumn(X) (((X)->colFlags & COLFLAG_HIDDEN)!=0) +#elif !defined(SQLITE_OMIT_VIRTUALTABLE) +# define IsHiddenColumn(X) (((X)->colFlags & COLFLAG_HIDDEN)!=0) +# define IsOrdinaryHiddenColumn(X) 0 +#else +# define IsHiddenColumn(X) 0 +# define IsOrdinaryHiddenColumn(X) 0 +#endif + + /* Does the table have a rowid */ #define HasRowid(X) (((X)->tabFlags & TF_WithoutRowid)==0) #define VisibleRowid(X) (((X)->tabFlags & TF_NoVisibleRowid)==0) @@ -12317,7 +14344,7 @@ struct FKey { ** key is set to NULL. CASCADE means that a DELETE or UPDATE of the ** referenced table row is propagated into the row that holds the ** foreign key. -** +** ** The following symbolic values are used to record which type ** of action to take. */ @@ -12338,7 +14365,7 @@ struct FKey { /* ** An instance of the following structure is passed as the first -** argument to sqlite3VdbeKeyCompare and is used to control the +** argument to sqlite3VdbeKeyCompare and is used to control the ** comparison of the two index keys. ** ** Note that aSortOrder[] and aColl[] have nField+1 slots. There @@ -12356,9 +14383,8 @@ struct KeyInfo { }; /* -** An instance of the following structure holds information about a -** single index record that has already been parsed out into individual -** values. +** This object holds a record which has been parsed out into individual +** fields, for the purposes of doing a comparison. ** ** A record is an object that contains one or more fields of data. ** Records are used to store the content of a table row and to store @@ -12366,20 +14392,40 @@ struct KeyInfo { ** the OP_MakeRecord opcode of the VDBE and is disassembled by the ** OP_Column opcode. ** -** This structure holds a record that has already been disassembled -** into its constituent fields. +** An instance of this object serves as a "key" for doing a search on +** an index b+tree. The goal of the search is to find the entry that +** is closed to the key described by this object. This object might hold +** just a prefix of the key. The number of fields is given by +** pKeyInfo->nField. ** -** The r1 and r2 member variables are only used by the optimized comparison -** functions vdbeRecordCompareInt() and vdbeRecordCompareString(). +** The r1 and r2 fields are the values to return if this key is less than +** or greater than a key in the btree, respectively. These are normally +** -1 and +1 respectively, but might be inverted to +1 and -1 if the b-tree +** is in DESC order. +** +** The key comparison functions actually return default_rc when they find +** an equals comparison. default_rc can be -1, 0, or +1. If there are +** multiple entries in the b-tree with the same key (when only looking +** at the first pKeyInfo->nFields,) then default_rc can be set to -1 to +** cause the search to find the last match, or +1 to cause the search to +** find the first match. +** +** The key comparison functions will set eqSeen to true if they ever +** get and equal results when comparing this structure to a b-tree record. +** When default_rc!=0, the search might end up on the record immediately +** before the first match or immediately after the last match. The +** eqSeen field will indicate whether or not an exact match exists in the +** b-tree. */ struct UnpackedRecord { KeyInfo *pKeyInfo; /* Collation and sort-order information */ + Mem *aMem; /* Values */ u16 nField; /* Number of entries in apMem[] */ i8 default_rc; /* Comparison result if keys are equal */ u8 errCode; /* Error detected by xRecordCompare (CORRUPT or NOMEM) */ - Mem *aMem; /* Values */ - int r1; /* Value to return if (lhs > rhs) */ - int r2; /* Value to return if (rhs < lhs) */ + i8 r1; /* Value to return if (lhs > rhs) */ + i8 r2; /* Value to return if (rhs < lhs) */ + u8 eqSeen; /* True if an equality comparison has been seen */ }; @@ -12397,7 +14443,7 @@ struct UnpackedRecord { ** In the Table structure describing Ex1, nCol==3 because there are ** three columns in the table. In the Index structure describing ** Ex2, nColumn==2 since 2 of the 3 columns of Ex1 are indexed. -** The value of aiColumn is {2, 0}. aiColumn[0]==2 because the +** The value of aiColumn is {2, 0}. aiColumn[0]==2 because the ** first column to be indexed (c3) has an index of 2 in Ex1.aCol[]. ** The second column to be indexed (c1) has an index of 0 in ** Ex1.aCol[], hence Ex2.aiColumn[1]==0. @@ -12405,7 +14451,7 @@ struct UnpackedRecord { ** The Index.onError field determines whether or not the indexed columns ** must be unique and what to do if they are not. When Index.onError=OE_None, ** it means this is not a unique index. Otherwise it is a unique index -** and the value of Index.onError indicate the which conflict resolution +** and the value of Index.onError indicate the which conflict resolution ** algorithm to employ whenever an attempt is made to insert a non-unique ** element. ** @@ -12426,7 +14472,7 @@ struct Index { Index *pNext; /* The next index associated with the same table */ Schema *pSchema; /* Schema containing this index */ u8 *aSortOrder; /* for each column: True==DESC, False==ASC */ - char **azColl; /* Array of collation sequence names for index */ + const char **azColl; /* Array of collation sequence names for index */ Expr *pPartIdxWhere; /* WHERE clause for partial indices */ ExprList *aColExpr; /* Column expressions */ int tnum; /* DB Page containing root of this index */ @@ -12470,7 +14516,7 @@ struct Index { #define XN_EXPR (-2) /* Indexed column is an expression */ /* -** Each sample stored in the sqlite_stat3 table is represented in memory +** Each sample stored in the sqlite_stat3 table is represented in memory ** using a structure of this type. See documentation at the top of the ** analyze.c source file for additional information. */ @@ -12565,9 +14611,9 @@ typedef int ynVar; ** to represent the greater-than-or-equal-to operator in the expression ** tree. ** -** If the expression is an SQL literal (TK_INTEGER, TK_FLOAT, TK_BLOB, +** If the expression is an SQL literal (TK_INTEGER, TK_FLOAT, TK_BLOB, ** or TK_STRING), then Expr.token contains the text of the SQL literal. If -** the expression is a variable (TK_VARIABLE), then Expr.token contains the +** the expression is a variable (TK_VARIABLE), then Expr.token contains the ** variable name. Finally, if the expression is an SQL function (TK_FUNCTION), ** then Expr.token contains the name of the function. ** @@ -12578,7 +14624,7 @@ typedef int ynVar; ** a CASE expression or an IN expression of the form " IN (, ...)". ** Expr.x.pSelect is used if the expression is a sub-select or an expression of ** the form " IN (SELECT ...)". If the EP_xIsSelect bit is set in the -** Expr.flags mask, then Expr.x.pSelect is valid. Otherwise, Expr.x.pList is +** Expr.flags mask, then Expr.x.pSelect is valid. Otherwise, Expr.x.pList is ** valid. ** ** An expression of the form ID or ID.ID refers to a column in a table. @@ -12589,8 +14635,8 @@ typedef int ynVar; ** value is also stored in the Expr.iAgg column in the aggregate so that ** it can be accessed after all aggregates are computed. ** -** If the expression is an unbound variable marker (a question mark -** character '?' in the original SQL) then the Expr.iTable holds the index +** If the expression is an unbound variable marker (a question mark +** character '?' in the original SQL) then the Expr.iTable holds the index ** number for that variable. ** ** If the expression is a subquery then Expr.iColumn holds an integer @@ -12629,7 +14675,7 @@ struct Expr { /* If the EP_TokenOnly flag is set in the Expr.flags mask, then no ** space is allocated for the fields below this point. An attempt to - ** access them will result in a segfault or malfunction. + ** access them will result in a segfault or malfunction. *********************************************************************/ Expr *pLeft; /* Left subnode */ @@ -12695,7 +14741,7 @@ struct Expr { #define EP_Propagate (EP_Collate|EP_Subquery) /* Propagate these bits up tree */ /* -** These macros can be used to test, set, or clear bits in the +** These macros can be used to test, set, or clear bits in the ** Expr.flags field. */ #define ExprHasProperty(E,P) (((E)->flags&(P))!=0) @@ -12714,8 +14760,8 @@ struct Expr { #endif /* -** Macros to determine the number of bytes required by a normal Expr -** struct, an Expr struct with the EP_Reduced flag set in Expr.flags +** Macros to determine the number of bytes required by a normal Expr +** struct, an Expr struct with the EP_Reduced flag set in Expr.flags ** and an Expr struct with the EP_TokenOnly flag set. */ #define EXPR_FULLSIZE sizeof(Expr) /* Full size */ @@ -12723,7 +14769,7 @@ struct Expr { #define EXPR_TOKENONLYSIZE offsetof(Expr,pLeft) /* Fewer features */ /* -** Flags passed to the sqlite3ExprDup() function. See the header comment +** Flags passed to the sqlite3ExprDup() function. See the header comment ** above sqlite3ExprDup() for details. */ #define EXPRDUP_REDUCE 0x0001 /* Used reduced-size Expr nodes */ @@ -12805,7 +14851,11 @@ struct IdList { ** tables in a join to 32 instead of 64. But it also reduces the size ** of the library by 738 bytes on ix86. */ -typedef u64 Bitmask; +#ifdef SQLITE_BITMASK_TYPE + typedef SQLITE_BITMASK_TYPE Bitmask; +#else + typedef u64 Bitmask; +#endif /* ** The number of bits in a Bitmask. "BMS" means "BitMask Size". @@ -12817,6 +14867,7 @@ typedef u64 Bitmask; */ #define MASKBIT(n) (((Bitmask)1)<<(n)) #define MASKBIT32(n) (((unsigned int)1)<<(n)) +#define ALLBITS ((Bitmask)-1) /* ** The following structure describes the FROM clause of a SELECT statement. @@ -12889,6 +14940,9 @@ struct SrcList { /* ** Flags appropriate for the wctrlFlags parameter of sqlite3WhereBegin() ** and the WhereInfo.wctrlFlags member. +** +** Value constraints (enforced via assert()): +** WHERE_USE_LIMIT == SF_FixedLimit */ #define WHERE_ORDERBY_NORMAL 0x0000 /* No-op */ #define WHERE_ORDERBY_MIN 0x0001 /* ORDER BY processing for min() func */ @@ -12905,6 +14959,8 @@ struct SrcList { #define WHERE_SORTBYGROUP 0x0800 /* Support sqlite3WhereIsSorted() */ #define WHERE_REOPEN_IDX 0x1000 /* Try to use OP_ReopenIdx */ #define WHERE_ONEPASS_MULTIROW 0x2000 /* ONEPASS is ok with multiple rows */ +#define WHERE_USE_LIMIT 0x4000 /* There is a constant LIMIT clause */ +#define WHERE_SEEK_TABLE 0x8000 /* Do not defer seeks on main table */ /* Allowed return values from sqlite3WhereIsDistinct() */ @@ -12922,12 +14978,12 @@ struct SrcList { ** pEList corresponds to the result set of a SELECT and is NULL for ** other statements. ** -** NameContexts can be nested. When resolving names, the inner-most +** NameContexts can be nested. When resolving names, the inner-most ** context is searched first. If no match is found, the next outer ** context is checked. If there is still no match, the next context ** is checked. This process continues until either a match is found ** or all contexts are check. When a match is found, the nRef member of -** the context containing the match is incremented. +** the context containing the match is incremented. ** ** Each subquery gets a new NameContext. The pNext field points to the ** NameContext in the parent query. Thus the process of scanning the @@ -12948,16 +15004,18 @@ struct NameContext { /* ** Allowed values for the NameContext, ncFlags field. ** -** Note: NC_MinMaxAgg must have the same value as SF_MinMaxAgg and -** SQLITE_FUNC_MINMAX. -** +** Value constraints (all checked via assert()): +** NC_HasAgg == SF_HasAgg +** NC_MinMaxAgg == SF_MinMaxAgg == SQLITE_FUNC_MINMAX +** */ #define NC_AllowAgg 0x0001 /* Aggregate functions are allowed here */ -#define NC_HasAgg 0x0002 /* One or more aggregate functions seen */ +#define NC_PartIdx 0x0002 /* True if resolving a partial index WHERE */ #define NC_IsCheck 0x0004 /* True if resolving names in a CHECK constraint */ #define NC_InAggFunc 0x0008 /* True if analyzing arguments to an agg func */ -#define NC_PartIdx 0x0010 /* True if resolving a partial index WHERE */ +#define NC_HasAgg 0x0010 /* One or more aggregate functions seen */ #define NC_IdxExpr 0x0020 /* True if resolving columns of CREATE INDEX */ +#define NC_VarSelect 0x0040 /* A correlated subquery has been seen */ #define NC_MinMaxAgg 0x1000 /* min/max aggregates seen. See note above */ /* @@ -12983,13 +15041,13 @@ struct NameContext { struct Select { ExprList *pEList; /* The fields of the result */ u8 op; /* One of: TK_UNION TK_ALL TK_INTERSECT TK_EXCEPT */ - u16 selFlags; /* Various SF_* values */ + LogEst nSelectRow; /* Estimated number of result rows */ + u32 selFlags; /* Various SF_* values */ int iLimit, iOffset; /* Memory registers holding LIMIT & OFFSET counters */ #if SELECTTRACE_ENABLED char zSelName[12]; /* Symbolic name of this SELECT use for debugging */ #endif int addrOpenEphm[2]; /* OP_OpenEphem opcodes related to this select */ - u64 nSelectRow; /* Estimated number of result rows */ SrcList *pSrc; /* The FROM clause */ Expr *pWhere; /* The WHERE clause */ ExprList *pGroupBy; /* The GROUP BY clause */ @@ -13005,22 +15063,30 @@ struct Select { /* ** Allowed values for Select.selFlags. The "SF" prefix stands for ** "Select Flag". +** +** Value constraints (all checked via assert()) +** SF_HasAgg == NC_HasAgg +** SF_MinMaxAgg == NC_MinMaxAgg == SQLITE_FUNC_MINMAX +** SF_FixedLimit == WHERE_USE_LIMIT */ -#define SF_Distinct 0x0001 /* Output should be DISTINCT */ -#define SF_All 0x0002 /* Includes the ALL keyword */ -#define SF_Resolved 0x0004 /* Identifiers have been resolved */ -#define SF_Aggregate 0x0008 /* Contains aggregate functions */ -#define SF_UsesEphemeral 0x0010 /* Uses the OpenEphemeral opcode */ -#define SF_Expanded 0x0020 /* sqlite3SelectExpand() called on this */ -#define SF_HasTypeInfo 0x0040 /* FROM subqueries have Table metadata */ -#define SF_Compound 0x0080 /* Part of a compound query */ -#define SF_Values 0x0100 /* Synthesized from VALUES clause */ -#define SF_MultiValue 0x0200 /* Single VALUES term with multiple rows */ -#define SF_NestedFrom 0x0400 /* Part of a parenthesized FROM clause */ -#define SF_MaybeConvert 0x0800 /* Need convertCompoundSelectToSubquery() */ -#define SF_MinMaxAgg 0x1000 /* Aggregate containing min() or max() */ -#define SF_Recursive 0x2000 /* The recursive part of a recursive CTE */ -#define SF_Converted 0x4000 /* By convertCompoundSelectToSubquery() */ +#define SF_Distinct 0x00001 /* Output should be DISTINCT */ +#define SF_All 0x00002 /* Includes the ALL keyword */ +#define SF_Resolved 0x00004 /* Identifiers have been resolved */ +#define SF_Aggregate 0x00008 /* Contains agg functions or a GROUP BY */ +#define SF_HasAgg 0x00010 /* Contains aggregate functions */ +#define SF_UsesEphemeral 0x00020 /* Uses the OpenEphemeral opcode */ +#define SF_Expanded 0x00040 /* sqlite3SelectExpand() called on this */ +#define SF_HasTypeInfo 0x00080 /* FROM subqueries have Table metadata */ +#define SF_Compound 0x00100 /* Part of a compound query */ +#define SF_Values 0x00200 /* Synthesized from VALUES clause */ +#define SF_MultiValue 0x00400 /* Single VALUES term with multiple rows */ +#define SF_NestedFrom 0x00800 /* Part of a parenthesized FROM clause */ +#define SF_MinMaxAgg 0x01000 /* Aggregate containing min() or max() */ +#define SF_Recursive 0x02000 /* The recursive part of a recursive CTE */ +#define SF_FixedLimit 0x04000 /* nSelectRow set by a constant LIMIT */ +#define SF_MaybeConvert 0x08000 /* Need convertCompoundSelectToSubquery() */ +#define SF_Converted 0x10000 /* By convertCompoundSelectToSubquery() */ +#define SF_IncludeHidden 0x20000 /* Include hidden columns in output */ /* @@ -13028,7 +15094,7 @@ struct Select { ** by one of the following macros. The "SRT" prefix means "SELECT Result ** Type". ** -** SRT_Union Store results as a key in a temporary index +** SRT_Union Store results as a key in a temporary index ** identified by pDest->iSDParm. ** ** SRT_Except Remove results from the temporary index pDest->iSDParm. @@ -13052,7 +15118,7 @@ struct Select { ** of the query. This destination implies "LIMIT 1". ** ** SRT_Set The result must be a single column. Store each -** row of result as the key in table pDest->iSDParm. +** row of result as the key in table pDest->iSDParm. ** Apply the affinity pDest->affSdst before storing ** results. Used to implement "IN (SELECT ...)". ** @@ -13120,11 +15186,11 @@ struct SelectDest { }; /* -** During code generation of statements that do inserts into AUTOINCREMENT +** During code generation of statements that do inserts into AUTOINCREMENT ** tables, the following information is attached to the Table.u.autoInc.p ** pointer of each autoincrement table to record some side information that ** the code generator needs. We have to keep per-table autoincrement -** information in case inserts are down within triggers. Triggers do not +** information in case inserts are done within triggers. Triggers do not ** normally coordinate their activities, but we do need to coordinate the ** loading and saving of autoincrement information. */ @@ -13143,7 +15209,7 @@ struct AutoincInfo { #endif /* -** At least one instance of the following structure is created for each +** At least one instance of the following structure is created for each ** trigger that may be fired while parsing an INSERT, UPDATE or DELETE ** statement. All such objects are stored in the linked list headed at ** Parse.pTriggerPrg and deleted once statement compilation has been @@ -13156,7 +15222,7 @@ struct AutoincInfo { ** values for both pTrigger and orconf. ** ** The TriggerPrg.aColmask[0] variable is set to a mask of old.* columns -** accessed (or set to 0 for triggers fired as a result of INSERT +** accessed (or set to 0 for triggers fired as a result of INSERT ** statements). Similarly, the TriggerPrg.aColmask[1] variable is set to ** a mask of new.* columns used by the program. */ @@ -13197,7 +15263,7 @@ struct TriggerPrg { ** is constant but the second part is reset at the beginning and end of ** each recursion. ** -** The nTableLock and aTableLock variables are only used if the shared-cache +** The nTableLock and aTableLock variables are only used if the shared-cache ** feature is enabled (if sqlite3Tsd()->useSharedData is true). They are ** used to store the set of table-locks required by the statement being ** compiled. Function sqlite3TableLock() is used to add entries to the @@ -13216,6 +15282,8 @@ struct Parse { u8 mayAbort; /* True if statement may throw an ABORT exception */ u8 hasCompound; /* Need to invoke convertCompoundSelectToSubquery() */ u8 okConstFactor; /* OK to factor out constants */ + u8 disableLookaside; /* Number of times lookaside has been disabled */ + u8 nColCache; /* Number of entries in aColCache[] */ int aTempReg[8]; /* Holding area for temporary registers */ int nRangeReg; /* Size of the temporary register block */ int iRangeReg; /* First register in temporary register block */ @@ -13225,6 +15293,7 @@ struct Parse { int nSet; /* Number of sets used so far */ int nOnce; /* Number of OP_Once instructions so far */ int nOpAlloc; /* Number of slots allocated for Vdbe.aOp[] */ + int szOpAlloc; /* Bytes of memory space allocated for Vdbe.aOp[] */ int iFixedOp; /* Never back out opcodes iFixedOp-1 or earlier */ int ckBase; /* Base register of data during check constraints */ int iSelfTab; /* Table of an index whose exprs are being coded */ @@ -13276,10 +15345,9 @@ struct Parse { ** in the recursive region. ************************************************************************/ - int nVar; /* Number of '?' variables seen in the SQL so far */ + ynVar nVar; /* Number of '?' variables seen in the SQL so far */ int nzVar; /* Number of available slots in azVar[] */ u8 iPkSortOrder; /* ASC or DESC for INTEGER PRIMARY KEY */ - u8 bFreeWith; /* True if pWith should be freed with parser */ u8 explain; /* True if the EXPLAIN flag is found on the query */ #ifndef SQLITE_OMIT_VIRTUALTABLE u8 declareVtab; /* True if inside sqlite3_declare_vtab() */ @@ -13306,6 +15374,7 @@ struct Parse { Table *pZombieTab; /* List of Table objects to delete after code gen */ TriggerPrg *pTriggerPrg; /* Linked list of coded triggers */ With *pWith; /* Current WITH clause, or NULL */ + With *pWithToFree; /* Free this WITH object at the end of the parse */ }; /* @@ -13328,26 +15397,42 @@ struct AuthContext { /* ** Bitfield flags for P5 value in various opcodes. +** +** Value constraints (enforced via assert()): +** OPFLAG_LENGTHARG == SQLITE_FUNC_LENGTH +** OPFLAG_TYPEOFARG == SQLITE_FUNC_TYPEOF +** OPFLAG_BULKCSR == BTREE_BULKLOAD +** OPFLAG_SEEKEQ == BTREE_SEEK_EQ +** OPFLAG_FORDELETE == BTREE_FORDELETE +** OPFLAG_SAVEPOSITION == BTREE_SAVEPOSITION +** OPFLAG_AUXDELETE == BTREE_AUXDELETE */ -#define OPFLAG_NCHANGE 0x01 /* Set to update db->nChange */ +#define OPFLAG_NCHANGE 0x01 /* OP_Insert: Set to update db->nChange */ + /* Also used in P2 (not P5) of OP_Delete */ #define OPFLAG_EPHEM 0x01 /* OP_Column: Ephemeral output is ok */ #define OPFLAG_LASTROWID 0x02 /* Set to update db->lastRowid */ #define OPFLAG_ISUPDATE 0x04 /* This OP_Insert is an sql UPDATE */ #define OPFLAG_APPEND 0x08 /* This is likely to be an append */ #define OPFLAG_USESEEKRESULT 0x10 /* Try to avoid a seek in BtreeInsert() */ +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +#define OPFLAG_ISNOOP 0x40 /* OP_Delete does pre-update-hook only */ +#endif #define OPFLAG_LENGTHARG 0x40 /* OP_Column only used for length() */ #define OPFLAG_TYPEOFARG 0x80 /* OP_Column only used for typeof() */ #define OPFLAG_BULKCSR 0x01 /* OP_Open** used to open bulk cursor */ #define OPFLAG_SEEKEQ 0x02 /* OP_Open** cursor uses EQ seek only */ -#define OPFLAG_P2ISREG 0x04 /* P2 to OP_Open** is a register number */ +#define OPFLAG_FORDELETE 0x08 /* OP_Open should use BTREE_FORDELETE */ +#define OPFLAG_P2ISREG 0x10 /* P2 to OP_Open** is a register number */ #define OPFLAG_PERMUTE 0x01 /* OP_Compare: use the permutation */ +#define OPFLAG_SAVEPOSITION 0x02 /* OP_Delete: keep cursor position */ +#define OPFLAG_AUXDELETE 0x04 /* OP_Delete: index in a DELETE op */ /* * Each trigger present in the database schema is stored as an instance of - * struct Trigger. + * struct Trigger. * * Pointers to instances of struct Trigger are stored in two ways. - * 1. In the "trigHash" hash table (part of the sqlite3* that represents the + * 1. In the "trigHash" hash table (part of the sqlite3* that represents the * database). This allows Trigger structures to be retrieved by name. * 2. All triggers associated with a single table form a linked list, using the * pNext member of struct Trigger. A pointer to the first element of the @@ -13373,7 +15458,7 @@ struct Trigger { /* ** A trigger is either a BEFORE or an AFTER trigger. The following constants -** determine which. +** determine which. ** ** If there are multiple triggers, you might of some BEFORE and some AFTER. ** In that cases, the constants below can be ORed together. @@ -13383,15 +15468,15 @@ struct Trigger { /* * An instance of struct TriggerStep is used to store a single SQL statement - * that is a part of a trigger-program. + * that is a part of a trigger-program. * * Instances of struct TriggerStep are stored in a singly linked list (linked - * using the "pNext" member) referenced by the "step_list" member of the + * using the "pNext" member) referenced by the "step_list" member of the * associated struct Trigger instance. The first element of the linked list is * the first step of the trigger-program. - * + * * The "op" member indicates whether this is a "DELETE", "INSERT", "UPDATE" or - * "SELECT" statement. The meanings of the other members is determined by the + * "SELECT" statement. The meanings of the other members is determined by the * value of "op" as follows: * * (op == TK_INSERT) @@ -13401,7 +15486,7 @@ struct Trigger { * zTarget -> Dequoted name of the table to insert into. * pExprList -> If this is an INSERT INTO ... VALUES ... statement, then * this stores values to be inserted. Otherwise NULL. - * pIdList -> If this is an INSERT INTO ... () VALUES ... + * pIdList -> If this is an INSERT INTO ... () VALUES ... * statement, then this stores the column-names to be * inserted into. * @@ -13409,7 +15494,7 @@ struct Trigger { * zTarget -> Dequoted name of the table to delete from. * pWhere -> The WHERE clause of the DELETE statement if one is specified. * Otherwise NULL. - * + * * (op == TK_UPDATE) * zTarget -> Dequoted name of the table to update. * pWhere -> The WHERE clause of the UPDATE statement if one is specified. @@ -13417,7 +15502,7 @@ struct Trigger { * pExprList -> A list of the columns to update and the expressions to update * them to. See sqlite3Update() documentation of "pChanges" * argument. - * + * */ struct TriggerStep { u8 op; /* One of TK_DELETE, TK_UPDATE, TK_INSERT, TK_SELECT */ @@ -13435,7 +15520,7 @@ struct TriggerStep { /* ** The following structure contains information used by the sqliteFix... ** routines as they walk the parse tree to make database references -** explicit. +** explicit. */ typedef struct DbFixer DbFixer; struct DbFixer { @@ -13455,13 +15540,20 @@ struct StrAccum { sqlite3 *db; /* Optional database for lookaside. Can be NULL */ char *zBase; /* A base allocation. Not from malloc. */ char *zText; /* The string collected so far */ - int nChar; /* Length of the string so far */ - int nAlloc; /* Amount of space allocated in zText */ - int mxAlloc; /* Maximum allowed allocation. 0 for no malloc usage */ + u32 nChar; /* Length of the string so far */ + u32 nAlloc; /* Amount of space allocated in zText */ + u32 mxAlloc; /* Maximum allowed allocation. 0 for no malloc usage */ u8 accError; /* STRACCUM_NOMEM or STRACCUM_TOOBIG */ + u8 printfFlags; /* SQLITE_PRINTF flags below */ }; #define STRACCUM_NOMEM 1 #define STRACCUM_TOOBIG 2 +#define SQLITE_PRINTF_INTERNAL 0x01 /* Internal-use-only converters allowed */ +#define SQLITE_PRINTF_SQLFUNC 0x02 /* SQL function arguments to VXPrintf */ +#define SQLITE_PRINTF_MALLOCED 0x04 /* True if xText is allocated space */ + +#define isMalloced(X) (((X)->printfFlags & SQLITE_PRINTF_MALLOCED)!=0) + /* ** A pointer to this structure is used to communicate information @@ -13489,6 +15581,7 @@ struct Sqlite3Config { int neverCorrupt; /* Database is always well-formed */ int szLookaside; /* Default lookaside buffer size */ int nLookaside; /* Default lookaside buffer count */ + int nStmtSpill; /* Stmt-journal spill-to-disk threshold */ sqlite3_mem_methods m; /* Low-level memory allocation interface */ sqlite3_mutex_methods mutex; /* Low-level mutex interface */ sqlite3_pcache_methods2 pcache2; /* Low-level page-cache interface */ @@ -13556,10 +15649,10 @@ struct Sqlite3Config { ** Context pointer passed down through the tree-walk. */ struct Walker { + Parse *pParse; /* Parser context. */ int (*xExprCallback)(Walker*, Expr*); /* Callback for expressions */ int (*xSelectCallback)(Walker*,Select*); /* Callback for SELECTs */ void (*xSelectCallback2)(Walker*,Select*);/* Second callback for SELECTs */ - Parse *pParse; /* Parser context. */ int walkerDepth; /* Number of subqueries */ u8 eCode; /* A small processing code */ union { /* Extra data for callback */ @@ -13568,6 +15661,8 @@ struct Walker { int iCur; /* A cursor number */ SrcList *pSrcList; /* FROM clause */ struct SrcCount *pSrcCount; /* Counting column references */ + struct CCurHint *pCCurHint; /* Used by codeCursorHint() */ + int *aiCol; /* array of column indexes */ } u; }; @@ -13577,6 +15672,7 @@ SQLITE_PRIVATE int sqlite3WalkExprList(Walker*, ExprList*); SQLITE_PRIVATE int sqlite3WalkSelect(Walker*, Select*); SQLITE_PRIVATE int sqlite3WalkSelectExpr(Walker*, Select*); SQLITE_PRIVATE int sqlite3WalkSelectFrom(Walker*, Select*); +SQLITE_PRIVATE int sqlite3ExprWalkNoop(Walker*, Expr*); /* ** Return code from the parse-tree walking primitives and their @@ -13635,7 +15731,23 @@ SQLITE_PRIVATE int sqlite3CantopenError(int); #define SQLITE_CORRUPT_BKPT sqlite3CorruptError(__LINE__) #define SQLITE_MISUSE_BKPT sqlite3MisuseError(__LINE__) #define SQLITE_CANTOPEN_BKPT sqlite3CantopenError(__LINE__) +#ifdef SQLITE_DEBUG +SQLITE_PRIVATE int sqlite3NomemError(int); +SQLITE_PRIVATE int sqlite3IoerrnomemError(int); +# define SQLITE_NOMEM_BKPT sqlite3NomemError(__LINE__) +# define SQLITE_IOERR_NOMEM_BKPT sqlite3IoerrnomemError(__LINE__) +#else +# define SQLITE_NOMEM_BKPT SQLITE_NOMEM +# define SQLITE_IOERR_NOMEM_BKPT SQLITE_IOERR_NOMEM +#endif +/* +** FTS3 and FTS4 both require virtual table support +*/ +#if defined(SQLITE_OMIT_VIRTUALTABLE) +# undef SQLITE_ENABLE_FTS3 +# undef SQLITE_ENABLE_FTS4 +#endif /* ** FTS4 is really an extension for FTS3. It is enabled using the @@ -13668,6 +15780,7 @@ SQLITE_PRIVATE int sqlite3CantopenError(int); # define sqlite3Isdigit(x) (sqlite3CtypeMap[(unsigned char)(x)]&0x04) # define sqlite3Isxdigit(x) (sqlite3CtypeMap[(unsigned char)(x)]&0x08) # define sqlite3Tolower(x) (sqlite3UpperToLower[(unsigned char)(x)]) +# define sqlite3Isquote(x) (sqlite3CtypeMap[(unsigned char)(x)]&0x80) #else # define sqlite3Toupper(x) toupper((unsigned char)(x)) # define sqlite3Isspace(x) isspace((unsigned char)(x)) @@ -13676,6 +15789,7 @@ SQLITE_PRIVATE int sqlite3CantopenError(int); # define sqlite3Isdigit(x) isdigit((unsigned char)(x)) # define sqlite3Isxdigit(x) isxdigit((unsigned char)(x)) # define sqlite3Tolower(x) tolower((unsigned char)(x)) +# define sqlite3Isquote(x) ((x)=='"'||(x)=='\''||(x)=='['||(x)=='`') #endif #ifndef SQLITE_OMIT_COMPILEOPTION_DIAGS SQLITE_PRIVATE int sqlite3IsIdChar(u8); @@ -13684,8 +15798,9 @@ SQLITE_PRIVATE int sqlite3IsIdChar(u8); /* ** Internal function prototypes */ -#define sqlite3StrICmp sqlite3_stricmp +SQLITE_PRIVATE int sqlite3StrICmp(const char*,const char*); SQLITE_PRIVATE int sqlite3Strlen30(const char*); +SQLITE_PRIVATE char *sqlite3ColumnType(Column*,char*); #define sqlite3StrNICmp sqlite3_strnicmp SQLITE_PRIVATE int sqlite3MallocInit(void); @@ -13694,6 +15809,7 @@ SQLITE_PRIVATE void *sqlite3Malloc(u64); SQLITE_PRIVATE void *sqlite3MallocZero(u64); SQLITE_PRIVATE void *sqlite3DbMallocZero(sqlite3*, u64); SQLITE_PRIVATE void *sqlite3DbMallocRaw(sqlite3*, u64); +SQLITE_PRIVATE void *sqlite3DbMallocRawNN(sqlite3*, u64); SQLITE_PRIVATE char *sqlite3DbStrDup(sqlite3*,const char*); SQLITE_PRIVATE char *sqlite3DbStrNDup(sqlite3*,const char*, u64); SQLITE_PRIVATE void *sqlite3Realloc(void*, u64); @@ -13723,7 +15839,7 @@ SQLITE_PRIVATE int sqlite3HeapNearlyFull(void); #ifdef SQLITE_USE_ALLOCA # define sqlite3StackAllocRaw(D,N) alloca(N) # define sqlite3StackAllocZero(D,N) memset(alloca(N), 0, N) -# define sqlite3StackFree(D,P) +# define sqlite3StackFree(D,P) #else # define sqlite3StackAllocRaw(D,N) sqlite3DbMallocRaw(D,N) # define sqlite3StackAllocZero(D,N) sqlite3DbMallocZero(D,N) @@ -13754,7 +15870,7 @@ SQLITE_PRIVATE void sqlite3MemoryBarrier(void); SQLITE_PRIVATE sqlite3_int64 sqlite3StatusValue(int); SQLITE_PRIVATE void sqlite3StatusUp(int, int); SQLITE_PRIVATE void sqlite3StatusDown(int, int); -SQLITE_PRIVATE void sqlite3StatusSet(int, int); +SQLITE_PRIVATE void sqlite3StatusHighwater(int, int); /* Access to mutexes used by sqlite3_status() */ SQLITE_PRIVATE sqlite3_mutex *sqlite3Pcache1Mutex(void); @@ -13776,10 +15892,8 @@ struct PrintfArguments { sqlite3_value **apArg; /* The argument values */ }; -#define SQLITE_PRINTF_INTERNAL 0x01 -#define SQLITE_PRINTF_SQLFUNC 0x02 -SQLITE_PRIVATE void sqlite3VXPrintf(StrAccum*, u32, const char*, va_list); -SQLITE_PRIVATE void sqlite3XPrintf(StrAccum*, u32, const char*, ...); +SQLITE_PRIVATE void sqlite3VXPrintf(StrAccum*, const char*, va_list); +SQLITE_PRIVATE void sqlite3XPrintf(StrAccum*, const char*, ...); SQLITE_PRIVATE char *sqlite3MPrintf(sqlite3*,const char*, ...); SQLITE_PRIVATE char *sqlite3VMPrintf(sqlite3*,const char*, va_list); #if defined(SQLITE_DEBUG) || defined(SQLITE_HAVE_OS_TRACE) @@ -13793,12 +15907,14 @@ SQLITE_PRIVATE void *sqlite3TestTextToPtr(const char*); SQLITE_PRIVATE void sqlite3TreeViewExpr(TreeView*, const Expr*, u8); SQLITE_PRIVATE void sqlite3TreeViewExprList(TreeView*, const ExprList*, u8, const char*); SQLITE_PRIVATE void sqlite3TreeViewSelect(TreeView*, const Select*, u8); +SQLITE_PRIVATE void sqlite3TreeViewWith(TreeView*, const With*, u8); #endif SQLITE_PRIVATE void sqlite3SetString(char **, sqlite3*, const char*); SQLITE_PRIVATE void sqlite3ErrorMsg(Parse*, const char*, ...); -SQLITE_PRIVATE int sqlite3Dequote(char*); +SQLITE_PRIVATE void sqlite3Dequote(char*); +SQLITE_PRIVATE void sqlite3TokenInit(Token*,char*); SQLITE_PRIVATE int sqlite3KeywordCode(const unsigned char*, int); SQLITE_PRIVATE int sqlite3RunParser(Parse*, const char*, char **); SQLITE_PRIVATE void sqlite3FinishCoding(Parse*); @@ -13807,10 +15923,14 @@ SQLITE_PRIVATE void sqlite3ReleaseTempReg(Parse*,int); SQLITE_PRIVATE int sqlite3GetTempRange(Parse*,int); SQLITE_PRIVATE void sqlite3ReleaseTempRange(Parse*,int,int); SQLITE_PRIVATE void sqlite3ClearTempRegCache(Parse*); +#ifdef SQLITE_DEBUG +SQLITE_PRIVATE int sqlite3NoTempsInRange(Parse*,int,int); +#endif SQLITE_PRIVATE Expr *sqlite3ExprAlloc(sqlite3*,int,const Token*,int); SQLITE_PRIVATE Expr *sqlite3Expr(sqlite3*,int,const char*); SQLITE_PRIVATE void sqlite3ExprAttachSubtrees(sqlite3*,Expr*,Expr*,Expr*); SQLITE_PRIVATE Expr *sqlite3PExpr(Parse*, int, Expr*, Expr*, const Token*); +SQLITE_PRIVATE void sqlite3PExprAddSelect(Parse*, Expr*, Select*); SQLITE_PRIVATE Expr *sqlite3ExprAnd(sqlite3*,Expr*, Expr*); SQLITE_PRIVATE Expr *sqlite3ExprFunction(Parse*,ExprList*, Token*); SQLITE_PRIVATE void sqlite3ExprAssignVarNumber(Parse*, Expr*); @@ -13827,20 +15947,24 @@ SQLITE_PRIVATE void sqlite3Pragma(Parse*,Token*,Token*,Token*,int); SQLITE_PRIVATE void sqlite3ResetAllSchemasOfConnection(sqlite3*); SQLITE_PRIVATE void sqlite3ResetOneSchema(sqlite3*,int); SQLITE_PRIVATE void sqlite3CollapseDatabaseArray(sqlite3*); -SQLITE_PRIVATE void sqlite3BeginParse(Parse*,int); SQLITE_PRIVATE void sqlite3CommitInternalChanges(sqlite3*); SQLITE_PRIVATE void sqlite3DeleteColumnNames(sqlite3*,Table*); SQLITE_PRIVATE int sqlite3ColumnsFromExprList(Parse*,ExprList*,i16*,Column**); +SQLITE_PRIVATE void sqlite3SelectAddColumnTypeAndCollation(Parse*,Table*,Select*); SQLITE_PRIVATE Table *sqlite3ResultSetOfSelect(Parse*,Select*); SQLITE_PRIVATE void sqlite3OpenMasterTable(Parse *, int); SQLITE_PRIVATE Index *sqlite3PrimaryKeyIndex(Table*); SQLITE_PRIVATE i16 sqlite3ColumnOfIndex(Index*, i16); SQLITE_PRIVATE void sqlite3StartTable(Parse*,Token*,Token*,int,int,int,int); -SQLITE_PRIVATE void sqlite3AddColumn(Parse*,Token*); +#if SQLITE_ENABLE_HIDDEN_COLUMNS +SQLITE_PRIVATE void sqlite3ColumnPropertiesFromName(Table*, Column*); +#else +# define sqlite3ColumnPropertiesFromName(T,C) /* no-op */ +#endif +SQLITE_PRIVATE void sqlite3AddColumn(Parse*,Token*,Token*); SQLITE_PRIVATE void sqlite3AddNotNull(Parse*, int); SQLITE_PRIVATE void sqlite3AddPrimaryKey(Parse*, ExprList*, int, int, int); SQLITE_PRIVATE void sqlite3AddCheckConstraint(Parse*, Expr*); -SQLITE_PRIVATE void sqlite3AddColumnType(Parse*,Token*); SQLITE_PRIVATE void sqlite3AddDefaultValue(Parse*,ExprSpan*); SQLITE_PRIVATE void sqlite3AddCollateType(Parse*, Token*); SQLITE_PRIVATE void sqlite3EndTable(Parse*,Token*,Token*,u8,Select*); @@ -13914,7 +16038,7 @@ SQLITE_PRIVATE Index *sqlite3CreateIndex(Parse*,Token*,Token*,SrcList*,ExprList* SQLITE_PRIVATE void sqlite3DropIndex(Parse*, SrcList*, int); SQLITE_PRIVATE int sqlite3Select(Parse*, Select*, SelectDest*); SQLITE_PRIVATE Select *sqlite3SelectNew(Parse*,ExprList*,SrcList*,Expr*,ExprList*, - Expr*,ExprList*,u16,Expr*,Expr*); + Expr*,ExprList*,u32,Expr*,Expr*); SQLITE_PRIVATE void sqlite3SelectDelete(sqlite3*, Select*); SQLITE_PRIVATE Table *sqlite3SrcListLookup(Parse*, SrcList*); SQLITE_PRIVATE int sqlite3IsReadOnly(Parse*, Table*, int); @@ -13926,7 +16050,7 @@ SQLITE_PRIVATE void sqlite3DeleteFrom(Parse*, SrcList*, Expr*); SQLITE_PRIVATE void sqlite3Update(Parse*, SrcList*, ExprList*, Expr*, int); SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin(Parse*,SrcList*,Expr*,ExprList*,ExprList*,u16,int); SQLITE_PRIVATE void sqlite3WhereEnd(WhereInfo*); -SQLITE_PRIVATE u64 sqlite3WhereOutputRowCount(WhereInfo*); +SQLITE_PRIVATE LogEst sqlite3WhereOutputRowCount(WhereInfo*); SQLITE_PRIVATE int sqlite3WhereIsDistinct(WhereInfo*); SQLITE_PRIVATE int sqlite3WhereIsOrdered(WhereInfo*); SQLITE_PRIVATE int sqlite3WhereIsSorted(WhereInfo*); @@ -13938,6 +16062,7 @@ SQLITE_PRIVATE int sqlite3WhereOkOnePass(WhereInfo*, int*); #define ONEPASS_MULTI 2 /* ONEPASS is valid for multiple rows */ SQLITE_PRIVATE void sqlite3ExprCodeLoadIndexColumn(Parse*, Index*, int, int, int); SQLITE_PRIVATE int sqlite3ExprCodeGetColumn(Parse*, Table*, int, int, int, u8); +SQLITE_PRIVATE void sqlite3ExprCodeGetColumnToReg(Parse*, Table*, int, int, int); SQLITE_PRIVATE void sqlite3ExprCodeGetColumnOfTable(Vdbe*, Table*, int, int, int); SQLITE_PRIVATE void sqlite3ExprCodeMove(Parse*, int, int, int); SQLITE_PRIVATE void sqlite3ExprCacheStore(Parse*, int, int, int); @@ -13947,6 +16072,7 @@ SQLITE_PRIVATE void sqlite3ExprCacheRemove(Parse*, int, int); SQLITE_PRIVATE void sqlite3ExprCacheClear(Parse*); SQLITE_PRIVATE void sqlite3ExprCacheAffinityChange(Parse*, int, int); SQLITE_PRIVATE void sqlite3ExprCode(Parse*, Expr*, int); +SQLITE_PRIVATE void sqlite3ExprCodeCopy(Parse*, Expr*, int); SQLITE_PRIVATE void sqlite3ExprCodeFactorable(Parse*, Expr*, int); SQLITE_PRIVATE void sqlite3ExprCodeAtInit(Parse*, Expr*, int, u8); SQLITE_PRIVATE int sqlite3ExprCodeTemp(Parse*, Expr*, int*); @@ -13992,6 +16118,9 @@ SQLITE_PRIVATE int sqlite3ExprIsConstant(Expr*); SQLITE_PRIVATE int sqlite3ExprIsConstantNotJoin(Expr*); SQLITE_PRIVATE int sqlite3ExprIsConstantOrFunction(Expr*, u8); SQLITE_PRIVATE int sqlite3ExprIsTableConstant(Expr*,int); +#ifdef SQLITE_ENABLE_CURSOR_HINTS +SQLITE_PRIVATE int sqlite3ExprContainsSubquery(Expr*); +#endif SQLITE_PRIVATE int sqlite3ExprIsInteger(Expr*, int*); SQLITE_PRIVATE int sqlite3ExprCanBeNull(const Expr*); SQLITE_PRIVATE int sqlite3ExprNeedsNoAffinityChange(const Expr*, char); @@ -14002,9 +16131,9 @@ SQLITE_PRIVATE void sqlite3GenerateRowIndexDelete(Parse*, Table*, int, int, int* SQLITE_PRIVATE int sqlite3GenerateIndexKey(Parse*, Index*, int, int, int, int*,Index*,int); SQLITE_PRIVATE void sqlite3ResolvePartIdxLabel(Parse*,int); SQLITE_PRIVATE void sqlite3GenerateConstraintChecks(Parse*,Table*,int*,int,int,int,int, - u8,u8,int,int*); + u8,u8,int,int*,int*); SQLITE_PRIVATE void sqlite3CompleteInsertion(Parse*,Table*,int,int,int,int*,int,int,int); -SQLITE_PRIVATE int sqlite3OpenTableAndIndices(Parse*, Table*, int, int, u8*, int*, int*); +SQLITE_PRIVATE int sqlite3OpenTableAndIndices(Parse*, Table*, int, u8, int, u8*, int*, int*); SQLITE_PRIVATE void sqlite3BeginWriteOperation(Parse*, int, int); SQLITE_PRIVATE void sqlite3MultiWrite(Parse*); SQLITE_PRIVATE void sqlite3MayAbort(Parse*); @@ -14021,11 +16150,11 @@ SQLITE_PRIVATE void sqlite3SelectSetName(Select*,const char*); #else # define sqlite3SelectSetName(A,B) #endif -SQLITE_PRIVATE void sqlite3FuncDefInsert(FuncDefHash*, FuncDef*); -SQLITE_PRIVATE FuncDef *sqlite3FindFunction(sqlite3*,const char*,int,int,u8,u8); -SQLITE_PRIVATE void sqlite3RegisterBuiltinFunctions(sqlite3*); +SQLITE_PRIVATE void sqlite3InsertBuiltinFuncs(FuncDef*,int); +SQLITE_PRIVATE FuncDef *sqlite3FindFunction(sqlite3*,const char*,int,u8,u8); +SQLITE_PRIVATE void sqlite3RegisterBuiltinFunctions(void); SQLITE_PRIVATE void sqlite3RegisterDateTimeFunctions(void); -SQLITE_PRIVATE void sqlite3RegisterGlobalFunctions(void); +SQLITE_PRIVATE void sqlite3RegisterPerConnectionBuiltinFunctions(sqlite3*); SQLITE_PRIVATE int sqlite3SafetyCheckOk(sqlite3*); SQLITE_PRIVATE int sqlite3SafetyCheckSickOrOk(sqlite3*); SQLITE_PRIVATE void sqlite3ChangeCookie(Parse*, int); @@ -14104,7 +16233,11 @@ SQLITE_PRIVATE LogEst sqlite3LogEstAdd(LogEst,LogEst); #ifndef SQLITE_OMIT_VIRTUALTABLE SQLITE_PRIVATE LogEst sqlite3LogEstFromDouble(double); #endif +#if defined(SQLITE_ENABLE_STMT_SCANSTATUS) || \ + defined(SQLITE_ENABLE_STAT3_OR_STAT4) || \ + defined(SQLITE_EXPLAIN_ESTIMATED_ROWS) SQLITE_PRIVATE u64 sqlite3LogEstToInt(LogEst); +#endif /* ** Routines to read and write variable-length integers. These used to @@ -14139,6 +16272,7 @@ SQLITE_PRIVATE int sqlite3Atoi64(const char*, i64*, int, u8); SQLITE_PRIVATE int sqlite3DecOrHexToI64(const char*, i64*); SQLITE_PRIVATE void sqlite3ErrorWithMsg(sqlite3*, int, const char*,...); SQLITE_PRIVATE void sqlite3Error(sqlite3*,int); +SQLITE_PRIVATE void sqlite3SystemError(sqlite3*,int); SQLITE_PRIVATE void *sqlite3HexToBlob(sqlite3*, const char *z, int n); SQLITE_PRIVATE u8 sqlite3HexToInt(int h); SQLITE_PRIVATE int sqlite3TwoPartName(Parse *, Token *, Token *, Token **); @@ -14171,7 +16305,7 @@ SQLITE_PRIVATE u8 sqlite3GetBoolean(const char *z,u8); SQLITE_PRIVATE const void *sqlite3ValueText(sqlite3_value*, u8); SQLITE_PRIVATE int sqlite3ValueBytes(sqlite3_value*, u8); -SQLITE_PRIVATE void sqlite3ValueSetStr(sqlite3_value*, int, const void *,u8, +SQLITE_PRIVATE void sqlite3ValueSetStr(sqlite3_value*, int, const void *,u8, void(*)(void*)); SQLITE_PRIVATE void sqlite3ValueSetNull(sqlite3_value*); SQLITE_PRIVATE void sqlite3ValueFree(sqlite3_value*); @@ -14181,11 +16315,12 @@ SQLITE_PRIVATE int sqlite3ValueFromExpr(sqlite3 *, Expr *, u8, u8, sqlite3_value SQLITE_PRIVATE void sqlite3ValueApplyAffinity(sqlite3_value *, u8, u8); #ifndef SQLITE_AMALGAMATION SQLITE_PRIVATE const unsigned char sqlite3OpcodeProperty[]; +SQLITE_PRIVATE const char sqlite3StrBINARY[]; SQLITE_PRIVATE const unsigned char sqlite3UpperToLower[]; SQLITE_PRIVATE const unsigned char sqlite3CtypeMap[]; SQLITE_PRIVATE const Token sqlite3IntTokens[]; SQLITE_PRIVATE SQLITE_WSD struct Sqlite3Config sqlite3Config; -SQLITE_PRIVATE SQLITE_WSD FuncDefHash sqlite3GlobalFunctions; +SQLITE_PRIVATE FuncDefHash sqlite3BuiltinFunctions; #ifndef SQLITE_OMIT_WSD SQLITE_PRIVATE int sqlite3PendingByte; #endif @@ -14220,7 +16355,6 @@ SQLITE_PRIVATE void sqlite3DeleteIndexSamples(sqlite3*,Index*); SQLITE_PRIVATE void sqlite3DefaultRowEst(Index*); SQLITE_PRIVATE void sqlite3RegisterLikeFunctions(sqlite3*, int); SQLITE_PRIVATE int sqlite3IsLikeFunction(sqlite3*,Expr*,int*,char*); -SQLITE_PRIVATE void sqlite3MinimumFileFormat(Parse*, int, int); SQLITE_PRIVATE void sqlite3SchemaClear(void *); SQLITE_PRIVATE Schema *sqlite3SchemaGet(sqlite3 *, Btree *); SQLITE_PRIVATE int sqlite3SchemaToIndex(sqlite3 *db, Schema *); @@ -14231,11 +16365,13 @@ SQLITE_PRIVATE KeyInfo *sqlite3KeyInfoOfIndex(Parse*, Index*); #ifdef SQLITE_DEBUG SQLITE_PRIVATE int sqlite3KeyInfoIsWriteable(KeyInfo*); #endif -SQLITE_PRIVATE int sqlite3CreateFunc(sqlite3 *, const char *, int, int, void *, +SQLITE_PRIVATE int sqlite3CreateFunc(sqlite3 *, const char *, int, int, void *, void (*)(sqlite3_context*,int,sqlite3_value **), void (*)(sqlite3_context*,int,sqlite3_value **), void (*)(sqlite3_context*), FuncDestructor *pDestructor ); +SQLITE_PRIVATE void sqlite3OomFault(sqlite3*); +SQLITE_PRIVATE void sqlite3OomClear(sqlite3*); SQLITE_PRIVATE int sqlite3ApiExit(sqlite3 *db, int); SQLITE_PRIVATE int sqlite3OpenTempDatabase(Parse *); @@ -14292,7 +16428,7 @@ SQLITE_PRIVATE int sqlite3Utf8To8(unsigned char*); # define sqlite3VtabRollback(X) # define sqlite3VtabCommit(X) # define sqlite3VtabInSync(db) 0 -# define sqlite3VtabLock(X) +# define sqlite3VtabLock(X) # define sqlite3VtabUnlock(X) # define sqlite3VtabUnlockList(X) # define sqlite3VtabSavepoint(X, Y, Z) SQLITE_OK @@ -14350,7 +16486,7 @@ SQLITE_PRIVATE void sqlite3WithPush(Parse*, With*, u8); ** no-op macros if OMIT_FOREIGN_KEY is defined. In this case no foreign ** key functionality is available. If OMIT_TRIGGER is defined but ** OMIT_FOREIGN_KEY is not, only some of the functions are no-oped. In -** this case foreign keys are parsed, but no other functionality is +** this case foreign keys are parsed, but no other functionality is ** provided (enforcement of FK constraints requires the triggers sub-system). */ #if !defined(SQLITE_OMIT_FOREIGN_KEY) && !defined(SQLITE_OMIT_TRIGGER) @@ -14411,19 +16547,14 @@ SQLITE_PRIVATE void sqlite3EndBenignMalloc(void); #define IN_INDEX_LOOP 0x0004 /* IN operator used as a loop */ SQLITE_PRIVATE int sqlite3FindInIndex(Parse *, Expr *, u32, int*); +SQLITE_PRIVATE int sqlite3JournalOpen(sqlite3_vfs *, const char *, sqlite3_file *, int, int); +SQLITE_PRIVATE int sqlite3JournalSize(sqlite3_vfs *); #ifdef SQLITE_ENABLE_ATOMIC_WRITE -SQLITE_PRIVATE int sqlite3JournalOpen(sqlite3_vfs *, const char *, sqlite3_file *, int, int); -SQLITE_PRIVATE int sqlite3JournalSize(sqlite3_vfs *); SQLITE_PRIVATE int sqlite3JournalCreate(sqlite3_file *); -SQLITE_PRIVATE int sqlite3JournalExists(sqlite3_file *p); -#else - #define sqlite3JournalSize(pVfs) ((pVfs)->szOsFile) - #define sqlite3JournalExists(p) 1 #endif +SQLITE_PRIVATE int sqlite3JournalIsInMemory(sqlite3_file *p); SQLITE_PRIVATE void sqlite3MemJournalOpen(sqlite3_file *); -SQLITE_PRIVATE int sqlite3MemJournalSize(void); -SQLITE_PRIVATE int sqlite3IsMemJournal(sqlite3_file *); SQLITE_PRIVATE void sqlite3ExprSetHeightAndFlags(Parse *pParse, Expr *p); #if SQLITE_MAX_EXPR_DEPTH>0 @@ -14454,7 +16585,7 @@ SQLITE_PRIVATE void sqlite3ParserTrace(FILE*, char *); /* ** If the SQLITE_ENABLE IOTRACE exists then the global variable ** sqlite3IoTrace is a pointer to a printf-like routine used to -** print I/O tracing messages. +** print I/O tracing messages. */ #ifdef SQLITE_ENABLE_IOTRACE # define IOTRACE(A) if( sqlite3IoTrace ){ sqlite3IoTrace A; } @@ -14488,7 +16619,7 @@ SQLITE_API SQLITE_EXTERN void (SQLITE_CDECL *sqlite3IoTrace)(const char*,...); ** that allocations that might have been satisfied by lookaside are not ** passed back to non-lookaside free() routines. Asserts such as the ** example above are placed on the non-lookaside free() routines to verify -** this constraint. +** this constraint. ** ** All of this is no-op for a production build. It only comes into ** play when the SQLITE_MEMDEBUG compile-time option is used. @@ -14595,6 +16726,7 @@ SQLITE_PRIVATE const unsigned char sqlite3UpperToLower[] = { ** isxdigit() 0x08 ** toupper() 0x20 ** SQLite identifier character 0x40 +** Quote character 0x80 ** ** Bit 0x20 is set if the mapped character requires translation to upper ** case. i.e. if the character is a lower-case ASCII character. @@ -14620,7 +16752,7 @@ SQLITE_PRIVATE const unsigned char sqlite3CtypeMap[256] = { 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, /* 08..0f ........ */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 10..17 ........ */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 18..1f ........ */ - 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, /* 20..27 !"#$%&' */ + 0x01, 0x00, 0x80, 0x00, 0x40, 0x00, 0x00, 0x80, /* 20..27 !"#$%&' */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 28..2f ()*+,-./ */ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, /* 30..37 01234567 */ 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 38..3f 89:;<=>? */ @@ -14628,8 +16760,8 @@ SQLITE_PRIVATE const unsigned char sqlite3CtypeMap[256] = { 0x00, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x02, /* 40..47 @ABCDEFG */ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, /* 48..4f HIJKLMNO */ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, /* 50..57 PQRSTUVW */ - 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x40, /* 58..5f XYZ[\]^_ */ - 0x00, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x22, /* 60..67 `abcdefg */ + 0x02, 0x02, 0x02, 0x80, 0x00, 0x00, 0x00, 0x40, /* 58..5f XYZ[\]^_ */ + 0x80, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x22, /* 60..67 `abcdefg */ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, /* 68..6f hijklmno */ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, /* 70..77 pqrstuvw */ 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, /* 78..7f xyz{|}~. */ @@ -14684,6 +16816,18 @@ SQLITE_PRIVATE const unsigned char sqlite3CtypeMap[256] = { # define SQLITE_SORTER_PMASZ 250 #endif +/* Statement journals spill to disk when their size exceeds the following +** threashold (in bytes). 0 means that statement journals are created and +** written to disk immediately (the default behavior for SQLite versions +** before 3.12.0). -1 means always keep the entire statement journal in +** memory. (The statement journal is also always held entirely in memory +** if journal_mode=MEMORY or if temp_store=MEMORY, regardless of this +** setting.) +*/ +#ifndef SQLITE_STMTJRNL_SPILL +# define SQLITE_STMTJRNL_SPILL (64*1024) +#endif + /* ** The following singleton contains the global configuration for ** the SQLite library. @@ -14698,6 +16842,7 @@ SQLITE_PRIVATE SQLITE_WSD struct Sqlite3Config sqlite3Config = { 0, /* neverCorrupt */ 128, /* szLookaside */ 500, /* nLookaside */ + SQLITE_STMTJRNL_SPILL, /* nStmtSpill */ {0,0,0,0,0,0,0,0}, /* m */ {0,0,0,0,0,0,0,0,0}, /* mutex */ {0,0,0,0,0,0,0,0,0,0,0,0,0},/* pcache2 */ @@ -14744,7 +16889,7 @@ SQLITE_PRIVATE SQLITE_WSD struct Sqlite3Config sqlite3Config = { ** database connections. After initialization, this table is ** read-only. */ -SQLITE_PRIVATE SQLITE_WSD FuncDefHash sqlite3GlobalFunctions; +SQLITE_PRIVATE FuncDefHash sqlite3BuiltinFunctions; /* ** Constant tokens for values 0 and 1. @@ -14786,6 +16931,11 @@ SQLITE_PRIVATE int sqlite3PendingByte = 0x40000000; */ SQLITE_PRIVATE const unsigned char sqlite3OpcodeProperty[] = OPFLG_INITIALIZER; +/* +** Name of the default collating sequence +*/ +SQLITE_PRIVATE const char sqlite3StrBINARY[] = "BINARY"; + /************** End of global.c **********************************************/ /************** Begin file ctime.c *******************************************/ /* @@ -14853,6 +17003,9 @@ static const char * const azCompileOpt[] = { #if SQLITE_DISABLE_LFS "DISABLE_LFS", #endif +#if SQLITE_ENABLE_8_3_NAMES + "ENABLE_8_3_NAMES", +#endif #if SQLITE_ENABLE_API_ARMOR "ENABLE_API_ARMOR", #endif @@ -14948,6 +17101,9 @@ static const char * const azCompileOpt[] = { #ifdef SQLITE_INT64_TYPE "INT64_TYPE", #endif +#ifdef SQLITE_LIKE_DOESNT_MATCH_BLOBS + "LIKE_DOESNT_MATCH_BLOBS", +#endif #if SQLITE_LOCK_TRACE "LOCK_TRACE", #endif @@ -15273,6 +17429,17 @@ SQLITE_API const char *SQLITE_STDCALL sqlite3_compileoption_get(int N){ # define SQLITE_MAX_SCHEMA_RETRY 50 #endif +/* +** VDBE_DISPLAY_P4 is true or false depending on whether or not the +** "explain" P4 display logic is enabled. +*/ +#if !defined(SQLITE_OMIT_EXPLAIN) || !defined(NDEBUG) \ + || defined(VDBE_PROFILE) || defined(SQLITE_DEBUG) +# define VDBE_DISPLAY_P4 1 +#else +# define VDBE_DISPLAY_P4 0 +#endif + /* ** SQL is translated into a sequence of instructions to be ** executed by a virtual machine. Each instruction is an instance @@ -15294,42 +17461,52 @@ typedef struct Explain Explain; /* Elements of the linked list at Vdbe.pAuxData */ typedef struct AuxData AuxData; +/* Types of VDBE cursors */ +#define CURTYPE_BTREE 0 +#define CURTYPE_SORTER 1 +#define CURTYPE_VTAB 2 +#define CURTYPE_PSEUDO 3 + /* -** A cursor is a pointer into a single BTree within a database file. -** The cursor can seek to a BTree entry with a particular key, or -** loop over all entries of the Btree. You can also insert new BTree -** entries or retrieve the key or data from the entry that the cursor -** is currently pointing to. +** A VdbeCursor is an superclass (a wrapper) for various cursor objects: ** -** Cursors can also point to virtual tables, sorters, or "pseudo-tables". -** A pseudo-table is a single-row table implemented by registers. -** -** Every cursor that the virtual machine has open is represented by an -** instance of the following structure. +** * A b-tree cursor +** - In the main database or in an ephemeral database +** - On either an index or a table +** * A sorter +** * A virtual table +** * A one-row "pseudotable" stored in a single register */ +typedef struct VdbeCursor VdbeCursor; struct VdbeCursor { - BtCursor *pCursor; /* The cursor structure of the backend */ - Btree *pBt; /* Separate file holding temporary table */ - KeyInfo *pKeyInfo; /* Info about index keys needed by index cursors */ - int seekResult; /* Result of previous sqlite3BtreeMoveto() */ - int pseudoTableReg; /* Register holding pseudotable content. */ - i16 nField; /* Number of fields in the header */ - u16 nHdrParsed; /* Number of header fields parsed so far */ -#ifdef SQLITE_DEBUG - u8 seekOp; /* Most recent seek operation on this cursor */ -#endif + u8 eCurType; /* One of the CURTYPE_* values above */ i8 iDb; /* Index of cursor database in db->aDb[] (or -1) */ u8 nullRow; /* True if pointing to a row with no data */ u8 deferredMoveto; /* A call to sqlite3BtreeMoveto() is needed */ + u8 isTable; /* True for rowid tables. False for indexes */ +#ifdef SQLITE_DEBUG + u8 seekOp; /* Most recent seek operation on this cursor */ + u8 wrFlag; /* The wrFlag argument to sqlite3BtreeCursor() */ +#endif Bool isEphemeral:1; /* True for an ephemeral table */ Bool useRandomRowid:1;/* Generate new record numbers semi-randomly */ - Bool isTable:1; /* True if a table requiring integer keys */ - Bool isOrdered:1; /* True if the underlying table is BTREE_UNORDERED */ + Bool isOrdered:1; /* True if the table is not BTREE_UNORDERED */ Pgno pgnoRoot; /* Root page of the open btree cursor */ - sqlite3_vtab_cursor *pVtabCursor; /* The cursor for a virtual table */ + i16 nField; /* Number of fields in the header */ + u16 nHdrParsed; /* Number of header fields parsed so far */ + union { + BtCursor *pCursor; /* CURTYPE_BTREE. Btree cursor */ + sqlite3_vtab_cursor *pVCur; /* CURTYPE_VTAB. Vtab cursor */ + int pseudoTableReg; /* CURTYPE_PSEUDO. Reg holding content. */ + VdbeSorter *pSorter; /* CURTYPE_SORTER. Sorter object */ + } uc; + Btree *pBt; /* Separate file holding temporary table */ + KeyInfo *pKeyInfo; /* Info about index keys needed by index cursors */ + int seekResult; /* Result of previous sqlite3BtreeMoveto() */ i64 seqCount; /* Sequence counter */ i64 movetoTarget; /* Argument to the deferred sqlite3BtreeMoveto() */ - VdbeSorter *pSorter; /* Sorter object for OP_SorterOpen cursors */ + VdbeCursor *pAltCursor; /* Associated index cursor from which to read */ + int *aAltMap; /* Mapping from table to index column numbers */ #ifdef SQLITE_ENABLE_COLUMN_USED_MASK u64 maskUsed; /* Mask of columns used by this cursor */ #endif @@ -15354,7 +17531,6 @@ struct VdbeCursor { ** static element declared in the structure. nField total array slots for ** aType[] and nField+1 array slots for aOffset[] */ }; -typedef struct VdbeCursor VdbeCursor; /* ** When a sub-program is executed (OP_Program), a structure of this type @@ -15388,6 +17564,7 @@ struct VdbeFrame { VdbeCursor **apCsr; /* Array of Vdbe cursors for parent frame */ void *token; /* Copy of SubProgram.token */ i64 lastRowid; /* Last insert rowid (sqlite3.lastRowid) */ + AuxData *pAuxData; /* Linked list of auxdata allocations */ int nCursor; /* Number of entries in apCsr */ int pc; /* Program Counter in parent (calling) frame */ int nOp; /* Size of aOp array */ @@ -15465,7 +17642,7 @@ struct Mem { #define MEM_Frame 0x0040 /* Value is a VdbeFrame object */ #define MEM_Undefined 0x0080 /* Value is undefined */ #define MEM_Cleared 0x0100 /* NULL set by OP_Null, not from data */ -#define MEM_TypeMask 0x01ff /* Mask of type bits */ +#define MEM_TypeMask 0x81ff /* Mask of type bits */ /* Whenever Mem contains a valid string or blob representation, one of @@ -15479,11 +17656,18 @@ struct Mem { #define MEM_Ephem 0x1000 /* Mem.z points to an ephemeral string */ #define MEM_Agg 0x2000 /* Mem.z points to an agg function context */ #define MEM_Zero 0x4000 /* Mem.i contains count of 0s appended to blob */ +#define MEM_Subtype 0x8000 /* Mem.eSubtype is valid */ #ifdef SQLITE_OMIT_INCRBLOB #undef MEM_Zero #define MEM_Zero 0x0000 #endif +/* Return TRUE if Mem X contains dynamically allocated content - anything +** that needs to be deallocated to avoid a leak. +*/ +#define VdbeMemDynamic(X) \ + (((X)->flags&(MEM_Agg|MEM_Dyn|MEM_RowSet|MEM_Frame))!=0) + /* ** Clear any existing type flags from a Mem and replace them with f */ @@ -15600,16 +17784,16 @@ struct Vdbe { #endif u16 nResColumn; /* Number of columns in one row of the result set */ u8 errorAction; /* Recovery action to do in case of an error */ + bft expired:1; /* True if the VM needs to be recompiled */ + bft doingRerun:1; /* True if rerunning after an auto-reprepare */ u8 minWriteFileFormat; /* Minimum file format for writable database files */ bft explain:2; /* True if EXPLAIN present on SQL command */ bft changeCntOn:1; /* True to update the change-counter */ - bft expired:1; /* True if the VM needs to be recompiled */ bft runOnlyOnce:1; /* Automatically expire on reset */ bft usesStmtJournal:1; /* True if uses a statement journal */ bft readOnly:1; /* True for statements that do not write */ bft bIsReader:1; /* True for statements that read */ bft isPrepareV2:1; /* True if prepared with prepare_v2() */ - bft doingRerun:1; /* True if rerunning after an auto-reprepare */ int nChange; /* Number of db changes made since last reset */ yDbMask btreeMask; /* Bitmask of db->aDb[] entries referenced */ yDbMask lockMask; /* Subset of btreeMask that requires a lock */ @@ -15647,22 +17831,42 @@ struct Vdbe { #define VDBE_MAGIC_HALT 0x519c2973 /* VDBE has completed execution */ #define VDBE_MAGIC_DEAD 0xb606c3c8 /* The VDBE has been deallocated */ +/* +** Structure used to store the context required by the +** sqlite3_preupdate_*() API functions. +*/ +struct PreUpdate { + Vdbe *v; + VdbeCursor *pCsr; /* Cursor to read old values from */ + int op; /* One of SQLITE_INSERT, UPDATE, DELETE */ + u8 *aRecord; /* old.* database record */ + KeyInfo keyinfo; + UnpackedRecord *pUnpacked; /* Unpacked version of aRecord[] */ + UnpackedRecord *pNewUnpacked; /* Unpacked version of new.* record */ + int iNewReg; /* Register for new.* values */ + i64 iKey1; /* First key value passed to hook */ + i64 iKey2; /* Second key value passed to hook */ + int iPKey; /* If not negative index of IPK column */ + Mem *aNew; /* Array of new.* values */ +}; + /* ** Function prototypes */ SQLITE_PRIVATE void sqlite3VdbeError(Vdbe*, const char *, ...); SQLITE_PRIVATE void sqlite3VdbeFreeCursor(Vdbe *, VdbeCursor*); void sqliteVdbePopStack(Vdbe*,int); -SQLITE_PRIVATE int sqlite3VdbeCursorMoveto(VdbeCursor*); +SQLITE_PRIVATE int sqlite3VdbeCursorMoveto(VdbeCursor**, int*); SQLITE_PRIVATE int sqlite3VdbeCursorRestore(VdbeCursor*); #if defined(SQLITE_DEBUG) || defined(VDBE_PROFILE) SQLITE_PRIVATE void sqlite3VdbePrintOp(FILE*, int, Op*); #endif SQLITE_PRIVATE u32 sqlite3VdbeSerialTypeLen(u32); -SQLITE_PRIVATE u32 sqlite3VdbeSerialType(Mem*, int); +SQLITE_PRIVATE u8 sqlite3VdbeOneByteSerialTypeLen(u8); +SQLITE_PRIVATE u32 sqlite3VdbeSerialType(Mem*, int, u32*); SQLITE_PRIVATE u32 sqlite3VdbeSerialPut(unsigned char*, Mem*, u32); SQLITE_PRIVATE u32 sqlite3VdbeSerialGet(const unsigned char*, u32, Mem*); -SQLITE_PRIVATE void sqlite3VdbeDeleteAuxData(Vdbe*, int, int); +SQLITE_PRIVATE void sqlite3VdbeDeleteAuxData(sqlite3*, AuxData**, int, int); int sqlite2BtreeKeyCompare(BtCursor *, const void *, int, int, int *); SQLITE_PRIVATE int sqlite3VdbeIdxKeyCompare(sqlite3*,VdbeCursor*,UnpackedRecord*,int*); @@ -15698,8 +17902,6 @@ SQLITE_PRIVATE int sqlite3VdbeMemNumerify(Mem*); SQLITE_PRIVATE void sqlite3VdbeMemCast(Mem*,u8,u8); SQLITE_PRIVATE int sqlite3VdbeMemFromBtree(BtCursor*,u32,u32,int,Mem*); SQLITE_PRIVATE void sqlite3VdbeMemRelease(Mem *p); -#define VdbeMemDynamic(X) \ - (((X)->flags&(MEM_Agg|MEM_Dyn|MEM_RowSet|MEM_Frame))!=0) SQLITE_PRIVATE int sqlite3VdbeMemFinalize(Mem*, FuncDef*); SQLITE_PRIVATE const char *sqlite3OpcodeName(int); SQLITE_PRIVATE int sqlite3VdbeMemGrow(Mem *pMem, int n, int preserve); @@ -15707,6 +17909,9 @@ SQLITE_PRIVATE int sqlite3VdbeMemClearAndResize(Mem *pMem, int n); SQLITE_PRIVATE int sqlite3VdbeCloseStatement(Vdbe *, int); SQLITE_PRIVATE void sqlite3VdbeFrameDelete(VdbeFrame*); SQLITE_PRIVATE int sqlite3VdbeFrameRestore(VdbeFrame *); +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +SQLITE_PRIVATE void sqlite3VdbePreUpdateHook(Vdbe*,VdbeCursor*,int,const char*,Table*,i64,int); +#endif SQLITE_PRIVATE int sqlite3VdbeTransferError(Vdbe *p); SQLITE_PRIVATE int sqlite3VdbeSorterInit(sqlite3 *, int, VdbeCursor *); @@ -15718,11 +17923,15 @@ SQLITE_PRIVATE int sqlite3VdbeSorterRewind(const VdbeCursor *, int *); SQLITE_PRIVATE int sqlite3VdbeSorterWrite(const VdbeCursor *, Mem *); SQLITE_PRIVATE int sqlite3VdbeSorterCompare(const VdbeCursor *, Mem *, int, int *); -#if !defined(SQLITE_OMIT_SHARED_CACHE) && SQLITE_THREADSAFE>0 +#if !defined(SQLITE_OMIT_SHARED_CACHE) SQLITE_PRIVATE void sqlite3VdbeEnter(Vdbe*); -SQLITE_PRIVATE void sqlite3VdbeLeave(Vdbe*); #else # define sqlite3VdbeEnter(X) +#endif + +#if !defined(SQLITE_OMIT_SHARED_CACHE) && SQLITE_THREADSAFE>0 +SQLITE_PRIVATE void sqlite3VdbeLeave(Vdbe*); +#else # define sqlite3VdbeLeave(X) #endif @@ -15760,15 +17969,15 @@ SQLITE_PRIVATE int sqlite3VdbeMemExpandBlob(Mem *); /* ** Variables in which to record status information. */ +#if SQLITE_PTRSIZE>4 +typedef sqlite3_int64 sqlite3StatValueType; +#else +typedef u32 sqlite3StatValueType; +#endif typedef struct sqlite3StatType sqlite3StatType; static SQLITE_WSD struct sqlite3StatType { -#if SQLITE_PTRSIZE>4 - sqlite3_int64 nowValue[10]; /* Current value */ - sqlite3_int64 mxValue[10]; /* Maximum value */ -#else - u32 nowValue[10]; /* Current value */ - u32 mxValue[10]; /* Maximum value */ -#endif + sqlite3StatValueType nowValue[10]; /* Current value */ + sqlite3StatValueType mxValue[10]; /* Maximum value */ } sqlite3Stat = { {0,}, {0,} }; /* @@ -15849,18 +18058,24 @@ SQLITE_PRIVATE void sqlite3StatusDown(int op, int N){ } /* -** Set the value of a status to X. The highwater mark is adjusted if -** necessary. The caller must hold the appropriate mutex. +** Adjust the highwater mark if necessary. +** The caller must hold the appropriate mutex. */ -SQLITE_PRIVATE void sqlite3StatusSet(int op, int X){ +SQLITE_PRIVATE void sqlite3StatusHighwater(int op, int X){ + sqlite3StatValueType newValue; wsdStatInit; + assert( X>=0 ); + newValue = (sqlite3StatValueType)X; assert( op>=0 && op=0 && opwsdStat.mxValue[op] ){ - wsdStat.mxValue[op] = wsdStat.nowValue[op]; + assert( op==SQLITE_STATUS_MALLOC_SIZE + || op==SQLITE_STATUS_PAGECACHE_SIZE + || op==SQLITE_STATUS_SCRATCH_SIZE + || op==SQLITE_STATUS_PARSER_STACK ); + if( newValue>wsdStat.mxValue[op] ){ + wsdStat.mxValue[op] = newValue; } } @@ -15993,10 +18208,10 @@ SQLITE_API int SQLITE_STDCALL sqlite3_db_status( + pSchema->idxHash.count + pSchema->fkeyHash.count ); - nByte += sqlite3MallocSize(pSchema->tblHash.ht); - nByte += sqlite3MallocSize(pSchema->trigHash.ht); - nByte += sqlite3MallocSize(pSchema->idxHash.ht); - nByte += sqlite3MallocSize(pSchema->fkeyHash.ht); + nByte += sqlite3_msize(pSchema->tblHash.ht); + nByte += sqlite3_msize(pSchema->trigHash.ht); + nByte += sqlite3_msize(pSchema->idxHash.ht); + nByte += sqlite3_msize(pSchema->fkeyHash.ht); for(p=sqliteHashFirst(&pSchema->trigHash); p; p=sqliteHashNext(p)){ sqlite3DeleteTrigger(db, (Trigger*)sqliteHashData(p)); @@ -16134,6 +18349,15 @@ SQLITE_API int SQLITE_STDCALL sqlite3_db_status( #ifndef SQLITE_OMIT_DATETIME_FUNCS +/* +** The MSVC CRT on Windows CE may not have a localtime() function. +** So declare a substitute. The substitute function itself is +** defined in "os_win.c". +*/ +#if !defined(SQLITE_OMIT_LOCALTIME) && defined(_WIN32_WCE) && \ + (!defined(SQLITE_MSVC_LOCALTIME_API) || !SQLITE_MSVC_LOCALTIME_API) +struct tm *__cdecl localtime(const time_t *); +#endif /* ** A structure for holding a single date and time. @@ -16149,38 +18373,54 @@ struct DateTime { char validHMS; /* True (1) if h,m,s are valid */ char validJD; /* True (1) if iJD is valid */ char validTZ; /* True (1) if tz is valid */ + char tzSet; /* Timezone was set explicitly */ }; /* -** Convert zDate into one or more integers. Additional arguments -** come in groups of 5 as follows: +** Convert zDate into one or more integers according to the conversion +** specifier zFormat. ** -** N number of digits in the integer -** min minimum allowed value of the integer -** max maximum allowed value of the integer -** nextC first character after the integer -** pVal where to write the integers value. +** zFormat[] contains 4 characters for each integer converted, except for +** the last integer which is specified by three characters. The meaning +** of a four-character format specifiers ABCD is: +** +** A: number of digits to convert. Always "2" or "4". +** B: minimum value. Always "0" or "1". +** C: maximum value, decoded as: +** a: 12 +** b: 14 +** c: 24 +** d: 31 +** e: 59 +** f: 9999 +** D: the separator character, or \000 to indicate this is the +** last number to convert. +** +** Example: To translate an ISO-8601 date YYYY-MM-DD, the format would +** be "40f-21a-20c". The "40f-" indicates the 4-digit year followed by "-". +** The "21a-" indicates the 2-digit month followed by "-". The "20c" indicates +** the 2-digit day which is the last integer in the set. ** -** Conversions continue until one with nextC==0 is encountered. ** The function returns the number of successful conversions. */ -static int getDigits(const char *zDate, ...){ +static int getDigits(const char *zDate, const char *zFormat, ...){ + /* The aMx[] array translates the 3rd character of each format + ** spec into a max size: a b c d e f */ + static const u16 aMx[] = { 12, 14, 24, 31, 59, 9999 }; va_list ap; - int val; - int N; - int min; - int max; - int nextC; - int *pVal; int cnt = 0; - va_start(ap, zDate); + char nextC; + va_start(ap, zFormat); do{ - N = va_arg(ap, int); - min = va_arg(ap, int); - max = va_arg(ap, int); - nextC = va_arg(ap, int); - pVal = va_arg(ap, int*); + char N = zFormat[0] - '0'; + char min = zFormat[1] - '0'; + int val = 0; + u16 max; + + assert( zFormat[2]>='a' && zFormat[2]<='f' ); + max = aMx[zFormat[2] - 'a']; + nextC = zFormat[3]; val = 0; while( N-- ){ if( !sqlite3Isdigit(*zDate) ){ @@ -16189,12 +18429,13 @@ static int getDigits(const char *zDate, ...){ val = val*10 + *zDate - '0'; zDate++; } - if( valmax || (nextC!=0 && nextC!=*zDate) ){ + if( val<(int)min || val>(int)max || (nextC!=0 && nextC!=*zDate) ){ goto end_getDigits; } - *pVal = val; + *va_arg(ap,int*) = val; zDate++; cnt++; + zFormat += 4; }while( nextC ); end_getDigits: va_end(ap); @@ -16235,13 +18476,14 @@ static int parseTimezone(const char *zDate, DateTime *p){ return c!=0; } zDate++; - if( getDigits(zDate, 2, 0, 14, ':', &nHr, 2, 0, 59, 0, &nMn)!=2 ){ + if( getDigits(zDate, "20b:20e", &nHr, &nMn)!=2 ){ return 1; } zDate += 5; p->tz = sgn*(nMn + nHr*60); zulu_time: while( sqlite3Isspace(*zDate) ){ zDate++; } + p->tzSet = 1; return *zDate!=0; } @@ -16255,13 +18497,13 @@ zulu_time: static int parseHhMmSs(const char *zDate, DateTime *p){ int h, m, s; double ms = 0.0; - if( getDigits(zDate, 2, 0, 24, ':', &h, 2, 0, 59, 0, &m)!=2 ){ + if( getDigits(zDate, "20c:20e", &h, &m)!=2 ){ return 1; } zDate += 5; if( *zDate==':' ){ zDate++; - if( getDigits(zDate, 2, 0, 59, 0, &s)!=1 ){ + if( getDigits(zDate, "20e", &s)!=1 ){ return 1; } zDate += 2; @@ -16349,7 +18591,7 @@ static int parseYyyyMmDd(const char *zDate, DateTime *p){ }else{ neg = 0; } - if( getDigits(zDate,4,0,9999,'-',&Y,2,1,12,'-',&M,2,1,31,0,&D)!=3 ){ + if( getDigits(zDate, "40f-21a-21d", &Y, &M, &D)!=3 ){ return 1; } zDate += 10; @@ -16484,6 +18726,7 @@ static void clearYMD_HMS_TZ(DateTime *p){ p->validTZ = 0; } +#ifndef SQLITE_OMIT_LOCALTIME /* ** On recent Windows platforms, the localtime_s() function is available ** as part of the "Secure CRT". It is essentially equivalent to @@ -16502,7 +18745,6 @@ static void clearYMD_HMS_TZ(DateTime *p){ #define HAVE_LOCALTIME_S 1 #endif -#ifndef SQLITE_OMIT_LOCALTIME /* ** The following routine implements the rough equivalent of localtime_r() ** using whatever operating-system specific localtime facility that @@ -16674,13 +18916,18 @@ static int parseModifier(sqlite3_context *pCtx, const char *zMod, DateTime *p){ } #ifndef SQLITE_OMIT_LOCALTIME else if( strcmp(z, "utc")==0 ){ - sqlite3_int64 c1; - computeJD(p); - c1 = localtimeOffset(p, pCtx, &rc); - if( rc==SQLITE_OK ){ - p->iJD -= c1; - clearYMD_HMS_TZ(p); - p->iJD += c1 - localtimeOffset(p, pCtx, &rc); + if( p->tzSet==0 ){ + sqlite3_int64 c1; + computeJD(p); + c1 = localtimeOffset(p, pCtx, &rc); + if( rc==SQLITE_OK ){ + p->iJD -= c1; + clearYMD_HMS_TZ(p); + p->iJD += c1 - localtimeOffset(p, pCtx, &rc); + } + p->tzSet = 1; + }else{ + rc = SQLITE_OK; } } #endif @@ -17028,7 +19275,7 @@ static void strftimeFunc( sqlite3_result_error_toobig(context); return; }else{ - z = sqlite3DbMallocRaw(db, (int)n); + z = sqlite3DbMallocRawNN(db, (int)n); if( z==0 ){ sqlite3_result_error_nomem(context); return; @@ -17197,7 +19444,7 @@ static void currentTimeFunc( ** external linkage. */ SQLITE_PRIVATE void sqlite3RegisterDateTimeFunctions(void){ - static SQLITE_WSD FuncDef aDateTimeFuncs[] = { + static FuncDef aDateTimeFuncs[] = { #ifndef SQLITE_OMIT_DATETIME_FUNCS DFUNCTION(julianday, -1, 0, 0, juliandayFunc ), DFUNCTION(date, -1, 0, 0, dateFunc ), @@ -17213,13 +19460,7 @@ SQLITE_PRIVATE void sqlite3RegisterDateTimeFunctions(void){ STR_FUNCTION(current_timestamp, 0, "%Y-%m-%d %H:%M:%S", 0, currentTimeFunc), #endif }; - int i; - FuncDefHash *pHash = &GLOBAL(FuncDefHash, sqlite3GlobalFunctions); - FuncDef *aFunc = (FuncDef*)&GLOBAL(FuncDef, aDateTimeFuncs); - - for(i=0; ipMethods ){ - rc = pId->pMethods->xClose(pId); + pId->pMethods->xClose(pId); pId->pMethods = 0; } - return rc; } SQLITE_PRIVATE int sqlite3OsRead(sqlite3_file *id, void *pBuf, int amt, i64 offset){ DO_OS_MALLOC_TEST(id); @@ -17336,8 +19597,8 @@ SQLITE_PRIVATE int sqlite3OsFileControl(sqlite3_file *id, int op, void *pArg){ #ifdef SQLITE_TEST if( op!=SQLITE_FCNTL_COMMIT_PHASETWO ){ /* Faults are not injected into COMMIT_PHASETWO because, assuming SQLite - ** is using a regular VFS, it is called after the corresponding - ** transaction has been committed. Injecting a fault at this point + ** is using a regular VFS, it is called after the corresponding + ** transaction has been committed. Injecting a fault at this point ** confuses the test scripts - the COMMIT comand returns SQLITE_NOMEM ** but the transaction is committed anyway. ** @@ -17406,10 +19667,10 @@ SQLITE_PRIVATE int sqlite3OsUnfetch(sqlite3_file *id, i64 iOff, void *p){ ** VFS methods. */ SQLITE_PRIVATE int sqlite3OsOpen( - sqlite3_vfs *pVfs, - const char *zPath, - sqlite3_file *pFile, - int flags, + sqlite3_vfs *pVfs, + const char *zPath, + sqlite3_file *pFile, + int flags, int *pFlagsOut ){ int rc; @@ -17428,18 +19689,18 @@ SQLITE_PRIVATE int sqlite3OsDelete(sqlite3_vfs *pVfs, const char *zPath, int dir return pVfs->xDelete(pVfs, zPath, dirSync); } SQLITE_PRIVATE int sqlite3OsAccess( - sqlite3_vfs *pVfs, - const char *zPath, - int flags, + sqlite3_vfs *pVfs, + const char *zPath, + int flags, int *pResOut ){ DO_OS_MALLOC_TEST(0); return pVfs->xAccess(pVfs, zPath, flags, pResOut); } SQLITE_PRIVATE int sqlite3OsFullPathname( - sqlite3_vfs *pVfs, - const char *zPath, - int nPathOut, + sqlite3_vfs *pVfs, + const char *zPath, + int nPathOut, char *zPathOut ){ DO_OS_MALLOC_TEST(0); @@ -17466,6 +19727,9 @@ SQLITE_PRIVATE int sqlite3OsRandomness(sqlite3_vfs *pVfs, int nByte, char *zBufO SQLITE_PRIVATE int sqlite3OsSleep(sqlite3_vfs *pVfs, int nMicro){ return pVfs->xSleep(pVfs, nMicro); } +SQLITE_PRIVATE int sqlite3OsGetLastError(sqlite3_vfs *pVfs){ + return pVfs->xGetLastError ? pVfs->xGetLastError(pVfs, 0, 0) : 0; +} SQLITE_PRIVATE int sqlite3OsCurrentTimeInt64(sqlite3_vfs *pVfs, sqlite3_int64 *pTimeOut){ int rc; /* IMPLEMENTATION-OF: R-49045-42493 SQLite will use the xCurrentTimeInt64() @@ -17485,13 +19749,13 @@ SQLITE_PRIVATE int sqlite3OsCurrentTimeInt64(sqlite3_vfs *pVfs, sqlite3_int64 *p } SQLITE_PRIVATE int sqlite3OsOpenMalloc( - sqlite3_vfs *pVfs, - const char *zFile, - sqlite3_file **ppFile, + sqlite3_vfs *pVfs, + const char *zFile, + sqlite3_file **ppFile, int flags, int *pOutFlags ){ - int rc = SQLITE_NOMEM; + int rc; sqlite3_file *pFile; pFile = (sqlite3_file *)sqlite3MallocZero(pVfs->szOsFile); if( pFile ){ @@ -17501,15 +19765,15 @@ SQLITE_PRIVATE int sqlite3OsOpenMalloc( }else{ *ppFile = pFile; } + }else{ + rc = SQLITE_NOMEM_BKPT; } return rc; } -SQLITE_PRIVATE int sqlite3OsCloseFree(sqlite3_file *pFile){ - int rc = SQLITE_OK; +SQLITE_PRIVATE void sqlite3OsCloseFree(sqlite3_file *pFile){ assert( pFile ); - rc = sqlite3OsClose(pFile); + sqlite3OsClose(pFile); sqlite3_free(pFile); - return rc; } /* @@ -17520,7 +19784,7 @@ SQLITE_PRIVATE int sqlite3OsCloseFree(sqlite3_file *pFile){ */ SQLITE_PRIVATE int sqlite3OsInit(void){ void *p = sqlite3_malloc(10); - if( p==0 ) return SQLITE_NOMEM; + if( p==0 ) return SQLITE_NOMEM_BKPT; sqlite3_free(p); return sqlite3_os_init(); } @@ -17947,10 +20211,11 @@ static void sqlite3MemFree(void *pPrior){ */ static int sqlite3MemSize(void *pPrior){ #ifdef SQLITE_MALLOCSIZE - return pPrior ? (int)SQLITE_MALLOCSIZE(pPrior) : 0; + assert( pPrior!=0 ); + return (int)SQLITE_MALLOCSIZE(pPrior); #else sqlite3_int64 *p; - if( pPrior==0 ) return 0; + assert( pPrior!=0 ); p = (sqlite3_int64*)pPrior; p--; return (int)p[0]; @@ -19080,7 +21345,7 @@ static void memsys3FreeUnsafe(void *pOld){ */ static int memsys3Size(void *p){ Mem3Block *pBlock; - if( p==0 ) return 0; + assert( p!=0 ); pBlock = (Mem3Block*)p; assert( (pBlock[-1].u.hdr.size4x&1)!=0 ); return (pBlock[-1].u.hdr.size4x&~3)*2 - 4; @@ -19319,7 +21584,7 @@ SQLITE_PRIVATE const sqlite3_mem_methods *sqlite3MemGetMemsys3(void){ ** ** This memory allocator uses the following algorithm: ** -** 1. All memory allocations sizes are rounded up to a power of 2. +** 1. All memory allocation sizes are rounded up to a power of 2. ** ** 2. If two adjacent free blocks are the halves of a larger block, ** then the two blocks are coalesced into the single larger block. @@ -19396,6 +21661,7 @@ static SQLITE_WSD struct Mem5Global { */ sqlite3_mutex *mutex; +#if defined(SQLITE_DEBUG) || defined(SQLITE_TEST) /* ** Performance statistics */ @@ -19407,11 +21673,12 @@ static SQLITE_WSD struct Mem5Global { u32 maxOut; /* Maximum instantaneous currentOut */ u32 maxCount; /* Maximum instantaneous currentCount */ u32 maxRequest; /* Largest allocation (exclusive of internal frag) */ +#endif /* ** Lists of free blocks. aiFreelist[0] is a list of free blocks of ** size mem5.szAtom. aiFreelist[1] holds blocks of size szAtom*2. - ** and so forth. + ** aiFreelist[2] holds free blocks of size szAtom*4. And so forth. */ int aiFreelist[LOGMAX+1]; @@ -19477,9 +21744,7 @@ static void memsys5Link(int i, int iLogsize){ } /* -** If the STATIC_MEM mutex is not already held, obtain it now. The mutex -** will already be held (obtained by code in malloc.c) if -** sqlite3GlobalConfig.bMemStat is true. +** Obtain or release the mutex needed to access global data structures. */ static void memsys5Enter(void){ sqlite3_mutex_enter(mem5.mutex); @@ -19489,17 +21754,15 @@ static void memsys5Leave(void){ } /* -** Return the size of an outstanding allocation, in bytes. The -** size returned omits the 8-byte header overhead. This only -** works for chunks that are currently checked out. +** Return the size of an outstanding allocation, in bytes. +** This only works for chunks that are currently checked out. */ static int memsys5Size(void *p){ - int iSize = 0; - if( p ){ - int i = (int)(((u8 *)p-mem5.zPool)/mem5.szAtom); - assert( i>=0 && i=0 && i0 ); + /* No more than 1GiB per allocation */ + if( nByte > 0x40000000 ) return 0; + +#if defined(SQLITE_DEBUG) || defined(SQLITE_TEST) /* Keep track of the maximum allocation request. Even unfulfilled ** requests are counted */ if( (u32)nByte>mem5.maxRequest ){ mem5.maxRequest = nByte; } +#endif - /* Abort if the requested allocation size is larger than the largest - ** power of two that we can represent using 32-bit signed integers. - */ - if( nByte > 0x40000000 ){ - return 0; - } /* Round nByte up to the next valid power of two */ - for(iFullSz=mem5.szAtom, iLogsize=0; iFullSz0 ); assert( mem5.currentOut>=(size*mem5.szAtom) ); mem5.currentCount--; mem5.currentOut -= size*mem5.szAtom; assert( mem5.currentOut>0 || mem5.currentCount==0 ); assert( mem5.currentCount>0 || mem5.currentOut==0 ); +#endif mem5.aCtrl[iBlock] = CTRL_FREE | iLogsize; while( ALWAYS(iLogsize>iLogsize) & 1 ){ iBuddy = iBlock - size; + assert( iBuddy>=0 ); }else{ iBuddy = iBlock + size; + if( iBuddy>=mem5.nBlock ) break; } - assert( iBuddy>=0 ); - if( (iBuddy+(1<mem5.nBlock ) break; if( mem5.aCtrl[iBuddy]!=(CTRL_FREE | iLogsize) ) break; memsys5Unlink(iBuddy, iLogsize); iLogsize++; @@ -19693,13 +21960,11 @@ static void *memsys5Realloc(void *pPrior, int nBytes){ if( nBytes<=nOld ){ return pPrior; } - memsys5Enter(); - p = memsys5MallocUnsafe(nBytes); + p = memsys5Malloc(nBytes); if( p ){ memcpy(p, pPrior, nOld); - memsys5FreeUnsafe(pPrior); + memsys5Free(pPrior); } - memsys5Leave(); return p; } @@ -20313,7 +22578,9 @@ struct sqlite3_mutex { #endif }; #if SQLITE_MUTEX_NREF -#define SQLITE3_MUTEX_INITIALIZER { PTHREAD_MUTEX_INITIALIZER, 0, 0, (pthread_t)0, 0 } +#define SQLITE3_MUTEX_INITIALIZER {PTHREAD_MUTEX_INITIALIZER,0,0,(pthread_t)0,0} +#elif defined(SQLITE_ENABLE_API_ARMOR) +#define SQLITE3_MUTEX_INITIALIZER { PTHREAD_MUTEX_INITIALIZER, 0 } #else #define SQLITE3_MUTEX_INITIALIZER { PTHREAD_MUTEX_INITIALIZER } #endif @@ -20707,8 +22974,8 @@ SQLITE_PRIVATE sqlite3_mutex_methods const *sqlite3DefaultMutex(void){ */ #ifdef SQLITE_PERFORMANCE_TRACE -/* -** hwtime.h contains inline assembler code for implementing +/* +** hwtime.h contains inline assembler code for implementing ** high-performance timing routines. */ /************** Include hwtime.h in the middle of os_common.h ****************/ @@ -20818,14 +23085,14 @@ static sqlite_uint64 g_elapsed; ** of code will give us the ability to simulate a disk I/O error. This ** is used for testing the I/O recovery logic. */ -#ifdef SQLITE_TEST -SQLITE_API int sqlite3_io_error_hit = 0; /* Total number of I/O Errors */ -SQLITE_API int sqlite3_io_error_hardhit = 0; /* Number of non-benign errors */ -SQLITE_API int sqlite3_io_error_pending = 0; /* Count down to first I/O error */ -SQLITE_API int sqlite3_io_error_persist = 0; /* True if I/O errors persist */ -SQLITE_API int sqlite3_io_error_benign = 0; /* True if errors are benign */ -SQLITE_API int sqlite3_diskfull_pending = 0; -SQLITE_API int sqlite3_diskfull = 0; +#if defined(SQLITE_TEST) +SQLITE_API extern int sqlite3_io_error_hit; +SQLITE_API extern int sqlite3_io_error_hardhit; +SQLITE_API extern int sqlite3_io_error_pending; +SQLITE_API extern int sqlite3_io_error_persist; +SQLITE_API extern int sqlite3_io_error_benign; +SQLITE_API extern int sqlite3_diskfull_pending; +SQLITE_API extern int sqlite3_diskfull; #define SimulateIOErrorBenign(X) sqlite3_io_error_benign=(X) #define SimulateIOError(CODE) \ if( (sqlite3_io_error_persist && sqlite3_io_error_hit) \ @@ -20851,17 +23118,17 @@ static void local_ioerr(){ #define SimulateIOErrorBenign(X) #define SimulateIOError(A) #define SimulateDiskfullError(A) -#endif +#endif /* defined(SQLITE_TEST) */ /* ** When testing, keep a count of the number of open files. */ -#ifdef SQLITE_TEST -SQLITE_API int sqlite3_open_file_count = 0; +#if defined(SQLITE_TEST) +SQLITE_API extern int sqlite3_open_file_count; #define OpenCounter(X) sqlite3_open_file_count+=(X) #else #define OpenCounter(X) -#endif +#endif /* defined(SQLITE_TEST) */ #endif /* !defined(_OS_COMMON_H_) */ @@ -21475,9 +23742,7 @@ SQLITE_PRIVATE int sqlite3MallocInit(void){ sqlite3MemSetDefault(); } memset(&mem0, 0, sizeof(mem0)); - if( sqlite3GlobalConfig.bCoreMutex ){ - mem0.mutex = sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_MEM); - } + mem0.mutex = sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_MEM); if( sqlite3GlobalConfig.pScratch && sqlite3GlobalConfig.szScratch>=100 && sqlite3GlobalConfig.nScratch>0 ){ int i, n, sz; @@ -21568,7 +23833,7 @@ static int mallocWithAlarm(int n, void **pp){ void *p; assert( sqlite3_mutex_held(mem0.mutex) ); nFull = sqlite3GlobalConfig.m.xRoundup(n); - sqlite3StatusSet(SQLITE_STATUS_MALLOC_SIZE, n); + sqlite3StatusHighwater(SQLITE_STATUS_MALLOC_SIZE, n); if( mem0.alarmThreshold>0 ){ sqlite3_int64 nUsed = sqlite3StatusValue(SQLITE_STATUS_MEMORY_USED); if( nUsed >= mem0.alarmThreshold - nFull ){ @@ -21660,7 +23925,7 @@ SQLITE_PRIVATE void *sqlite3ScratchMalloc(int n){ assert( n>0 ); sqlite3_mutex_enter(mem0.mutex); - sqlite3StatusSet(SQLITE_STATUS_SCRATCH_SIZE, n); + sqlite3StatusHighwater(SQLITE_STATUS_SCRATCH_SIZE, n); if( mem0.nScratchFree && sqlite3GlobalConfig.szScratch>=n ){ p = mem0.pScratchFree; mem0.pScratchFree = mem0.pScratchFree->pNext; @@ -21704,7 +23969,7 @@ SQLITE_PRIVATE void sqlite3ScratchFree(void *p){ scratchAllocOut--; #endif - if( p>=sqlite3GlobalConfig.pScratch && p=db->lookaside.pStart && plookaside.pEnd; + return SQLITE_WITHIN(p, db->lookaside.pStart, db->lookaside.pEnd); } #else #define isLookaside(A,B) 0 @@ -21755,6 +24020,7 @@ SQLITE_PRIVATE int sqlite3MallocSize(void *p){ return sqlite3GlobalConfig.m.xSize(p); } SQLITE_PRIVATE int sqlite3DbMallocSize(sqlite3 *db, void *p){ + assert( p!=0 ); if( db==0 || !isLookaside(db,p) ){ #if SQLITE_DEBUG if( db==0 ){ @@ -21774,7 +24040,7 @@ SQLITE_PRIVATE int sqlite3DbMallocSize(sqlite3 *db, void *p){ SQLITE_API sqlite3_uint64 SQLITE_STDCALL sqlite3_msize(void *p){ assert( sqlite3MemdebugNoType(p, (u8)~MEMTYPE_HEAP) ); assert( sqlite3MemdebugHasType(p, MEMTYPE_HEAP) ); - return (sqlite3_uint64)sqlite3GlobalConfig.m.xSize(p); + return p ? sqlite3GlobalConfig.m.xSize(p) : 0; } /* @@ -21862,7 +24128,7 @@ SQLITE_PRIVATE void *sqlite3Realloc(void *pOld, u64 nBytes){ pNew = pOld; }else if( sqlite3GlobalConfig.bMemstat ){ sqlite3_mutex_enter(mem0.mutex); - sqlite3StatusSet(SQLITE_STATUS_MALLOC_SIZE, (int)nBytes); + sqlite3StatusHighwater(SQLITE_STATUS_MALLOC_SIZE, (int)nBytes); nDiff = nNew - nOld; if( sqlite3StatusValue(SQLITE_STATUS_MEMORY_USED) >= mem0.alarmThreshold-nDiff ){ @@ -21920,16 +24186,31 @@ SQLITE_PRIVATE void *sqlite3MallocZero(u64 n){ ** the mallocFailed flag in the connection pointer. */ SQLITE_PRIVATE void *sqlite3DbMallocZero(sqlite3 *db, u64 n){ - void *p = sqlite3DbMallocRaw(db, n); - if( p ){ - memset(p, 0, (size_t)n); - } + void *p; + testcase( db==0 ); + p = sqlite3DbMallocRaw(db, n); + if( p ) memset(p, 0, (size_t)n); + return p; +} + + +/* Finish the work of sqlite3DbMallocRawNN for the unusual and +** slower case when the allocation cannot be fulfilled using lookaside. +*/ +static SQLITE_NOINLINE void *dbMallocRawFinish(sqlite3 *db, u64 n){ + void *p; + assert( db!=0 ); + p = sqlite3Malloc(n); + if( !p ) sqlite3OomFault(db); + sqlite3MemdebugSetType(p, + (db->lookaside.bDisable==0) ? MEMTYPE_LOOKASIDE : MEMTYPE_HEAP); return p; } /* -** Allocate and zero memory. If the allocation fails, make -** the mallocFailed flag in the connection pointer. +** Allocate memory, either lookaside (if possible) or heap. +** If the allocation fails, set the mallocFailed flag in +** the connection pointer. ** ** If db!=0 and db->mallocFailed is true (indicating a prior malloc ** failure on the same database connection) then always return 0. @@ -21944,64 +24225,73 @@ SQLITE_PRIVATE void *sqlite3DbMallocZero(sqlite3 *db, u64 n){ ** ** In other words, if a subsequent malloc (ex: "b") worked, it is assumed ** that all prior mallocs (ex: "a") worked too. +** +** The sqlite3MallocRawNN() variant guarantees that the "db" parameter is +** not a NULL pointer. */ SQLITE_PRIVATE void *sqlite3DbMallocRaw(sqlite3 *db, u64 n){ void *p; - assert( db==0 || sqlite3_mutex_held(db->mutex) ); - assert( db==0 || db->pnBytesFreed==0 ); + if( db ) return sqlite3DbMallocRawNN(db, n); + p = sqlite3Malloc(n); + sqlite3MemdebugSetType(p, MEMTYPE_HEAP); + return p; +} +SQLITE_PRIVATE void *sqlite3DbMallocRawNN(sqlite3 *db, u64 n){ #ifndef SQLITE_OMIT_LOOKASIDE - if( db ){ - LookasideSlot *pBuf; - if( db->mallocFailed ){ - return 0; - } - if( db->lookaside.bEnabled ){ - if( n>db->lookaside.sz ){ - db->lookaside.anStat[1]++; - }else if( (pBuf = db->lookaside.pFree)==0 ){ - db->lookaside.anStat[2]++; - }else{ - db->lookaside.pFree = pBuf->pNext; - db->lookaside.nOut++; - db->lookaside.anStat[0]++; - if( db->lookaside.nOut>db->lookaside.mxOut ){ - db->lookaside.mxOut = db->lookaside.nOut; - } - return (void*)pBuf; + LookasideSlot *pBuf; + assert( db!=0 ); + assert( sqlite3_mutex_held(db->mutex) ); + assert( db->pnBytesFreed==0 ); + if( db->lookaside.bDisable==0 ){ + assert( db->mallocFailed==0 ); + if( n>db->lookaside.sz ){ + db->lookaside.anStat[1]++; + }else if( (pBuf = db->lookaside.pFree)==0 ){ + db->lookaside.anStat[2]++; + }else{ + db->lookaside.pFree = pBuf->pNext; + db->lookaside.nOut++; + db->lookaside.anStat[0]++; + if( db->lookaside.nOut>db->lookaside.mxOut ){ + db->lookaside.mxOut = db->lookaside.nOut; } + return (void*)pBuf; } + }else if( db->mallocFailed ){ + return 0; } #else - if( db && db->mallocFailed ){ + assert( db!=0 ); + assert( sqlite3_mutex_held(db->mutex) ); + assert( db->pnBytesFreed==0 ); + if( db->mallocFailed ){ return 0; } #endif - p = sqlite3Malloc(n); - if( !p && db ){ - db->mallocFailed = 1; - } - sqlite3MemdebugSetType(p, - (db && db->lookaside.bEnabled) ? MEMTYPE_LOOKASIDE : MEMTYPE_HEAP); - return p; + return dbMallocRawFinish(db, n); } +/* Forward declaration */ +static SQLITE_NOINLINE void *dbReallocFinish(sqlite3 *db, void *p, u64 n); + /* ** Resize the block of memory pointed to by p to n bytes. If the ** resize fails, set the mallocFailed flag in the connection object. */ SQLITE_PRIVATE void *sqlite3DbRealloc(sqlite3 *db, void *p, u64 n){ + assert( db!=0 ); + if( p==0 ) return sqlite3DbMallocRawNN(db, n); + assert( sqlite3_mutex_held(db->mutex) ); + if( isLookaside(db,p) && n<=db->lookaside.sz ) return p; + return dbReallocFinish(db, p, n); +} +static SQLITE_NOINLINE void *dbReallocFinish(sqlite3 *db, void *p, u64 n){ void *pNew = 0; assert( db!=0 ); - assert( sqlite3_mutex_held(db->mutex) ); + assert( p!=0 ); if( db->mallocFailed==0 ){ - if( p==0 ){ - return sqlite3DbMallocRaw(db, n); - } if( isLookaside(db, p) ){ - if( n<=db->lookaside.sz ){ - return p; - } - pNew = sqlite3DbMallocRaw(db, n); + pNew = sqlite3DbMallocRawNN(db, n); if( pNew ){ memcpy(pNew, p, db->lookaside.sz); sqlite3DbFree(db, p); @@ -22012,10 +24302,10 @@ SQLITE_PRIVATE void *sqlite3DbRealloc(sqlite3 *db, void *p, u64 n){ sqlite3MemdebugSetType(p, MEMTYPE_HEAP); pNew = sqlite3_realloc64(p, n); if( !pNew ){ - db->mallocFailed = 1; + sqlite3OomFault(db); } sqlite3MemdebugSetType(pNew, - (db->lookaside.bEnabled ? MEMTYPE_LOOKASIDE : MEMTYPE_HEAP)); + (db->lookaside.bDisable==0 ? MEMTYPE_LOOKASIDE : MEMTYPE_HEAP)); } } return pNew; @@ -22057,11 +24347,12 @@ SQLITE_PRIVATE char *sqlite3DbStrDup(sqlite3 *db, const char *z){ } SQLITE_PRIVATE char *sqlite3DbStrNDup(sqlite3 *db, const char *z, u64 n){ char *zNew; + assert( db!=0 ); if( z==0 ){ return 0; } assert( (n&0x7fffffff)==n ); - zNew = sqlite3DbMallocRaw(db, n+1); + zNew = sqlite3DbMallocRawNN(db, n+1); if( zNew ){ memcpy(zNew, z, (size_t)n); zNew[n] = 0; @@ -22077,13 +24368,45 @@ SQLITE_PRIVATE void sqlite3SetString(char **pz, sqlite3 *db, const char *zNew){ *pz = sqlite3DbStrDup(db, zNew); } +/* +** Call this routine to record the fact that an OOM (out-of-memory) error +** has happened. This routine will set db->mallocFailed, and also +** temporarily disable the lookaside memory allocator and interrupt +** any running VDBEs. +*/ +SQLITE_PRIVATE void sqlite3OomFault(sqlite3 *db){ + if( db->mallocFailed==0 && db->bBenignMalloc==0 ){ + db->mallocFailed = 1; + if( db->nVdbeExec>0 ){ + db->u1.isInterrupted = 1; + } + db->lookaside.bDisable++; + } +} + +/* +** This routine reactivates the memory allocator and clears the +** db->mallocFailed flag as necessary. +** +** The memory allocator is not restarted if there are running +** VDBEs. +*/ +SQLITE_PRIVATE void sqlite3OomClear(sqlite3 *db){ + if( db->mallocFailed && db->nVdbeExec==0 ){ + db->mallocFailed = 0; + db->u1.isInterrupted = 0; + assert( db->lookaside.bDisable>0 ); + db->lookaside.bDisable--; + } +} + /* ** Take actions at the end of an API call to indicate an OOM error */ static SQLITE_NOINLINE int apiOomError(sqlite3 *db){ - db->mallocFailed = 0; + sqlite3OomClear(db); sqlite3Error(db, SQLITE_NOMEM); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } /* @@ -22130,26 +24453,26 @@ SQLITE_PRIVATE int sqlite3ApiExit(sqlite3* db, int rc){ ** Conversion types fall into various categories as defined by the ** following enumeration. */ -#define etRADIX 1 /* Integer types. %d, %x, %o, and so forth */ -#define etFLOAT 2 /* Floating point. %f */ -#define etEXP 3 /* Exponentional notation. %e and %E */ -#define etGENERIC 4 /* Floating or exponential, depending on exponent. %g */ -#define etSIZE 5 /* Return number of characters processed so far. %n */ -#define etSTRING 6 /* Strings. %s */ -#define etDYNSTRING 7 /* Dynamically allocated strings. %z */ -#define etPERCENT 8 /* Percent symbol. %% */ -#define etCHARX 9 /* Characters. %c */ +#define etRADIX 0 /* Integer types. %d, %x, %o, and so forth */ +#define etFLOAT 1 /* Floating point. %f */ +#define etEXP 2 /* Exponentional notation. %e and %E */ +#define etGENERIC 3 /* Floating or exponential, depending on exponent. %g */ +#define etSIZE 4 /* Return number of characters processed so far. %n */ +#define etSTRING 5 /* Strings. %s */ +#define etDYNSTRING 6 /* Dynamically allocated strings. %z */ +#define etPERCENT 7 /* Percent symbol. %% */ +#define etCHARX 8 /* Characters. %c */ /* The rest are extensions, not normally found in printf() */ -#define etSQLESCAPE 10 /* Strings with '\'' doubled. %q */ -#define etSQLESCAPE2 11 /* Strings with '\'' doubled and enclosed in '', +#define etSQLESCAPE 9 /* Strings with '\'' doubled. %q */ +#define etSQLESCAPE2 10 /* Strings with '\'' doubled and enclosed in '', NULL pointers replaced by SQL NULL. %Q */ -#define etTOKEN 12 /* a pointer to a Token structure */ -#define etSRCLIST 13 /* a pointer to a SrcList */ -#define etPOINTER 14 /* The %p conversion */ -#define etSQLESCAPE3 15 /* %w -> Strings with '\"' doubled */ -#define etORDINAL 16 /* %r -> 1st, 2nd, 3rd, 4th, etc. English only */ +#define etTOKEN 11 /* a pointer to a Token structure */ +#define etSRCLIST 12 /* a pointer to a SrcList */ +#define etPOINTER 13 /* The %p conversion */ +#define etSQLESCAPE3 14 /* %w -> Strings with '\"' doubled */ +#define etORDINAL 15 /* %r -> 1st, 2nd, 3rd, 4th, etc. English only */ -#define etINVALID 0 /* Any unrecognized conversion type */ +#define etINVALID 16 /* Any unrecognized conversion type */ /* @@ -22286,7 +24609,6 @@ static char *getTextArg(PrintfArguments *p){ */ SQLITE_PRIVATE void sqlite3VXPrintf( StrAccum *pAccum, /* Accumulate results here */ - u32 bFlags, /* SQLITE_PRINTF_* flags */ const char *fmt, /* Format string */ va_list ap /* arguments */ ){ @@ -22305,7 +24627,7 @@ SQLITE_PRIVATE void sqlite3VXPrintf( etByte flag_long; /* True if "l" flag is present */ etByte flag_longlong; /* True if the "ll" flag is present */ etByte done; /* Loop termination flag */ - etByte xtype = 0; /* Conversion paradigm */ + etByte xtype = etINVALID; /* Conversion paradigm */ u8 bArgList; /* True for SQLITE_PRINTF_SQLFUNC */ u8 useIntern; /* Ok to use internal conversions (ex: %T) */ char prefix; /* Prefix character. "+" or "-" or " " or '\0'. */ @@ -22326,11 +24648,11 @@ SQLITE_PRIVATE void sqlite3VXPrintf( char buf[etBUFSIZE]; /* Conversion buffer */ bufpt = 0; - if( bFlags ){ - if( (bArgList = (bFlags & SQLITE_PRINTF_SQLFUNC))!=0 ){ + if( pAccum->printfFlags ){ + if( (bArgList = (pAccum->printfFlags & SQLITE_PRINTF_SQLFUNC))!=0 ){ pArgList = va_arg(ap, PrintfArguments*); } - useIntern = bFlags & SQLITE_PRINTF_INTERNAL; + useIntern = pAccum->printfFlags & SQLITE_PRINTF_INTERNAL; }else{ bArgList = useIntern = 0; } @@ -22385,6 +24707,12 @@ SQLITE_PRIVATE void sqlite3VXPrintf( testcase( wx>0x7fffffff ); width = wx & 0x7fffffff; } + assert( width>=0 ); +#ifdef SQLITE_PRINTF_PRECISION_LIMIT + if( width>SQLITE_PRINTF_PRECISION_LIMIT ){ + width = SQLITE_PRINTF_PRECISION_LIMIT; + } +#endif /* Get the precision */ if( c=='.' ){ @@ -22411,6 +24739,14 @@ SQLITE_PRIVATE void sqlite3VXPrintf( }else{ precision = -1; } + assert( precision>=(-1) ); +#ifdef SQLITE_PRINTF_PRECISION_LIMIT + if( precision>SQLITE_PRINTF_PRECISION_LIMIT ){ + precision = SQLITE_PRINTF_PRECISION_LIMIT; + } +#endif + + /* Get the conversion type modifier */ if( c=='l' ){ flag_long = 1; @@ -22841,7 +25177,7 @@ SQLITE_PRIVATE void sqlite3VXPrintf( if( width>0 && flag_leftjustify ) sqlite3AppendChar(pAccum, width, ' '); if( zExtra ){ - sqlite3_free(zExtra); + sqlite3DbFree(pAccum->db, zExtra); zExtra = 0; } }/* End for loop over the format string */ @@ -22867,8 +25203,9 @@ static int sqlite3StrAccumEnlarge(StrAccum *p, int N){ setStrAccumError(p, STRACCUM_TOOBIG); return N; }else{ - char *zOld = (p->zText==p->zBase ? 0 : p->zText); + char *zOld = isMalloced(p) ? p->zText : 0; i64 szNew = p->nChar; + assert( (p->zText==0 || p->zText==p->zBase)==!isMalloced(p) ); szNew += N + 1; if( szNew+p->nChar<=p->mxAlloc ){ /* Force exponential buffer size growth as long as it does not overflow, @@ -22889,9 +25226,10 @@ static int sqlite3StrAccumEnlarge(StrAccum *p, int N){ } if( zNew ){ assert( p->zText!=0 || p->nChar==0 ); - if( zOld==0 && p->nChar>0 ) memcpy(zNew, p->zText, p->nChar); + if( !isMalloced(p) && p->nChar>0 ) memcpy(zNew, p->zText, p->nChar); p->zText = zNew; p->nAlloc = sqlite3DbMallocSize(p->db, zNew); + p->printfFlags |= SQLITE_PRINTF_MALLOCED; }else{ sqlite3StrAccumReset(p); setStrAccumError(p, STRACCUM_NOMEM); @@ -22909,6 +25247,7 @@ SQLITE_PRIVATE void sqlite3AppendChar(StrAccum *p, int N, char c){ if( p->nChar+(i64)N >= p->nAlloc && (N = sqlite3StrAccumEnlarge(p, N))<=0 ){ return; } + assert( (p->zText==p->zBase)==!isMalloced(p) ); while( (N--)>0 ) p->zText[p->nChar++] = c; } @@ -22926,6 +25265,7 @@ static void SQLITE_NOINLINE enlargeAndAppend(StrAccum *p, const char *z, int N){ memcpy(&p->zText[p->nChar], z, N); p->nChar += N; } + assert( (p->zText==0 || p->zText==p->zBase)==!isMalloced(p) ); } /* @@ -22961,11 +25301,13 @@ SQLITE_PRIVATE void sqlite3StrAccumAppendAll(StrAccum *p, const char *z){ */ SQLITE_PRIVATE char *sqlite3StrAccumFinish(StrAccum *p){ if( p->zText ){ + assert( (p->zText==p->zBase)==!isMalloced(p) ); p->zText[p->nChar] = 0; - if( p->mxAlloc>0 && p->zText==p->zBase ){ + if( p->mxAlloc>0 && !isMalloced(p) ){ p->zText = sqlite3DbMallocRaw(p->db, p->nChar+1 ); if( p->zText ){ memcpy(p->zText, p->zBase, p->nChar+1); + p->printfFlags |= SQLITE_PRINTF_MALLOCED; }else{ setStrAccumError(p, STRACCUM_NOMEM); } @@ -22978,8 +25320,10 @@ SQLITE_PRIVATE char *sqlite3StrAccumFinish(StrAccum *p){ ** Reset an StrAccum string. Reclaim all malloced memory. */ SQLITE_PRIVATE void sqlite3StrAccumReset(StrAccum *p){ - if( p->zText!=p->zBase ){ + assert( (p->zText==0 || p->zText==p->zBase)==!isMalloced(p) ); + if( isMalloced(p) ){ sqlite3DbFree(p->db, p->zText); + p->printfFlags &= ~SQLITE_PRINTF_MALLOCED; } p->zText = 0; } @@ -23005,6 +25349,7 @@ SQLITE_PRIVATE void sqlite3StrAccumInit(StrAccum *p, sqlite3 *db, char *zBase, i p->nAlloc = n; p->mxAlloc = mx; p->accError = 0; + p->printfFlags = 0; } /* @@ -23018,10 +25363,11 @@ SQLITE_PRIVATE char *sqlite3VMPrintf(sqlite3 *db, const char *zFormat, va_list a assert( db!=0 ); sqlite3StrAccumInit(&acc, db, zBase, sizeof(zBase), db->aLimit[SQLITE_LIMIT_LENGTH]); - sqlite3VXPrintf(&acc, SQLITE_PRINTF_INTERNAL, zFormat, ap); + acc.printfFlags = SQLITE_PRINTF_INTERNAL; + sqlite3VXPrintf(&acc, zFormat, ap); z = sqlite3StrAccumFinish(&acc); if( acc.accError==STRACCUM_NOMEM ){ - db->mallocFailed = 1; + sqlite3OomFault(db); } return z; } @@ -23058,7 +25404,7 @@ SQLITE_API char *SQLITE_STDCALL sqlite3_vmprintf(const char *zFormat, va_list ap if( sqlite3_initialize() ) return 0; #endif sqlite3StrAccumInit(&acc, 0, zBase, sizeof(zBase), SQLITE_MAX_LENGTH); - sqlite3VXPrintf(&acc, 0, zFormat, ap); + sqlite3VXPrintf(&acc, zFormat, ap); z = sqlite3StrAccumFinish(&acc); return z; } @@ -23103,7 +25449,7 @@ SQLITE_API char *SQLITE_STDCALL sqlite3_vsnprintf(int n, char *zBuf, const char } #endif sqlite3StrAccumInit(&acc, 0, zBuf, n, 0); - sqlite3VXPrintf(&acc, 0, zFormat, ap); + sqlite3VXPrintf(&acc, zFormat, ap); return sqlite3StrAccumFinish(&acc); } SQLITE_API char *SQLITE_CDECL sqlite3_snprintf(int n, char *zBuf, const char *zFormat, ...){ @@ -23134,7 +25480,7 @@ static void renderLogMsg(int iErrCode, const char *zFormat, va_list ap){ char zMsg[SQLITE_PRINT_BUF_SIZE*3]; /* Complete log message */ sqlite3StrAccumInit(&acc, 0, zMsg, sizeof(zMsg), 0); - sqlite3VXPrintf(&acc, 0, zFormat, ap); + sqlite3VXPrintf(&acc, zFormat, ap); sqlite3GlobalConfig.xLog(sqlite3GlobalConfig.pLogArg, iErrCode, sqlite3StrAccumFinish(&acc)); } @@ -23163,7 +25509,7 @@ SQLITE_PRIVATE void sqlite3DebugPrintf(const char *zFormat, ...){ char zBuf[500]; sqlite3StrAccumInit(&acc, 0, zBuf, sizeof(zBuf), 0); va_start(ap,zFormat); - sqlite3VXPrintf(&acc, 0, zFormat, ap); + sqlite3VXPrintf(&acc, zFormat, ap); va_end(ap); sqlite3StrAccumFinish(&acc); fprintf(stdout,"%s", zBuf); @@ -23176,10 +25522,10 @@ SQLITE_PRIVATE void sqlite3DebugPrintf(const char *zFormat, ...){ ** variable-argument wrapper around sqlite3VXPrintf(). The bFlags argument ** can contain the bit SQLITE_PRINTF_INTERNAL enable internal formats. */ -SQLITE_PRIVATE void sqlite3XPrintf(StrAccum *p, u32 bFlags, const char *zFormat, ...){ +SQLITE_PRIVATE void sqlite3XPrintf(StrAccum *p, const char *zFormat, ...){ va_list ap; va_start(ap,zFormat); - sqlite3VXPrintf(p, bFlags, zFormat, ap); + sqlite3VXPrintf(p, zFormat, ap); va_end(ap); } @@ -23250,7 +25596,7 @@ static void sqlite3TreeViewLine(TreeView *p, const char *zFormat, ...){ sqlite3StrAccumAppend(&acc, p->bLine[i] ? "|-- " : "'-- ", 4); } va_start(ap, zFormat); - sqlite3VXPrintf(&acc, 0, zFormat, ap); + sqlite3VXPrintf(&acc, zFormat, ap); va_end(ap); if( zBuf[acc.nChar-1]!='\n' ) sqlite3StrAccumAppend(&acc, "\n", 1); sqlite3StrAccumFinish(&acc); @@ -23266,6 +25612,45 @@ static void sqlite3TreeViewItem(TreeView *p, const char *zLabel,u8 moreFollows){ sqlite3TreeViewLine(p, "%s", zLabel); } +/* +** Generate a human-readable description of a WITH clause. +*/ +SQLITE_PRIVATE void sqlite3TreeViewWith(TreeView *pView, const With *pWith, u8 moreToFollow){ + int i; + if( pWith==0 ) return; + if( pWith->nCte==0 ) return; + if( pWith->pOuter ){ + sqlite3TreeViewLine(pView, "WITH (0x%p, pOuter=0x%p)",pWith,pWith->pOuter); + }else{ + sqlite3TreeViewLine(pView, "WITH (0x%p)", pWith); + } + if( pWith->nCte>0 ){ + pView = sqlite3TreeViewPush(pView, 1); + for(i=0; inCte; i++){ + StrAccum x; + char zLine[1000]; + const struct Cte *pCte = &pWith->a[i]; + sqlite3StrAccumInit(&x, 0, zLine, sizeof(zLine), 0); + sqlite3XPrintf(&x, "%s", pCte->zName); + if( pCte->pCols && pCte->pCols->nExpr>0 ){ + char cSep = '('; + int j; + for(j=0; jpCols->nExpr; j++){ + sqlite3XPrintf(&x, "%c%s", cSep, pCte->pCols->a[j].zName); + cSep = ','; + } + sqlite3XPrintf(&x, ")"); + } + sqlite3XPrintf(&x, " AS"); + sqlite3StrAccumFinish(&x); + sqlite3TreeViewItem(pView, zLine, inCte-1); + sqlite3TreeViewSelect(pView, pCte->pSelect, 0); + sqlite3TreeViewPop(pView); + } + sqlite3TreeViewPop(pView); + } +} + /* ** Generate a human-readable description of a the Select object. @@ -23274,10 +25659,16 @@ SQLITE_PRIVATE void sqlite3TreeViewSelect(TreeView *pView, const Select *p, u8 m int n = 0; int cnt = 0; pView = sqlite3TreeViewPush(pView, moreToFollow); + if( p->pWith ){ + sqlite3TreeViewWith(pView, p->pWith, 1); + cnt = 1; + sqlite3TreeViewPush(pView, 1); + } do{ - sqlite3TreeViewLine(pView, "SELECT%s%s (0x%p) selFlags=0x%x", + sqlite3TreeViewLine(pView, "SELECT%s%s (0x%p) selFlags=0x%x nSelectRow=%d", ((p->selFlags & SF_Distinct) ? " DISTINCT" : ""), - ((p->selFlags & SF_Aggregate) ? " agg_flag" : ""), p, p->selFlags + ((p->selFlags & SF_Aggregate) ? " agg_flag" : ""), p, p->selFlags, + (int)p->nSelectRow ); if( cnt++ ) sqlite3TreeViewPop(pView); if( p->pPrior ){ @@ -23302,20 +25693,20 @@ SQLITE_PRIVATE void sqlite3TreeViewSelect(TreeView *pView, const Select *p, u8 m StrAccum x; char zLine[100]; sqlite3StrAccumInit(&x, 0, zLine, sizeof(zLine), 0); - sqlite3XPrintf(&x, 0, "{%d,*}", pItem->iCursor); + sqlite3XPrintf(&x, "{%d,*}", pItem->iCursor); if( pItem->zDatabase ){ - sqlite3XPrintf(&x, 0, " %s.%s", pItem->zDatabase, pItem->zName); + sqlite3XPrintf(&x, " %s.%s", pItem->zDatabase, pItem->zName); }else if( pItem->zName ){ - sqlite3XPrintf(&x, 0, " %s", pItem->zName); + sqlite3XPrintf(&x, " %s", pItem->zName); } if( pItem->pTab ){ - sqlite3XPrintf(&x, 0, " tabname=%Q", pItem->pTab->zName); + sqlite3XPrintf(&x, " tabname=%Q", pItem->pTab->zName); } if( pItem->zAlias ){ - sqlite3XPrintf(&x, 0, " (AS %s)", pItem->zAlias); + sqlite3XPrintf(&x, " (AS %s)", pItem->zAlias); } if( pItem->fg.jointype & JT_LEFT ){ - sqlite3XPrintf(&x, 0, " LEFT-JOIN"); + sqlite3XPrintf(&x, " LEFT-JOIN"); } sqlite3StrAccumFinish(&x); sqlite3TreeViewItem(pView, zLine, ipSrc->nSrc-1); @@ -23481,6 +25872,12 @@ SQLITE_PRIVATE void sqlite3TreeViewExpr(TreeView *pView, const Expr *pExpr, u8 m case TK_ISNULL: zUniOp = "ISNULL"; break; case TK_NOTNULL: zUniOp = "NOTNULL"; break; + case TK_SPAN: { + sqlite3TreeViewLine(pView, "SPAN %Q", pExpr->u.zToken); + sqlite3TreeViewExpr(pView, pExpr->pLeft, 0); + break; + } + case TK_COLLATE: { sqlite3TreeViewLine(pView, "COLLATE %Q", pExpr->u.zToken); sqlite3TreeViewExpr(pView, pExpr->pLeft, 0); @@ -23832,7 +26229,7 @@ SQLITE_PRIVATE int sqlite3ThreadCreate( *ppThread = 0; p = sqlite3Malloc(sizeof(*p)); - if( p==0 ) return SQLITE_NOMEM; + if( p==0 ) return SQLITE_NOMEM_BKPT; memset(p, 0, sizeof(*p)); p->xTask = xTask; p->pIn = pIn; @@ -23858,7 +26255,7 @@ SQLITE_PRIVATE int sqlite3ThreadJoin(SQLiteThread *p, void **ppOut){ int rc; assert( ppOut!=0 ); - if( NEVER(p==0) ) return SQLITE_NOMEM; + if( NEVER(p==0) ) return SQLITE_NOMEM_BKPT; if( p->done ){ *ppOut = p->pOut; rc = SQLITE_OK; @@ -23923,7 +26320,7 @@ SQLITE_PRIVATE int sqlite3ThreadCreate( assert( xTask!=0 ); *ppThread = 0; p = sqlite3Malloc(sizeof(*p)); - if( p==0 ) return SQLITE_NOMEM; + if( p==0 ) return SQLITE_NOMEM_BKPT; /* If the SQLITE_TESTCTRL_FAULT_INSTALL callback is registered to a ** function that returns SQLITE_ERROR when passed the argument 200, that ** forces worker threads to run sequentially and deterministically @@ -23955,7 +26352,7 @@ SQLITE_PRIVATE int sqlite3ThreadJoin(SQLiteThread *p, void **ppOut){ BOOL bRc; assert( ppOut!=0 ); - if( NEVER(p==0) ) return SQLITE_NOMEM; + if( NEVER(p==0) ) return SQLITE_NOMEM_BKPT; if( p->xTask==0 ){ /* assert( p->id==GetCurrentThreadId() ); */ rc = WAIT_OBJECT_0; @@ -24003,7 +26400,7 @@ SQLITE_PRIVATE int sqlite3ThreadCreate( assert( xTask!=0 ); *ppThread = 0; p = sqlite3Malloc(sizeof(*p)); - if( p==0 ) return SQLITE_NOMEM; + if( p==0 ) return SQLITE_NOMEM_BKPT; if( (SQLITE_PTR_TO_INT(p)/17)&1 ){ p->xTask = xTask; p->pIn = pIn; @@ -24019,7 +26416,7 @@ SQLITE_PRIVATE int sqlite3ThreadCreate( SQLITE_PRIVATE int sqlite3ThreadJoin(SQLiteThread *p, void **ppOut){ assert( ppOut!=0 ); - if( NEVER(p==0) ) return SQLITE_NOMEM; + if( NEVER(p==0) ) return SQLITE_NOMEM_BKPT; if( p->xTask ){ *ppOut = p->xTask(p->pIn); }else{ @@ -24030,7 +26427,7 @@ SQLITE_PRIVATE int sqlite3ThreadJoin(SQLiteThread *p, void **ppOut){ #if defined(SQLITE_TEST) { void *pTstAlloc = sqlite3Malloc(10); - if (!pTstAlloc) return SQLITE_NOMEM; + if (!pTstAlloc) return SQLITE_NOMEM_BKPT; sqlite3_free(pTstAlloc); } #endif @@ -24083,13 +26480,13 @@ SQLITE_PRIVATE int sqlite3ThreadJoin(SQLiteThread *p, void **ppOut){ /* #include */ /* #include "vdbeInt.h" */ -#ifndef SQLITE_AMALGAMATION +#if !defined(SQLITE_AMALGAMATION) && SQLITE_BYTEORDER==0 /* ** The following constant value is used by the SQLITE_BIGENDIAN and ** SQLITE_LITTLEENDIAN macros. */ SQLITE_PRIVATE const int sqlite3one = 1; -#endif /* SQLITE_AMALGAMATION */ +#endif /* SQLITE_AMALGAMATION && SQLITE_BYTEORDER==0 */ /* ** This lookup table is used to help decode the first byte of @@ -24277,7 +26674,7 @@ SQLITE_PRIVATE SQLITE_NOINLINE int sqlite3VdbeMemTranslate(Mem *pMem, u8 desired rc = sqlite3VdbeMemMakeWriteable(pMem); if( rc!=SQLITE_OK ){ assert( rc==SQLITE_NOMEM ); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } zIn = (u8*)pMem->z; zTerm = &zIn[pMem->n&~1]; @@ -24319,7 +26716,7 @@ SQLITE_PRIVATE SQLITE_NOINLINE int sqlite3VdbeMemTranslate(Mem *pMem, u8 desired zTerm = &zIn[pMem->n]; zOut = sqlite3DbMallocRaw(pMem->db, len); if( !zOut ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } z = zOut; @@ -24362,7 +26759,7 @@ SQLITE_PRIVATE SQLITE_NOINLINE int sqlite3VdbeMemTranslate(Mem *pMem, u8 desired c = pMem->flags; sqlite3VdbeMemRelease(pMem); - pMem->flags = MEM_Str|MEM_Term|(c&MEM_AffMask); + pMem->flags = MEM_Str|MEM_Term|(c&(MEM_AffMask|MEM_Subtype)); pMem->enc = desiredEnc; pMem->z = (char*)zOut; pMem->zMalloc = pMem->z; @@ -24688,13 +27085,49 @@ SQLITE_PRIVATE int sqlite3Strlen30(const char *z){ return 0x3fffffff & (int)strlen(z); } +/* +** Return the declared type of a column. Or return zDflt if the column +** has no declared type. +** +** The column type is an extra string stored after the zero-terminator on +** the column name if and only if the COLFLAG_HASTYPE flag is set. +*/ +SQLITE_PRIVATE char *sqlite3ColumnType(Column *pCol, char *zDflt){ + if( (pCol->colFlags & COLFLAG_HASTYPE)==0 ) return zDflt; + return pCol->zName + strlen(pCol->zName) + 1; +} + +/* +** Helper function for sqlite3Error() - called rarely. Broken out into +** a separate routine to avoid unnecessary register saves on entry to +** sqlite3Error(). +*/ +static SQLITE_NOINLINE void sqlite3ErrorFinish(sqlite3 *db, int err_code){ + if( db->pErr ) sqlite3ValueSetNull(db->pErr); + sqlite3SystemError(db, err_code); +} + /* ** Set the current error code to err_code and clear any prior error message. +** Also set iSysErrno (by calling sqlite3System) if the err_code indicates +** that would be appropriate. */ SQLITE_PRIVATE void sqlite3Error(sqlite3 *db, int err_code){ assert( db!=0 ); db->errCode = err_code; - if( db->pErr ) sqlite3ValueSetNull(db->pErr); + if( err_code || db->pErr ) sqlite3ErrorFinish(db, err_code); +} + +/* +** Load the sqlite3.iSysErrno field if that is an appropriate thing +** to do based on the SQLite error code in rc. +*/ +SQLITE_PRIVATE void sqlite3SystemError(sqlite3 *db, int rc){ + if( rc==SQLITE_IOERR_NOMEM ) return; + rc &= 0xff; + if( rc==SQLITE_CANTOPEN || rc==SQLITE_IOERR ){ + db->iSysErrno = sqlite3OsGetLastError(db->pVfs); + } } /* @@ -24721,6 +27154,7 @@ SQLITE_PRIVATE void sqlite3Error(sqlite3 *db, int err_code){ SQLITE_PRIVATE void sqlite3ErrorWithMsg(sqlite3 *db, int err_code, const char *zFormat, ...){ assert( db!=0 ); db->errCode = err_code; + sqlite3SystemError(db, err_code); if( zFormat==0 ){ sqlite3Error(db, err_code); }else if( db->pErr || (db->pErr = sqlite3ValueNew(db))!=0 ){ @@ -24784,18 +27218,13 @@ SQLITE_PRIVATE void sqlite3ErrorMsg(Parse *pParse, const char *zFormat, ...){ ** brackets from around identifiers. For example: "[a-b-c]" becomes ** "a-b-c". */ -SQLITE_PRIVATE int sqlite3Dequote(char *z){ +SQLITE_PRIVATE void sqlite3Dequote(char *z){ char quote; int i, j; - if( z==0 ) return -1; + if( z==0 ) return; quote = z[0]; - switch( quote ){ - case '\'': break; - case '"': break; - case '`': break; /* For MySQL compatibility */ - case '[': quote = ']'; break; /* For MS SqlServer compatibility */ - default: return -1; - } + if( !sqlite3Isquote(quote) ) return; + if( quote=='[' ) quote = ']'; for(i=1, j=0;; i++){ assert( z[i] ); if( z[i]==quote ){ @@ -24810,7 +27239,14 @@ SQLITE_PRIVATE int sqlite3Dequote(char *z){ } } z[j] = 0; - return j; +} + +/* +** Generate a Token object from a string +*/ +SQLITE_PRIVATE void sqlite3TokenInit(Token *p, char *z){ + p->z = z; + p->n = sqlite3Strlen30(z); } /* Convenient short-hand */ @@ -24827,16 +27263,25 @@ SQLITE_PRIVATE int sqlite3Dequote(char *z){ ** independence" that SQLite uses internally when comparing identifiers. */ SQLITE_API int SQLITE_STDCALL sqlite3_stricmp(const char *zLeft, const char *zRight){ - register unsigned char *a, *b; if( zLeft==0 ){ return zRight ? -1 : 0; }else if( zRight==0 ){ return 1; } + return sqlite3StrICmp(zLeft, zRight); +} +SQLITE_PRIVATE int sqlite3StrICmp(const char *zLeft, const char *zRight){ + unsigned char *a, *b; + int c; a = (unsigned char *)zLeft; b = (unsigned char *)zRight; - while( *a!=0 && UpperToLower[*a]==UpperToLower[*b]){ a++; b++; } - return UpperToLower[*a] - UpperToLower[*b]; + for(;;){ + c = (int)UpperToLower[*a] - (int)UpperToLower[*b]; + if( c || *a==0 ) break; + a++; + b++; + } + return c; } SQLITE_API int SQLITE_STDCALL sqlite3_strnicmp(const char *zLeft, const char *zRight, int N){ register unsigned char *a, *b; @@ -24886,7 +27331,7 @@ SQLITE_PRIVATE int sqlite3AtoF(const char *z, double *pResult, int length, u8 en int eValid = 1; /* True exponent is either not used or is well-formed */ double result; int nDigits = 0; - int nonNum = 0; + int nonNum = 0; /* True if input contains UTF16 with high byte non-zero */ assert( enc==SQLITE_UTF8 || enc==SQLITE_UTF16LE || enc==SQLITE_UTF16BE ); *pResult = 0.0; /* Default return value, in case of an error */ @@ -24899,7 +27344,7 @@ SQLITE_PRIVATE int sqlite3AtoF(const char *z, double *pResult, int length, u8 en assert( SQLITE_UTF16LE==2 && SQLITE_UTF16BE==3 ); for(i=3-enc; i=zEnd ) goto do_atof_calc; @@ -24947,7 +27390,12 @@ SQLITE_PRIVATE int sqlite3AtoF(const char *z, double *pResult, int length, u8 en if( *z=='e' || *z=='E' ){ z+=incr; eValid = 0; - if( z>=zEnd ) goto do_atof_calc; + + /* This branch is needed to avoid a (harmless) buffer overread. The + ** special comment alerts the mutation tester that the correct answer + ** is obtained even if the branch is omitted */ + if( z>=zEnd ) goto do_atof_calc; /*PREVENTS-HARMLESS-OVERREAD*/ + /* get sign of exponent */ if( *z=='-' ){ esign = -1; @@ -24964,9 +27412,7 @@ SQLITE_PRIVATE int sqlite3AtoF(const char *z, double *pResult, int length, u8 en } /* skip trailing spaces */ - if( nDigits && eValid ){ - while( z0 ){ - while( s<(LARGEST_INT64/10) && e>0 ) e--,s*=10; - }else{ - while( !(s%10) && e>0 ) e--,s/=10; + /* Attempt to reduce exponent. + ** + ** Branches that are not required for the correct answer but which only + ** help to obtain the correct answer faster are marked with special + ** comments, as a hint to the mutation tester. + */ + while( e>0 ){ /*OPTIMIZATION-IF-TRUE*/ + if( esign>0 ){ + if( s>=(LARGEST_INT64/10) ) break; /*OPTIMIZATION-IF-FALSE*/ + s *= 10; + }else{ + if( s%10!=0 ) break; /*OPTIMIZATION-IF-FALSE*/ + s /= 10; + } + e--; } /* adjust the sign of significand */ s = sign<0 ? -s : s; - /* if exponent, scale significand as appropriate - ** and store in result. */ - if( e ){ + if( e==0 ){ /*OPTIMIZATION-IF-TRUE*/ + result = (double)s; + }else{ LONGDOUBLE_TYPE scale = 1.0; /* attempt to handle extremely small/large numbers better */ - if( e>307 && e<342 ){ - while( e%308 ) { scale *= 1.0e+1; e -= 1; } - if( esign<0 ){ - result = s / scale; - result /= 1.0e+308; - }else{ - result = s * scale; - result *= 1.0e+308; - } - }else if( e>=342 ){ - if( esign<0 ){ - result = 0.0*s; - }else{ - result = 1e308*1e308*s; /* Infinity */ + if( e>307 ){ /*OPTIMIZATION-IF-TRUE*/ + if( e<342 ){ /*OPTIMIZATION-IF-TRUE*/ + while( e%308 ) { scale *= 1.0e+1; e -= 1; } + if( esign<0 ){ + result = s / scale; + result /= 1.0e+308; + }else{ + result = s * scale; + result *= 1.0e+308; + } + }else{ assert( e>=342 ); + if( esign<0 ){ + result = 0.0*s; + }else{ + result = 1e308*1e308*s; /* Infinity */ + } } }else{ /* 1.0e+22 is the largest power of 10 than can be @@ -25025,8 +27481,6 @@ do_atof_calc: result = s * scale; } } - } else { - result = (double)s; } } @@ -25034,7 +27488,7 @@ do_atof_calc: *pResult = result; /* return true if number and no extra non-whitespace chracters after */ - return z>=zEnd && nDigits>0 && eValid && nonNum==0; + return z==zEnd && nDigits>0 && eValid && nonNum==0; #else return !sqlite3Atoi64(z, pResult, length, enc); #endif /* SQLITE_OMIT_FLOATING_POINT */ @@ -25096,7 +27550,7 @@ SQLITE_PRIVATE int sqlite3Atoi64(const char *zNum, i64 *pNum, int length, u8 enc int neg = 0; /* assume positive */ int i; int c = 0; - int nonNum = 0; + int nonNum = 0; /* True if input contains UTF16 with high byte non-zero */ const char *zStart; const char *zEnd = zNum + length; assert( enc==SQLITE_UTF8 || enc==SQLITE_UTF16LE || enc==SQLITE_UTF16BE ); @@ -25107,7 +27561,7 @@ SQLITE_PRIVATE int sqlite3Atoi64(const char *zNum, i64 *pNum, int length, u8 enc assert( SQLITE_UTF16LE==2 && SQLITE_UTF16BE==3 ); for(i=3-enc; i19*incr || nonNum ){ + if( &zNum[i]19*incr /* Too many digits */ + || nonNum /* UTF16 with high-order bytes non-zero */ + ){ /* zNum is empty or contains non-numeric text or is longer ** than 19 digits (thus guaranteeing that it is too large) */ return 1; @@ -25176,7 +27634,6 @@ SQLITE_PRIVATE int sqlite3DecOrHexToI64(const char *z, i64 *pOut){ #ifndef SQLITE_OMIT_HEX_INTEGER if( z[0]=='0' && (z[1]=='x' || z[1]=='X') - && sqlite3Isxdigit(z[2]) ){ u64 u = 0; int i, k; @@ -25423,7 +27880,8 @@ SQLITE_PRIVATE u8 sqlite3GetVarint(const unsigned char *p, u64 *v){ /* a: p0<<28 | p2<<14 | p4 (unmasked) */ if (!(a&0x80)) { - /* we can skip these cause they were (effectively) done above in calc'ing s */ + /* we can skip these cause they were (effectively) done above + ** while calculating s */ /* a &= (0x7f<<28)|(0x7f<<14)|(0x7f); */ /* b &= (0x7f<<14)|(0x7f); */ b = b<<7; @@ -25645,7 +28103,7 @@ SQLITE_PRIVATE u8 sqlite3GetVarint32(const unsigned char *p, u32 *v){ */ SQLITE_PRIVATE int sqlite3VarintLen(u64 v){ int i; - for(i=1; (v >>= 7)!=0; i++){ assert( i<9 ); } + for(i=1; (v >>= 7)!=0; i++){ assert( i<10 ); } return i; } @@ -25676,10 +28134,12 @@ SQLITE_PRIVATE u32 sqlite3Get4byte(const u8 *p){ SQLITE_PRIVATE void sqlite3Put4byte(unsigned char *p, u32 v){ #if SQLITE_BYTEORDER==4321 memcpy(p,&v,4); -#elif SQLITE_BYTEORDER==1234 && defined(__GNUC__) && GCC_VERSION>=4003000 +#elif SQLITE_BYTEORDER==1234 && !defined(SQLITE_DISABLE_INTRINSIC) \ + && defined(__GNUC__) && GCC_VERSION>=4003000 u32 x = __builtin_bswap32(v); memcpy(p,&x,4); -#elif SQLITE_BYTEORDER==1234 && defined(_MSC_VER) && _MSC_VER>=1300 +#elif SQLITE_BYTEORDER==1234 && !defined(SQLITE_DISABLE_INTRINSIC) \ + && defined(_MSC_VER) && _MSC_VER>=1300 u32 x = _byteswap_ulong(v); memcpy(p,&x,4); #else @@ -25719,7 +28179,7 @@ SQLITE_PRIVATE void *sqlite3HexToBlob(sqlite3 *db, const char *z, int n){ char *zBlob; int i; - zBlob = (char *)sqlite3DbMallocRaw(db, n/2 + 1); + zBlob = (char *)sqlite3DbMallocRawNN(db, n/2 + 1); n--; if( zBlob ){ for(i=0; i255 ){ y += 40; x >>= 4; } + while( x>255 ){ y += 40; x >>= 4; } /*OPTIMIZATION-IF-TRUE*/ while( x>15 ){ y += 10; x >>= 1; } } return a[x&7] + y - 10; @@ -25958,21 +28418,32 @@ SQLITE_PRIVATE LogEst sqlite3LogEstFromDouble(double x){ } #endif /* SQLITE_OMIT_VIRTUALTABLE */ +#if defined(SQLITE_ENABLE_STMT_SCANSTATUS) || \ + defined(SQLITE_ENABLE_STAT3_OR_STAT4) || \ + defined(SQLITE_EXPLAIN_ESTIMATED_ROWS) /* ** Convert a LogEst into an integer. +** +** Note that this routine is only used when one or more of various +** non-standard compile-time options is enabled. */ SQLITE_PRIVATE u64 sqlite3LogEstToInt(LogEst x){ u64 n; - if( x<10 ) return 1; n = x%10; x /= 10; if( n>=5 ) n -= 2; else if( n>=1 ) n -= 1; - if( x>=3 ){ - return x>60 ? (u64)LARGEST_INT64 : (n+8)<<(x-3); - } - return (n+8)>>(3-x); +#if defined(SQLITE_ENABLE_STMT_SCANSTATUS) || \ + defined(SQLITE_EXPLAIN_ESTIMATED_ROWS) + if( x>60 ) return (u64)LARGEST_INT64; +#else + /* If only SQLITE_ENABLE_STAT3_OR_STAT4 is on, then the largest input + ** possible to this routine is 310, resulting in a maximum x of 31 */ + assert( x<=60 ); +#endif + return x>=3 ? (n+8)<<(x-3) : (n+8)>>(3-x); } +#endif /* defined SCANSTAT or STAT4 or ESTIMATED_ROWS */ /************** End of util.c ************************************************/ /************** Begin file hash.c ********************************************/ @@ -26033,7 +28504,7 @@ SQLITE_PRIVATE void sqlite3HashClear(Hash *pH){ static unsigned int strHash(const char *z){ unsigned int h = 0; unsigned char c; - while( (c = (unsigned char)*z++)!=0 ){ + while( (c = (unsigned char)*z++)!=0 ){ /*OPTIMIZATION-IF-TRUE*/ h = (h<<3) ^ h ^ sqlite3UpperToLower[c]; } return h; @@ -26126,7 +28597,7 @@ static HashElem *findElementWithHash( int count; /* Number of elements left to test */ unsigned int h; /* The computed hash */ - if( pH->ht ){ + if( pH->ht ){ /*OPTIMIZATION-IF-TRUE*/ struct _ht *pEntry; h = strHash(pKey) % pH->htsize; pEntry = &pH->ht[h]; @@ -26247,175 +28718,178 @@ SQLITE_PRIVATE void *sqlite3HashInsert(Hash *pH, const char *pKey, void *data){ /************** End of hash.c ************************************************/ /************** Begin file opcodes.c *****************************************/ /* Automatically generated. Do not edit */ -/* See the mkopcodec.awk script for details. */ -#if !defined(SQLITE_OMIT_EXPLAIN) || defined(VDBE_PROFILE) || defined(SQLITE_DEBUG) +/* See the tool/mkopcodec.tcl script for details. */ +#if !defined(SQLITE_OMIT_EXPLAIN) \ + || defined(VDBE_PROFILE) \ + || defined(SQLITE_DEBUG) #if defined(SQLITE_ENABLE_EXPLAIN_COMMENTS) || defined(SQLITE_DEBUG) # define OpHelp(X) "\0" X #else # define OpHelp(X) #endif SQLITE_PRIVATE const char *sqlite3OpcodeName(int i){ - static const char *const azName[] = { "?", - /* 1 */ "Savepoint" OpHelp(""), - /* 2 */ "AutoCommit" OpHelp(""), - /* 3 */ "Transaction" OpHelp(""), - /* 4 */ "SorterNext" OpHelp(""), - /* 5 */ "PrevIfOpen" OpHelp(""), - /* 6 */ "NextIfOpen" OpHelp(""), - /* 7 */ "Prev" OpHelp(""), - /* 8 */ "Next" OpHelp(""), - /* 9 */ "Checkpoint" OpHelp(""), - /* 10 */ "JournalMode" OpHelp(""), - /* 11 */ "Vacuum" OpHelp(""), - /* 12 */ "VFilter" OpHelp("iplan=r[P3] zplan='P4'"), - /* 13 */ "VUpdate" OpHelp("data=r[P3@P2]"), - /* 14 */ "Goto" OpHelp(""), - /* 15 */ "Gosub" OpHelp(""), - /* 16 */ "Return" OpHelp(""), - /* 17 */ "InitCoroutine" OpHelp(""), - /* 18 */ "EndCoroutine" OpHelp(""), - /* 19 */ "Not" OpHelp("r[P2]= !r[P1]"), - /* 20 */ "Yield" OpHelp(""), - /* 21 */ "HaltIfNull" OpHelp("if r[P3]=null halt"), - /* 22 */ "Halt" OpHelp(""), - /* 23 */ "Integer" OpHelp("r[P2]=P1"), - /* 24 */ "Int64" OpHelp("r[P2]=P4"), - /* 25 */ "String" OpHelp("r[P2]='P4' (len=P1)"), - /* 26 */ "Null" OpHelp("r[P2..P3]=NULL"), - /* 27 */ "SoftNull" OpHelp("r[P1]=NULL"), - /* 28 */ "Blob" OpHelp("r[P2]=P4 (len=P1)"), - /* 29 */ "Variable" OpHelp("r[P2]=parameter(P1,P4)"), - /* 30 */ "Move" OpHelp("r[P2@P3]=r[P1@P3]"), - /* 31 */ "Copy" OpHelp("r[P2@P3+1]=r[P1@P3+1]"), - /* 32 */ "SCopy" OpHelp("r[P2]=r[P1]"), - /* 33 */ "ResultRow" OpHelp("output=r[P1@P2]"), - /* 34 */ "CollSeq" OpHelp(""), - /* 35 */ "Function0" OpHelp("r[P3]=func(r[P2@P5])"), - /* 36 */ "Function" OpHelp("r[P3]=func(r[P2@P5])"), - /* 37 */ "AddImm" OpHelp("r[P1]=r[P1]+P2"), - /* 38 */ "MustBeInt" OpHelp(""), - /* 39 */ "RealAffinity" OpHelp(""), - /* 40 */ "Cast" OpHelp("affinity(r[P1])"), - /* 41 */ "Permutation" OpHelp(""), - /* 42 */ "Compare" OpHelp("r[P1@P3] <-> r[P2@P3]"), - /* 43 */ "Jump" OpHelp(""), - /* 44 */ "Once" OpHelp(""), - /* 45 */ "If" OpHelp(""), - /* 46 */ "IfNot" OpHelp(""), - /* 47 */ "Column" OpHelp("r[P3]=PX"), - /* 48 */ "Affinity" OpHelp("affinity(r[P1@P2])"), - /* 49 */ "MakeRecord" OpHelp("r[P3]=mkrec(r[P1@P2])"), - /* 50 */ "Count" OpHelp("r[P2]=count()"), - /* 51 */ "ReadCookie" OpHelp(""), - /* 52 */ "SetCookie" OpHelp(""), - /* 53 */ "ReopenIdx" OpHelp("root=P2 iDb=P3"), - /* 54 */ "OpenRead" OpHelp("root=P2 iDb=P3"), - /* 55 */ "OpenWrite" OpHelp("root=P2 iDb=P3"), - /* 56 */ "OpenAutoindex" OpHelp("nColumn=P2"), - /* 57 */ "OpenEphemeral" OpHelp("nColumn=P2"), - /* 58 */ "SorterOpen" OpHelp(""), - /* 59 */ "SequenceTest" OpHelp("if( cursor[P1].ctr++ ) pc = P2"), - /* 60 */ "OpenPseudo" OpHelp("P3 columns in r[P2]"), - /* 61 */ "Close" OpHelp(""), - /* 62 */ "ColumnsUsed" OpHelp(""), - /* 63 */ "SeekLT" OpHelp("key=r[P3@P4]"), - /* 64 */ "SeekLE" OpHelp("key=r[P3@P4]"), - /* 65 */ "SeekGE" OpHelp("key=r[P3@P4]"), - /* 66 */ "SeekGT" OpHelp("key=r[P3@P4]"), - /* 67 */ "Seek" OpHelp("intkey=r[P2]"), - /* 68 */ "NoConflict" OpHelp("key=r[P3@P4]"), - /* 69 */ "NotFound" OpHelp("key=r[P3@P4]"), - /* 70 */ "Found" OpHelp("key=r[P3@P4]"), - /* 71 */ "Or" OpHelp("r[P3]=(r[P1] || r[P2])"), - /* 72 */ "And" OpHelp("r[P3]=(r[P1] && r[P2])"), - /* 73 */ "NotExists" OpHelp("intkey=r[P3]"), - /* 74 */ "Sequence" OpHelp("r[P2]=cursor[P1].ctr++"), - /* 75 */ "NewRowid" OpHelp("r[P2]=rowid"), - /* 76 */ "IsNull" OpHelp("if r[P1]==NULL goto P2"), - /* 77 */ "NotNull" OpHelp("if r[P1]!=NULL goto P2"), - /* 78 */ "Ne" OpHelp("if r[P1]!=r[P3] goto P2"), - /* 79 */ "Eq" OpHelp("if r[P1]==r[P3] goto P2"), - /* 80 */ "Gt" OpHelp("if r[P1]>r[P3] goto P2"), - /* 81 */ "Le" OpHelp("if r[P1]<=r[P3] goto P2"), - /* 82 */ "Lt" OpHelp("if r[P1]=r[P3] goto P2"), - /* 84 */ "Insert" OpHelp("intkey=r[P3] data=r[P2]"), - /* 85 */ "BitAnd" OpHelp("r[P3]=r[P1]&r[P2]"), - /* 86 */ "BitOr" OpHelp("r[P3]=r[P1]|r[P2]"), - /* 87 */ "ShiftLeft" OpHelp("r[P3]=r[P2]<>r[P1]"), - /* 89 */ "Add" OpHelp("r[P3]=r[P1]+r[P2]"), - /* 90 */ "Subtract" OpHelp("r[P3]=r[P2]-r[P1]"), - /* 91 */ "Multiply" OpHelp("r[P3]=r[P1]*r[P2]"), - /* 92 */ "Divide" OpHelp("r[P3]=r[P2]/r[P1]"), - /* 93 */ "Remainder" OpHelp("r[P3]=r[P2]%r[P1]"), - /* 94 */ "Concat" OpHelp("r[P3]=r[P2]+r[P1]"), - /* 95 */ "InsertInt" OpHelp("intkey=P3 data=r[P2]"), - /* 96 */ "BitNot" OpHelp("r[P1]= ~r[P1]"), - /* 97 */ "String8" OpHelp("r[P2]='P4'"), - /* 98 */ "Delete" OpHelp(""), - /* 99 */ "ResetCount" OpHelp(""), - /* 100 */ "SorterCompare" OpHelp("if key(P1)!=trim(r[P3],P4) goto P2"), - /* 101 */ "SorterData" OpHelp("r[P2]=data"), - /* 102 */ "RowKey" OpHelp("r[P2]=key"), - /* 103 */ "RowData" OpHelp("r[P2]=data"), - /* 104 */ "Rowid" OpHelp("r[P2]=rowid"), - /* 105 */ "NullRow" OpHelp(""), - /* 106 */ "Last" OpHelp(""), - /* 107 */ "SorterSort" OpHelp(""), - /* 108 */ "Sort" OpHelp(""), - /* 109 */ "Rewind" OpHelp(""), - /* 110 */ "SorterInsert" OpHelp(""), - /* 111 */ "IdxInsert" OpHelp("key=r[P2]"), - /* 112 */ "IdxDelete" OpHelp("key=r[P2@P3]"), - /* 113 */ "IdxRowid" OpHelp("r[P2]=rowid"), - /* 114 */ "IdxLE" OpHelp("key=r[P3@P4]"), - /* 115 */ "IdxGT" OpHelp("key=r[P3@P4]"), - /* 116 */ "IdxLT" OpHelp("key=r[P3@P4]"), - /* 117 */ "IdxGE" OpHelp("key=r[P3@P4]"), - /* 118 */ "Destroy" OpHelp(""), - /* 119 */ "Clear" OpHelp(""), - /* 120 */ "ResetSorter" OpHelp(""), - /* 121 */ "CreateIndex" OpHelp("r[P2]=root iDb=P1"), - /* 122 */ "CreateTable" OpHelp("r[P2]=root iDb=P1"), - /* 123 */ "ParseSchema" OpHelp(""), - /* 124 */ "LoadAnalysis" OpHelp(""), - /* 125 */ "DropTable" OpHelp(""), - /* 126 */ "DropIndex" OpHelp(""), - /* 127 */ "DropTrigger" OpHelp(""), - /* 128 */ "IntegrityCk" OpHelp(""), - /* 129 */ "RowSetAdd" OpHelp("rowset(P1)=r[P2]"), - /* 130 */ "RowSetRead" OpHelp("r[P3]=rowset(P1)"), - /* 131 */ "RowSetTest" OpHelp("if r[P3] in rowset(P1) goto P2"), - /* 132 */ "Program" OpHelp(""), - /* 133 */ "Real" OpHelp("r[P2]=P4"), - /* 134 */ "Param" OpHelp(""), - /* 135 */ "FkCounter" OpHelp("fkctr[P1]+=P2"), - /* 136 */ "FkIfZero" OpHelp("if fkctr[P1]==0 goto P2"), - /* 137 */ "MemMax" OpHelp("r[P1]=max(r[P1],r[P2])"), - /* 138 */ "IfPos" OpHelp("if r[P1]>0 then r[P1]-=P3, goto P2"), - /* 139 */ "SetIfNotPos" OpHelp("if r[P1]<=0 then r[P2]=P3"), - /* 140 */ "IfNotZero" OpHelp("if r[P1]!=0 then r[P1]-=P3, goto P2"), - /* 141 */ "DecrJumpZero" OpHelp("if (--r[P1])==0 goto P2"), - /* 142 */ "JumpZeroIncr" OpHelp("if (r[P1]++)==0 ) goto P2"), - /* 143 */ "AggStep0" OpHelp("accum=r[P3] step(r[P2@P5])"), - /* 144 */ "AggStep" OpHelp("accum=r[P3] step(r[P2@P5])"), - /* 145 */ "AggFinal" OpHelp("accum=r[P1] N=P2"), - /* 146 */ "IncrVacuum" OpHelp(""), - /* 147 */ "Expire" OpHelp(""), - /* 148 */ "TableLock" OpHelp("iDb=P1 root=P2 write=P3"), - /* 149 */ "VBegin" OpHelp(""), - /* 150 */ "VCreate" OpHelp(""), - /* 151 */ "VDestroy" OpHelp(""), - /* 152 */ "VOpen" OpHelp(""), - /* 153 */ "VColumn" OpHelp("r[P3]=vcolumn(P2)"), - /* 154 */ "VNext" OpHelp(""), - /* 155 */ "VRename" OpHelp(""), - /* 156 */ "Pagecount" OpHelp(""), - /* 157 */ "MaxPgcnt" OpHelp(""), - /* 158 */ "Init" OpHelp("Start at P2"), - /* 159 */ "Noop" OpHelp(""), - /* 160 */ "Explain" OpHelp(""), + static const char *const azName[] = { + /* 0 */ "Savepoint" OpHelp(""), + /* 1 */ "AutoCommit" OpHelp(""), + /* 2 */ "Transaction" OpHelp(""), + /* 3 */ "SorterNext" OpHelp(""), + /* 4 */ "PrevIfOpen" OpHelp(""), + /* 5 */ "NextIfOpen" OpHelp(""), + /* 6 */ "Prev" OpHelp(""), + /* 7 */ "Next" OpHelp(""), + /* 8 */ "Checkpoint" OpHelp(""), + /* 9 */ "JournalMode" OpHelp(""), + /* 10 */ "Vacuum" OpHelp(""), + /* 11 */ "VFilter" OpHelp("iplan=r[P3] zplan='P4'"), + /* 12 */ "VUpdate" OpHelp("data=r[P3@P2]"), + /* 13 */ "Goto" OpHelp(""), + /* 14 */ "Gosub" OpHelp(""), + /* 15 */ "InitCoroutine" OpHelp(""), + /* 16 */ "Yield" OpHelp(""), + /* 17 */ "MustBeInt" OpHelp(""), + /* 18 */ "Jump" OpHelp(""), + /* 19 */ "Not" OpHelp("r[P2]= !r[P1]"), + /* 20 */ "Once" OpHelp(""), + /* 21 */ "If" OpHelp(""), + /* 22 */ "IfNot" OpHelp(""), + /* 23 */ "SeekLT" OpHelp("key=r[P3@P4]"), + /* 24 */ "SeekLE" OpHelp("key=r[P3@P4]"), + /* 25 */ "SeekGE" OpHelp("key=r[P3@P4]"), + /* 26 */ "SeekGT" OpHelp("key=r[P3@P4]"), + /* 27 */ "Or" OpHelp("r[P3]=(r[P1] || r[P2])"), + /* 28 */ "And" OpHelp("r[P3]=(r[P1] && r[P2])"), + /* 29 */ "NoConflict" OpHelp("key=r[P3@P4]"), + /* 30 */ "NotFound" OpHelp("key=r[P3@P4]"), + /* 31 */ "Found" OpHelp("key=r[P3@P4]"), + /* 32 */ "NotExists" OpHelp("intkey=r[P3]"), + /* 33 */ "Last" OpHelp(""), + /* 34 */ "IsNull" OpHelp("if r[P1]==NULL goto P2"), + /* 35 */ "NotNull" OpHelp("if r[P1]!=NULL goto P2"), + /* 36 */ "Ne" OpHelp("if r[P1]!=r[P3] goto P2"), + /* 37 */ "Eq" OpHelp("if r[P1]==r[P3] goto P2"), + /* 38 */ "Gt" OpHelp("if r[P1]>r[P3] goto P2"), + /* 39 */ "Le" OpHelp("if r[P1]<=r[P3] goto P2"), + /* 40 */ "Lt" OpHelp("if r[P1]=r[P3] goto P2"), + /* 42 */ "SorterSort" OpHelp(""), + /* 43 */ "BitAnd" OpHelp("r[P3]=r[P1]&r[P2]"), + /* 44 */ "BitOr" OpHelp("r[P3]=r[P1]|r[P2]"), + /* 45 */ "ShiftLeft" OpHelp("r[P3]=r[P2]<>r[P1]"), + /* 47 */ "Add" OpHelp("r[P3]=r[P1]+r[P2]"), + /* 48 */ "Subtract" OpHelp("r[P3]=r[P2]-r[P1]"), + /* 49 */ "Multiply" OpHelp("r[P3]=r[P1]*r[P2]"), + /* 50 */ "Divide" OpHelp("r[P3]=r[P2]/r[P1]"), + /* 51 */ "Remainder" OpHelp("r[P3]=r[P2]%r[P1]"), + /* 52 */ "Concat" OpHelp("r[P3]=r[P2]+r[P1]"), + /* 53 */ "Sort" OpHelp(""), + /* 54 */ "BitNot" OpHelp("r[P1]= ~r[P1]"), + /* 55 */ "Rewind" OpHelp(""), + /* 56 */ "IdxLE" OpHelp("key=r[P3@P4]"), + /* 57 */ "IdxGT" OpHelp("key=r[P3@P4]"), + /* 58 */ "IdxLT" OpHelp("key=r[P3@P4]"), + /* 59 */ "IdxGE" OpHelp("key=r[P3@P4]"), + /* 60 */ "RowSetRead" OpHelp("r[P3]=rowset(P1)"), + /* 61 */ "RowSetTest" OpHelp("if r[P3] in rowset(P1) goto P2"), + /* 62 */ "Program" OpHelp(""), + /* 63 */ "FkIfZero" OpHelp("if fkctr[P1]==0 goto P2"), + /* 64 */ "IfPos" OpHelp("if r[P1]>0 then r[P1]-=P3, goto P2"), + /* 65 */ "IfNotZero" OpHelp("if r[P1]!=0 then r[P1]-=P3, goto P2"), + /* 66 */ "DecrJumpZero" OpHelp("if (--r[P1])==0 goto P2"), + /* 67 */ "IncrVacuum" OpHelp(""), + /* 68 */ "VNext" OpHelp(""), + /* 69 */ "Init" OpHelp("Start at P2"), + /* 70 */ "Return" OpHelp(""), + /* 71 */ "EndCoroutine" OpHelp(""), + /* 72 */ "HaltIfNull" OpHelp("if r[P3]=null halt"), + /* 73 */ "Halt" OpHelp(""), + /* 74 */ "Integer" OpHelp("r[P2]=P1"), + /* 75 */ "Int64" OpHelp("r[P2]=P4"), + /* 76 */ "String" OpHelp("r[P2]='P4' (len=P1)"), + /* 77 */ "Null" OpHelp("r[P2..P3]=NULL"), + /* 78 */ "SoftNull" OpHelp("r[P1]=NULL"), + /* 79 */ "Blob" OpHelp("r[P2]=P4 (len=P1)"), + /* 80 */ "Variable" OpHelp("r[P2]=parameter(P1,P4)"), + /* 81 */ "Move" OpHelp("r[P2@P3]=r[P1@P3]"), + /* 82 */ "Copy" OpHelp("r[P2@P3+1]=r[P1@P3+1]"), + /* 83 */ "SCopy" OpHelp("r[P2]=r[P1]"), + /* 84 */ "IntCopy" OpHelp("r[P2]=r[P1]"), + /* 85 */ "ResultRow" OpHelp("output=r[P1@P2]"), + /* 86 */ "CollSeq" OpHelp(""), + /* 87 */ "Function0" OpHelp("r[P3]=func(r[P2@P5])"), + /* 88 */ "Function" OpHelp("r[P3]=func(r[P2@P5])"), + /* 89 */ "AddImm" OpHelp("r[P1]=r[P1]+P2"), + /* 90 */ "RealAffinity" OpHelp(""), + /* 91 */ "Cast" OpHelp("affinity(r[P1])"), + /* 92 */ "Permutation" OpHelp(""), + /* 93 */ "Compare" OpHelp("r[P1@P3] <-> r[P2@P3]"), + /* 94 */ "Column" OpHelp("r[P3]=PX"), + /* 95 */ "Affinity" OpHelp("affinity(r[P1@P2])"), + /* 96 */ "MakeRecord" OpHelp("r[P3]=mkrec(r[P1@P2])"), + /* 97 */ "String8" OpHelp("r[P2]='P4'"), + /* 98 */ "Count" OpHelp("r[P2]=count()"), + /* 99 */ "ReadCookie" OpHelp(""), + /* 100 */ "SetCookie" OpHelp(""), + /* 101 */ "ReopenIdx" OpHelp("root=P2 iDb=P3"), + /* 102 */ "OpenRead" OpHelp("root=P2 iDb=P3"), + /* 103 */ "OpenWrite" OpHelp("root=P2 iDb=P3"), + /* 104 */ "OpenAutoindex" OpHelp("nColumn=P2"), + /* 105 */ "OpenEphemeral" OpHelp("nColumn=P2"), + /* 106 */ "SorterOpen" OpHelp(""), + /* 107 */ "SequenceTest" OpHelp("if( cursor[P1].ctr++ ) pc = P2"), + /* 108 */ "OpenPseudo" OpHelp("P3 columns in r[P2]"), + /* 109 */ "Close" OpHelp(""), + /* 110 */ "ColumnsUsed" OpHelp(""), + /* 111 */ "Sequence" OpHelp("r[P2]=cursor[P1].ctr++"), + /* 112 */ "NewRowid" OpHelp("r[P2]=rowid"), + /* 113 */ "Insert" OpHelp("intkey=r[P3] data=r[P2]"), + /* 114 */ "InsertInt" OpHelp("intkey=P3 data=r[P2]"), + /* 115 */ "Delete" OpHelp(""), + /* 116 */ "ResetCount" OpHelp(""), + /* 117 */ "SorterCompare" OpHelp("if key(P1)!=trim(r[P3],P4) goto P2"), + /* 118 */ "SorterData" OpHelp("r[P2]=data"), + /* 119 */ "RowKey" OpHelp("r[P2]=key"), + /* 120 */ "RowData" OpHelp("r[P2]=data"), + /* 121 */ "Rowid" OpHelp("r[P2]=rowid"), + /* 122 */ "NullRow" OpHelp(""), + /* 123 */ "SorterInsert" OpHelp(""), + /* 124 */ "IdxInsert" OpHelp("key=r[P2]"), + /* 125 */ "IdxDelete" OpHelp("key=r[P2@P3]"), + /* 126 */ "Seek" OpHelp("Move P3 to P1.rowid"), + /* 127 */ "IdxRowid" OpHelp("r[P2]=rowid"), + /* 128 */ "Destroy" OpHelp(""), + /* 129 */ "Clear" OpHelp(""), + /* 130 */ "ResetSorter" OpHelp(""), + /* 131 */ "CreateIndex" OpHelp("r[P2]=root iDb=P1"), + /* 132 */ "CreateTable" OpHelp("r[P2]=root iDb=P1"), + /* 133 */ "Real" OpHelp("r[P2]=P4"), + /* 134 */ "ParseSchema" OpHelp(""), + /* 135 */ "LoadAnalysis" OpHelp(""), + /* 136 */ "DropTable" OpHelp(""), + /* 137 */ "DropIndex" OpHelp(""), + /* 138 */ "DropTrigger" OpHelp(""), + /* 139 */ "IntegrityCk" OpHelp(""), + /* 140 */ "RowSetAdd" OpHelp("rowset(P1)=r[P2]"), + /* 141 */ "Param" OpHelp(""), + /* 142 */ "FkCounter" OpHelp("fkctr[P1]+=P2"), + /* 143 */ "MemMax" OpHelp("r[P1]=max(r[P1],r[P2])"), + /* 144 */ "OffsetLimit" OpHelp("if r[P1]>0 then r[P2]=r[P1]+max(0,r[P3]) else r[P2]=(-1)"), + /* 145 */ "AggStep0" OpHelp("accum=r[P3] step(r[P2@P5])"), + /* 146 */ "AggStep" OpHelp("accum=r[P3] step(r[P2@P5])"), + /* 147 */ "AggFinal" OpHelp("accum=r[P1] N=P2"), + /* 148 */ "Expire" OpHelp(""), + /* 149 */ "TableLock" OpHelp("iDb=P1 root=P2 write=P3"), + /* 150 */ "VBegin" OpHelp(""), + /* 151 */ "VCreate" OpHelp(""), + /* 152 */ "VDestroy" OpHelp(""), + /* 153 */ "VOpen" OpHelp(""), + /* 154 */ "VColumn" OpHelp("r[P3]=vcolumn(P2)"), + /* 155 */ "VRename" OpHelp(""), + /* 156 */ "Pagecount" OpHelp(""), + /* 157 */ "MaxPgcnt" OpHelp(""), + /* 158 */ "CursorHint" OpHelp(""), + /* 159 */ "Noop" OpHelp(""), + /* 160 */ "Explain" OpHelp(""), }; return azName[i]; } @@ -26496,6 +28970,19 @@ SQLITE_PRIVATE const char *sqlite3OpcodeName(int i){ # endif #endif +/* Use pread() and pwrite() if they are available */ +#if defined(__APPLE__) +# define HAVE_PREAD 1 +# define HAVE_PWRITE 1 +#endif +#if defined(HAVE_PREAD64) && defined(HAVE_PWRITE64) +# undef USE_PREAD +# define USE_PREAD64 1 +#elif defined(HAVE_PREAD) && defined(HAVE_PWRITE) +# undef USE_PREAD64 +# define USE_PREAD 1 +#endif + /* ** standard include files. */ @@ -26574,6 +29061,11 @@ SQLITE_PRIVATE const char *sqlite3OpcodeName(int i){ */ #define MAX_PATHNAME 512 +/* +** Maximum supported symbolic links +*/ +#define SQLITE_MAX_SYMLINKS 100 + /* Always cast the getpid() return type for compatibility with ** kernel modules in VxWorks. */ #define osGetpid(X) (pid_t)getpid() @@ -26683,8 +29175,6 @@ static pid_t randomnessPid = 0; #define UNIXFILE_DELETE 0x20 /* Delete on close */ #define UNIXFILE_URI 0x40 /* Filename might have query parameters */ #define UNIXFILE_NOLOCK 0x80 /* Do no file locking */ -#define UNIXFILE_WARNED 0x0100 /* verifyDbFile() warnings issued */ -#define UNIXFILE_BLOCK 0x0200 /* Next SHM lock might block */ /* ** Include code that is common to all os_*.c files @@ -26728,8 +29218,8 @@ static pid_t randomnessPid = 0; */ #ifdef SQLITE_PERFORMANCE_TRACE -/* -** hwtime.h contains inline assembler code for implementing +/* +** hwtime.h contains inline assembler code for implementing ** high-performance timing routines. */ /************** Include hwtime.h in the middle of os_common.h ****************/ @@ -26839,14 +29329,14 @@ static sqlite_uint64 g_elapsed; ** of code will give us the ability to simulate a disk I/O error. This ** is used for testing the I/O recovery logic. */ -#ifdef SQLITE_TEST -SQLITE_API int sqlite3_io_error_hit = 0; /* Total number of I/O Errors */ -SQLITE_API int sqlite3_io_error_hardhit = 0; /* Number of non-benign errors */ -SQLITE_API int sqlite3_io_error_pending = 0; /* Count down to first I/O error */ -SQLITE_API int sqlite3_io_error_persist = 0; /* True if I/O errors persist */ -SQLITE_API int sqlite3_io_error_benign = 0; /* True if errors are benign */ -SQLITE_API int sqlite3_diskfull_pending = 0; -SQLITE_API int sqlite3_diskfull = 0; +#if defined(SQLITE_TEST) +SQLITE_API extern int sqlite3_io_error_hit; +SQLITE_API extern int sqlite3_io_error_hardhit; +SQLITE_API extern int sqlite3_io_error_pending; +SQLITE_API extern int sqlite3_io_error_persist; +SQLITE_API extern int sqlite3_io_error_benign; +SQLITE_API extern int sqlite3_diskfull_pending; +SQLITE_API extern int sqlite3_diskfull; #define SimulateIOErrorBenign(X) sqlite3_io_error_benign=(X) #define SimulateIOError(CODE) \ if( (sqlite3_io_error_persist && sqlite3_io_error_hit) \ @@ -26872,17 +29362,17 @@ static void local_ioerr(){ #define SimulateIOErrorBenign(X) #define SimulateIOError(A) #define SimulateDiskfullError(A) -#endif +#endif /* defined(SQLITE_TEST) */ /* ** When testing, keep a count of the number of open files. */ -#ifdef SQLITE_TEST -SQLITE_API int sqlite3_open_file_count = 0; +#if defined(SQLITE_TEST) +SQLITE_API extern int sqlite3_open_file_count; #define OpenCounter(X) sqlite3_open_file_count+=(X) #else #define OpenCounter(X) -#endif +#endif /* defined(SQLITE_TEST) */ #endif /* !defined(_OS_COMMON_H_) */ @@ -26947,19 +29437,6 @@ static int posixOpen(const char *zFile, int flags, int mode){ return open(zFile, flags, mode); } -/* -** On some systems, calls to fchown() will trigger a message in a security -** log if they come from non-root processes. So avoid calling fchown() if -** we are not running as root. -*/ -static int posixFchown(int fd, uid_t uid, gid_t gid){ -#if OS_VXWORKS - return 0; -#else - return geteuid() ? 0 : fchown(fd,uid,gid); -#endif -} - /* Forward reference */ static int openDirectory(const char*, int*); static int unixGetpagesize(void); @@ -27025,7 +29502,7 @@ static struct unix_syscall { #else { "pread64", (sqlite3_syscall_ptr)0, 0 }, #endif -#define osPread64 ((ssize_t(*)(int,void*,size_t,off_t))aSyscall[10].pCurrent) +#define osPread64 ((ssize_t(*)(int,void*,size_t,off64_t))aSyscall[10].pCurrent) { "write", (sqlite3_syscall_ptr)write, 0 }, #define osWrite ((ssize_t(*)(int,const void*,size_t))aSyscall[11].pCurrent) @@ -27043,10 +29520,10 @@ static struct unix_syscall { #else { "pwrite64", (sqlite3_syscall_ptr)0, 0 }, #endif -#define osPwrite64 ((ssize_t(*)(int,const void*,size_t,off_t))\ +#define osPwrite64 ((ssize_t(*)(int,const void*,size_t,off64_t))\ aSyscall[13].pCurrent) - { "fchmod", (sqlite3_syscall_ptr)fchmod, 0 }, + { "fchmod", (sqlite3_syscall_ptr)fchmod, 0 }, #define osFchmod ((int(*)(int,mode_t))aSyscall[14].pCurrent) #if defined(HAVE_POSIX_FALLOCATE) && HAVE_POSIX_FALLOCATE @@ -27068,29 +29545,74 @@ static struct unix_syscall { { "rmdir", (sqlite3_syscall_ptr)rmdir, 0 }, #define osRmdir ((int(*)(const char*))aSyscall[19].pCurrent) - { "fchown", (sqlite3_syscall_ptr)posixFchown, 0 }, +#if defined(HAVE_FCHOWN) + { "fchown", (sqlite3_syscall_ptr)fchown, 0 }, +#else + { "fchown", (sqlite3_syscall_ptr)0, 0 }, +#endif #define osFchown ((int(*)(int,uid_t,gid_t))aSyscall[20].pCurrent) + { "geteuid", (sqlite3_syscall_ptr)geteuid, 0 }, +#define osGeteuid ((uid_t(*)(void))aSyscall[21].pCurrent) + #if !defined(SQLITE_OMIT_WAL) || SQLITE_MAX_MMAP_SIZE>0 - { "mmap", (sqlite3_syscall_ptr)mmap, 0 }, -#define osMmap ((void*(*)(void*,size_t,int,int,int,off_t))aSyscall[21].pCurrent) + { "mmap", (sqlite3_syscall_ptr)mmap, 0 }, +#else + { "mmap", (sqlite3_syscall_ptr)0, 0 }, +#endif +#define osMmap ((void*(*)(void*,size_t,int,int,int,off_t))aSyscall[22].pCurrent) +#if !defined(SQLITE_OMIT_WAL) || SQLITE_MAX_MMAP_SIZE>0 { "munmap", (sqlite3_syscall_ptr)munmap, 0 }, -#define osMunmap ((void*(*)(void*,size_t))aSyscall[22].pCurrent) +#else + { "munmap", (sqlite3_syscall_ptr)0, 0 }, +#endif +#define osMunmap ((void*(*)(void*,size_t))aSyscall[23].pCurrent) -#if HAVE_MREMAP +#if HAVE_MREMAP && (!defined(SQLITE_OMIT_WAL) || SQLITE_MAX_MMAP_SIZE>0) { "mremap", (sqlite3_syscall_ptr)mremap, 0 }, #else { "mremap", (sqlite3_syscall_ptr)0, 0 }, #endif -#define osMremap ((void*(*)(void*,size_t,size_t,int,...))aSyscall[23].pCurrent) - { "getpagesize", (sqlite3_syscall_ptr)unixGetpagesize, 0 }, -#define osGetpagesize ((int(*)(void))aSyscall[24].pCurrent) +#define osMremap ((void*(*)(void*,size_t,size_t,int,...))aSyscall[24].pCurrent) +#if !defined(SQLITE_OMIT_WAL) || SQLITE_MAX_MMAP_SIZE>0 + { "getpagesize", (sqlite3_syscall_ptr)unixGetpagesize, 0 }, +#else + { "getpagesize", (sqlite3_syscall_ptr)0, 0 }, #endif +#define osGetpagesize ((int(*)(void))aSyscall[25].pCurrent) + +#if defined(HAVE_READLINK) + { "readlink", (sqlite3_syscall_ptr)readlink, 0 }, +#else + { "readlink", (sqlite3_syscall_ptr)0, 0 }, +#endif +#define osReadlink ((ssize_t(*)(const char*,char*,size_t))aSyscall[26].pCurrent) + +#if defined(HAVE_LSTAT) + { "lstat", (sqlite3_syscall_ptr)lstat, 0 }, +#else + { "lstat", (sqlite3_syscall_ptr)0, 0 }, +#endif +#define osLstat ((int(*)(const char*,struct stat*))aSyscall[27].pCurrent) }; /* End of the overrideable system calls */ + +/* +** On some systems, calls to fchown() will trigger a message in a security +** log if they come from non-root processes. So avoid calling fchown() if +** we are not running as root. +*/ +static int robustFchown(int fd, uid_t uid, gid_t gid){ +#if defined(HAVE_FCHOWN) + return osGeteuid() ? 0 : osFchown(fd,uid,gid); +#else + return 0; +#endif +} + /* ** This is the xSetSystemCall() method of sqlite3_vfs for all of the ** "unix" VFSes. Return SQLITE_OK opon successfully updating the @@ -27375,23 +29897,12 @@ static int robust_ftruncate(int h, sqlite3_int64 sz){ ** should handle ENOLCK, ENOTSUP, EOPNOTSUPP separately. */ static int sqliteErrorFromPosixError(int posixError, int sqliteIOErr) { + assert( (sqliteIOErr == SQLITE_IOERR_LOCK) || + (sqliteIOErr == SQLITE_IOERR_UNLOCK) || + (sqliteIOErr == SQLITE_IOERR_RDLOCK) || + (sqliteIOErr == SQLITE_IOERR_CHECKRESERVEDLOCK) ); switch (posixError) { -#if 0 - /* At one point this code was not commented out. In theory, this branch - ** should never be hit, as this function should only be called after - ** a locking-related function (i.e. fcntl()) has returned non-zero with - ** the value of errno as the first argument. Since a system call has failed, - ** errno should be non-zero. - ** - ** Despite this, if errno really is zero, we still don't want to return - ** SQLITE_OK. The system call failed, and *some* SQLite error should be - ** propagated back to the caller. Commenting this branch out means errno==0 - ** will be handled by the "default:" case below. - */ - case 0: - return SQLITE_OK; -#endif - + case EACCES: case EAGAIN: case ETIMEDOUT: case EBUSY: @@ -27401,41 +29912,9 @@ static int sqliteErrorFromPosixError(int posixError, int sqliteIOErr) { * introspection, in which it actually means what it says */ return SQLITE_BUSY; - case EACCES: - /* EACCES is like EAGAIN during locking operations, but not any other time*/ - if( (sqliteIOErr == SQLITE_IOERR_LOCK) || - (sqliteIOErr == SQLITE_IOERR_UNLOCK) || - (sqliteIOErr == SQLITE_IOERR_RDLOCK) || - (sqliteIOErr == SQLITE_IOERR_CHECKRESERVEDLOCK) ){ - return SQLITE_BUSY; - } - /* else fall through */ case EPERM: return SQLITE_PERM; -#if EOPNOTSUPP!=ENOTSUP - case EOPNOTSUPP: - /* something went terribly awry, unless during file system support - * introspection, in which it actually means what it says */ -#endif -#ifdef ENOTSUP - case ENOTSUP: - /* invalid fd, unless during file system support introspection, in which - * it actually means what it says */ -#endif - case EIO: - case EBADF: - case EINVAL: - case ENOTCONN: - case ENODEV: - case ENXIO: - case ENOENT: -#ifdef ESTALE /* ESTALE is not defined on Interix systems */ - case ESTALE: -#endif - case ENOSYS: - /* these should force the client to close the file and reconnect */ - default: return sqliteIOErr; } @@ -27719,7 +30198,7 @@ static unixInodeInfo *inodeList = 0; /* ** -** This function - unixLogError_x(), is only ever called via the macro +** This function - unixLogErrorAtLine(), is only ever called via the macro ** unixLogError(). ** ** It is invoked after an error occurs in an OS function and errno has been @@ -27888,7 +30367,7 @@ static int findInodeInfo( rc = osFstat(fd, &statbuf); if( rc!=0 ){ storeLastErrno(pFile, errno); -#ifdef EOVERFLOW +#if defined(EOVERFLOW) && defined(SQLITE_DISABLE_LFS) if( pFile->lastErrno==EOVERFLOW ) return SQLITE_NOLFS; #endif return SQLITE_IOERR; @@ -27933,7 +30412,7 @@ static int findInodeInfo( if( pInode==0 ){ pInode = sqlite3_malloc64( sizeof(*pInode) ); if( pInode==0 ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } memset(pInode, 0, sizeof(*pInode)); memcpy(&pInode->fileId, &fileId, sizeof(fileId)); @@ -27975,30 +30454,25 @@ static int fileHasMoved(unixFile *pFile){ static void verifyDbFile(unixFile *pFile){ struct stat buf; int rc; - if( pFile->ctrlFlags & UNIXFILE_WARNED ){ - /* One or more of the following warnings have already been issued. Do not - ** repeat them so as not to clutter the error log */ - return; - } + + /* These verifications occurs for the main database only */ + if( pFile->ctrlFlags & UNIXFILE_NOLOCK ) return; + rc = osFstat(pFile->h, &buf); if( rc!=0 ){ sqlite3_log(SQLITE_WARNING, "cannot fstat db file %s", pFile->zPath); - pFile->ctrlFlags |= UNIXFILE_WARNED; return; } - if( buf.st_nlink==0 && (pFile->ctrlFlags & UNIXFILE_DELETE)==0 ){ + if( buf.st_nlink==0 ){ sqlite3_log(SQLITE_WARNING, "file unlinked while open: %s", pFile->zPath); - pFile->ctrlFlags |= UNIXFILE_WARNED; return; } if( buf.st_nlink>1 ){ sqlite3_log(SQLITE_WARNING, "multiple links to file: %s", pFile->zPath); - pFile->ctrlFlags |= UNIXFILE_WARNED; return; } if( fileHasMoved(pFile) ){ sqlite3_log(SQLITE_WARNING, "file renamed while open: %s", pFile->zPath); - pFile->ctrlFlags |= UNIXFILE_WARNED; return; } } @@ -28018,6 +30492,7 @@ static int unixCheckReservedLock(sqlite3_file *id, int *pResOut){ SimulateIOError( return SQLITE_IOERR_CHECKRESERVEDLOCK; ); assert( pFile ); + assert( pFile->eFileLock<=SHARED_LOCK ); unixEnterMutex(); /* Because pFile->pInode is shared across threads */ /* Check if a thread in this process holds such a lock */ @@ -28074,9 +30549,7 @@ static int unixFileLock(unixFile *pFile, struct flock *pLock){ unixInodeInfo *pInode = pFile->pInode; assert( unixMutexHeld() ); assert( pInode!=0 ); - if( ((pFile->ctrlFlags & UNIXFILE_EXCL)!=0 || pInode->bProcessLock) - && ((pFile->ctrlFlags & UNIXFILE_RDONLY)==0) - ){ + if( (pFile->ctrlFlags & (UNIXFILE_EXCL|UNIXFILE_RDONLY))==UNIXFILE_EXCL ){ if( pInode->bProcessLock==0 ){ struct flock lock; assert( pInode->nLock==0 ); @@ -28126,7 +30599,7 @@ static int unixLock(sqlite3_file *id, int eFileLock){ ** lock transitions in terms of the POSIX advisory shared and exclusive ** lock primitives (called read-locks and write-locks below, to avoid ** confusion with SQLite lock names). The algorithms are complicated - ** slightly in order to be compatible with windows systems simultaneously + ** slightly in order to be compatible with Windows95 systems simultaneously ** accessing the same database file, in case that is ever required. ** ** Symbols defined in os.h indentify the 'pending byte' and the 'reserved @@ -28134,8 +30607,14 @@ static int unixLock(sqlite3_file *id, int eFileLock){ ** range', a range of 510 bytes at a well known offset. ** ** To obtain a SHARED lock, a read-lock is obtained on the 'pending - ** byte'. If this is successful, a random byte from the 'shared byte - ** range' is read-locked and the lock on the 'pending byte' released. + ** byte'. If this is successful, 'shared byte range' is read-locked + ** and the lock on the 'pending byte' released. (Legacy note: When + ** SQLite was first developed, Windows95 systems were still very common, + ** and Widnows95 lacks a shared-lock capability. So on Windows95, a + ** single randomly selected by from the 'shared byte range' is locked. + ** Windows95 is now pretty much extinct, but this work-around for the + ** lack of shared-locks on Windows95 lives on, for backwards + ** compatibility.) ** ** A process may only obtain a RESERVED lock after it has a SHARED lock. ** A RESERVED lock is implemented by grabbing a write-lock on the @@ -28154,11 +30633,6 @@ static int unixLock(sqlite3_file *id, int eFileLock){ ** range'. Since all other locks require a read-lock on one of the bytes ** within this range, this ensures that no other locks are held on the ** database. - ** - ** The reason a single byte cannot be used instead of the 'shared byte - ** range' is that some versions of windows do not support read-locks. By - ** locking a random byte from a range, concurrent SHARED locks may exist - ** even if the locking primitive used is always a write-lock. */ int rc = SQLITE_OK; unixFile *pFile = (unixFile*)id; @@ -28428,9 +30902,7 @@ static int posixUnlock(sqlite3_file *id, int eFileLock, int handleNFSUnlock){ if( unixFileLock(pFile, &lock)==(-1) ){ tErrno = errno; rc = SQLITE_IOERR_UNLOCK; - if( IS_LOCK_ERROR(rc) ){ - storeLastErrno(pFile, tErrno); - } + storeLastErrno(pFile, tErrno); goto end_unlock; } lock.l_type = F_RDLCK; @@ -28452,9 +30924,7 @@ static int posixUnlock(sqlite3_file *id, int eFileLock, int handleNFSUnlock){ if( unixFileLock(pFile, &lock)==(-1) ){ tErrno = errno; rc = SQLITE_IOERR_UNLOCK; - if( IS_LOCK_ERROR(rc) ){ - storeLastErrno(pFile, tErrno); - } + storeLastErrno(pFile, tErrno); goto end_unlock; } }else @@ -28705,17 +31175,7 @@ static int dotlockCheckReservedLock(sqlite3_file *id, int *pResOut) { SimulateIOError( return SQLITE_IOERR_CHECKRESERVEDLOCK; ); assert( pFile ); - - /* Check if a thread in this process holds such a lock */ - if( pFile->eFileLock>SHARED_LOCK ){ - /* Either this connection or some other connection in the same process - ** holds a lock on the file. No need to check further. */ - reserved = 1; - }else{ - /* The lock is held if and only if the lockfile exists */ - const char *zLockFile = (const char*)pFile->lockingContext; - reserved = osAccess(zLockFile, 0)==0; - } + reserved = osAccess((const char*)pFile->lockingContext, 0)==0; OSTRACE(("TEST WR-LOCK %d %d %d (dotlock)\n", pFile->h, rc, reserved)); *pResOut = reserved; return rc; @@ -28777,7 +31237,7 @@ static int dotlockLock(sqlite3_file *id, int eFileLock) { rc = SQLITE_BUSY; } else { rc = sqliteErrorFromPosixError(tErrno, SQLITE_IOERR_LOCK); - if( IS_LOCK_ERROR(rc) ){ + if( rc!=SQLITE_BUSY ){ storeLastErrno(pFile, tErrno); } } @@ -28824,14 +31284,12 @@ static int dotlockUnlock(sqlite3_file *id, int eFileLock) { /* To fully unlock the database, delete the lock file */ assert( eFileLock==NO_LOCK ); rc = osRmdir(zLockFile); - if( rc<0 && errno==ENOTDIR ) rc = osUnlink(zLockFile); if( rc<0 ){ int tErrno = errno; - rc = 0; - if( ENOENT != tErrno ){ + if( tErrno==ENOENT ){ + rc = SQLITE_OK; + }else{ rc = SQLITE_IOERR_UNLOCK; - } - if( IS_LOCK_ERROR(rc) ){ storeLastErrno(pFile, tErrno); } return rc; @@ -28844,14 +31302,11 @@ static int dotlockUnlock(sqlite3_file *id, int eFileLock) { ** Close a file. Make sure the lock has been released before closing. */ static int dotlockClose(sqlite3_file *id) { - int rc = SQLITE_OK; - if( id ){ - unixFile *pFile = (unixFile*)id; - dotlockUnlock(id, NO_LOCK); - sqlite3_free(pFile->lockingContext); - rc = closeUnixFile(id); - } - return rc; + unixFile *pFile = (unixFile*)id; + assert( id!=0 ); + dotlockUnlock(id, NO_LOCK); + sqlite3_free(pFile->lockingContext); + return closeUnixFile(id); } /****************** End of the dot-file lock implementation ******************* ******************************************************************************/ @@ -28917,10 +31372,8 @@ static int flockCheckReservedLock(sqlite3_file *id, int *pResOut){ int tErrno = errno; /* unlock failed with an error */ lrc = SQLITE_IOERR_UNLOCK; - if( IS_LOCK_ERROR(lrc) ){ - storeLastErrno(pFile, tErrno); - rc = lrc; - } + storeLastErrno(pFile, tErrno); + rc = lrc; } } else { int tErrno = errno; @@ -29053,12 +31506,9 @@ static int flockUnlock(sqlite3_file *id, int eFileLock) { ** Close a file. */ static int flockClose(sqlite3_file *id) { - int rc = SQLITE_OK; - if( id ){ - flockUnlock(id, NO_LOCK); - rc = closeUnixFile(id); - } - return rc; + assert( id!=0 ); + flockUnlock(id, NO_LOCK); + return closeUnixFile(id); } #endif /* SQLITE_ENABLE_LOCKING_STYLE && !OS_VXWORK */ @@ -29683,23 +32133,22 @@ static int afpUnlock(sqlite3_file *id, int eFileLock) { */ static int afpClose(sqlite3_file *id) { int rc = SQLITE_OK; - if( id ){ - unixFile *pFile = (unixFile*)id; - afpUnlock(id, NO_LOCK); - unixEnterMutex(); - if( pFile->pInode && pFile->pInode->nLock ){ - /* If there are outstanding locks, do not actually close the file just - ** yet because that would clear those locks. Instead, add the file - ** descriptor to pInode->aPending. It will be automatically closed when - ** the last lock is cleared. - */ - setPendingFd(pFile); - } - releaseInodeInfo(pFile); - sqlite3_free(pFile->lockingContext); - rc = closeUnixFile(id); - unixLeaveMutex(); + unixFile *pFile = (unixFile*)id; + assert( id!=0 ); + afpUnlock(id, NO_LOCK); + unixEnterMutex(); + if( pFile->pInode && pFile->pInode->nLock ){ + /* If there are outstanding locks, do not actually close the file just + ** yet because that would clear those locks. Instead, add the file + ** descriptor to pInode->aPending. It will be automatically closed when + ** the last lock is cleared. + */ + setPendingFd(pFile); } + releaseInodeInfo(pFile); + sqlite3_free(pFile->lockingContext); + rc = closeUnixFile(id); + unixLeaveMutex(); return rc; } @@ -29778,13 +32227,9 @@ static int seekAndRead(unixFile *id, sqlite3_int64 offset, void *pBuf, int cnt){ SimulateIOError( got = -1 ); #else newOffset = lseek(id->h, offset, SEEK_SET); - SimulateIOError( newOffset-- ); - if( newOffset!=offset ){ - if( newOffset == -1 ){ - storeLastErrno((unixFile*)id, errno); - }else{ - storeLastErrno((unixFile*)id, 0); - } + SimulateIOError( newOffset = -1 ); + if( newOffset<0 ){ + storeLastErrno((unixFile*)id, errno); return -1; } got = osRead(id->h, pBuf, cnt); @@ -29883,6 +32328,7 @@ static int seekAndWriteFd( assert( nBuf==(nBuf&0x1ffff) ); assert( fd>2 ); + assert( piErrno!=0 ); nBuf &= 0x1ffff; TIMER_START; @@ -29893,11 +32339,10 @@ static int seekAndWriteFd( #else do{ i64 iSeek = lseek(fd, iOff, SEEK_SET); - SimulateIOError( iSeek-- ); - - if( iSeek!=iOff ){ - if( piErrno ) *piErrno = (iSeek==-1 ? errno : 0); - return -1; + SimulateIOError( iSeek = -1 ); + if( iSeek<0 ){ + rc = -1; + break; } rc = osWrite(fd, pBuf, nBuf); }while( rc<0 && errno==EINTR ); @@ -29906,7 +32351,7 @@ static int seekAndWriteFd( TIMER_END; OSTRACE(("WRITE %-3d %5d %7lld %llu\n", fd, rc, iOff, TIMER_ELAPSED)); - if( rc<0 && piErrno ) *piErrno = errno; + if( rc<0 ) *piErrno = errno; return rc; } @@ -29969,7 +32414,7 @@ static int unixWrite( } #endif -#if SQLITE_MAX_MMAP_SIZE>0 +#if defined(SQLITE_MMAP_READWRITE) && SQLITE_MAX_MMAP_SIZE>0 /* Deal with as much of this write request as possible by transfering ** data from the memory mapping using memcpy(). */ if( offsetmmapSize ){ @@ -30090,10 +32535,15 @@ static int full_fsync(int fd, int fullSync, int dataOnly){ #endif /* If we compiled with the SQLITE_NO_SYNC flag, then syncing is a - ** no-op + ** no-op. But go ahead and call fstat() to validate the file + ** descriptor as we need a method to provoke a failure during + ** coverate testing. */ #ifdef SQLITE_NO_SYNC - rc = SQLITE_OK; + { + struct stat buf; + rc = osFstat(fd, &buf); + } #elif HAVE_FULLFSYNC if( fullSync ){ rc = osFcntl(fd, F_FULLFSYNC, 0); @@ -30159,16 +32609,20 @@ static int openDirectory(const char *zFilename, int *pFd){ char zDirname[MAX_PATHNAME+1]; sqlite3_snprintf(MAX_PATHNAME, zDirname, "%s", zFilename); - for(ii=(int)strlen(zDirname); ii>1 && zDirname[ii]!='/'; ii--); + for(ii=(int)strlen(zDirname); ii>0 && zDirname[ii]!='/'; ii--); if( ii>0 ){ zDirname[ii] = '\0'; - fd = robust_open(zDirname, O_RDONLY|O_BINARY, 0); - if( fd>=0 ){ - OSTRACE(("OPENDIR %-3d %s\n", fd, zDirname)); - } + }else{ + if( zDirname[0]!='/' ) zDirname[0] = '.'; + zDirname[1] = 0; + } + fd = robust_open(zDirname, O_RDONLY|O_BINARY, 0); + if( fd>=0 ){ + OSTRACE(("OPENDIR %-3d %s\n", fd, zDirname)); } *pFd = fd; - return (fd>=0?SQLITE_OK:unixLogError(SQLITE_CANTOPEN_BKPT, "open", zDirname)); + if( fd>=0 ) return SQLITE_OK; + return unixLogError(SQLITE_CANTOPEN_BKPT, "openDirectory", zDirname); } /* @@ -30221,10 +32675,11 @@ static int unixSync(sqlite3_file *id, int flags){ OSTRACE(("DIRSYNC %s (have_fullfsync=%d fullsync=%d)\n", pFile->zPath, HAVE_FULLFSYNC, isFullsync)); rc = osOpenDirectory(pFile->zPath, &dirfd); - if( rc==SQLITE_OK && dirfd>=0 ){ + if( rc==SQLITE_OK ){ full_fsync(dirfd, 0, 0); robust_close(pFile, dirfd, __LINE__); - }else if( rc==SQLITE_CANTOPEN ){ + }else{ + assert( rc==SQLITE_CANTOPEN ); rc = SQLITE_OK; } pFile->ctrlFlags &= ~UNIXFILE_DIRSYNC; @@ -30356,18 +32811,14 @@ static int fcntlSizeHint(unixFile *pFile, i64 nByte){ int nWrite = 0; /* Number of bytes written by seekAndWrite */ i64 iWrite; /* Next offset to write to */ - iWrite = ((buf.st_size + 2*nBlk - 1)/nBlk)*nBlk-1; + iWrite = (buf.st_size/nBlk)*nBlk + nBlk - 1; assert( iWrite>=buf.st_size ); - assert( (iWrite/nBlk)==((buf.st_size+nBlk-1)/nBlk) ); assert( ((iWrite+1)%nBlk)==0 ); - for(/*no-op*/; iWrite=nSize ) iWrite = nSize - 1; nWrite = seekAndWrite(pFile, iWrite, "", 1); if( nWrite!=1 ) return SQLITE_IOERR_WRITE; } - if( nWrite==0 || (nSize%nBlk) ){ - nWrite = seekAndWrite(pFile, nSize-1, "", 1); - if( nWrite!=1 ) return SQLITE_IOERR_WRITE; - } #endif } } @@ -30415,10 +32866,6 @@ static int unixGetTempname(int nBuf, char *zBuf); static int unixFileControl(sqlite3_file *id, int op, void *pArg){ unixFile *pFile = (unixFile*)id; switch( op ){ - case SQLITE_FCNTL_WAL_BLOCK: { - /* pFile->ctrlFlags |= UNIXFILE_BLOCK; // Deferred feature */ - return SQLITE_OK; - } case SQLITE_FCNTL_LOCKSTATE: { *(int*)pArg = pFile->eFileLock; return SQLITE_OK; @@ -30745,10 +33192,9 @@ static int unixShmSystemLock( assert( n==1 || lockType!=F_RDLCK ); /* Locks are within range */ - assert( n>=1 && n=1 && n<=SQLITE_SHM_NLOCK ); if( pShmNode->h>=0 ){ - int lkType; /* Initialize the locking parameters */ memset(&f, 0, sizeof(f)); f.l_type = lockType; @@ -30756,10 +33202,8 @@ static int unixShmSystemLock( f.l_start = ofst; f.l_len = n; - lkType = (pFile->ctrlFlags & UNIXFILE_BLOCK)!=0 ? F_SETLKW : F_SETLK; - rc = osFcntl(pShmNode->h, lkType, &f); + rc = osFcntl(pShmNode->h, F_SETLK, &f); rc = (rc!=(-1)) ? SQLITE_OK : SQLITE_BUSY; - pFile->ctrlFlags &= ~UNIXFILE_BLOCK; } /* Update the global lock state and do debug tracing */ @@ -30826,7 +33270,7 @@ static int unixShmRegionPerMap(void){ static void unixShmPurge(unixFile *pFd){ unixShmNode *p = pFd->pInode->pShmNode; assert( unixMutexHeld() ); - if( p && p->nRef==0 ){ + if( p && ALWAYS(p->nRef==0) ){ int nShmPerMap = unixShmRegionPerMap(); int i; assert( p->pInode==pFd->pInode ); @@ -30893,7 +33337,7 @@ static int unixOpenSharedMemory(unixFile *pDbFd){ /* Allocate space for the new unixShm object. */ p = sqlite3_malloc64( sizeof(*p) ); - if( p==0 ) return SQLITE_NOMEM; + if( p==0 ) return SQLITE_NOMEM_BKPT; memset(p, 0, sizeof(*p)); assert( pDbFd->pShm==0 ); @@ -30913,7 +33357,7 @@ static int unixOpenSharedMemory(unixFile *pDbFd){ ** a new *-shm file is created, an attempt will be made to create it ** with the same permissions. */ - if( osFstat(pDbFd->h, &sStat) && pInode->bProcessLock==0 ){ + if( osFstat(pDbFd->h, &sStat) ){ rc = SQLITE_IOERR_FSTAT; goto shm_open_err; } @@ -30925,7 +33369,7 @@ static int unixOpenSharedMemory(unixFile *pDbFd){ #endif pShmNode = sqlite3_malloc64( sizeof(*pShmNode) + nShmFilename ); if( pShmNode==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto shm_open_err; } memset(pShmNode, 0, sizeof(*pShmNode)+nShmFilename); @@ -30941,10 +33385,12 @@ static int unixOpenSharedMemory(unixFile *pDbFd){ pShmNode->h = -1; pDbFd->pInode->pShmNode = pShmNode; pShmNode->pInode = pDbFd->pInode; - pShmNode->mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); - if( pShmNode->mutex==0 ){ - rc = SQLITE_NOMEM; - goto shm_open_err; + if( sqlite3GlobalConfig.bCoreMutex ){ + pShmNode->mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); + if( pShmNode->mutex==0 ){ + rc = SQLITE_NOMEM_BKPT; + goto shm_open_err; + } } if( pInode->bProcessLock==0 ){ @@ -30963,7 +33409,7 @@ static int unixOpenSharedMemory(unixFile *pDbFd){ ** is owned by the same user that owns the original database. Otherwise, ** the original owner will not be able to connect. */ - osFchown(pShmNode->h, sStat.st_uid, sStat.st_gid); + robustFchown(pShmNode->h, sStat.st_uid, sStat.st_gid); /* Check to see if another process is holding the dead-man switch. ** If not, truncate the file to zero length. @@ -31100,7 +33546,8 @@ static int unixShmMap( /* Write to the last byte of each newly allocated or extended page */ assert( (nByte % pgsz)==0 ); for(iPg=(sStat.st_size/pgsz); iPg<(nByte/pgsz); iPg++){ - if( seekAndWriteFd(pShmNode->h, iPg*pgsz + pgsz-1, "", 1, 0)!=1 ){ + int x = 0; + if( seekAndWriteFd(pShmNode->h, iPg*pgsz + pgsz-1, "", 1, &x)!=1 ){ const char *zFile = pShmNode->zFilename; rc = unixLogError(SQLITE_IOERR_SHMSIZE, "write", zFile); goto shmpage_out; @@ -31115,7 +33562,7 @@ static int unixShmMap( pShmNode->apRegion, nReqRegion*sizeof(char *) ); if( !apNew ){ - rc = SQLITE_IOERR_NOMEM; + rc = SQLITE_IOERR_NOMEM_BKPT; goto shmpage_out; } pShmNode->apRegion = apNew; @@ -31135,7 +33582,7 @@ static int unixShmMap( }else{ pMem = sqlite3_malloc64(szRegion); if( pMem==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto shmpage_out; } memset(pMem, 0, szRegion); @@ -31394,7 +33841,9 @@ static void unixRemapfile( assert( pFd->mmapSizeActual>=pFd->mmapSize ); assert( MAP_FAILED!=0 ); +#ifdef SQLITE_MMAP_READWRITE if( (pFd->ctrlFlags & UNIXFILE_RDONLY)==0 ) flags |= PROT_WRITE; +#endif if( pOrig ){ #if HAVE_MREMAP @@ -31466,17 +33915,14 @@ static void unixRemapfile( ** recreated as a result of outstanding references) or an SQLite error ** code otherwise. */ -static int unixMapfile(unixFile *pFd, i64 nByte){ - i64 nMap = nByte; - int rc; - +static int unixMapfile(unixFile *pFd, i64 nMap){ assert( nMap>=0 || pFd->nFetchOut==0 ); + assert( nMap>0 || (pFd->mmapSize==0 && pFd->pMapRegion==0) ); if( pFd->nFetchOut>0 ) return SQLITE_OK; if( nMap<0 ){ struct stat statbuf; /* Low-level file information */ - rc = osFstat(pFd->h, &statbuf); - if( rc!=SQLITE_OK ){ + if( osFstat(pFd->h, &statbuf) ){ return SQLITE_IOERR_FSTAT; } nMap = statbuf.st_size; @@ -31485,12 +33931,9 @@ static int unixMapfile(unixFile *pFd, i64 nByte){ nMap = pFd->mmapSizeMax; } + assert( nMap>0 || (pFd->mmapSize==0 && pFd->pMapRegion==0) ); if( nMap!=pFd->mmapSize ){ - if( nMap>0 ){ - unixRemapfile(pFd, nMap); - }else{ - unixUnmapfile(pFd); - } + unixRemapfile(pFd, nMap); } return SQLITE_OK; @@ -31917,7 +34360,7 @@ static int fillInUnixFile( pNew->pId = vxworksFindFileId(zFilename); if( pNew->pId==0 ){ ctrlFlags |= UNIXFILE_NOLOCK; - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } #endif @@ -31973,7 +34416,7 @@ static int fillInUnixFile( afpLockingContext *pCtx; pNew->lockingContext = pCtx = sqlite3_malloc64( sizeof(*pCtx) ); if( pCtx==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else{ /* NB: zFilename exists and remains valid until the file is closed ** according to requirement F11141. So we do not need to make a @@ -32003,7 +34446,7 @@ static int fillInUnixFile( nFilename = (int)strlen(zFilename) + 6; zLockFile = (char *)sqlite3_malloc64(nFilename); if( zLockFile==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else{ sqlite3_snprintf(nFilename, zLockFile, "%s" DOTLOCK_SUFFIX, zFilename); } @@ -32026,7 +34469,7 @@ static int fillInUnixFile( if( zSemName[n]=='/' ) zSemName[n] = '_'; pNew->pInode->pSem = sem_open(zSemName, O_CREAT, 0666, 1); if( pNew->pInode->pSem == SEM_FAILED ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; pNew->pInode->aSemName[0] = '\0'; } } @@ -32059,29 +34502,31 @@ static int fillInUnixFile( */ static const char *unixTempFileDir(void){ static const char *azDirs[] = { - 0, 0, 0, "/var/tmp", "/usr/tmp", "/tmp", - 0 /* List terminator */ + "." }; - unsigned int i; + unsigned int i = 0; struct stat buf; - const char *zDir = 0; + const char *zDir = sqlite3_temp_directory; - azDirs[0] = sqlite3_temp_directory; - if( !azDirs[1] ) azDirs[1] = getenv("SQLITE_TMPDIR"); - if( !azDirs[2] ) azDirs[2] = getenv("TMPDIR"); - for(i=0; i=sizeof(azDirs)/sizeof(azDirs[0]) ) break; + zDir = azDirs[i++]; } - return zDir; + return 0; } /* @@ -32090,38 +34535,26 @@ static const char *unixTempFileDir(void){ ** pVfs->mxPathname bytes. */ static int unixGetTempname(int nBuf, char *zBuf){ - static const unsigned char zChars[] = - "abcdefghijklmnopqrstuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "0123456789"; - unsigned int i, j; const char *zDir; + int iLimit = 0; /* It's odd to simulate an io-error here, but really this is just ** using the io-error infrastructure to test that SQLite handles this ** function failing. */ + zBuf[0] = 0; SimulateIOError( return SQLITE_IOERR ); zDir = unixTempFileDir(); - if( zDir==0 ) zDir = "."; - - /* Check that the output buffer is large enough for the temporary file - ** name. If it is not, return SQLITE_ERROR. - */ - if( (strlen(zDir) + strlen(SQLITE_TEMP_FILE_PREFIX) + 18) >= (size_t)nBuf ){ - return SQLITE_ERROR; - } - + if( zDir==0 ) return SQLITE_IOERR_GETTEMPPATH; do{ - sqlite3_snprintf(nBuf-18, zBuf, "%s/"SQLITE_TEMP_FILE_PREFIX, zDir); - j = (int)strlen(zBuf); - sqlite3_randomness(15, &zBuf[j]); - for(i=0; i<15; i++, j++){ - zBuf[j] = (char)zChars[ ((unsigned char)zBuf[j])%(sizeof(zChars)-1) ]; - } - zBuf[j] = 0; - zBuf[j+1] = 0; + u64 r; + sqlite3_randomness(sizeof(r), &r); + assert( nBuf>2 ); + zBuf[nBuf-2] = 0; + sqlite3_snprintf(nBuf, zBuf, "%s/"SQLITE_TEMP_FILE_PREFIX"%llx%c", + zDir, r, 0); + if( zBuf[nBuf-2]!=0 || (iLimit++)>10 ) return SQLITE_ERROR; }while( osAccess(zBuf,0)==0 ); return SQLITE_OK; } @@ -32243,16 +34676,19 @@ static int findCreateFileMode( ** used by the test_multiplex.c module. */ nDb = sqlite3Strlen30(zPath) - 1; -#ifdef SQLITE_ENABLE_8_3_NAMES - while( nDb>0 && sqlite3Isalnum(zPath[nDb]) ) nDb--; - if( nDb==0 || zPath[nDb]!='-' ) return SQLITE_OK; -#else while( zPath[nDb]!='-' ){ +#ifndef SQLITE_ENABLE_8_3_NAMES + /* In the normal case (8+3 filenames disabled) the journal filename + ** is guaranteed to contain a '-' character. */ assert( nDb>0 ); - assert( zPath[nDb]!='\n' ); + assert( sqlite3Isalnum(zPath[nDb]) ); +#else + /* If 8+3 names are possible, then the journal file might not contain + ** a '-' character. So check for that case and return early. */ + if( nDb==0 || zPath[nDb]=='.' ) return SQLITE_OK; +#endif nDb--; } -#endif memcpy(zDb, zPath, nDb); zDb[nDb] = '\0'; @@ -32380,7 +34816,7 @@ static int unixOpen( }else{ pUnused = sqlite3_malloc64(sizeof(*pUnused)); if( !pUnused ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } } p->pUnused = pUnused; @@ -32393,7 +34829,7 @@ static int unixOpen( }else if( !zName ){ /* If zName is NULL, the upper layer is requesting a temp file. */ assert(isDelete && !syncDir); - rc = unixGetTempname(MAX_PATHNAME+2, zTmpname); + rc = unixGetTempname(pVfs->mxPathname, zTmpname); if( rc!=SQLITE_OK ){ return rc; } @@ -32426,7 +34862,8 @@ static int unixOpen( } fd = robust_open(zName, openFlags, openMode); OSTRACE(("OPENX %-3d %s 0%o\n", fd, zName, openFlags)); - if( fd<0 && errno!=EISDIR && isReadWrite && !isExclusive ){ + assert( !isExclusive || (openFlags & O_CREAT)!=0 ); + if( fd<0 && errno!=EISDIR && isReadWrite ){ /* Failed to open the file for read/write access. Try read-only. */ flags &= ~(SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE); openFlags &= ~(O_RDWR|O_CREAT); @@ -32445,7 +34882,7 @@ static int unixOpen( ** the same as the original database. */ if( flags & (SQLITE_OPEN_WAL|SQLITE_OPEN_MAIN_JOURNAL) ){ - osFchown(fd, uid, gid); + robustFchown(fd, uid, gid); } } assert( fd>=0 ); @@ -32465,7 +34902,7 @@ static int unixOpen( zPath = sqlite3_mprintf("%s", zName); if( zPath==0 ){ robust_close(p, fd, __LINE__); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } #else osUnlink(zName); @@ -32476,9 +34913,6 @@ static int unixOpen( p->openFlags = openFlags; } #endif - - noLock = eType!=SQLITE_OPEN_MAIN_DB; - #if defined(__APPLE__) || SQLITE_ENABLE_LOCKING_STYLE if( fstatfs(fd, &fsInfo) == -1 ){ @@ -32497,6 +34931,7 @@ static int unixOpen( /* Set up appropriate ctrlFlags */ if( isDelete ) ctrlFlags |= UNIXFILE_DELETE; if( isReadonly ) ctrlFlags |= UNIXFILE_RDONLY; + noLock = eType!=SQLITE_OPEN_MAIN_DB; if( noLock ) ctrlFlags |= UNIXFILE_NOLOCK; if( syncDir ) ctrlFlags |= UNIXFILE_DIRSYNC; if( flags & SQLITE_OPEN_URI ) ctrlFlags |= UNIXFILE_URI; @@ -32573,16 +35008,12 @@ static int unixDelete( int fd; rc = osOpenDirectory(zPath, &fd); if( rc==SQLITE_OK ){ -#if OS_VXWORKS - if( fsync(fd)==-1 ) -#else - if( fsync(fd) ) -#endif - { + if( full_fsync(fd,0,0) ){ rc = unixLogError(SQLITE_IOERR_DIR_FSYNC, "fsync", zPath); } robust_close(0, fd, __LINE__); - }else if( rc==SQLITE_CANTOPEN ){ + }else{ + assert( rc==SQLITE_CANTOPEN ); rc = SQLITE_OK; } } @@ -32606,33 +35037,49 @@ static int unixAccess( int flags, /* What do we want to learn about the zPath file? */ int *pResOut /* Write result boolean here */ ){ - int amode = 0; UNUSED_PARAMETER(NotUsed); SimulateIOError( return SQLITE_IOERR_ACCESS; ); - switch( flags ){ - case SQLITE_ACCESS_EXISTS: - amode = F_OK; - break; - case SQLITE_ACCESS_READWRITE: - amode = W_OK|R_OK; - break; - case SQLITE_ACCESS_READ: - amode = R_OK; - break; + assert( pResOut!=0 ); - default: - assert(!"Invalid flags argument"); - } - *pResOut = (osAccess(zPath, amode)==0); - if( flags==SQLITE_ACCESS_EXISTS && *pResOut ){ + /* The spec says there are three possible values for flags. But only + ** two of them are actually used */ + assert( flags==SQLITE_ACCESS_EXISTS || flags==SQLITE_ACCESS_READWRITE ); + + if( flags==SQLITE_ACCESS_EXISTS ){ struct stat buf; - if( 0==osStat(zPath, &buf) && buf.st_size==0 ){ - *pResOut = 0; - } + *pResOut = (0==osStat(zPath, &buf) && buf.st_size>0); + }else{ + *pResOut = osAccess(zPath, W_OK|R_OK)==0; } return SQLITE_OK; } +/* +** +*/ +static int mkFullPathname( + const char *zPath, /* Input path */ + char *zOut, /* Output buffer */ + int nOut /* Allocated size of buffer zOut */ +){ + int nPath = sqlite3Strlen30(zPath); + int iOff = 0; + if( zPath[0]!='/' ){ + if( osGetcwd(zOut, nOut-2)==0 ){ + return unixLogError(SQLITE_CANTOPEN_BKPT, "getcwd", zPath); + } + iOff = sqlite3Strlen30(zOut); + zOut[iOff++] = '/'; + } + if( (iOff+nPath+1)>nOut ){ + /* SQLite assumes that xFullPathname() nul-terminates the output buffer + ** even if it returns an error. */ + zOut[iOff] = '\0'; + return SQLITE_CANTOPEN_BKPT; + } + sqlite3_snprintf(nOut-iOff, &zOut[iOff], "%s", zPath); + return SQLITE_OK; +} /* ** Turn a relative pathname into a full pathname. The relative path @@ -32649,6 +35096,17 @@ static int unixFullPathname( int nOut, /* Size of output buffer in bytes */ char *zOut /* Output buffer */ ){ +#if !defined(HAVE_READLINK) || !defined(HAVE_LSTAT) + return mkFullPathname(zPath, zOut, nOut); +#else + int rc = SQLITE_OK; + int nByte; + int nLink = 1; /* Number of symbolic links followed so far */ + const char *zIn = zPath; /* Input path for each iteration of loop */ + char *zDel = 0; + + assert( pVfs->mxPathname==MAX_PATHNAME ); + UNUSED_PARAMETER(pVfs); /* It's odd to simulate an io-error here, but really this is just ** using the io-error infrastructure to test that SQLite handles this @@ -32657,21 +35115,62 @@ static int unixFullPathname( */ SimulateIOError( return SQLITE_ERROR ); - assert( pVfs->mxPathname==MAX_PATHNAME ); - UNUSED_PARAMETER(pVfs); + do { - zOut[nOut-1] = '\0'; - if( zPath[0]=='/' ){ - sqlite3_snprintf(nOut, zOut, "%s", zPath); - }else{ - int nCwd; - if( osGetcwd(zOut, nOut-1)==0 ){ - return unixLogError(SQLITE_CANTOPEN_BKPT, "getcwd", zPath); + /* Call stat() on path zIn. Set bLink to true if the path is a symbolic + ** link, or false otherwise. */ + int bLink = 0; + struct stat buf; + if( osLstat(zIn, &buf)!=0 ){ + if( errno!=ENOENT ){ + rc = unixLogError(SQLITE_CANTOPEN_BKPT, "lstat", zIn); + } + }else{ + bLink = S_ISLNK(buf.st_mode); } - nCwd = (int)strlen(zOut); - sqlite3_snprintf(nOut-nCwd, &zOut[nCwd], "/%s", zPath); - } - return SQLITE_OK; + + if( bLink ){ + if( zDel==0 ){ + zDel = sqlite3_malloc(nOut); + if( zDel==0 ) rc = SQLITE_NOMEM_BKPT; + }else if( ++nLink>SQLITE_MAX_SYMLINKS ){ + rc = SQLITE_CANTOPEN_BKPT; + } + + if( rc==SQLITE_OK ){ + nByte = osReadlink(zIn, zDel, nOut-1); + if( nByte<0 ){ + rc = unixLogError(SQLITE_CANTOPEN_BKPT, "readlink", zIn); + }else{ + if( zDel[0]!='/' ){ + int n; + for(n = sqlite3Strlen30(zIn); n>0 && zIn[n-1]!='/'; n--); + if( nByte+n+1>nOut ){ + rc = SQLITE_CANTOPEN_BKPT; + }else{ + memmove(&zDel[n], zDel, nByte+1); + memcpy(zDel, zIn, n); + nByte += n; + } + } + zDel[nByte] = '\0'; + } + } + + zIn = zDel; + } + + assert( rc!=SQLITE_OK || zIn!=zOut || zIn[0]=='/' ); + if( rc==SQLITE_OK && zIn!=zOut ){ + rc = mkFullPathname(zIn, zOut, nOut); + } + if( bLink==0 ) break; + zIn = zOut; + }while( rc==SQLITE_OK ); + + sqlite3_free(zDel); + return rc; +#endif /* HAVE_READLINK && HAVE_LSTAT */ } @@ -32840,11 +35339,8 @@ static int unixCurrentTimeInt64(sqlite3_vfs *NotUsed, sqlite3_int64 *piNow){ *piNow = unixEpoch + 1000*(sqlite3_int64)sNow.tv_sec + sNow.tv_nsec/1000000; #else struct timeval sNow; - if( gettimeofday(&sNow, 0)==0 ){ - *piNow = unixEpoch + 1000*(sqlite3_int64)sNow.tv_sec + sNow.tv_usec/1000; - }else{ - rc = SQLITE_ERROR; - } + (void)gettimeofday(&sNow, 0); /* Cannot fail given valid arguments */ + *piNow = unixEpoch + 1000*(sqlite3_int64)sNow.tv_sec + sNow.tv_usec/1000; #endif #ifdef SQLITE_TEST @@ -32856,6 +35352,7 @@ static int unixCurrentTimeInt64(sqlite3_vfs *NotUsed, sqlite3_int64 *piNow){ return rc; } +#ifndef SQLITE_OMIT_DEPRECATED /* ** Find the current time (in Universal Coordinated Time). Write the ** current time and date as a Julian Day number into *prNow and @@ -32869,19 +35366,21 @@ static int unixCurrentTime(sqlite3_vfs *NotUsed, double *prNow){ *prNow = i/86400000.0; return rc; } +#else +# define unixCurrentTime 0 +#endif /* -** We added the xGetLastError() method with the intention of providing -** better low-level error messages when operating-system problems come up -** during SQLite operation. But so far, none of that has been implemented -** in the core. So this routine is never called. For now, it is merely -** a place-holder. +** The xGetLastError() method is designed to return a better +** low-level error message when operating-system problems come up +** during SQLite operation. Only the integer return code is currently +** used. */ static int unixGetLastError(sqlite3_vfs *NotUsed, int NotUsed2, char *NotUsed3){ UNUSED_PARAMETER(NotUsed); UNUSED_PARAMETER(NotUsed2); UNUSED_PARAMETER(NotUsed3); - return 0; + return errno; } @@ -33136,7 +35635,7 @@ static int proxyCreateLockPath(const char *lockPath){ } buf[i] = lockPath[i]; } - OSTRACE(("CREATELOCKPATH proxy lock path=%s pid=%d\n", lockPath, osGetpid(0))); + OSTRACE(("CREATELOCKPATH proxy lock path=%s pid=%d\n",lockPath,osGetpid(0))); return 0; } @@ -33172,7 +35671,7 @@ static int proxyCreateUnixFile( }else{ pUnused = sqlite3_malloc64(sizeof(*pUnused)); if( !pUnused ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } } if( fd<0 ){ @@ -33205,7 +35704,7 @@ static int proxyCreateUnixFile( pNew = (unixFile *)sqlite3_malloc64(sizeof(*pNew)); if( pNew==NULL ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto end_create_proxy; } memset(pNew, 0, sizeof(unixFile)); @@ -33548,7 +36047,7 @@ static int proxyTakeConch(unixFile *pFile){ writeSize = PROXY_PATHINDEX + strlen(&writeBuffer[PROXY_PATHINDEX]); robust_ftruncate(conchFile->h, writeSize); rc = unixWrite((sqlite3_file *)conchFile, writeBuffer, writeSize, 0); - fsync(conchFile->h); + full_fsync(conchFile->h,0,0); /* If we created a new conch file (not just updated the contents of a ** valid conch file), try to match the permissions of the database */ @@ -33618,7 +36117,7 @@ static int proxyTakeConch(unixFile *pFile){ if( tempLockPath ){ pCtx->lockProxyPath = sqlite3DbStrDup(0, tempLockPath); if( !pCtx->lockProxyPath ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } } } @@ -33683,7 +36182,7 @@ static int proxyCreateConchPathname(char *dbPath, char **pConchPath){ ** the name of the original database file. */ *pConchPath = conchPath = (char *)sqlite3_malloc64(len + 8); if( conchPath==0 ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } memcpy(conchPath, dbPath, len+1); @@ -33799,7 +36298,7 @@ static int proxyTransformUnixFile(unixFile *pFile, const char *path) { pCtx = sqlite3_malloc64( sizeof(*pCtx) ); if( pCtx==0 ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } memset(pCtx, 0, sizeof(*pCtx)); @@ -33835,7 +36334,7 @@ static int proxyTransformUnixFile(unixFile *pFile, const char *path) { if( rc==SQLITE_OK ){ pCtx->dbPath = sqlite3DbStrDup(0, dbPath); if( pCtx->dbPath==NULL ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } } if( rc==SQLITE_OK ){ @@ -34021,7 +36520,7 @@ static int proxyUnlock(sqlite3_file *id, int eFileLock) { ** Close a file that uses proxy locks. */ static int proxyClose(sqlite3_file *id) { - if( id ){ + if( ALWAYS(id) ){ unixFile *pFile = (unixFile*)id; proxyLockingContext *pCtx = (proxyLockingContext *)pFile->lockingContext; unixFile *lockProxy = pCtx->lockProxy; @@ -34165,7 +36664,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_os_init(void){ /* Double-check that the aSyscall[] array has been constructed ** correctly. See ticket [bb3a86e890c8e96ab] */ - assert( ArraySize(aSyscall)==25 ); + assert( ArraySize(aSyscall)==28 ); /* Register all VFSes defined in the aVfs[] array */ for(i=0; i<(sizeof(aVfs)/sizeof(sqlite3_vfs)); i++){ @@ -34248,8 +36747,8 @@ SQLITE_API int SQLITE_STDCALL sqlite3_os_end(void){ */ #ifdef SQLITE_PERFORMANCE_TRACE -/* -** hwtime.h contains inline assembler code for implementing +/* +** hwtime.h contains inline assembler code for implementing ** high-performance timing routines. */ /************** Include hwtime.h in the middle of os_common.h ****************/ @@ -34359,14 +36858,14 @@ static sqlite_uint64 g_elapsed; ** of code will give us the ability to simulate a disk I/O error. This ** is used for testing the I/O recovery logic. */ -#ifdef SQLITE_TEST -SQLITE_API int sqlite3_io_error_hit = 0; /* Total number of I/O Errors */ -SQLITE_API int sqlite3_io_error_hardhit = 0; /* Number of non-benign errors */ -SQLITE_API int sqlite3_io_error_pending = 0; /* Count down to first I/O error */ -SQLITE_API int sqlite3_io_error_persist = 0; /* True if I/O errors persist */ -SQLITE_API int sqlite3_io_error_benign = 0; /* True if errors are benign */ -SQLITE_API int sqlite3_diskfull_pending = 0; -SQLITE_API int sqlite3_diskfull = 0; +#if defined(SQLITE_TEST) +SQLITE_API extern int sqlite3_io_error_hit; +SQLITE_API extern int sqlite3_io_error_hardhit; +SQLITE_API extern int sqlite3_io_error_pending; +SQLITE_API extern int sqlite3_io_error_persist; +SQLITE_API extern int sqlite3_io_error_benign; +SQLITE_API extern int sqlite3_diskfull_pending; +SQLITE_API extern int sqlite3_diskfull; #define SimulateIOErrorBenign(X) sqlite3_io_error_benign=(X) #define SimulateIOError(CODE) \ if( (sqlite3_io_error_persist && sqlite3_io_error_hit) \ @@ -34392,17 +36891,17 @@ static void local_ioerr(){ #define SimulateIOErrorBenign(X) #define SimulateIOError(A) #define SimulateDiskfullError(A) -#endif +#endif /* defined(SQLITE_TEST) */ /* ** When testing, keep a count of the number of open files. */ -#ifdef SQLITE_TEST -SQLITE_API int sqlite3_open_file_count = 0; +#if defined(SQLITE_TEST) +SQLITE_API extern int sqlite3_open_file_count; #define OpenCounter(X) sqlite3_open_file_count+=(X) #else #define OpenCounter(X) -#endif +#endif /* defined(SQLITE_TEST) */ #endif /* !defined(_OS_COMMON_H_) */ @@ -34465,6 +36964,10 @@ SQLITE_API int sqlite3_open_file_count = 0; # define NTDDI_WINBLUE 0x06030000 #endif +#ifndef NTDDI_WINTHRESHOLD +# define NTDDI_WINTHRESHOLD 0x06040000 +#endif + /* ** Check to see if the GetVersionEx[AW] functions are deprecated on the ** target system. GetVersionEx was first deprecated in Win8.1. @@ -34477,6 +36980,19 @@ SQLITE_API int sqlite3_open_file_count = 0; # endif #endif +/* +** Check to see if the CreateFileMappingA function is supported on the +** target system. It is unavailable when using "mincore.lib" on Win10. +** When compiling for Windows 10, always assume "mincore.lib" is in use. +*/ +#ifndef SQLITE_WIN32_CREATEFILEMAPPINGA +# if defined(NTDDI_VERSION) && NTDDI_VERSION >= NTDDI_WINTHRESHOLD +# define SQLITE_WIN32_CREATEFILEMAPPINGA 0 +# else +# define SQLITE_WIN32_CREATEFILEMAPPINGA 1 +# endif +#endif + /* ** This constant should already be defined (in the "WinDef.h" SDK file). */ @@ -34716,11 +37232,23 @@ struct winFile { # define SQLITE_WIN32_HEAP_CREATE (TRUE) #endif +/* + * This is cache size used in the calculation of the initial size of the + * Win32-specific heap. It cannot be negative. + */ +#ifndef SQLITE_WIN32_CACHE_SIZE +# if SQLITE_DEFAULT_CACHE_SIZE>=0 +# define SQLITE_WIN32_CACHE_SIZE (SQLITE_DEFAULT_CACHE_SIZE) +# else +# define SQLITE_WIN32_CACHE_SIZE (-(SQLITE_DEFAULT_CACHE_SIZE)) +# endif +#endif + /* * The initial size of the Win32-specific heap. This value may be zero. */ #ifndef SQLITE_WIN32_HEAP_INIT_SIZE -# define SQLITE_WIN32_HEAP_INIT_SIZE ((SQLITE_DEFAULT_CACHE_SIZE) * \ +# define SQLITE_WIN32_HEAP_INIT_SIZE ((SQLITE_WIN32_CACHE_SIZE) * \ (SQLITE_DEFAULT_PAGE_SIZE) + 4194304) #endif @@ -34883,8 +37411,9 @@ static struct win_syscall { #define osCreateFileW ((HANDLE(WINAPI*)(LPCWSTR,DWORD,DWORD, \ LPSECURITY_ATTRIBUTES,DWORD,DWORD,HANDLE))aSyscall[5].pCurrent) -#if (!SQLITE_OS_WINRT && defined(SQLITE_WIN32_HAS_ANSI) && \ - (!defined(SQLITE_OMIT_WAL) || SQLITE_MAX_MMAP_SIZE>0)) +#if !SQLITE_OS_WINRT && defined(SQLITE_WIN32_HAS_ANSI) && \ + (!defined(SQLITE_OMIT_WAL) || SQLITE_MAX_MMAP_SIZE>0) && \ + SQLITE_WIN32_CREATEFILEMAPPINGA { "CreateFileMappingA", (SYSCALL)CreateFileMappingA, 0 }, #else { "CreateFileMappingA", (SYSCALL)0, 0 }, @@ -35114,8 +37643,7 @@ static struct win_syscall { #define osGetTickCount ((DWORD(WINAPI*)(VOID))aSyscall[33].pCurrent) -#if defined(SQLITE_WIN32_HAS_ANSI) && defined(SQLITE_WIN32_GETVERSIONEX) && \ - SQLITE_WIN32_GETVERSIONEX +#if defined(SQLITE_WIN32_HAS_ANSI) && SQLITE_WIN32_GETVERSIONEX { "GetVersionExA", (SYSCALL)GetVersionExA, 0 }, #else { "GetVersionExA", (SYSCALL)0, 0 }, @@ -35125,7 +37653,7 @@ static struct win_syscall { LPOSVERSIONINFOA))aSyscall[34].pCurrent) #if !SQLITE_OS_WINRT && defined(SQLITE_WIN32_HAS_WIDE) && \ - defined(SQLITE_WIN32_GETVERSIONEX) && SQLITE_WIN32_GETVERSIONEX + SQLITE_WIN32_GETVERSIONEX { "GetVersionExW", (SYSCALL)GetVersionExW, 0 }, #else { "GetVersionExW", (SYSCALL)0, 0 }, @@ -35594,7 +38122,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_win32_compact_heap(LPUINT pnLargest){ if( lastErrno==NO_ERROR ){ sqlite3_log(SQLITE_NOMEM, "failed to HeapCompact (no space), heap=%p", (void*)hHeap); - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else{ sqlite3_log(SQLITE_ERROR, "failed to HeapCompact (%lu), heap=%p", osGetLastError(), (void*)hHeap); @@ -35620,8 +38148,8 @@ SQLITE_API int SQLITE_STDCALL sqlite3_win32_reset_heap(){ int rc; MUTEX_LOGIC( sqlite3_mutex *pMaster; ) /* The main static mutex */ MUTEX_LOGIC( sqlite3_mutex *pMem; ) /* The memsys static mutex */ - MUTEX_LOGIC( pMaster = sqlite3_mutex_alloc(SQLITE_MUTEX_STATIC_MASTER); ) - MUTEX_LOGIC( pMem = sqlite3_mutex_alloc(SQLITE_MUTEX_STATIC_MEM); ) + MUTEX_LOGIC( pMaster = sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_MASTER); ) + MUTEX_LOGIC( pMem = sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_MEM); ) sqlite3_mutex_enter(pMaster); sqlite3_mutex_enter(pMem); winMemAssertMagic(); @@ -35666,6 +38194,12 @@ SQLITE_API void SQLITE_STDCALL sqlite3_win32_write_debug(const char *zBuf, int n int nMin = MIN(nBuf, (SQLITE_WIN32_DBG_BUF_SIZE - 1)); /* may be negative. */ if( nMin<-1 ) nMin = -1; /* all negative values become -1. */ assert( nMin==-1 || nMin==0 || nMin0 ){ memset(zDbgBuf, 0, SQLITE_WIN32_DBG_BUF_SIZE); @@ -35736,7 +38270,7 @@ SQLITE_PRIVATE DWORD sqlite3Win32Wait(HANDLE hObject){ ** the LockFileEx() API. */ -#if !defined(SQLITE_WIN32_GETVERSIONEX) || !SQLITE_WIN32_GETVERSIONEX +#if !SQLITE_WIN32_GETVERSIONEX # define osIsNT() (1) #elif SQLITE_OS_WINCE || SQLITE_OS_WINRT || !defined(SQLITE_WIN32_HAS_ANSI) # define osIsNT() (1) @@ -35757,7 +38291,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_win32_is_nt(void){ ** kernel. */ return 1; -#elif defined(SQLITE_WIN32_GETVERSIONEX) && SQLITE_WIN32_GETVERSIONEX +#elif SQLITE_WIN32_GETVERSIONEX if( osInterlockedCompareExchange(&sqlite3_os_type, 0, 0)==0 ){ #if defined(SQLITE_WIN32_HAS_ANSI) OSVERSIONINFOA sInfo; @@ -35914,7 +38448,7 @@ static int winMemInit(void *pAppData){ "failed to HeapCreate (%lu), flags=%u, initSize=%lu, maxSize=%lu", osGetLastError(), SQLITE_WIN32_HEAP_FLAGS, dwInitialSize, dwMaximumSize); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pWinMemData->bOwned = TRUE; assert( pWinMemData->bOwned ); @@ -35924,7 +38458,7 @@ static int winMemInit(void *pAppData){ if( !pWinMemData->hHeap ){ sqlite3_log(SQLITE_NOMEM, "failed to GetProcessHeap (%lu)", osGetLastError()); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pWinMemData->bOwned = FALSE; assert( !pWinMemData->bOwned ); @@ -35991,147 +38525,244 @@ SQLITE_PRIVATE void sqlite3MemSetDefault(void){ #endif /* SQLITE_WIN32_MALLOC */ /* -** Convert a UTF-8 string to Microsoft Unicode (UTF-16?). +** Convert a UTF-8 string to Microsoft Unicode. ** -** Space to hold the returned string is obtained from malloc. +** Space to hold the returned string is obtained from sqlite3_malloc(). */ -static LPWSTR winUtf8ToUnicode(const char *zFilename){ +static LPWSTR winUtf8ToUnicode(const char *zText){ int nChar; - LPWSTR zWideFilename; + LPWSTR zWideText; - nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1, NULL, 0); + nChar = osMultiByteToWideChar(CP_UTF8, 0, zText, -1, NULL, 0); if( nChar==0 ){ return 0; } - zWideFilename = sqlite3MallocZero( nChar*sizeof(zWideFilename[0]) ); - if( zWideFilename==0 ){ + zWideText = sqlite3MallocZero( nChar*sizeof(WCHAR) ); + if( zWideText==0 ){ return 0; } - nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1, zWideFilename, + nChar = osMultiByteToWideChar(CP_UTF8, 0, zText, -1, zWideText, nChar); if( nChar==0 ){ - sqlite3_free(zWideFilename); - zWideFilename = 0; + sqlite3_free(zWideText); + zWideText = 0; } - return zWideFilename; + return zWideText; } /* -** Convert Microsoft Unicode to UTF-8. Space to hold the returned string is -** obtained from sqlite3_malloc(). +** Convert a Microsoft Unicode string to UTF-8. +** +** Space to hold the returned string is obtained from sqlite3_malloc(). */ -static char *winUnicodeToUtf8(LPCWSTR zWideFilename){ +static char *winUnicodeToUtf8(LPCWSTR zWideText){ int nByte; - char *zFilename; + char *zText; - nByte = osWideCharToMultiByte(CP_UTF8, 0, zWideFilename, -1, 0, 0, 0, 0); + nByte = osWideCharToMultiByte(CP_UTF8, 0, zWideText, -1, 0, 0, 0, 0); if( nByte == 0 ){ return 0; } - zFilename = sqlite3MallocZero( nByte ); - if( zFilename==0 ){ + zText = sqlite3MallocZero( nByte ); + if( zText==0 ){ return 0; } - nByte = osWideCharToMultiByte(CP_UTF8, 0, zWideFilename, -1, zFilename, nByte, + nByte = osWideCharToMultiByte(CP_UTF8, 0, zWideText, -1, zText, nByte, 0, 0); if( nByte == 0 ){ - sqlite3_free(zFilename); - zFilename = 0; + sqlite3_free(zText); + zText = 0; } - return zFilename; + return zText; } /* -** Convert an ANSI string to Microsoft Unicode, based on the -** current codepage settings for file apis. +** Convert an ANSI string to Microsoft Unicode, using the ANSI or OEM +** code page. ** -** Space to hold the returned string is obtained -** from sqlite3_malloc. +** Space to hold the returned string is obtained from sqlite3_malloc(). */ -static LPWSTR winMbcsToUnicode(const char *zFilename){ +static LPWSTR winMbcsToUnicode(const char *zText, int useAnsi){ int nByte; - LPWSTR zMbcsFilename; - int codepage = osAreFileApisANSI() ? CP_ACP : CP_OEMCP; + LPWSTR zMbcsText; + int codepage = useAnsi ? CP_ACP : CP_OEMCP; - nByte = osMultiByteToWideChar(codepage, 0, zFilename, -1, NULL, + nByte = osMultiByteToWideChar(codepage, 0, zText, -1, NULL, 0)*sizeof(WCHAR); if( nByte==0 ){ return 0; } - zMbcsFilename = sqlite3MallocZero( nByte*sizeof(zMbcsFilename[0]) ); - if( zMbcsFilename==0 ){ + zMbcsText = sqlite3MallocZero( nByte*sizeof(WCHAR) ); + if( zMbcsText==0 ){ return 0; } - nByte = osMultiByteToWideChar(codepage, 0, zFilename, -1, zMbcsFilename, + nByte = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText, nByte); if( nByte==0 ){ - sqlite3_free(zMbcsFilename); - zMbcsFilename = 0; + sqlite3_free(zMbcsText); + zMbcsText = 0; } - return zMbcsFilename; + return zMbcsText; } /* -** Convert Microsoft Unicode to multi-byte character string, based on the -** user's ANSI codepage. +** Convert a Microsoft Unicode string to a multi-byte character string, +** using the ANSI or OEM code page. ** -** Space to hold the returned string is obtained from -** sqlite3_malloc(). +** Space to hold the returned string is obtained from sqlite3_malloc(). */ -static char *winUnicodeToMbcs(LPCWSTR zWideFilename){ +static char *winUnicodeToMbcs(LPCWSTR zWideText, int useAnsi){ int nByte; - char *zFilename; - int codepage = osAreFileApisANSI() ? CP_ACP : CP_OEMCP; + char *zText; + int codepage = useAnsi ? CP_ACP : CP_OEMCP; - nByte = osWideCharToMultiByte(codepage, 0, zWideFilename, -1, 0, 0, 0, 0); + nByte = osWideCharToMultiByte(codepage, 0, zWideText, -1, 0, 0, 0, 0); if( nByte == 0 ){ return 0; } - zFilename = sqlite3MallocZero( nByte ); - if( zFilename==0 ){ + zText = sqlite3MallocZero( nByte ); + if( zText==0 ){ return 0; } - nByte = osWideCharToMultiByte(codepage, 0, zWideFilename, -1, zFilename, + nByte = osWideCharToMultiByte(codepage, 0, zWideText, -1, zText, nByte, 0, 0); if( nByte == 0 ){ - sqlite3_free(zFilename); - zFilename = 0; + sqlite3_free(zText); + zText = 0; } - return zFilename; + return zText; } /* -** Convert multibyte character string to UTF-8. Space to hold the -** returned string is obtained from sqlite3_malloc(). +** Convert a multi-byte character string to UTF-8. +** +** Space to hold the returned string is obtained from sqlite3_malloc(). */ -SQLITE_API char *SQLITE_STDCALL sqlite3_win32_mbcs_to_utf8(const char *zFilename){ - char *zFilenameUtf8; +static char *winMbcsToUtf8(const char *zText, int useAnsi){ + char *zTextUtf8; LPWSTR zTmpWide; - zTmpWide = winMbcsToUnicode(zFilename); + zTmpWide = winMbcsToUnicode(zText, useAnsi); if( zTmpWide==0 ){ return 0; } - zFilenameUtf8 = winUnicodeToUtf8(zTmpWide); + zTextUtf8 = winUnicodeToUtf8(zTmpWide); sqlite3_free(zTmpWide); - return zFilenameUtf8; + return zTextUtf8; } /* -** Convert UTF-8 to multibyte character string. Space to hold the -** returned string is obtained from sqlite3_malloc(). +** Convert a UTF-8 string to a multi-byte character string. +** +** Space to hold the returned string is obtained from sqlite3_malloc(). */ -SQLITE_API char *SQLITE_STDCALL sqlite3_win32_utf8_to_mbcs(const char *zFilename){ - char *zFilenameMbcs; +static char *winUtf8ToMbcs(const char *zText, int useAnsi){ + char *zTextMbcs; LPWSTR zTmpWide; - zTmpWide = winUtf8ToUnicode(zFilename); + zTmpWide = winUtf8ToUnicode(zText); if( zTmpWide==0 ){ return 0; } - zFilenameMbcs = winUnicodeToMbcs(zTmpWide); + zTextMbcs = winUnicodeToMbcs(zTmpWide, useAnsi); sqlite3_free(zTmpWide); - return zFilenameMbcs; + return zTextMbcs; +} + +/* +** This is a public wrapper for the winUtf8ToUnicode() function. +*/ +SQLITE_API LPWSTR SQLITE_STDCALL sqlite3_win32_utf8_to_unicode(const char *zText){ +#ifdef SQLITE_ENABLE_API_ARMOR + if( !zText ){ + (void)SQLITE_MISUSE_BKPT; + return 0; + } +#endif +#ifndef SQLITE_OMIT_AUTOINIT + if( sqlite3_initialize() ) return 0; +#endif + return winUtf8ToUnicode(zText); +} + +/* +** This is a public wrapper for the winUnicodeToUtf8() function. +*/ +SQLITE_API char *SQLITE_STDCALL sqlite3_win32_unicode_to_utf8(LPCWSTR zWideText){ +#ifdef SQLITE_ENABLE_API_ARMOR + if( !zWideText ){ + (void)SQLITE_MISUSE_BKPT; + return 0; + } +#endif +#ifndef SQLITE_OMIT_AUTOINIT + if( sqlite3_initialize() ) return 0; +#endif + return winUnicodeToUtf8(zWideText); +} + +/* +** This is a public wrapper for the winMbcsToUtf8() function. +*/ +SQLITE_API char *SQLITE_STDCALL sqlite3_win32_mbcs_to_utf8(const char *zText){ +#ifdef SQLITE_ENABLE_API_ARMOR + if( !zText ){ + (void)SQLITE_MISUSE_BKPT; + return 0; + } +#endif +#ifndef SQLITE_OMIT_AUTOINIT + if( sqlite3_initialize() ) return 0; +#endif + return winMbcsToUtf8(zText, osAreFileApisANSI()); +} + +/* +** This is a public wrapper for the winMbcsToUtf8() function. +*/ +SQLITE_API char *SQLITE_STDCALL sqlite3_win32_mbcs_to_utf8_v2(const char *zText, int useAnsi){ +#ifdef SQLITE_ENABLE_API_ARMOR + if( !zText ){ + (void)SQLITE_MISUSE_BKPT; + return 0; + } +#endif +#ifndef SQLITE_OMIT_AUTOINIT + if( sqlite3_initialize() ) return 0; +#endif + return winMbcsToUtf8(zText, useAnsi); +} + +/* +** This is a public wrapper for the winUtf8ToMbcs() function. +*/ +SQLITE_API char *SQLITE_STDCALL sqlite3_win32_utf8_to_mbcs(const char *zText){ +#ifdef SQLITE_ENABLE_API_ARMOR + if( !zText ){ + (void)SQLITE_MISUSE_BKPT; + return 0; + } +#endif +#ifndef SQLITE_OMIT_AUTOINIT + if( sqlite3_initialize() ) return 0; +#endif + return winUtf8ToMbcs(zText, osAreFileApisANSI()); +} + +/* +** This is a public wrapper for the winUtf8ToMbcs() function. +*/ +SQLITE_API char *SQLITE_STDCALL sqlite3_win32_utf8_to_mbcs_v2(const char *zText, int useAnsi){ +#ifdef SQLITE_ENABLE_API_ARMOR + if( !zText ){ + (void)SQLITE_MISUSE_BKPT; + return 0; + } +#endif +#ifndef SQLITE_OMIT_AUTOINIT + if( sqlite3_initialize() ) return 0; +#endif + return winUtf8ToMbcs(zText, useAnsi); } /* @@ -36161,7 +38792,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_win32_set_directory(DWORD type, LPCWSTR zV if( zValue && zValue[0] ){ zValueUtf8 = winUnicodeToUtf8(zValue); if ( zValueUtf8==0 ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } } sqlite3_free(*ppDirectory); @@ -36233,7 +38864,7 @@ static int winGetLastErrorMsg(DWORD lastErrno, int nBuf, char *zBuf){ if( dwLen > 0 ){ /* allocate a buffer and convert to UTF8 */ sqlite3BeginBenignMalloc(); - zOut = sqlite3_win32_mbcs_to_utf8(zTemp); + zOut = winMbcsToUtf8(zTemp, osAreFileApisANSI()); sqlite3EndBenignMalloc(); /* free the system buffer allocated by FormatMessage */ osLocalFree(zTemp); @@ -36375,16 +39006,17 @@ static void winLogIoerr(int nRetry, int lineno){ } } -#if SQLITE_OS_WINCE -/************************************************************************* -** This section contains code for WinCE only. -*/ -#if !defined(SQLITE_MSVC_LOCALTIME_API) || !SQLITE_MSVC_LOCALTIME_API /* -** The MSVC CRT on Windows CE may not have a localtime() function. So -** create a substitute. +** This #if does not rely on the SQLITE_OS_WINCE define because the +** corresponding section in "date.c" cannot use it. */ -/* #include */ +#if !defined(SQLITE_OMIT_LOCALTIME) && defined(_WIN32_WCE) && \ + (!defined(SQLITE_MSVC_LOCALTIME_API) || !SQLITE_MSVC_LOCALTIME_API) +/* +** The MSVC CRT on Windows CE may not have a localtime() function. +** So define a substitute. +*/ +/* # include */ struct tm *__cdecl localtime(const time_t *t) { static struct tm y; @@ -36408,6 +39040,10 @@ struct tm *__cdecl localtime(const time_t *t) } #endif +#if SQLITE_OS_WINCE +/************************************************************************* +** This section contains code for WinCE only. +*/ #define HANDLE_TO_WINFILE(a) (winFile*)&((char*)a)[-(int)offsetof(winFile,h)] /* @@ -36438,7 +39074,7 @@ static int winceCreateLock(const char *zFilename, winFile *pFile){ zName = winUtf8ToUnicode(zFilename); if( zName==0 ){ /* out of memory */ - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } /* Initialize the local lockdata */ @@ -36991,7 +39627,7 @@ static int winWrite( "offset=%lld, lock=%d\n", osGetCurrentProcessId(), pFile, pFile->h, pBuf, amt, offset, pFile->locktype)); -#if SQLITE_MAX_MMAP_SIZE>0 +#if defined(SQLITE_MMAP_READWRITE) && SQLITE_MAX_MMAP_SIZE>0 /* Deal with as much of this write request as possible by transfering ** data from the memory mapping using memcpy(). */ if( offsetmmapSize ){ @@ -37421,9 +40057,8 @@ static int winLock(sqlite3_file *id, int locktype){ ** the PENDING_LOCK byte is temporary. */ newLocktype = pFile->locktype; - if( (pFile->locktype==NO_LOCK) - || ( (locktype==EXCLUSIVE_LOCK) - && (pFile->locktype==RESERVED_LOCK)) + if( pFile->locktype==NO_LOCK + || (locktype==EXCLUSIVE_LOCK && pFile->locktype<=RESERVED_LOCK) ){ int cnt = 3; while( cnt-->0 && (res = winLockFile(&pFile->h, SQLITE_LOCKFILE_FLAGS, @@ -37539,7 +40174,7 @@ static int winCheckReservedLock(sqlite3_file *id, int *pResOut){ res = 1; OSTRACE(("TEST-WR-LOCK file=%p, result=%d (local)\n", pFile->h, res)); }else{ - res = winLockFile(&pFile->h, SQLITE_LOCKFILEEX_FLAGS,RESERVED_BYTE, 0, 1, 0); + res = winLockFile(&pFile->h, SQLITE_LOCKFILEEX_FLAGS,RESERVED_BYTE,0,1,0); if( res ){ winUnlockFile(&pFile->h, RESERVED_BYTE, 0, 1, 0); } @@ -37629,7 +40264,7 @@ static int winFileControl(sqlite3_file *id, int op, void *pArg){ OSTRACE(("FCNTL file=%p, rc=SQLITE_OK\n", pFile->h)); return SQLITE_OK; } - case SQLITE_LAST_ERRNO: { + case SQLITE_FCNTL_LAST_ERRNO: { *(int*)pArg = (int)pFile->lastErrno; OSTRACE(("FCNTL file=%p, rc=SQLITE_OK\n", pFile->h)); return SQLITE_OK; @@ -37987,12 +40622,12 @@ static int winOpenSharedMemory(winFile *pDbFd){ ** allocate space for a new winShmNode and filename. */ p = sqlite3MallocZero( sizeof(*p) ); - if( p==0 ) return SQLITE_IOERR_NOMEM; + if( p==0 ) return SQLITE_IOERR_NOMEM_BKPT; nName = sqlite3Strlen30(pDbFd->zPath); pNew = sqlite3MallocZero( sizeof(*pShmNode) + nName + 17 ); if( pNew==0 ){ sqlite3_free(p); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } pNew->zFilename = (char*)&pNew[1]; sqlite3_snprintf(nName+15, pNew->zFilename, "%s-shm", pDbFd->zPath); @@ -38017,10 +40652,12 @@ static int winOpenSharedMemory(winFile *pDbFd){ pShmNode->pNext = winShmNodeList; winShmNodeList = pShmNode; - pShmNode->mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); - if( pShmNode->mutex==0 ){ - rc = SQLITE_IOERR_NOMEM; - goto shm_open_err; + if( sqlite3GlobalConfig.bCoreMutex ){ + pShmNode->mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); + if( pShmNode->mutex==0 ){ + rc = SQLITE_IOERR_NOMEM_BKPT; + goto shm_open_err; + } } rc = winOpen(pDbFd->pVfs, @@ -38324,7 +40961,7 @@ static int winShmMap( pShmNode->aRegion, (iRegion+1)*sizeof(apNew[0]) ); if( !apNew ){ - rc = SQLITE_IOERR_NOMEM; + rc = SQLITE_IOERR_NOMEM_BKPT; goto shmpage_out; } pShmNode->aRegion = apNew; @@ -38341,7 +40978,7 @@ static int winShmMap( hMap = osCreateFileMappingW(pShmNode->hFile.h, NULL, PAGE_READWRITE, 0, nByte, NULL ); -#elif defined(SQLITE_WIN32_HAS_ANSI) +#elif defined(SQLITE_WIN32_HAS_ANSI) && SQLITE_WIN32_CREATEFILEMAPPINGA hMap = osCreateFileMappingA(pShmNode->hFile.h, NULL, PAGE_READWRITE, 0, nByte, NULL ); @@ -38485,17 +41122,19 @@ static int winMapfile(winFile *pFd, sqlite3_int64 nByte){ DWORD flags = FILE_MAP_READ; winUnmapfile(pFd); +#ifdef SQLITE_MMAP_READWRITE if( (pFd->ctrlFlags & WINFILE_RDONLY)==0 ){ protect = PAGE_READWRITE; flags |= FILE_MAP_WRITE; } +#endif #if SQLITE_OS_WINRT pFd->hMap = osCreateFileMappingFromApp(pFd->h, NULL, protect, nMap, NULL); #elif defined(SQLITE_WIN32_HAS_WIDE) pFd->hMap = osCreateFileMappingW(pFd->h, NULL, protect, (DWORD)((nMap>>32) & 0xffffffff), (DWORD)(nMap & 0xffffffff), NULL); -#elif defined(SQLITE_WIN32_HAS_ANSI) +#elif defined(SQLITE_WIN32_HAS_ANSI) && SQLITE_WIN32_CREATEFILEMAPPINGA pFd->hMap = osCreateFileMappingA(pFd->h, NULL, protect, (DWORD)((nMap>>32) & 0xffffffff), (DWORD)(nMap & 0xffffffff), NULL); @@ -38676,7 +41315,7 @@ static char *winConvertToUtf8Filename(const void *zFilename){ } #ifdef SQLITE_WIN32_HAS_ANSI else{ - zConverted = sqlite3_win32_mbcs_to_utf8(zFilename); + zConverted = winMbcsToUtf8(zFilename, osAreFileApisANSI()); } #endif /* caller will handle out of memory */ @@ -38697,7 +41336,7 @@ static void *winConvertFromUtf8Filename(const char *zFilename){ } #ifdef SQLITE_WIN32_HAS_ANSI else{ - zConverted = sqlite3_win32_utf8_to_mbcs(zFilename); + zConverted = winUtf8ToMbcs(zFilename, osAreFileApisANSI()); } #endif /* caller will handle out of memory */ @@ -38752,7 +41391,7 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ zBuf = sqlite3MallocZero( nBuf ); if( !zBuf ){ OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } /* Figure out the effective temporary directory. First, check if one @@ -38810,7 +41449,7 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ if( !zConverted ){ sqlite3_free(zBuf); OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } if( winIsDir(zConverted) ){ sqlite3_snprintf(nMax, zBuf, "%s", zDir); @@ -38823,7 +41462,7 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ if( !zConverted ){ sqlite3_free(zBuf); OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } if( cygwin_conv_path( osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A, zDir, @@ -38844,7 +41483,7 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ sqlite3_free(zConverted); sqlite3_free(zBuf); OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } sqlite3_snprintf(nMax, zBuf, "%s", zUtf8); sqlite3_free(zUtf8); @@ -38862,7 +41501,7 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ if( !zWidePath ){ sqlite3_free(zBuf); OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } if( osGetTempPathW(nMax, zWidePath)==0 ){ sqlite3_free(zWidePath); @@ -38880,7 +41519,7 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ sqlite3_free(zWidePath); sqlite3_free(zBuf); OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } } #ifdef SQLITE_WIN32_HAS_ANSI @@ -38890,7 +41529,7 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ if( !zMbcsPath ){ sqlite3_free(zBuf); OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } if( osGetTempPathA(nMax, zMbcsPath)==0 ){ sqlite3_free(zBuf); @@ -38898,14 +41537,14 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ return winLogError(SQLITE_IOERR_GETTEMPPATH, osGetLastError(), "winGetTempname3", 0); } - zUtf8 = sqlite3_win32_mbcs_to_utf8(zMbcsPath); + zUtf8 = winMbcsToUtf8(zMbcsPath, osAreFileApisANSI()); if( zUtf8 ){ sqlite3_snprintf(nMax, zBuf, "%s", zUtf8); sqlite3_free(zUtf8); }else{ sqlite3_free(zBuf); OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } } #endif /* SQLITE_WIN32_HAS_ANSI */ @@ -39097,7 +41736,7 @@ static int winOpen( if( zConverted==0 ){ sqlite3_free(zTmpname); OSTRACE(("OPEN name=%s, rc=SQLITE_IOERR_NOMEM", zUtf8Name)); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } if( winIsDir(zConverted) ){ @@ -39297,7 +41936,7 @@ static int winDelete( zConverted = winConvertFromUtf8Filename(zFilename); if( zConverted==0 ){ OSTRACE(("DELETE name=%s, rc=SQLITE_IOERR_NOMEM\n", zFilename)); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } if( osIsNT() ){ do { @@ -39405,7 +42044,7 @@ static int winAccess( zConverted = winConvertFromUtf8Filename(zFilename); if( zConverted==0 ){ OSTRACE(("ACCESS name=%s, rc=SQLITE_IOERR_NOMEM\n", zFilename)); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } if( osIsNT() ){ int cnt = 0; @@ -39532,7 +42171,7 @@ static int winFullPathname( */ char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 ); if( !zOut ){ - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } if( cygwin_conv_path( (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A) | @@ -39544,7 +42183,7 @@ static int winFullPathname( char *zUtf8 = winConvertToUtf8Filename(zOut); if( !zUtf8 ){ sqlite3_free(zOut); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s", sqlite3_data_directory, winGetDirSep(), zUtf8); @@ -39554,7 +42193,7 @@ static int winFullPathname( }else{ char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 ); if( !zOut ){ - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } if( cygwin_conv_path( (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A), @@ -39566,7 +42205,7 @@ static int winFullPathname( char *zUtf8 = winConvertToUtf8Filename(zOut); if( !zUtf8 ){ sqlite3_free(zOut); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8); sqlite3_free(zUtf8); @@ -39626,7 +42265,7 @@ static int winFullPathname( } zConverted = winConvertFromUtf8Filename(zRelative); if( zConverted==0 ){ - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } if( osIsNT() ){ LPWSTR zTemp; @@ -39640,7 +42279,7 @@ static int winFullPathname( zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) ); if( zTemp==0 ){ sqlite3_free(zConverted); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } nByte = osGetFullPathNameW((LPCWSTR)zConverted, nByte, zTemp, 0); if( nByte==0 ){ @@ -39666,7 +42305,7 @@ static int winFullPathname( zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) ); if( zTemp==0 ){ sqlite3_free(zConverted); - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } nByte = osGetFullPathNameA((char*)zConverted, nByte, zTemp, 0); if( nByte==0 ){ @@ -39676,7 +42315,7 @@ static int winFullPathname( "winFullPathname4", zRelative); } sqlite3_free(zConverted); - zOut = sqlite3_win32_mbcs_to_utf8(zTemp); + zOut = winMbcsToUtf8(zTemp, osAreFileApisANSI()); sqlite3_free(zTemp); } #endif @@ -39685,7 +42324,7 @@ static int winFullPathname( sqlite3_free(zOut); return SQLITE_OK; }else{ - return SQLITE_IOERR_NOMEM; + return SQLITE_IOERR_NOMEM_BKPT; } #endif } @@ -39760,65 +42399,85 @@ static void winDlClose(sqlite3_vfs *pVfs, void *pHandle){ #define winDlClose 0 #endif +/* State information for the randomness gatherer. */ +typedef struct EntropyGatherer EntropyGatherer; +struct EntropyGatherer { + unsigned char *a; /* Gather entropy into this buffer */ + int na; /* Size of a[] in bytes */ + int i; /* XOR next input into a[i] */ + int nXor; /* Number of XOR operations done */ +}; + +#if !defined(SQLITE_TEST) && !defined(SQLITE_OMIT_RANDOMNESS) +/* Mix sz bytes of entropy into p. */ +static void xorMemory(EntropyGatherer *p, unsigned char *x, int sz){ + int j, k; + for(j=0, k=p->i; ja[k++] ^= x[j]; + if( k>=p->na ) k = 0; + } + p->i = k; + p->nXor += sz; +} +#endif /* !defined(SQLITE_TEST) && !defined(SQLITE_OMIT_RANDOMNESS) */ /* ** Write up to nBuf bytes of randomness into zBuf. */ static int winRandomness(sqlite3_vfs *pVfs, int nBuf, char *zBuf){ - int n = 0; - UNUSED_PARAMETER(pVfs); #if defined(SQLITE_TEST) || defined(SQLITE_OMIT_RANDOMNESS) - n = nBuf; + UNUSED_PARAMETER(pVfs); memset(zBuf, 0, nBuf); + return nBuf; #else - if( sizeof(SYSTEMTIME)<=nBuf-n ){ + EntropyGatherer e; + UNUSED_PARAMETER(pVfs); + memset(zBuf, 0, nBuf); +#if defined(_MSC_VER) && _MSC_VER>=1400 && !SQLITE_OS_WINCE + rand_s((unsigned int*)zBuf); /* rand_s() is not available with MinGW */ +#endif /* defined(_MSC_VER) && _MSC_VER>=1400 */ + e.a = (unsigned char*)zBuf; + e.na = nBuf; + e.nXor = 0; + e.i = 0; + { SYSTEMTIME x; osGetSystemTime(&x); - memcpy(&zBuf[n], &x, sizeof(x)); - n += sizeof(x); + xorMemory(&e, (unsigned char*)&x, sizeof(SYSTEMTIME)); } - if( sizeof(DWORD)<=nBuf-n ){ + { DWORD pid = osGetCurrentProcessId(); - memcpy(&zBuf[n], &pid, sizeof(pid)); - n += sizeof(pid); + xorMemory(&e, (unsigned char*)&pid, sizeof(DWORD)); } #if SQLITE_OS_WINRT - if( sizeof(ULONGLONG)<=nBuf-n ){ + { ULONGLONG cnt = osGetTickCount64(); - memcpy(&zBuf[n], &cnt, sizeof(cnt)); - n += sizeof(cnt); + xorMemory(&e, (unsigned char*)&cnt, sizeof(ULONGLONG)); } #else - if( sizeof(DWORD)<=nBuf-n ){ + { DWORD cnt = osGetTickCount(); - memcpy(&zBuf[n], &cnt, sizeof(cnt)); - n += sizeof(cnt); + xorMemory(&e, (unsigned char*)&cnt, sizeof(DWORD)); } -#endif - if( sizeof(LARGE_INTEGER)<=nBuf-n ){ +#endif /* SQLITE_OS_WINRT */ + { LARGE_INTEGER i; osQueryPerformanceCounter(&i); - memcpy(&zBuf[n], &i, sizeof(i)); - n += sizeof(i); + xorMemory(&e, (unsigned char*)&i, sizeof(LARGE_INTEGER)); } #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && SQLITE_WIN32_USE_UUID - if( sizeof(UUID)<=nBuf-n ){ + { UUID id; memset(&id, 0, sizeof(UUID)); osUuidCreate(&id); - memcpy(&zBuf[n], &id, sizeof(UUID)); - n += sizeof(UUID); - } - if( sizeof(UUID)<=nBuf-n ){ - UUID id; + xorMemory(&e, (unsigned char*)&id, sizeof(UUID)); memset(&id, 0, sizeof(UUID)); osUuidCreateSequential(&id); - memcpy(&zBuf[n], &id, sizeof(UUID)); - n += sizeof(UUID); + xorMemory(&e, (unsigned char*)&id, sizeof(UUID)); } -#endif -#endif /* defined(SQLITE_TEST) || defined(SQLITE_ZERO_PRNG_SEED) */ - return n; +#endif /* !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && SQLITE_WIN32_USE_UUID */ + return e.nXor>nBuf ? nBuf : e.nXor; +#endif /* defined(SQLITE_TEST) || defined(SQLITE_OMIT_RANDOMNESS) */ } @@ -39934,8 +42593,10 @@ static int winCurrentTime(sqlite3_vfs *pVfs, double *prNow){ ** sqlite3_errmsg(), possibly making IO errors easier to debug. */ static int winGetLastError(sqlite3_vfs *pVfs, int nBuf, char *zBuf){ + DWORD e = osGetLastError(); UNUSED_PARAMETER(pVfs); - return winGetLastErrorMsg(osGetLastError(), nBuf, zBuf); + if( nBuf>0 ) winGetLastErrorMsg(e, nBuf, zBuf); + return e; } /* @@ -40073,7 +42734,8 @@ SQLITE_API int SQLITE_STDCALL sqlite3_os_end(void){ /* Round the union size down to the nearest pointer boundary, since that's how ** it will be aligned within the Bitvec struct. */ -#define BITVEC_USIZE (((BITVEC_SZ-(3*sizeof(u32)))/sizeof(Bitvec*))*sizeof(Bitvec*)) +#define BITVEC_USIZE \ + (((BITVEC_SZ-(3*sizeof(u32)))/sizeof(Bitvec*))*sizeof(Bitvec*)) /* Type of the array "element" for the bitmap representation. ** Should be a power of 2, and ideally, evenly divide into BITVEC_USIZE. @@ -40208,7 +42870,7 @@ SQLITE_PRIVATE int sqlite3BitvecSet(Bitvec *p, u32 i){ i = i%p->iDivisor; if( p->u.apSub[bin]==0 ){ p->u.apSub[bin] = sqlite3BitvecCreate( p->iDivisor ); - if( p->u.apSub[bin]==0 ) return SQLITE_NOMEM; + if( p->u.apSub[bin]==0 ) return SQLITE_NOMEM_BKPT; } p = p->u.apSub[bin]; } @@ -40243,7 +42905,7 @@ bitvec_set_rehash: int rc; u32 *aiValues = sqlite3StackAllocRaw(0, sizeof(p->u.aHash)); if( aiValues==0 ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; }else{ memcpy(aiValues, p->u.aHash, sizeof(p->u.aHash)); memset(p->u.apSub, 0, sizeof(p->u.apSub)); @@ -40459,13 +43121,36 @@ bitvec_end: /* #include "sqliteInt.h" */ /* -** A complete page cache is an instance of this structure. +** A complete page cache is an instance of this structure. Every +** entry in the cache holds a single page of the database file. The +** btree layer only operates on the cached copy of the database pages. +** +** A page cache entry is "clean" if it exactly matches what is currently +** on disk. A page is "dirty" if it has been modified and needs to be +** persisted to disk. +** +** pDirty, pDirtyTail, pSynced: +** All dirty pages are linked into the doubly linked list using +** PgHdr.pDirtyNext and pDirtyPrev. The list is maintained in LRU order +** such that p was added to the list more recently than p->pDirtyNext. +** PCache.pDirty points to the first (newest) element in the list and +** pDirtyTail to the last (oldest). +** +** The PCache.pSynced variable is used to optimize searching for a dirty +** page to eject from the cache mid-transaction. It is better to eject +** a page that does not require a journal sync than one that does. +** Therefore, pSynced is maintained to that it *almost* always points +** to either the oldest page in the pDirty/pDirtyTail list that has a +** clear PGHDR_NEED_SYNC flag or to a page that is older than this one +** (so that the right page to eject can be found by following pDirtyPrev +** pointers). */ struct PCache { PgHdr *pDirty, *pDirtyTail; /* List of dirty pages in LRU order */ PgHdr *pSynced; /* Last synced page in dirty page list */ int nRefSum; /* Sum of ref counts over all pages */ int szCache; /* Configured cache size */ + int szSpill; /* Size before spilling occurs */ int szPage; /* Size of every page in this cache */ int szExtra; /* Size of extra space for each page */ u8 bPurgeable; /* True if pages are on backing store */ @@ -40475,6 +43160,95 @@ struct PCache { sqlite3_pcache *pCache; /* Pluggable cache module */ }; +/********************************** Test and Debug Logic **********************/ +/* +** Debug tracing macros. Enable by by changing the "0" to "1" and +** recompiling. +** +** When sqlite3PcacheTrace is 1, single line trace messages are issued. +** When sqlite3PcacheTrace is 2, a dump of the pcache showing all cache entries +** is displayed for many operations, resulting in a lot of output. +*/ +#if defined(SQLITE_DEBUG) && 0 + int sqlite3PcacheTrace = 2; /* 0: off 1: simple 2: cache dumps */ + int sqlite3PcacheMxDump = 9999; /* Max cache entries for pcacheDump() */ +# define pcacheTrace(X) if(sqlite3PcacheTrace){sqlite3DebugPrintf X;} + void pcacheDump(PCache *pCache){ + int N; + int i, j; + sqlite3_pcache_page *pLower; + PgHdr *pPg; + unsigned char *a; + + if( sqlite3PcacheTrace<2 ) return; + if( pCache->pCache==0 ) return; + N = sqlite3PcachePagecount(pCache); + if( N>sqlite3PcacheMxDump ) N = sqlite3PcacheMxDump; + for(i=1; i<=N; i++){ + pLower = sqlite3GlobalConfig.pcache2.xFetch(pCache->pCache, i, 0); + if( pLower==0 ) continue; + pPg = (PgHdr*)pLower->pExtra; + printf("%3d: nRef %2d flgs %02x data ", i, pPg->nRef, pPg->flags); + a = (unsigned char *)pLower->pBuf; + for(j=0; j<12; j++) printf("%02x", a[j]); + printf("\n"); + if( pPg->pPage==0 ){ + sqlite3GlobalConfig.pcache2.xUnpin(pCache->pCache, pLower, 0); + } + } + } + #else +# define pcacheTrace(X) +# define pcacheDump(X) +#endif + +/* +** Check invariants on a PgHdr entry. Return true if everything is OK. +** Return false if any invariant is violated. +** +** This routine is for use inside of assert() statements only. For +** example: +** +** assert( sqlite3PcachePageSanity(pPg) ); +*/ +#if SQLITE_DEBUG +SQLITE_PRIVATE int sqlite3PcachePageSanity(PgHdr *pPg){ + PCache *pCache; + assert( pPg!=0 ); + assert( pPg->pgno>0 ); /* Page number is 1 or more */ + pCache = pPg->pCache; + assert( pCache!=0 ); /* Every page has an associated PCache */ + if( pPg->flags & PGHDR_CLEAN ){ + assert( (pPg->flags & PGHDR_DIRTY)==0 );/* Cannot be both CLEAN and DIRTY */ + assert( pCache->pDirty!=pPg ); /* CLEAN pages not on dirty list */ + assert( pCache->pDirtyTail!=pPg ); + } + /* WRITEABLE pages must also be DIRTY */ + if( pPg->flags & PGHDR_WRITEABLE ){ + assert( pPg->flags & PGHDR_DIRTY ); /* WRITEABLE implies DIRTY */ + } + /* NEED_SYNC can be set independently of WRITEABLE. This can happen, + ** for example, when using the sqlite3PagerDontWrite() optimization: + ** (1) Page X is journalled, and gets WRITEABLE and NEED_SEEK. + ** (2) Page X moved to freelist, WRITEABLE is cleared + ** (3) Page X reused, WRITEABLE is set again + ** If NEED_SYNC had been cleared in step 2, then it would not be reset + ** in step 3, and page might be written into the database without first + ** syncing the rollback journal, which might cause corruption on a power + ** loss. + ** + ** Another example is when the database page size is smaller than the + ** disk sector size. When any page of a sector is journalled, all pages + ** in that sector are marked NEED_SYNC even if they are still CLEAN, just + ** in case they are later modified, since all pages in the same sector + ** must be journalled and synced before any of those pages can be safely + ** written. + */ + return 1; +} +#endif /* SQLITE_DEBUG */ + + /********************************** Linked List Management ********************/ /* Allowed values for second argument to pcacheManageDirtyList() */ @@ -40491,17 +43265,16 @@ struct PCache { static void pcacheManageDirtyList(PgHdr *pPage, u8 addRemove){ PCache *p = pPage->pCache; + pcacheTrace(("%p.DIRTYLIST.%s %d\n", p, + addRemove==1 ? "REMOVE" : addRemove==2 ? "ADD" : "FRONT", + pPage->pgno)); if( addRemove & PCACHE_DIRTYLIST_REMOVE ){ assert( pPage->pDirtyNext || pPage==p->pDirtyTail ); assert( pPage->pDirtyPrev || pPage==p->pDirty ); /* Update the PCache1.pSynced variable if necessary. */ if( p->pSynced==pPage ){ - PgHdr *pSynced = pPage->pDirtyPrev; - while( pSynced && (pSynced->flags&PGHDR_NEED_SYNC) ){ - pSynced = pSynced->pDirtyPrev; - } - p->pSynced = pSynced; + p->pSynced = pPage->pDirtyPrev; } if( pPage->pDirtyNext ){ @@ -40513,10 +43286,15 @@ static void pcacheManageDirtyList(PgHdr *pPage, u8 addRemove){ if( pPage->pDirtyPrev ){ pPage->pDirtyPrev->pDirtyNext = pPage->pDirtyNext; }else{ + /* If there are now no dirty pages in the cache, set eCreate to 2. + ** This is an optimization that allows sqlite3PcacheFetch() to skip + ** searching for a dirty page to eject from the cache when it might + ** otherwise have to. */ assert( pPage==p->pDirty ); p->pDirty = pPage->pDirtyNext; - if( p->pDirty==0 && p->bPurgeable ){ - assert( p->eCreate==1 ); + assert( p->bPurgeable || p->eCreate==2 ); + if( p->pDirty==0 ){ /*OPTIMIZATION-IF-TRUE*/ + assert( p->bPurgeable==0 || p->eCreate==1 ); p->eCreate = 2; } } @@ -40538,10 +43316,19 @@ static void pcacheManageDirtyList(PgHdr *pPage, u8 addRemove){ } } p->pDirty = pPage; - if( !p->pSynced && 0==(pPage->flags&PGHDR_NEED_SYNC) ){ + + /* If pSynced is NULL and this page has a clear NEED_SYNC flag, set + ** pSynced to point to it. Checking the NEED_SYNC flag is an + ** optimization, as if pSynced points to a page with the NEED_SYNC + ** flag set sqlite3PcacheFetchStress() searches through all newer + ** entries of the dirty-list for a page with NEED_SYNC clear anyway. */ + if( !p->pSynced + && 0==(pPage->flags&PGHDR_NEED_SYNC) /*OPTIMIZATION-IF-FALSE*/ + ){ p->pSynced = pPage; } } + pcacheDump(p); } /* @@ -40550,15 +43337,15 @@ static void pcacheManageDirtyList(PgHdr *pPage, u8 addRemove){ */ static void pcacheUnpin(PgHdr *p){ if( p->pCache->bPurgeable ){ + pcacheTrace(("%p.UNPIN %d\n", p->pCache, p->pgno)); sqlite3GlobalConfig.pcache2.xUnpin(p->pCache->pCache, p->pPage, 0); + pcacheDump(p->pCache); } } /* -** Compute the number of pages of cache requested. p->szCache is the +** Compute the number of pages of cache requested. p->szCache is the ** cache size requested by the "PRAGMA cache_size" statement. -** -** */ static int numberOfCachePages(PCache *p){ if( p->szCache>=0 ){ @@ -40621,6 +43408,8 @@ SQLITE_PRIVATE int sqlite3PcacheOpen( p->xStress = xStress; p->pStress = pStress; p->szCache = 100; + p->szSpill = 1; + pcacheTrace(("%p.OPEN szPage %d bPurgeable %d\n",p,szPage,bPurgeable)); return sqlite3PcacheSetPageSize(p, szPage); } @@ -40636,13 +43425,14 @@ SQLITE_PRIVATE int sqlite3PcacheSetPageSize(PCache *pCache, int szPage){ szPage, pCache->szExtra + ROUND8(sizeof(PgHdr)), pCache->bPurgeable ); - if( pNew==0 ) return SQLITE_NOMEM; + if( pNew==0 ) return SQLITE_NOMEM_BKPT; sqlite3GlobalConfig.pcache2.xCachesize(pNew, numberOfCachePages(pCache)); if( pCache->pCache ){ sqlite3GlobalConfig.pcache2.xDestroy(pCache->pCache); } pCache->pCache = pNew; pCache->szPage = szPage; + pcacheTrace(("%p.PAGESIZE %d\n",pCache,szPage)); } return SQLITE_OK; } @@ -40677,11 +43467,13 @@ SQLITE_PRIVATE sqlite3_pcache_page *sqlite3PcacheFetch( int createFlag /* If true, create page if it does not exist already */ ){ int eCreate; + sqlite3_pcache_page *pRes; assert( pCache!=0 ); assert( pCache->pCache!=0 ); assert( createFlag==3 || createFlag==0 ); assert( pgno>0 ); + assert( pCache->eCreate==((pCache->bPurgeable && pCache->pDirty) ? 1 : 2) ); /* eCreate defines what to do if the page does not exist. ** 0 Do not allocate a new page. (createFlag==0) @@ -40694,12 +43486,15 @@ SQLITE_PRIVATE sqlite3_pcache_page *sqlite3PcacheFetch( assert( eCreate==0 || eCreate==1 || eCreate==2 ); assert( createFlag==0 || pCache->eCreate==eCreate ); assert( createFlag==0 || eCreate==1+(!pCache->bPurgeable||!pCache->pDirty) ); - return sqlite3GlobalConfig.pcache2.xFetch(pCache->pCache, pgno, eCreate); + pRes = sqlite3GlobalConfig.pcache2.xFetch(pCache->pCache, pgno, eCreate); + pcacheTrace(("%p.FETCH %d%s (result: %p)\n",pCache,pgno, + createFlag?" create":"",pRes)); + return pRes; } /* ** If the sqlite3PcacheFetch() routine is unable to allocate a new -** page because new clean pages are available for reuse and the cache +** page because no clean pages are available for reuse and the cache ** size limit has been reached, then this routine can be invoked to ** try harder to allocate a page. This routine might invoke the stress ** callback to spill dirty pages to the journal. It will then try to @@ -40716,36 +43511,43 @@ SQLITE_PRIVATE int sqlite3PcacheFetchStress( PgHdr *pPg; if( pCache->eCreate==2 ) return 0; - - /* Find a dirty page to write-out and recycle. First try to find a - ** page that does not require a journal-sync (one with PGHDR_NEED_SYNC - ** cleared), but if that is not possible settle for any other - ** unreferenced dirty page. - */ - for(pPg=pCache->pSynced; - pPg && (pPg->nRef || (pPg->flags&PGHDR_NEED_SYNC)); - pPg=pPg->pDirtyPrev - ); - pCache->pSynced = pPg; - if( !pPg ){ - for(pPg=pCache->pDirtyTail; pPg && pPg->nRef; pPg=pPg->pDirtyPrev); - } - if( pPg ){ - int rc; + if( sqlite3PcachePagecount(pCache)>pCache->szSpill ){ + /* Find a dirty page to write-out and recycle. First try to find a + ** page that does not require a journal-sync (one with PGHDR_NEED_SYNC + ** cleared), but if that is not possible settle for any other + ** unreferenced dirty page. + ** + ** If the LRU page in the dirty list that has a clear PGHDR_NEED_SYNC + ** flag is currently referenced, then the following may leave pSynced + ** set incorrectly (pointing to other than the LRU page with NEED_SYNC + ** cleared). This is Ok, as pSynced is just an optimization. */ + for(pPg=pCache->pSynced; + pPg && (pPg->nRef || (pPg->flags&PGHDR_NEED_SYNC)); + pPg=pPg->pDirtyPrev + ); + pCache->pSynced = pPg; + if( !pPg ){ + for(pPg=pCache->pDirtyTail; pPg && pPg->nRef; pPg=pPg->pDirtyPrev); + } + if( pPg ){ + int rc; #ifdef SQLITE_LOG_CACHE_SPILL - sqlite3_log(SQLITE_FULL, - "spill page %d making room for %d - cache used: %d/%d", - pPg->pgno, pgno, - sqlite3GlobalConfig.pcache.xPagecount(pCache->pCache), + sqlite3_log(SQLITE_FULL, + "spill page %d making room for %d - cache used: %d/%d", + pPg->pgno, pgno, + sqlite3GlobalConfig.pcache.xPagecount(pCache->pCache), numberOfCachePages(pCache)); #endif - rc = pCache->xStress(pCache->pStress, pPg); - if( rc!=SQLITE_OK && rc!=SQLITE_BUSY ){ - return rc; + pcacheTrace(("%p.SPILL %d\n",pCache,pPg->pgno)); + rc = pCache->xStress(pCache->pStress, pPg); + pcacheDump(pCache); + if( rc!=SQLITE_OK && rc!=SQLITE_BUSY ){ + return rc; + } } } *ppPage = sqlite3GlobalConfig.pcache2.xFetch(pCache->pCache, pgno, 2); - return *ppPage==0 ? SQLITE_NOMEM : SQLITE_OK; + return *ppPage==0 ? SQLITE_NOMEM_BKPT : SQLITE_OK; } /* @@ -40798,6 +43600,7 @@ SQLITE_PRIVATE PgHdr *sqlite3PcacheFetchFinish( } pCache->nRefSum++; pPgHdr->nRef++; + assert( sqlite3PcachePageSanity(pPgHdr) ); return pPgHdr; } @@ -40811,8 +43614,11 @@ SQLITE_PRIVATE void SQLITE_NOINLINE sqlite3PcacheRelease(PgHdr *p){ if( (--p->nRef)==0 ){ if( p->flags&PGHDR_CLEAN ){ pcacheUnpin(p); - }else if( p->pDirtyPrev!=0 ){ - /* Move the page to the head of the dirty list. */ + }else if( p->pDirtyPrev!=0 ){ /*OPTIMIZATION-IF-FALSE*/ + /* Move the page to the head of the dirty list. If p->pDirtyPrev==0, + ** then page p is already at the head of the dirty list and the + ** following call would be a no-op. Hence the OPTIMIZATION-IF-FALSE + ** tag above. */ pcacheManageDirtyList(p, PCACHE_DIRTYLIST_FRONT); } } @@ -40823,6 +43629,7 @@ SQLITE_PRIVATE void SQLITE_NOINLINE sqlite3PcacheRelease(PgHdr *p){ */ SQLITE_PRIVATE void sqlite3PcacheRef(PgHdr *p){ assert(p->nRef>0); + assert( sqlite3PcachePageSanity(p) ); p->nRef++; p->pCache->nRefSum++; } @@ -40834,6 +43641,7 @@ SQLITE_PRIVATE void sqlite3PcacheRef(PgHdr *p){ */ SQLITE_PRIVATE void sqlite3PcacheDrop(PgHdr *p){ assert( p->nRef==1 ); + assert( sqlite3PcachePageSanity(p) ); if( p->flags&PGHDR_DIRTY ){ pcacheManageDirtyList(p, PCACHE_DIRTYLIST_REMOVE); } @@ -40847,13 +43655,16 @@ SQLITE_PRIVATE void sqlite3PcacheDrop(PgHdr *p){ */ SQLITE_PRIVATE void sqlite3PcacheMakeDirty(PgHdr *p){ assert( p->nRef>0 ); - if( p->flags & (PGHDR_CLEAN|PGHDR_DONT_WRITE) ){ + assert( sqlite3PcachePageSanity(p) ); + if( p->flags & (PGHDR_CLEAN|PGHDR_DONT_WRITE) ){ /*OPTIMIZATION-IF-FALSE*/ p->flags &= ~PGHDR_DONT_WRITE; if( p->flags & PGHDR_CLEAN ){ p->flags ^= (PGHDR_DIRTY|PGHDR_CLEAN); + pcacheTrace(("%p.DIRTY %d\n",p->pCache,p->pgno)); assert( (p->flags & (PGHDR_DIRTY|PGHDR_CLEAN))==PGHDR_DIRTY ); pcacheManageDirtyList(p, PCACHE_DIRTYLIST_ADD); } + assert( sqlite3PcachePageSanity(p) ); } } @@ -40862,11 +43673,14 @@ SQLITE_PRIVATE void sqlite3PcacheMakeDirty(PgHdr *p){ ** make it so. */ SQLITE_PRIVATE void sqlite3PcacheMakeClean(PgHdr *p){ - if( (p->flags & PGHDR_DIRTY) ){ + assert( sqlite3PcachePageSanity(p) ); + if( ALWAYS((p->flags & PGHDR_DIRTY)!=0) ){ assert( (p->flags & PGHDR_CLEAN)==0 ); pcacheManageDirtyList(p, PCACHE_DIRTYLIST_REMOVE); p->flags &= ~(PGHDR_DIRTY|PGHDR_NEED_SYNC|PGHDR_WRITEABLE); p->flags |= PGHDR_CLEAN; + pcacheTrace(("%p.CLEAN %d\n",p->pCache,p->pgno)); + assert( sqlite3PcachePageSanity(p) ); if( p->nRef==0 ){ pcacheUnpin(p); } @@ -40878,11 +43692,24 @@ SQLITE_PRIVATE void sqlite3PcacheMakeClean(PgHdr *p){ */ SQLITE_PRIVATE void sqlite3PcacheCleanAll(PCache *pCache){ PgHdr *p; + pcacheTrace(("%p.CLEAN-ALL\n",pCache)); while( (p = pCache->pDirty)!=0 ){ sqlite3PcacheMakeClean(p); } } +/* +** Clear the PGHDR_NEED_SYNC and PGHDR_WRITEABLE flag from all dirty pages. +*/ +SQLITE_PRIVATE void sqlite3PcacheClearWritable(PCache *pCache){ + PgHdr *p; + pcacheTrace(("%p.CLEAR-WRITEABLE\n",pCache)); + for(p=pCache->pDirty; p; p=p->pDirtyNext){ + p->flags &= ~(PGHDR_NEED_SYNC|PGHDR_WRITEABLE); + } + pCache->pSynced = pCache->pDirtyTail; +} + /* ** Clear the PGHDR_NEED_SYNC flag from all dirty pages. */ @@ -40901,6 +43728,8 @@ SQLITE_PRIVATE void sqlite3PcacheMove(PgHdr *p, Pgno newPgno){ PCache *pCache = p->pCache; assert( p->nRef>0 ); assert( newPgno>0 ); + assert( sqlite3PcachePageSanity(p) ); + pcacheTrace(("%p.MOVE %d -> %d\n",pCache,p->pgno,newPgno)); sqlite3GlobalConfig.pcache2.xRekey(pCache->pCache, p->pPage, p->pgno,newPgno); p->pgno = newPgno; if( (p->flags&PGHDR_DIRTY) && (p->flags&PGHDR_NEED_SYNC) ){ @@ -40921,6 +43750,7 @@ SQLITE_PRIVATE void sqlite3PcacheTruncate(PCache *pCache, Pgno pgno){ if( pCache->pCache ){ PgHdr *p; PgHdr *pNext; + pcacheTrace(("%p.TRUNCATE %d\n",pCache,pgno)); for(p=pCache->pDirty; p; p=pNext){ pNext = p->pDirtyNext; /* This routine never gets call with a positive pgno except right @@ -40928,7 +43758,7 @@ SQLITE_PRIVATE void sqlite3PcacheTruncate(PCache *pCache, Pgno pgno){ ** it must be that pgno==0. */ assert( p->pgno>0 ); - if( ALWAYS(p->pgno>pgno) ){ + if( p->pgno>pgno ){ assert( p->flags&PGHDR_DIRTY ); sqlite3PcacheMakeClean(p); } @@ -40951,6 +43781,7 @@ SQLITE_PRIVATE void sqlite3PcacheTruncate(PCache *pCache, Pgno pgno){ */ SQLITE_PRIVATE void sqlite3PcacheClose(PCache *pCache){ assert( pCache->pCache!=0 ); + pcacheTrace(("%p.CLOSE\n",pCache)); sqlite3GlobalConfig.pcache2.xDestroy(pCache->pCache); } @@ -41086,6 +43917,25 @@ SQLITE_PRIVATE void sqlite3PcacheSetCachesize(PCache *pCache, int mxPage){ numberOfCachePages(pCache)); } +/* +** Set the suggested cache-spill value. Make no changes if if the +** argument is zero. Return the effective cache-spill size, which will +** be the larger of the szSpill and szCache. +*/ +SQLITE_PRIVATE int sqlite3PcacheSetSpillsize(PCache *p, int mxPage){ + int res; + assert( p->pCache!=0 ); + if( mxPage ){ + if( mxPage<0 ){ + mxPage = (int)((-1024*(i64)mxPage)/(p->szPage+p->szExtra)); + } + p->szSpill = mxPage; + } + res = numberOfCachePages(p); + if( resszSpill ) res = p->szSpill; + return res; +} + /* ** Free up as much memory as possible from the page cache. */ @@ -41100,6 +43950,17 @@ SQLITE_PRIVATE void sqlite3PcacheShrink(PCache *pCache){ */ SQLITE_PRIVATE int sqlite3HeaderSizePcache(void){ return ROUND8(sizeof(PgHdr)); } +/* +** Return the number of dirty pages currently in the cache, as a percentage +** of the configured cache size. +*/ +SQLITE_PRIVATE int sqlite3PCachePercentDirty(PCache *pCache){ + PgHdr *pDirty; + int nDirty = 0; + int nCache = numberOfCachePages(pCache); + for(pDirty=pCache->pDirty; pDirty; pDirty=pDirty->pDirtyNext) nDirty++; + return nCache ? (int)(((i64)nDirty * 100) / nCache) : 0; +} #if defined(SQLITE_CHECK_PAGES) || defined(SQLITE_DEBUG) /* @@ -41184,7 +44045,7 @@ SQLITE_PRIVATE void sqlite3PcacheIterateDirty(PCache *pCache, void (*xIter)(PgHd ** that is allocated when the page cache is created. The size of the local ** bulk allocation can be adjusted using ** -** sqlite3_config(SQLITE_CONFIG_PAGECACHE, 0, 0, N). +** sqlite3_config(SQLITE_CONFIG_PAGECACHE, (void*)0, 0, N). ** ** If N is positive, then N pages worth of memory are allocated using a single ** sqlite3Malloc() call and that memory is used for the first N pages allocated. @@ -41439,7 +44300,7 @@ static void *pcache1Alloc(int nByte){ pcache1.nFreeSlot--; pcache1.bUnderPressure = pcache1.nFreeSlot=0 ); - sqlite3StatusSet(SQLITE_STATUS_PAGECACHE_SIZE, nByte); + sqlite3StatusHighwater(SQLITE_STATUS_PAGECACHE_SIZE, nByte); sqlite3StatusUp(SQLITE_STATUS_PAGECACHE_USED, 1); } sqlite3_mutex_leave(pcache1.mutex); @@ -41453,7 +44314,7 @@ static void *pcache1Alloc(int nByte){ if( p ){ int sz = sqlite3MallocSize(p); sqlite3_mutex_enter(pcache1.mutex); - sqlite3StatusSet(SQLITE_STATUS_PAGECACHE_SIZE, nByte); + sqlite3StatusHighwater(SQLITE_STATUS_PAGECACHE_SIZE, nByte); sqlite3StatusUp(SQLITE_STATUS_PAGECACHE_OVERFLOW, sz); sqlite3_mutex_leave(pcache1.mutex); } @@ -41467,9 +44328,8 @@ static void *pcache1Alloc(int nByte){ ** Free an allocated buffer obtained from pcache1Alloc(). */ static void pcache1Free(void *p){ - int nFreed = 0; if( p==0 ) return; - if( p>=pcache1.pStart && pnFresh==0 ){ + if( p->nFresh==0 ){ /*OPTIMIZATION-IF-FALSE*/ + /* We could allocate a fresh RowSetEntry each time one is needed, but it + ** is more efficient to pull a preallocated entry from the pool */ struct RowSetChunk *pNew; - pNew = sqlite3DbMallocRaw(p->db, sizeof(*pNew)); + pNew = sqlite3DbMallocRawNN(p->db, sizeof(*pNew)); if( pNew==0 ){ return 0; } @@ -42570,7 +45436,9 @@ SQLITE_PRIVATE void sqlite3RowSetInsert(RowSet *p, i64 rowid){ pEntry->pRight = 0; pLast = p->pLast; if( pLast ){ - if( (p->rsFlags & ROWSET_SORTED)!=0 && rowid<=pLast->v ){ + if( rowid<=pLast->v ){ /*OPTIMIZATION-IF-FALSE*/ + /* Avoid unnecessary sorts by preserving the ROWSET_SORTED flags + ** where possible */ p->rsFlags &= ~ROWSET_SORTED; } pLast->pRight = pEntry; @@ -42692,23 +45560,29 @@ static struct RowSetEntry *rowSetNDeepTree( ){ struct RowSetEntry *p; /* Root of the new tree */ struct RowSetEntry *pLeft; /* Left subtree */ - if( *ppList==0 ){ - return 0; + if( *ppList==0 ){ /*OPTIMIZATION-IF-TRUE*/ + /* Prevent unnecessary deep recursion when we run out of entries */ + return 0; } - if( iDepth==1 ){ + if( iDepth>1 ){ /*OPTIMIZATION-IF-TRUE*/ + /* This branch causes a *balanced* tree to be generated. A valid tree + ** is still generated without this branch, but the tree is wildly + ** unbalanced and inefficient. */ + pLeft = rowSetNDeepTree(ppList, iDepth-1); + p = *ppList; + if( p==0 ){ /*OPTIMIZATION-IF-FALSE*/ + /* It is safe to always return here, but the resulting tree + ** would be unbalanced */ + return pLeft; + } + p->pLeft = pLeft; + *ppList = p->pRight; + p->pRight = rowSetNDeepTree(ppList, iDepth-1); + }else{ p = *ppList; *ppList = p->pRight; p->pLeft = p->pRight = 0; - return p; } - pLeft = rowSetNDeepTree(ppList, iDepth-1); - p = *ppList; - if( p==0 ){ - return pLeft; - } - p->pLeft = pLeft; - *ppList = p->pRight; - p->pRight = rowSetNDeepTree(ppList, iDepth-1); return p; } @@ -42735,59 +45609,37 @@ static struct RowSetEntry *rowSetListToTree(struct RowSetEntry *pList){ return p; } -/* -** Take all the entries on p->pEntry and on the trees in p->pForest and -** sort them all together into one big ordered list on p->pEntry. -** -** This routine should only be called once in the life of a RowSet. -*/ -static void rowSetToList(RowSet *p){ - - /* This routine is called only once */ - assert( p!=0 && (p->rsFlags & ROWSET_NEXT)==0 ); - - if( (p->rsFlags & ROWSET_SORTED)==0 ){ - p->pEntry = rowSetEntrySort(p->pEntry); - } - - /* While this module could theoretically support it, sqlite3RowSetNext() - ** is never called after sqlite3RowSetText() for the same RowSet. So - ** there is never a forest to deal with. Should this change, simply - ** remove the assert() and the #if 0. */ - assert( p->pForest==0 ); -#if 0 - while( p->pForest ){ - struct RowSetEntry *pTree = p->pForest->pLeft; - if( pTree ){ - struct RowSetEntry *pHead, *pTail; - rowSetTreeToList(pTree, &pHead, &pTail); - p->pEntry = rowSetEntryMerge(p->pEntry, pHead); - } - p->pForest = p->pForest->pRight; - } -#endif - p->rsFlags |= ROWSET_NEXT; /* Verify this routine is never called again */ -} - /* ** Extract the smallest element from the RowSet. ** Write the element into *pRowid. Return 1 on success. Return ** 0 if the RowSet is already empty. ** ** After this routine has been called, the sqlite3RowSetInsert() -** routine may not be called again. +** routine may not be called again. +** +** This routine may not be called after sqlite3RowSetTest() has +** been used. Older versions of RowSet allowed that, but as the +** capability was not used by the code generator, it was removed +** for code economy. */ SQLITE_PRIVATE int sqlite3RowSetNext(RowSet *p, i64 *pRowid){ assert( p!=0 ); + assert( p->pForest==0 ); /* Cannot be used with sqlite3RowSetText() */ /* Merge the forest into a single sorted list on first call */ - if( (p->rsFlags & ROWSET_NEXT)==0 ) rowSetToList(p); + if( (p->rsFlags & ROWSET_NEXT)==0 ){ /*OPTIMIZATION-IF-FALSE*/ + if( (p->rsFlags & ROWSET_SORTED)==0 ){ /*OPTIMIZATION-IF-FALSE*/ + p->pEntry = rowSetEntrySort(p->pEntry); + } + p->rsFlags |= ROWSET_SORTED|ROWSET_NEXT; + } /* Return the next entry on the list */ if( p->pEntry ){ *pRowid = p->pEntry->v; p->pEntry = p->pEntry->pRight; - if( p->pEntry==0 ){ + if( p->pEntry==0 ){ /*OPTIMIZATION-IF-TRUE*/ + /* Free memory immediately, rather than waiting on sqlite3_finalize() */ sqlite3RowSetClear(p); } return 1; @@ -42810,13 +45662,15 @@ SQLITE_PRIVATE int sqlite3RowSetTest(RowSet *pRowSet, int iBatch, sqlite3_int64 /* This routine is never called after sqlite3RowSetNext() */ assert( pRowSet!=0 && (pRowSet->rsFlags & ROWSET_NEXT)==0 ); - /* Sort entries into the forest on the first test of a new batch + /* Sort entries into the forest on the first test of a new batch. + ** To save unnecessary work, only do this when the batch number changes. */ - if( iBatch!=pRowSet->iBatch ){ + if( iBatch!=pRowSet->iBatch ){ /*OPTIMIZATION-IF-FALSE*/ p = pRowSet->pEntry; if( p ){ struct RowSetEntry **ppPrevTree = &pRowSet->pForest; - if( (pRowSet->rsFlags & ROWSET_SORTED)==0 ){ + if( (pRowSet->rsFlags & ROWSET_SORTED)==0 ){ /*OPTIMIZATION-IF-FALSE*/ + /* Only sort the current set of entiries if they need it */ p = rowSetEntrySort(p); } for(pTree = pRowSet->pForest; pTree; pTree=pTree->pRight){ @@ -42936,6 +45790,7 @@ SQLITE_PRIVATE int sqlite3RowSetTest(RowSet *pRowSet, int iBatch, sqlite3_int64 # define sqlite3WalHeapMemory(z) 0 # define sqlite3WalFramesize(z) 0 # define sqlite3WalFindFrame(x,y,z) 0 +# define sqlite3WalFile(x) 0 #else #define WAL_SAVEPOINT_NDATA 4 @@ -43018,6 +45873,11 @@ SQLITE_PRIVATE int sqlite3WalExclusiveMode(Wal *pWal, int op); */ SQLITE_PRIVATE int sqlite3WalHeapMemory(Wal *pWal); +#ifdef SQLITE_ENABLE_SNAPSHOT +SQLITE_PRIVATE int sqlite3WalSnapshotGet(Wal *pWal, sqlite3_snapshot **ppSnapshot); +SQLITE_PRIVATE void sqlite3WalSnapshotOpen(Wal *pWal, sqlite3_snapshot *pSnapshot); +#endif + #ifdef SQLITE_ENABLE_ZIPVFS /* If the WAL file is not empty, return the number of bytes of content ** stored in each frame (i.e. the db page-size when the WAL was created). @@ -43025,6 +45885,9 @@ SQLITE_PRIVATE int sqlite3WalHeapMemory(Wal *pWal); SQLITE_PRIVATE int sqlite3WalFramesize(Wal *pWal); #endif +/* Return the sqlite3_file object for the WAL file */ +SQLITE_PRIVATE sqlite3_file *sqlite3WalFile(Wal *pWal); + #endif /* ifndef SQLITE_OMIT_WAL */ #endif /* _WAL_H_ */ @@ -43437,6 +46300,7 @@ int sqlite3PagerTrace=1; /* True to enable tracing */ */ #define MAX_SECTOR_SIZE 0x10000 + /* ** An instance of the following structure is allocated for each active ** savepoint and statement transaction in the system. All such structures @@ -43632,6 +46496,7 @@ struct Pager { u8 useJournal; /* Use a rollback journal on this file */ u8 noSync; /* Do not sync the journal if true */ u8 fullSync; /* Do extra syncs of the journal for robustness */ + u8 extraSync; /* sync directory after journal delete */ u8 ckptSyncFlags; /* SYNC_NORMAL or SYNC_FULL for checkpoint */ u8 walSyncFlags; /* SYNC_NORMAL or SYNC_FULL for wal writes */ u8 syncFlags; /* SYNC_NORMAL or SYNC_FULL otherwise */ @@ -43879,6 +46744,7 @@ static int assert_pager_state(Pager *p){ ** state. */ if( MEMDB ){ + assert( !isOpen(p->fd) ); assert( p->noSync ); assert( p->journalMode==PAGER_JOURNALMODE_OFF || p->journalMode==PAGER_JOURNALMODE_MEMORY @@ -43965,7 +46831,7 @@ static int assert_pager_state(Pager *p){ ** back to OPEN state. */ assert( pPager->errCode!=SQLITE_OK ); - assert( sqlite3PcacheRefCount(pPager->pPCache)>0 ); + assert( sqlite3PcacheRefCount(pPager->pPCache)>0 || pPager->tempFile ); break; } @@ -44177,6 +47043,8 @@ static int jrnlBufferSize(Pager *pPager){ return JOURNAL_HDR_SZ(pPager) + JOURNAL_PG_SZ(pPager); } +#else +# define jrnlBufferSize(x) 0 #endif /* @@ -44337,6 +47205,7 @@ static i64 journalHdrOffset(Pager *pPager){ static int zeroJournalHdr(Pager *pPager, int doTruncate){ int rc = SQLITE_OK; /* Return code */ assert( isOpen(pPager->jfd) ); + assert( !sqlite3JournalIsInMemory(pPager->jfd) ); if( pPager->journalOff ){ const i64 iLimit = pPager->journalSizeLimit; /* Local cache of jsl */ @@ -44718,7 +47587,7 @@ static void releaseAllSavepoints(Pager *pPager){ for(ii=0; iinSavepoint; ii++){ sqlite3BitvecDestroy(pPager->aSavepoint[ii].pInSavepoint); } - if( !pPager->exclusiveMode || sqlite3IsMemJournal(pPager->sjfd) ){ + if( !pPager->exclusiveMode || sqlite3JournalIsInMemory(pPager->sjfd) ){ sqlite3OsClose(pPager->sjfd); } sqlite3_free(pPager->aSavepoint); @@ -44824,13 +47693,17 @@ static void pager_unlock(Pager *pPager){ ** it can safely move back to PAGER_OPEN state. This happens in both ** normal and exclusive-locking mode. */ + assert( pPager->errCode==SQLITE_OK || !MEMDB ); if( pPager->errCode ){ - assert( !MEMDB ); - pager_reset(pPager); - pPager->changeCountDone = pPager->tempFile; - pPager->eState = PAGER_OPEN; - pPager->errCode = SQLITE_OK; + if( pPager->tempFile==0 ){ + pager_reset(pPager); + pPager->changeCountDone = 0; + pPager->eState = PAGER_OPEN; + }else{ + pPager->eState = (isOpen(pPager->jfd) ? PAGER_OPEN : PAGER_READER); + } if( USEFETCH(pPager) ) sqlite3OsUnfetch(pPager->fd, 0, 0); + pPager->errCode = SQLITE_OK; } pPager->journalOff = 0; @@ -44874,6 +47747,29 @@ static int pager_error(Pager *pPager, int rc){ static int pager_truncate(Pager *pPager, Pgno nPage); +/* +** The write transaction open on pPager is being committed (bCommit==1) +** or rolled back (bCommit==0). +** +** Return TRUE if and only if all dirty pages should be flushed to disk. +** +** Rules: +** +** * For non-TEMP databases, always sync to disk. This is necessary +** for transactions to be durable. +** +** * Sync TEMP database only on a COMMIT (not a ROLLBACK) when the backing +** file has been created already (via a spill on pagerStress()) and +** when the number of dirty pages in memory exceeds 25% of the total +** cache size. +*/ +static int pagerFlushOnCommit(Pager *pPager, int bCommit){ + if( pPager->tempFile==0 ) return 1; + if( !bCommit ) return 0; + if( !isOpen(pPager->fd) ) return 0; + return (sqlite3PCachePercentDirty(pPager->pPCache)>=25); +} + /* ** This routine ends a transaction. A transaction is usually ended by ** either a COMMIT or a ROLLBACK operation. This routine may be called @@ -44956,8 +47852,8 @@ static int pager_end_transaction(Pager *pPager, int hasMaster, int bCommit){ assert( !pagerUseWal(pPager) ); /* Finalize the journal file. */ - if( sqlite3IsMemJournal(pPager->jfd) ){ - assert( pPager->journalMode==PAGER_JOURNALMODE_MEMORY ); + if( sqlite3JournalIsInMemory(pPager->jfd) ){ + /* assert( pPager->journalMode==PAGER_JOURNALMODE_MEMORY ); */ sqlite3OsClose(pPager->jfd); }else if( pPager->journalMode==PAGER_JOURNALMODE_TRUNCATE ){ if( pPager->journalOff==0 ){ @@ -44977,22 +47873,23 @@ static int pager_end_transaction(Pager *pPager, int hasMaster, int bCommit){ }else if( pPager->journalMode==PAGER_JOURNALMODE_PERSIST || (pPager->exclusiveMode && pPager->journalMode!=PAGER_JOURNALMODE_WAL) ){ - rc = zeroJournalHdr(pPager, hasMaster); + rc = zeroJournalHdr(pPager, hasMaster||pPager->tempFile); pPager->journalOff = 0; }else{ /* This branch may be executed with Pager.journalMode==MEMORY if ** a hot-journal was just rolled back. In this case the journal ** file should be closed and deleted. If this connection writes to - ** the database file, it will do so using an in-memory journal. + ** the database file, it will do so using an in-memory journal. */ - int bDelete = (!pPager->tempFile && sqlite3JournalExists(pPager->jfd)); + int bDelete = !pPager->tempFile; + assert( sqlite3JournalIsInMemory(pPager->jfd)==0 ); assert( pPager->journalMode==PAGER_JOURNALMODE_DELETE || pPager->journalMode==PAGER_JOURNALMODE_MEMORY || pPager->journalMode==PAGER_JOURNALMODE_WAL ); sqlite3OsClose(pPager->jfd); if( bDelete ){ - rc = sqlite3OsDelete(pPager->pVfs, pPager->zJournal, 0); + rc = sqlite3OsDelete(pPager->pVfs, pPager->zJournal, pPager->extraSync); } } } @@ -45011,8 +47908,14 @@ static int pager_end_transaction(Pager *pPager, int hasMaster, int bCommit){ sqlite3BitvecDestroy(pPager->pInJournal); pPager->pInJournal = 0; pPager->nRec = 0; - sqlite3PcacheCleanAll(pPager->pPCache); - sqlite3PcacheTruncate(pPager->pPCache, pPager->dbSize); + if( rc==SQLITE_OK ){ + if( pagerFlushOnCommit(pPager, bCommit) ){ + sqlite3PcacheCleanAll(pPager->pPCache); + }else{ + sqlite3PcacheClearWritable(pPager->pPCache); + } + sqlite3PcacheTruncate(pPager->pPCache, pPager->dbSize); + } if( pagerUseWal(pPager) ){ /* Drop the WAL write-lock, if any. Also, if the connection was in @@ -45296,7 +48199,7 @@ static int pager_playback_one_page( pPg = sqlite3PagerLookup(pPager, pgno); } assert( pPg || !MEMDB ); - assert( pPager->eState!=PAGER_OPEN || pPg==0 ); + assert( pPager->eState!=PAGER_OPEN || pPg==0 || pPager->tempFile ); PAGERTRACE(("PLAYBACK %d page %d hash(%08x) %s\n", PAGERID(pPager), pgno, pager_datahash(pPager->pageSize, (u8*)aData), (isMainJrnl?"main-journal":"sub-journal") @@ -45318,9 +48221,9 @@ static int pager_playback_one_page( pPager->dbFileSize = pgno; } if( pPager->pBackup ){ - CODEC1(pPager, aData, pgno, 3, rc=SQLITE_NOMEM); + CODEC1(pPager, aData, pgno, 3, rc=SQLITE_NOMEM_BKPT); sqlite3BackupUpdate(pPager->pBackup, pgno, (u8*)aData); - CODEC2(pPager, aData, pgno, 7, rc=SQLITE_NOMEM, aData); + CODEC2(pPager, aData, pgno, 7, rc=SQLITE_NOMEM_BKPT, aData); } }else if( !isMainJrnl && pPg==0 ){ /* If this is a rollback of a savepoint and data was not written to @@ -45342,11 +48245,10 @@ static int pager_playback_one_page( assert( isSavepnt ); assert( (pPager->doNotSpill & SPILLFLAG_ROLLBACK)==0 ); pPager->doNotSpill |= SPILLFLAG_ROLLBACK; - rc = sqlite3PagerAcquire(pPager, pgno, &pPg, 1); + rc = sqlite3PagerGet(pPager, pgno, &pPg, 1); assert( (pPager->doNotSpill & SPILLFLAG_ROLLBACK)!=0 ); pPager->doNotSpill &= ~SPILLFLAG_ROLLBACK; if( rc!=SQLITE_OK ) return rc; - pPg->flags &= ~PGHDR_NEED_READ; sqlite3PcacheMakeDirty(pPg); } if( pPg ){ @@ -45360,29 +48262,10 @@ static int pager_playback_one_page( pData = pPg->pData; memcpy(pData, (u8*)aData, pPager->pageSize); pPager->xReiniter(pPg); - if( isMainJrnl && (!isSavepnt || *pOffset<=pPager->journalHdr) ){ - /* If the contents of this page were just restored from the main - ** journal file, then its content must be as they were when the - ** transaction was first opened. In this case we can mark the page - ** as clean, since there will be no need to write it out to the - ** database. - ** - ** There is one exception to this rule. If the page is being rolled - ** back as part of a savepoint (or statement) rollback from an - ** unsynced portion of the main journal file, then it is not safe - ** to mark the page as clean. This is because marking the page as - ** clean will clear the PGHDR_NEED_SYNC flag. Since the page is - ** already in the journal file (recorded in Pager.pInJournal) and - ** the PGHDR_NEED_SYNC flag is cleared, if the page is written to - ** again within this transaction, it will be marked as dirty but - ** the PGHDR_NEED_SYNC flag will not be set. It could then potentially - ** be written out into the database file before its journal file - ** segment is synced. If a crash occurs during or following this, - ** database corruption may ensue. - */ - assert( !pagerUseWal(pPager) ); - sqlite3PcacheMakeClean(pPg); - } + /* It used to be that sqlite3PcacheMakeClean(pPg) was called here. But + ** that call was dangerous and had no detectable benefit since the cache + ** is normally cleaned by sqlite3PcacheCleanAll() after rollback and so + ** has been removed. */ pager_set_pagehash(pPg); /* If this was page 1, then restore the value of Pager.dbFileVers. @@ -45392,7 +48275,7 @@ static int pager_playback_one_page( } /* Decode the page just read from disk */ - CODEC1(pPager, pData, pPg->pgno, 3, rc=SQLITE_NOMEM); + CODEC1(pPager, pData, pPg->pgno, 3, rc=SQLITE_NOMEM_BKPT); sqlite3PcacheRelease(pPg); } return rc; @@ -45458,7 +48341,7 @@ static int pager_delmaster(Pager *pPager, const char *zMaster){ pMaster = (sqlite3_file *)sqlite3MallocZero(pVfs->szOsFile * 2); pJournal = (sqlite3_file *)(((u8 *)pMaster) + pVfs->szOsFile); if( !pMaster ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else{ const int flags = (SQLITE_OPEN_READONLY|SQLITE_OPEN_MASTER_JOURNAL); rc = sqlite3OsOpen(pVfs, zMaster, pMaster, flags, 0); @@ -45475,7 +48358,7 @@ static int pager_delmaster(Pager *pPager, const char *zMaster){ nMasterPtr = pVfs->mxPathname+1; zMasterJournal = sqlite3Malloc(nMasterJournal + nMasterPtr + 1); if( !zMasterJournal ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto delmaster_out; } zMasterPtr = &zMasterJournal[nMasterJournal+1]; @@ -45723,7 +48606,7 @@ static int pager_playback(Pager *pPager, int isHot){ ** TODO: Technically the following is an error because it assumes that ** buffer Pager.pTmpSpace is (mxPathname+1) bytes or larger. i.e. that ** (pPager->pageSize >= pPager->pVfs->mxPathname+1). Using os_unix.c, - ** mxPathname is 512, which is the same as the minimum allowable value + ** mxPathname is 512, which is the same as the minimum allowable value ** for pageSize. */ zMaster = pPager->pTmpSpace; @@ -45945,7 +48828,7 @@ static int readDbPage(PgHdr *pPg, u32 iFrame){ memcpy(&pPager->dbFileVers, dbFileVers, sizeof(pPager->dbFileVers)); } } - CODEC1(pPager, pPg->pData, pgno, 3, rc = SQLITE_NOMEM); + CODEC1(pPager, pPg->pData, pgno, 3, rc = SQLITE_NOMEM_BKPT); PAGER_INCR(sqlite3_pager_readdb_count); PAGER_INCR(pPager->nRead); @@ -46173,6 +49056,8 @@ static int pagerPagecount(Pager *pPager, Pgno *pnPage){ */ assert( pPager->eState==PAGER_OPEN ); assert( pPager->eLock>=SHARED_LOCK ); + assert( isOpen(pPager->fd) ); + assert( pPager->tempFile==0 ); nPage = sqlite3WalDbsize(pPager->pWal); /* If the number of pages in the database is not available from the @@ -46180,14 +49065,11 @@ static int pagerPagecount(Pager *pPager, Pgno *pnPage){ ** the database file. If the size of the database file is not an ** integer multiple of the page-size, round up the result. */ - if( nPage==0 ){ + if( nPage==0 && ALWAYS(isOpen(pPager->fd)) ){ i64 n = 0; /* Size of db file in bytes */ - assert( isOpen(pPager->fd) || pPager->tempFile ); - if( isOpen(pPager->fd) ){ - int rc = sqlite3OsFileSize(pPager->fd, &n); - if( rc!=SQLITE_OK ){ - return rc; - } + int rc = sqlite3OsFileSize(pPager->fd, &n); + if( rc!=SQLITE_OK ){ + return rc; } nPage = (Pgno)((n+pPager->pageSize-1) / pPager->pageSize); } @@ -46305,7 +49187,7 @@ static int pagerPlaybackSavepoint(Pager *pPager, PagerSavepoint *pSavepoint){ if( pSavepoint ){ pDone = sqlite3BitvecCreate(pSavepoint->nOrig); if( !pDone ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } } @@ -46401,12 +49283,21 @@ static int pagerPlaybackSavepoint(Pager *pPager, PagerSavepoint *pSavepoint){ } /* -** Change the maximum number of in-memory pages that are allowed. +** Change the maximum number of in-memory pages that are allowed +** before attempting to recycle clean and unused pages. */ SQLITE_PRIVATE void sqlite3PagerSetCachesize(Pager *pPager, int mxPage){ sqlite3PcacheSetCachesize(pPager->pPCache, mxPage); } +/* +** Change the maximum number of in-memory pages that are allowed +** before attempting to spill pages to journal. +*/ +SQLITE_PRIVATE int sqlite3PagerSetSpillsize(Pager *pPager, int mxPage){ + return sqlite3PcacheSetSpillsize(pPager->pPCache, mxPage); +} + /* ** Invoke SQLITE_FCNTL_MMAP_SIZE based on the current value of szMmap. */ @@ -46443,7 +49334,7 @@ SQLITE_PRIVATE void sqlite3PagerShrink(Pager *pPager){ ** The "level" in pgFlags & PAGER_SYNCHRONOUS_MASK sets the robustness ** of the database to damage due to OS crashes or power failures by ** changing the number of syncs()s when writing the journals. -** There are three levels: +** There are four levels: ** ** OFF sqlite3OsSync() is never called. This is the default ** for temporary and transient files. @@ -46463,6 +49354,10 @@ SQLITE_PRIVATE void sqlite3PagerShrink(Pager *pPager){ ** assurance that the journal will not be corrupted to the ** point of causing damage to the database during rollback. ** +** EXTRA This is like FULL except that is also syncs the directory +** that contains the rollback journal after the rollback +** journal is unlinked. +** ** The above is for a rollback-journal mode. For WAL mode, OFF continues ** to mean that no syncs ever occur. NORMAL means that the WAL is synced ** prior to the start of checkpoint and that the database file is synced @@ -46470,7 +49365,8 @@ SQLITE_PRIVATE void sqlite3PagerShrink(Pager *pPager){ ** was written back into the database. But no sync operations occur for ** an ordinary commit in NORMAL mode with WAL. FULL means that the WAL ** file is synced following each commit operation, in addition to the -** syncs associated with NORMAL. +** syncs associated with NORMAL. There is no difference between FULL +** and EXTRA for WAL mode. ** ** Do not confuse synchronous=FULL with SQLITE_SYNC_FULL. The ** SQLITE_SYNC_FULL macro means to use the MacOSX-style full-fsync @@ -46489,9 +49385,15 @@ SQLITE_PRIVATE void sqlite3PagerSetFlags( unsigned pgFlags /* Various flags */ ){ unsigned level = pgFlags & PAGER_SYNCHRONOUS_MASK; - assert( level>=1 && level<=3 ); - pPager->noSync = (level==1 || pPager->tempFile) ?1:0; - pPager->fullSync = (level==3 && !pPager->tempFile) ?1:0; + if( pPager->tempFile ){ + pPager->noSync = 1; + pPager->fullSync = 0; + pPager->extraSync = 0; + }else{ + pPager->noSync = level==PAGER_SYNCHRONOUS_OFF ?1:0; + pPager->fullSync = level>=PAGER_SYNCHRONOUS_FULL ?1:0; + pPager->extraSync = level==PAGER_SYNCHRONOUS_EXTRA ?1:0; + } if( pPager->noSync ){ pPager->syncFlags = 0; pPager->ckptSyncFlags = 0; @@ -46653,7 +49555,7 @@ SQLITE_PRIVATE int sqlite3PagerSetPagesize(Pager *pPager, u32 *pPageSize, int nR } if( rc==SQLITE_OK ){ pNew = (char *)sqlite3PageMalloc(pageSize); - if( !pNew ) rc = SQLITE_NOMEM; + if( !pNew ) rc = SQLITE_NOMEM_BKPT; } if( rc==SQLITE_OK ){ @@ -46929,7 +49831,7 @@ static int pagerAcquireMapPage( *ppPage = p = (PgHdr *)sqlite3MallocZero(sizeof(PgHdr) + pPager->nExtra); if( p==0 ){ sqlite3OsUnfetch(pPager->fd, (i64)(pgno-1) * pPager->pageSize, pData); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } p->pExtra = (void *)&p[1]; p->flags = PGHDR_MMAP; @@ -47243,8 +50145,9 @@ static int pager_write_pagelist(Pager *pPager, PgHdr *pList){ /* This function is only called for rollback pagers in WRITER_DBMOD state. */ assert( !pagerUseWal(pPager) ); - assert( pPager->eState==PAGER_WRITER_DBMOD ); + assert( pPager->tempFile || pPager->eState==PAGER_WRITER_DBMOD ); assert( pPager->eLock==EXCLUSIVE_LOCK ); + assert( isOpen(pPager->fd) || pList->pDirty==0 ); /* If the file is a temp-file has not yet been opened, open it now. It ** is not possible for rc to be other than SQLITE_OK if this branch @@ -47287,7 +50190,7 @@ static int pager_write_pagelist(Pager *pPager, PgHdr *pList){ if( pList->pgno==1 ) pager_write_changecounter(pList); /* Encode the database */ - CODEC2(pPager, pList->pData, pgno, 6, return SQLITE_NOMEM, pData); + CODEC2(pPager, pList->pData, pgno, 6, return SQLITE_NOMEM_BKPT, pData); /* Write out the page data. */ rc = sqlite3OsWrite(pPager->fd, pData, pPager->pageSize, offset); @@ -47332,11 +50235,14 @@ static int pager_write_pagelist(Pager *pPager, PgHdr *pList){ static int openSubJournal(Pager *pPager){ int rc = SQLITE_OK; if( !isOpen(pPager->sjfd) ){ + const int flags = SQLITE_OPEN_SUBJOURNAL | SQLITE_OPEN_READWRITE + | SQLITE_OPEN_CREATE | SQLITE_OPEN_EXCLUSIVE + | SQLITE_OPEN_DELETEONCLOSE; + int nStmtSpill = sqlite3Config.nStmtSpill; if( pPager->journalMode==PAGER_JOURNALMODE_MEMORY || pPager->subjInMemory ){ - sqlite3MemJournalOpen(pPager->sjfd); - }else{ - rc = pagerOpentemp(pPager, pPager->sjfd, SQLITE_OPEN_SUBJOURNAL); + nStmtSpill = -1; } + rc = sqlite3JournalOpen(pPager->pVfs, 0, pPager->sjfd, flags, nStmtSpill); } return rc; } @@ -47374,7 +50280,7 @@ static int subjournalPage(PgHdr *pPg){ i64 offset = (i64)pPager->nSubRec*(4+pPager->pageSize); char *pData2; - CODEC2(pPager, pData, pPg->pgno, 7, return SQLITE_NOMEM, pData2); + CODEC2(pPager, pData, pPg->pgno, 7, return SQLITE_NOMEM_BKPT, pData2); PAGERTRACE(("STMT-JOURNAL %d page %d\n", PAGERID(pPager), pPg->pgno)); rc = write32bits(pPager->sjfd, offset, pPg->pgno); if( rc==SQLITE_OK ){ @@ -47482,6 +50388,25 @@ static int pagerStress(void *p, PgHdr *pPg){ return pager_error(pPager, rc); } +/* +** Flush all unreferenced dirty pages to disk. +*/ +SQLITE_PRIVATE int sqlite3PagerFlush(Pager *pPager){ + int rc = pPager->errCode; + if( !MEMDB ){ + PgHdr *pList = sqlite3PcacheDirtyList(pPager->pPCache); + assert( assert_pager_state(pPager) ); + while( rc==SQLITE_OK && pList ){ + PgHdr *pNext = pList->pDirty; + if( pList->nRef==0 ){ + rc = pagerStress((void*)pPager, pList); + } + pList = pNext; + } + } + + return rc; +} /* ** Allocate and initialize a new Pager object and put a pointer to it @@ -47538,18 +50463,8 @@ SQLITE_PRIVATE int sqlite3PagerOpen( int nUri = 0; /* Number of bytes of URI args at *zUri */ /* Figure out how much space is required for each journal file-handle - ** (there are two of them, the main journal and the sub-journal). This - ** is the maximum space required for an in-memory journal file handle - ** and a regular journal file-handle. Note that a "regular journal-handle" - ** may be a wrapper capable of caching the first portion of the journal - ** file in memory to implement the atomic-write optimization (see - ** source file journal.c). - */ - if( sqlite3JournalSize(pVfs)>sqlite3MemJournalSize() ){ - journalFileSize = ROUND8(sqlite3JournalSize(pVfs)); - }else{ - journalFileSize = ROUND8(sqlite3MemJournalSize()); - } + ** (there are two of them, the main journal and the sub-journal). */ + journalFileSize = ROUND8(sqlite3JournalSize(pVfs)); /* Set the output variable to NULL in case an error occurs. */ *ppPager = 0; @@ -47559,7 +50474,7 @@ SQLITE_PRIVATE int sqlite3PagerOpen( memDb = 1; if( zFilename && zFilename[0] ){ zPathname = sqlite3DbStrDup(0, zFilename); - if( zPathname==0 ) return SQLITE_NOMEM; + if( zPathname==0 ) return SQLITE_NOMEM_BKPT; nPathname = sqlite3Strlen30(zPathname); zFilename = 0; } @@ -47575,7 +50490,7 @@ SQLITE_PRIVATE int sqlite3PagerOpen( nPathname = pVfs->mxPathname+1; zPathname = sqlite3DbMallocRaw(0, nPathname*2); if( zPathname==0 ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } zPathname[0] = 0; /* Make sure initialized even if FullPathname() fails */ rc = sqlite3OsFullPathname(pVfs, zFilename, nPathname, zPathname); @@ -47628,7 +50543,7 @@ SQLITE_PRIVATE int sqlite3PagerOpen( assert( EIGHT_BYTE_ALIGNMENT(SQLITE_INT_TO_PTR(journalFileSize)) ); if( !pPtr ){ sqlite3DbFree(0, zPathname); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pPager = (Pager*)(pPtr); pPager->pPCache = (PCache*)(pPtr += ROUND8(sizeof(*pPager))); @@ -47777,11 +50692,13 @@ act_like_temp_file: pPager->noSync = pPager->tempFile; if( pPager->noSync ){ assert( pPager->fullSync==0 ); + assert( pPager->extraSync==0 ); assert( pPager->syncFlags==0 ); assert( pPager->walSyncFlags==0 ); assert( pPager->ckptSyncFlags==0 ); }else{ pPager->fullSync = 1; + pPager->extraSync = 0; pPager->syncFlags = SQLITE_SYNC_NORMAL; pPager->walSyncFlags = SQLITE_SYNC_NORMAL | WAL_SYNC_TRANSACTIONS; pPager->ckptSyncFlags = SQLITE_SYNC_NORMAL; @@ -47898,6 +50815,7 @@ static int hasHotJournal(Pager *pPager, int *pExists){ if( rc==SQLITE_OK && !locked ){ Pgno nPage; /* Number of pages in database file */ + assert( pPager->tempFile==0 ); rc = pagerPagecount(pPager, &nPage); if( rc==SQLITE_OK ){ /* If the database is zero pages in size, that means that either (1) the @@ -47959,7 +50877,7 @@ static int hasHotJournal(Pager *pPager, int *pExists){ /* ** This function is called to obtain a shared lock on the database file. -** It is illegal to call sqlite3PagerAcquire() until after this function +** It is illegal to call sqlite3PagerGet() until after this function ** has been successfully called. If a shared-lock is already held when ** this function is called, it is a no-op. ** @@ -47990,17 +50908,17 @@ SQLITE_PRIVATE int sqlite3PagerSharedLock(Pager *pPager){ /* This routine is only called from b-tree and only when there are no ** outstanding pages. This implies that the pager state should either ** be OPEN or READER. READER is only possible if the pager is or was in - ** exclusive access mode. - */ + ** exclusive access mode. */ assert( sqlite3PcacheRefCount(pPager->pPCache)==0 ); assert( assert_pager_state(pPager) ); assert( pPager->eState==PAGER_OPEN || pPager->eState==PAGER_READER ); - if( NEVER(MEMDB && pPager->errCode) ){ return pPager->errCode; } + assert( pPager->errCode==SQLITE_OK ); if( !pagerUseWal(pPager) && pPager->eState==PAGER_OPEN ){ int bHotJournal = 1; /* True if there exists a hot journal-file */ assert( !MEMDB ); + assert( pPager->tempFile==0 || pPager->eLock==EXCLUSIVE_LOCK ); rc = pager_wait_on_lock(pPager, SHARED_LOCK); if( rc!=SQLITE_OK ){ @@ -48086,7 +51004,7 @@ SQLITE_PRIVATE int sqlite3PagerSharedLock(Pager *pPager){ assert( rc==SQLITE_OK ); rc = pagerSyncHotJournal(pPager); if( rc==SQLITE_OK ){ - rc = pager_playback(pPager, 1); + rc = pager_playback(pPager, !pPager->tempFile); pPager->eState = PAGER_OPEN; } }else if( !pPager->exclusiveMode ){ @@ -48182,7 +51100,7 @@ SQLITE_PRIVATE int sqlite3PagerSharedLock(Pager *pPager){ rc = pagerBeginReadTransaction(pPager); } - if( pPager->eState==PAGER_OPEN && rc==SQLITE_OK ){ + if( pPager->tempFile==0 && pPager->eState==PAGER_OPEN && rc==SQLITE_OK ){ rc = pagerPagecount(pPager, &pPager->dbSize); } @@ -48262,7 +51180,7 @@ static void pagerUnlockIfUnused(Pager *pPager){ ** Since Lookup() never goes to disk, it never has to deal with locks ** or journal files. */ -SQLITE_PRIVATE int sqlite3PagerAcquire( +SQLITE_PRIVATE int sqlite3PagerGet( Pager *pPager, /* The pager open on the database file */ Pgno pgno, /* Page number to fetch */ DbPage **ppPage, /* Write a pointer to the page here */ @@ -48315,7 +51233,7 @@ SQLITE_PRIVATE int sqlite3PagerAcquire( ); if( rc==SQLITE_OK && pData ){ - if( pPager->eState>PAGER_READER ){ + if( pPager->eState>PAGER_READER || pPager->tempFile ){ pPg = sqlite3PagerLookup(pPager, pgno); } if( pPg==0 ){ @@ -48342,7 +51260,7 @@ SQLITE_PRIVATE int sqlite3PagerAcquire( if( rc!=SQLITE_OK ) goto pager_acquire_err; if( pBase==0 ){ pPg = *ppPage = 0; - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto pager_acquire_err; } } @@ -48382,7 +51300,8 @@ SQLITE_PRIVATE int sqlite3PagerAcquire( goto pager_acquire_err; } - if( MEMDB || pPager->dbSizefd) ){ + assert( !isOpen(pPager->fd) || !MEMDB ); + if( !isOpen(pPager->fd) || pPager->dbSizepPager->mxPgno ){ rc = SQLITE_FULL; goto pager_acquire_err; @@ -48516,7 +51435,7 @@ static int pager_open_journal(Pager *pPager){ if( !pagerUseWal(pPager) && pPager->journalMode!=PAGER_JOURNALMODE_OFF ){ pPager->pInJournal = sqlite3BitvecCreate(pPager->dbSize); if( pPager->pInJournal==0 ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } /* Open the journal file if it is not already open. */ @@ -48524,24 +51443,24 @@ static int pager_open_journal(Pager *pPager){ if( pPager->journalMode==PAGER_JOURNALMODE_MEMORY ){ sqlite3MemJournalOpen(pPager->jfd); }else{ - const int flags = /* VFS flags to open journal file */ - SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE| - (pPager->tempFile ? - (SQLITE_OPEN_DELETEONCLOSE|SQLITE_OPEN_TEMP_JOURNAL): - (SQLITE_OPEN_MAIN_JOURNAL) - ); + int flags = SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE; + int nSpill; + if( pPager->tempFile ){ + flags |= (SQLITE_OPEN_DELETEONCLOSE|SQLITE_OPEN_TEMP_JOURNAL); + nSpill = sqlite3Config.nStmtSpill; + }else{ + flags |= SQLITE_OPEN_MAIN_JOURNAL; + nSpill = jrnlBufferSize(pPager); + } + /* Verify that the database still has the same name as it did when ** it was originally opened. */ rc = databaseIsUnmoved(pPager); if( rc==SQLITE_OK ){ -#ifdef SQLITE_ENABLE_ATOMIC_WRITE - rc = sqlite3JournalOpen( - pVfs, pPager->zJournal, pPager->jfd, flags, jrnlBufferSize(pPager) + rc = sqlite3JournalOpen ( + pVfs, pPager->zJournal, pPager->jfd, flags, nSpill ); -#else - rc = sqlite3OsOpen(pVfs, pPager->zJournal, pPager->jfd, flags, 0); -#endif } } assert( rc!=SQLITE_OK || isOpen(pPager->jfd) ); @@ -48608,7 +51527,7 @@ SQLITE_PRIVATE int sqlite3PagerBegin(Pager *pPager, int exFlag, int subjInMemory if( rc!=SQLITE_OK ){ return rc; } - sqlite3WalExclusiveMode(pPager->pWal, 1); + (void)sqlite3WalExclusiveMode(pPager->pWal, 1); } /* Grab the write lock on the log file. If successful, upgrade to @@ -48671,7 +51590,7 @@ static SQLITE_NOINLINE int pagerAddPageToRollbackJournal(PgHdr *pPg){ assert( pPg->pgno!=PAGER_MJ_PGNO(pPager) ); assert( pPager->journalHdr<=pPager->journalOff ); - CODEC2(pPager, pPg->pData, pPg->pgno, 7, return SQLITE_NOMEM, pData2); + CODEC2(pPager, pPg->pData, pPg->pgno, 7, return SQLITE_NOMEM_BKPT, pData2); cksum = pager_cksum(pPager, (u8*)pData2); /* Even if an IO or diskfull error occurs while journalling the @@ -48848,7 +51767,7 @@ static SQLITE_NOINLINE int pagerWriteLargeSector(PgHdr *pPg){ PgHdr *pPage; if( pg==pPg->pgno || !sqlite3BitvecTest(pPager->pInJournal, pg) ){ if( pg!=PAGER_MJ_PGNO(pPager) ){ - rc = sqlite3PagerGet(pPager, pg, &pPage); + rc = sqlite3PagerGet(pPager, pg, &pPage, 0); if( rc==SQLITE_OK ){ rc = pager_write(pPage); if( pPage->flags&PGHDR_NEED_SYNC ){ @@ -48905,12 +51824,14 @@ SQLITE_PRIVATE int sqlite3PagerWrite(PgHdr *pPg){ Pager *pPager = pPg->pPager; assert( (pPg->flags & PGHDR_MMAP)==0 ); assert( pPager->eState>=PAGER_WRITER_LOCKED ); - assert( pPager->eState!=PAGER_ERROR ); assert( assert_pager_state(pPager) ); - if( (pPg->flags & PGHDR_WRITEABLE)!=0 && pPager->dbSize>=pPg->pgno ){ + if( pPager->errCode ){ + return pPager->errCode; + }else if( (pPg->flags & PGHDR_WRITEABLE)!=0 && pPager->dbSize>=pPg->pgno ){ if( pPager->nSavepoint ) return subjournalPageIfRequired(pPg); return SQLITE_OK; }else if( pPager->sectorSize > (u32)pPager->pageSize ){ + assert( pPager->tempFile==0 ); return pagerWriteLargeSector(pPg); }else{ return pager_write(pPg); @@ -48941,14 +51862,21 @@ SQLITE_PRIVATE int sqlite3PagerIswriteable(DbPage *pPg){ ** ** Tests show that this optimization can quadruple the speed of large ** DELETE operations. +** +** This optimization cannot be used with a temp-file, as the page may +** have been dirty at the start of the transaction. In that case, if +** memory pressure forces page pPg out of the cache, the data does need +** to be written out to disk so that it may be read back in if the +** current transaction is rolled back. */ SQLITE_PRIVATE void sqlite3PagerDontWrite(PgHdr *pPg){ Pager *pPager = pPg->pPager; - if( (pPg->flags&PGHDR_DIRTY) && pPager->nSavepoint==0 ){ + if( !pPager->tempFile && (pPg->flags&PGHDR_DIRTY) && pPager->nSavepoint==0 ){ PAGERTRACE(("DONT_WRITE page %d of %d\n", pPg->pgno, PAGERID(pPager))); IOTRACE(("CLEAN %p %d\n", pPager, pPg->pgno)) pPg->flags |= PGHDR_DONT_WRITE; pPg->flags &= ~PGHDR_WRITEABLE; + testcase( pPg->flags & PGHDR_NEED_SYNC ); pager_set_pagehash(pPg); } } @@ -49007,7 +51935,7 @@ static int pager_incr_changecounter(Pager *pPager, int isDirectMode){ assert( !pPager->tempFile && isOpen(pPager->fd) ); /* Open page 1 of the file for writing. */ - rc = sqlite3PagerGet(pPager, 1, &pPgHdr); + rc = sqlite3PagerGet(pPager, 1, &pPgHdr, 0); assert( pPgHdr==0 || rc==SQLITE_OK ); /* If page one was fetched successfully, and this function is not @@ -49027,7 +51955,7 @@ static int pager_incr_changecounter(Pager *pPager, int isDirectMode){ if( DIRECT_MODE ){ const void *zBuf; assert( pPager->dbFileSize>0 ); - CODEC2(pPager, pPgHdr->pData, 1, 6, rc=SQLITE_NOMEM, zBuf); + CODEC2(pPager, pPgHdr->pData, 1, 6, rc=SQLITE_NOMEM_BKPT, zBuf); if( rc==SQLITE_OK ){ rc = sqlite3OsWrite(pPager->fd, zBuf, pPager->pageSize, 0); pPager->aStat[PAGER_STAT_WRITE]++; @@ -49085,14 +52013,17 @@ SQLITE_PRIVATE int sqlite3PagerSync(Pager *pPager, const char *zMaster){ ** returned. */ SQLITE_PRIVATE int sqlite3PagerExclusiveLock(Pager *pPager){ - int rc = SQLITE_OK; - assert( pPager->eState==PAGER_WRITER_CACHEMOD - || pPager->eState==PAGER_WRITER_DBMOD - || pPager->eState==PAGER_WRITER_LOCKED - ); + int rc = pPager->errCode; assert( assert_pager_state(pPager) ); - if( 0==pagerUseWal(pPager) ){ - rc = pager_wait_on_lock(pPager, EXCLUSIVE_LOCK); + if( rc==SQLITE_OK ){ + assert( pPager->eState==PAGER_WRITER_CACHEMOD + || pPager->eState==PAGER_WRITER_DBMOD + || pPager->eState==PAGER_WRITER_LOCKED + ); + assert( assert_pager_state(pPager) ); + if( 0==pagerUseWal(pPager) ){ + rc = pager_wait_on_lock(pPager, EXCLUSIVE_LOCK); + } } return rc; } @@ -49140,17 +52071,21 @@ SQLITE_PRIVATE int sqlite3PagerCommitPhaseOne( /* If a prior error occurred, report that error again. */ if( NEVER(pPager->errCode) ) return pPager->errCode; + /* Provide the ability to easily simulate an I/O error during testing */ + if( sqlite3FaultSim(400) ) return SQLITE_IOERR; + PAGERTRACE(("DATABASE SYNC: File=%s zMaster=%s nSize=%d\n", pPager->zFilename, zMaster, pPager->dbSize)); /* If no database changes have been made, return early. */ if( pPager->eStatetempFile ); + assert( isOpen(pPager->fd) || pPager->tempFile ); + if( 0==pagerFlushOnCommit(pPager, 1) ){ /* If this is an in-memory db, or no pages have been written to, or this ** function has already been called, it is mostly a no-op. However, any - ** backup in progress needs to be restarted. - */ + ** backup in progress needs to be restarted. */ sqlite3BackupRestart(pPager->pBackup); }else{ if( pagerUseWal(pPager) ){ @@ -49159,7 +52094,7 @@ SQLITE_PRIVATE int sqlite3PagerCommitPhaseOne( if( pList==0 ){ /* Must have at least one page for the WAL commit flag. ** Ticket [2d1a5c67dfc2363e44f29d9bbd57f] 2011-05-18 */ - rc = sqlite3PagerGet(pPager, 1, &pPageOne); + rc = sqlite3PagerGet(pPager, 1, &pPageOne, 0); pList = pPageOne; pList->pDirty = 0; } @@ -49489,10 +52424,10 @@ SQLITE_PRIVATE void sqlite3PagerCacheStat(Pager *pPager, int eStat, int reset, i } /* -** Return true if this is an in-memory pager. +** Return true if this is an in-memory or temp-file backed pager. */ SQLITE_PRIVATE int sqlite3PagerIsMemdb(Pager *pPager){ - return MEMDB; + return pPager->tempFile; } /* @@ -49523,7 +52458,7 @@ static SQLITE_NOINLINE int pagerOpenSavepoint(Pager *pPager, int nSavepoint){ pPager->aSavepoint, sizeof(PagerSavepoint)*nSavepoint ); if( !aNew ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } memset(&aNew[nCurrent], 0, (nSavepoint-nCurrent) * sizeof(PagerSavepoint)); pPager->aSavepoint = aNew; @@ -49539,7 +52474,7 @@ static SQLITE_NOINLINE int pagerOpenSavepoint(Pager *pPager, int nSavepoint){ aNew[ii].iSubRec = pPager->nSubRec; aNew[ii].pInSavepoint = sqlite3BitvecCreate(pPager->dbSize); if( !aNew[ii].pInSavepoint ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } if( pagerUseWal(pPager) ){ sqlite3WalSavepoint(pPager->pWal, aNew[ii].aWalData); @@ -49617,7 +52552,7 @@ SQLITE_PRIVATE int sqlite3PagerSavepoint(Pager *pPager, int op, int iSavepoint){ if( op==SAVEPOINT_RELEASE ){ if( nNew==0 && isOpen(pPager->sjfd) ){ /* Only truncate if it is an in-memory sub-journal. */ - if( sqlite3IsMemJournal(pPager->sjfd) ){ + if( sqlite3JournalIsInMemory(pPager->sjfd) ){ rc = sqlite3OsTruncate(pPager->sjfd, 0); assert( rc==SQLITE_OK ); } @@ -49656,7 +52591,7 @@ SQLITE_PRIVATE const char *sqlite3PagerFilename(Pager *pPager, int nullIfMemDb){ /* ** Return the VFS structure for the pager. */ -SQLITE_PRIVATE const sqlite3_vfs *sqlite3PagerVfs(Pager *pPager){ +SQLITE_PRIVATE sqlite3_vfs *sqlite3PagerVfs(Pager *pPager){ return pPager->pVfs; } @@ -49669,6 +52604,18 @@ SQLITE_PRIVATE sqlite3_file *sqlite3PagerFile(Pager *pPager){ return pPager->fd; } +/* +** Return the file handle for the journal file (if it exists). +** This will be either the rollback journal or the WAL file. +*/ +SQLITE_PRIVATE sqlite3_file *sqlite3PagerJrnlFile(Pager *pPager){ +#if SQLITE_OMIT_WAL + return pPager->jfd; +#else + return pPager->pWal ? sqlite3WalFile(pPager->pWal) : pPager->jfd; +#endif +} + /* ** Return the full pathname of the journal file. */ @@ -49676,14 +52623,6 @@ SQLITE_PRIVATE const char *sqlite3PagerJournalname(Pager *pPager){ return pPager->zJournal; } -/* -** Return true if fsync() calls are disabled for this pager. Return FALSE -** if fsync()s are executed normally. -*/ -SQLITE_PRIVATE int sqlite3PagerNosync(Pager *pPager){ - return pPager->noSync; -} - #ifdef SQLITE_HAS_CODEC /* ** Set or retrieve the codec for this pager @@ -49768,7 +52707,8 @@ SQLITE_PRIVATE int sqlite3PagerMovepage(Pager *pPager, DbPage *pPg, Pgno pgno, i /* In order to be able to rollback, an in-memory database must journal ** the page we are moving from. */ - if( MEMDB ){ + assert( pPager->tempFile || !MEMDB ); + if( pPager->tempFile ){ rc = sqlite3PagerWrite(pPg); if( rc ) return rc; } @@ -49825,7 +52765,7 @@ SQLITE_PRIVATE int sqlite3PagerMovepage(Pager *pPager, DbPage *pPg, Pgno pgno, i assert( !pPgOld || pPgOld->nRef==1 ); if( pPgOld ){ pPg->flags |= (pPgOld->flags&PGHDR_NEED_SYNC); - if( MEMDB ){ + if( pPager->tempFile ){ /* Do not discard pages from an in-memory database since we might ** need to rollback later. Just move the page out of the way. */ sqlite3PcacheMove(pPgOld, pPager->dbSize+1); @@ -49842,8 +52782,7 @@ SQLITE_PRIVATE int sqlite3PagerMovepage(Pager *pPager, DbPage *pPg, Pgno pgno, i ** to exist, in case the transaction needs to roll back. Use pPgOld ** as the original page since it has already been allocated. */ - if( MEMDB ){ - assert( pPgOld ); + if( pPager->tempFile && pPgOld ){ sqlite3PcacheMove(pPgOld, origPgno); sqlite3PagerUnrefNotNull(pPgOld); } @@ -49864,7 +52803,7 @@ SQLITE_PRIVATE int sqlite3PagerMovepage(Pager *pPager, DbPage *pPg, Pgno pgno, i ** the journal file twice, but that is not a problem. */ PgHdr *pPgHdr; - rc = sqlite3PagerGet(pPager, needSyncPgno, &pPgHdr); + rc = sqlite3PagerGet(pPager, needSyncPgno, &pPgHdr, 0); if( rc!=SQLITE_OK ){ if( needSyncPgno<=pPager->dbOrigSize ){ assert( pPager->pTmpSpace!=0 ); @@ -50095,7 +53034,8 @@ SQLITE_PRIVATE sqlite3_backup **sqlite3PagerBackupPtr(Pager *pPager){ ** Unless this is an in-memory or temporary database, clear the pager cache. */ SQLITE_PRIVATE void sqlite3PagerClearCache(Pager *pPager){ - if( !MEMDB && pPager->tempFile==0 ) pager_reset(pPager); + assert( MEMDB==0 || pPager->tempFile ); + if( pPager->tempFile==0 ) pager_reset(pPager); } #endif @@ -50130,6 +53070,7 @@ SQLITE_PRIVATE int sqlite3PagerWalCallback(Pager *pPager){ */ SQLITE_PRIVATE int sqlite3PagerWalSupported(Pager *pPager){ const sqlite3_io_methods *pMethods = pPager->fd->pMethods; + if( pPager->noLock ) return 0; return pPager->exclusiveMode || (pMethods->iVersion>=2 && pMethods->xShmMap); } @@ -50273,11 +53214,40 @@ SQLITE_PRIVATE int sqlite3PagerCloseWal(Pager *pPager){ pPager->pageSize, (u8*)pPager->pTmpSpace); pPager->pWal = 0; pagerFixMaplimit(pPager); + if( rc && !pPager->exclusiveMode ) pagerUnlockDb(pPager, SHARED_LOCK); } } return rc; } +#ifdef SQLITE_ENABLE_SNAPSHOT +/* +** If this is a WAL database, obtain a snapshot handle for the snapshot +** currently open. Otherwise, return an error. +*/ +SQLITE_PRIVATE int sqlite3PagerSnapshotGet(Pager *pPager, sqlite3_snapshot **ppSnapshot){ + int rc = SQLITE_ERROR; + if( pPager->pWal ){ + rc = sqlite3WalSnapshotGet(pPager->pWal, ppSnapshot); + } + return rc; +} + +/* +** If this is a WAL database, store a pointer to pSnapshot. Next time a +** read transaction is opened, attempt to read from the snapshot it +** identifies. If this is not a WAL database, return an error. +*/ +SQLITE_PRIVATE int sqlite3PagerSnapshotOpen(Pager *pPager, sqlite3_snapshot *pSnapshot){ + int rc = SQLITE_OK; + if( pPager->pWal ){ + sqlite3WalSnapshotOpen(pPager->pWal, pSnapshot); + }else{ + rc = SQLITE_ERROR; + } + return rc; +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ #endif /* !SQLITE_OMIT_WAL */ #ifdef SQLITE_ENABLE_ZIPVFS @@ -50573,7 +53543,8 @@ SQLITE_PRIVATE int sqlite3WalTrace = 0; /* ** Indices of various locking bytes. WAL_NREADER is the number -** of available reader locks and should be at least 3. +** of available reader locks and should be at least 3. The default +** is SQLITE_SHM_NLOCK==8 and WAL_NREADER==5. */ #define WAL_WRITE_LOCK 0 #define WAL_ALL_BUT_WRITE 1 @@ -50593,7 +53564,10 @@ typedef struct WalCkptInfo WalCkptInfo; ** The following object holds a copy of the wal-index header content. ** ** The actual header in the wal-index consists of two copies of this -** object. +** object followed by one instance of the WalCkptInfo object. +** For all versions of SQLite through 3.10.0 and probably beyond, +** the locking bytes (WalCkptInfo.aLock) start at offset 120 and +** the total header size is 136 bytes. ** ** The szPage value can be any power of 2 between 512 and 32768, inclusive. ** Or it can be 1 to represent a 65536-byte page. The latter case was @@ -50626,6 +53600,16 @@ struct WalIndexHdr { ** However, a WAL_WRITE_LOCK thread can move the value of nBackfill from ** mxFrame back to zero when the WAL is reset. ** +** nBackfillAttempted is the largest value of nBackfill that a checkpoint +** has attempted to achieve. Normally nBackfill==nBackfillAtempted, however +** the nBackfillAttempted is set before any backfilling is done and the +** nBackfill is only set after all backfilling completes. So if a checkpoint +** crashes, nBackfillAttempted might be larger than nBackfill. The +** WalIndexHdr.mxFrame must never be less than nBackfillAttempted. +** +** The aLock[] field is a set of bytes used for locking. These bytes should +** never be read or written. +** ** There is one entry in aReadMark[] for each reader lock. If a reader ** holds read-lock K, then the value in aReadMark[K] is no greater than ** the mxFrame for that reader. The value READMARK_NOT_USED (0xffffffff) @@ -50665,6 +53649,9 @@ struct WalIndexHdr { struct WalCkptInfo { u32 nBackfill; /* Number of WAL frames backfilled into DB */ u32 aReadMark[WAL_NREADER]; /* Reader marks */ + u8 aLock[SQLITE_SHM_NLOCK]; /* Reserved space for locks */ + u32 nBackfillAttempted; /* WAL frames perhaps written, or maybe not */ + u32 notUsed0; /* Available for future enhancements */ }; #define READMARK_NOT_USED 0xffffffff @@ -50674,9 +53661,8 @@ struct WalCkptInfo { ** only support mandatory file-locks, we do not read or write data ** from the region of the file on which locks are applied. */ -#define WALINDEX_LOCK_OFFSET (sizeof(WalIndexHdr)*2 + sizeof(WalCkptInfo)) -#define WALINDEX_LOCK_RESERVED 16 -#define WALINDEX_HDR_SIZE (WALINDEX_LOCK_OFFSET+WALINDEX_LOCK_RESERVED) +#define WALINDEX_LOCK_OFFSET (sizeof(WalIndexHdr)*2+offsetof(WalCkptInfo,aLock)) +#define WALINDEX_HDR_SIZE (sizeof(WalIndexHdr)*2+sizeof(WalCkptInfo)) /* Size of header before each frame in wal */ #define WAL_FRAME_HDRSIZE 24 @@ -50730,11 +53716,15 @@ struct Wal { u8 padToSectorBoundary; /* Pad transactions out to the next sector */ WalIndexHdr hdr; /* Wal-index header for current transaction */ u32 minFrame; /* Ignore wal frames before this one */ + u32 iReCksum; /* On commit, recalculate checksums from here */ const char *zWalName; /* Name of WAL file */ u32 nCkpt; /* Checkpoint sequence counter in the wal-header */ #ifdef SQLITE_DEBUG u8 lockError; /* True if a locking error has occurred */ #endif +#ifdef SQLITE_ENABLE_SNAPSHOT + WalIndexHdr *pSnapshot; /* Start transaction here if not NULL */ +#endif }; /* @@ -50827,7 +53817,7 @@ static int walIndexPage(Wal *pWal, int iPage, volatile u32 **ppPage){ apNew = (volatile u32 **)sqlite3_realloc64((void *)pWal->apWiData, nByte); if( !apNew ){ *ppPage = 0; - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } memset((void*)&apNew[pWal->nWiData], 0, sizeof(u32*)*(iPage+1-pWal->nWiData)); @@ -50839,7 +53829,7 @@ static int walIndexPage(Wal *pWal, int iPage, volatile u32 **ppPage){ if( pWal->apWiData[iPage]==0 ){ if( pWal->exclusiveMode==WAL_HEAPMEMORY_MODE ){ pWal->apWiData[iPage] = (u32 volatile *)sqlite3MallocZero(WALINDEX_PGSZ); - if( !pWal->apWiData[iPage] ) rc = SQLITE_NOMEM; + if( !pWal->apWiData[iPage] ) rc = SQLITE_NOMEM_BKPT; }else{ rc = sqlite3OsShmMap(pWal->pDbFd, iPage, WALINDEX_PGSZ, pWal->writeLock, (void volatile **)&pWal->apWiData[iPage] @@ -50980,14 +53970,18 @@ static void walEncodeFrame( assert( WAL_FRAME_HDRSIZE==24 ); sqlite3Put4byte(&aFrame[0], iPage); sqlite3Put4byte(&aFrame[4], nTruncate); - memcpy(&aFrame[8], pWal->hdr.aSalt, 8); + if( pWal->iReCksum==0 ){ + memcpy(&aFrame[8], pWal->hdr.aSalt, 8); - nativeCksum = (pWal->hdr.bigEndCksum==SQLITE_BIGENDIAN); - walChecksumBytes(nativeCksum, aFrame, 8, aCksum, aCksum); - walChecksumBytes(nativeCksum, aData, pWal->szPage, aCksum, aCksum); + nativeCksum = (pWal->hdr.bigEndCksum==SQLITE_BIGENDIAN); + walChecksumBytes(nativeCksum, aFrame, 8, aCksum, aCksum); + walChecksumBytes(nativeCksum, aData, pWal->szPage, aCksum, aCksum); - sqlite3Put4byte(&aFrame[16], aCksum[0]); - sqlite3Put4byte(&aFrame[20], aCksum[1]); + sqlite3Put4byte(&aFrame[16], aCksum[0]); + sqlite3Put4byte(&aFrame[20], aCksum[1]); + }else{ + memset(&aFrame[8], 0, 16); + } } /* @@ -51090,10 +54084,9 @@ static void walUnlockShared(Wal *pWal, int lockIdx){ SQLITE_SHM_UNLOCK | SQLITE_SHM_SHARED); WALTRACE(("WAL%p: release SHARED-%s\n", pWal, walLockName(lockIdx))); } -static int walLockExclusive(Wal *pWal, int lockIdx, int n, int fBlock){ +static int walLockExclusive(Wal *pWal, int lockIdx, int n){ int rc; if( pWal->exclusiveMode ) return SQLITE_OK; - if( fBlock ) sqlite3OsFileControl(pWal->pDbFd, SQLITE_FCNTL_WAL_BLOCK, 0); rc = sqlite3OsShmLock(pWal->pDbFd, lockIdx, n, SQLITE_SHM_LOCK | SQLITE_SHM_EXCLUSIVE); WALTRACE(("WAL%p: acquire EXCLUSIVE-%s cnt=%d %s\n", pWal, @@ -51379,7 +54372,7 @@ static int walIndexRecover(Wal *pWal){ assert( pWal->writeLock ); iLock = WAL_ALL_BUT_WRITE + pWal->ckptLock; nLock = SQLITE_SHM_NLOCK - iLock; - rc = walLockExclusive(pWal, iLock, nLock, 0); + rc = walLockExclusive(pWal, iLock, nLock); if( rc ){ return rc; } @@ -51451,7 +54444,7 @@ static int walIndexRecover(Wal *pWal){ szFrame = szPage + WAL_FRAME_HDRSIZE; aFrame = (u8 *)sqlite3_malloc64(szFrame); if( !aFrame ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto recovery_error; } aData = &aFrame[WAL_FRAME_HDRSIZE]; @@ -51500,6 +54493,7 @@ finished: */ pInfo = walCkptInfo(pWal); pInfo->nBackfill = 0; + pInfo->nBackfillAttempted = pWal->hdr.mxFrame; pInfo->aReadMark[0] = 0; for(i=1; iaReadMark[i] = READMARK_NOT_USED; if( pWal->hdr.mxFrame ) pInfo->aReadMark[1] = pWal->hdr.mxFrame; @@ -51571,7 +54565,11 @@ SQLITE_PRIVATE int sqlite3WalOpen( /* In the amalgamation, the os_unix.c and os_win.c source files come before ** this source file. Verify that the #defines of the locking byte offsets ** in os_unix.c and os_win.c agree with the WALINDEX_LOCK_OFFSET value. + ** For that matter, if the lock offset ever changes from its initial design + ** value of 120, we need to know that so there is an assert() to check it. */ + assert( 120==WALINDEX_LOCK_OFFSET ); + assert( 136==WALINDEX_HDR_SIZE ); #ifdef WIN_SHM_BASE assert( WIN_SHM_BASE==WALINDEX_LOCK_OFFSET ); #endif @@ -51584,7 +54582,7 @@ SQLITE_PRIVATE int sqlite3WalOpen( *ppWal = 0; pRet = (Wal*)sqlite3MallocZero(sizeof(Wal) + pVfs->szOsFile); if( !pRet ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pRet->pVfs = pVfs; @@ -51848,7 +54846,7 @@ static int walIteratorInit(Wal *pWal, WalIterator **pp){ + iLast*sizeof(ht_slot); p = (WalIterator *)sqlite3_malloc64(nByte); if( !p ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } memset(p, 0, nByte); p->nSegment = nSegment; @@ -51860,7 +54858,7 @@ static int walIteratorInit(Wal *pWal, WalIterator **pp){ sizeof(ht_slot) * (iLast>HASHTABLE_NPAGE?HASHTABLE_NPAGE:iLast) ); if( !aTmp ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } for(i=0; rc==SQLITE_OK && ihdr.aSalt[1], &salt1, 4); walIndexWriteHdr(pWal); pInfo->nBackfill = 0; + pInfo->nBackfillAttempted = 0; pInfo->aReadMark[1] = 0; for(i=2; iaReadMark[i] = READMARK_NOT_USED; assert( pInfo->aReadMark[0]==0 ); @@ -52066,6 +55065,8 @@ static int walCheckpoint( i64 nSize; /* Current size of database file */ u32 nBackfill = pInfo->nBackfill; + pInfo->nBackfillAttempted = mxSafeFrame; + /* Sync the WAL to disk */ if( sync_flags ){ rc = sqlite3OsSync(pWal->pWalFd, sync_flags); @@ -52358,7 +55359,7 @@ static int walIndexReadHdr(Wal *pWal, int *pChanged){ walUnlockShared(pWal, WAL_WRITE_LOCK); rc = SQLITE_READONLY_RECOVERY; } - }else if( SQLITE_OK==(rc = walLockExclusive(pWal, WAL_WRITE_LOCK, 1, 1)) ){ + }else if( SQLITE_OK==(rc = walLockExclusive(pWal, WAL_WRITE_LOCK, 1)) ){ pWal->writeLock = 1; if( SQLITE_OK==(rc = walIndexPage(pWal, 0, &page0)) ){ badHdr = walIndexTryHdr(pWal, pChanged); @@ -52449,6 +55450,7 @@ static int walTryBeginRead(Wal *pWal, int *pChanged, int useWal, int cnt){ int mxI; /* Index of largest aReadMark[] value */ int i; /* Loop counter */ int rc = SQLITE_OK; /* Return code */ + u32 mxFrame; /* Wal frame to lock to */ assert( pWal->readLock<0 ); /* Not currently locked */ @@ -52512,7 +55514,12 @@ static int walTryBeginRead(Wal *pWal, int *pChanged, int useWal, int cnt){ } pInfo = walCkptInfo(pWal); - if( !useWal && pInfo->nBackfill==pWal->hdr.mxFrame ){ + if( !useWal && pInfo->nBackfill==pWal->hdr.mxFrame +#ifdef SQLITE_ENABLE_SNAPSHOT + && (pWal->pSnapshot==0 || pWal->hdr.mxFrame==0 + || 0==memcmp(&pWal->hdr, pWal->pSnapshot, sizeof(WalIndexHdr))) +#endif + ){ /* The WAL has been completely backfilled (or it is empty). ** and can be safely ignored. */ @@ -52550,85 +55557,88 @@ static int walTryBeginRead(Wal *pWal, int *pChanged, int useWal, int cnt){ */ mxReadMark = 0; mxI = 0; + mxFrame = pWal->hdr.mxFrame; +#ifdef SQLITE_ENABLE_SNAPSHOT + if( pWal->pSnapshot && pWal->pSnapshot->mxFramepSnapshot->mxFrame; + } +#endif for(i=1; iaReadMark[i]; - if( mxReadMark<=thisMark && thisMark<=pWal->hdr.mxFrame ){ + if( mxReadMark<=thisMark && thisMark<=mxFrame ){ assert( thisMark!=READMARK_NOT_USED ); mxReadMark = thisMark; mxI = i; } } - /* There was once an "if" here. The extra "{" is to preserve indentation. */ - { - if( (pWal->readOnly & WAL_SHM_RDONLY)==0 - && (mxReadMarkhdr.mxFrame || mxI==0) - ){ - for(i=1; iaReadMark[i] = pWal->hdr.mxFrame; - mxI = i; - walUnlockExclusive(pWal, WAL_READ_LOCK(i), 1); - break; - }else if( rc!=SQLITE_BUSY ){ - return rc; - } + if( (pWal->readOnly & WAL_SHM_RDONLY)==0 + && (mxReadMarkaReadMark[i] = mxFrame; + mxI = i; + walUnlockExclusive(pWal, WAL_READ_LOCK(i), 1); + break; + }else if( rc!=SQLITE_BUSY ){ + return rc; } } - if( mxI==0 ){ - assert( rc==SQLITE_BUSY || (pWal->readOnly & WAL_SHM_RDONLY)!=0 ); - return rc==SQLITE_BUSY ? WAL_RETRY : SQLITE_READONLY_CANTLOCK; - } + } + if( mxI==0 ){ + assert( rc==SQLITE_BUSY || (pWal->readOnly & WAL_SHM_RDONLY)!=0 ); + return rc==SQLITE_BUSY ? WAL_RETRY : SQLITE_READONLY_CANTLOCK; + } - rc = walLockShared(pWal, WAL_READ_LOCK(mxI)); - if( rc ){ - return rc==SQLITE_BUSY ? WAL_RETRY : rc; - } - /* Now that the read-lock has been obtained, check that neither the - ** value in the aReadMark[] array or the contents of the wal-index - ** header have changed. - ** - ** It is necessary to check that the wal-index header did not change - ** between the time it was read and when the shared-lock was obtained - ** on WAL_READ_LOCK(mxI) was obtained to account for the possibility - ** that the log file may have been wrapped by a writer, or that frames - ** that occur later in the log than pWal->hdr.mxFrame may have been - ** copied into the database by a checkpointer. If either of these things - ** happened, then reading the database with the current value of - ** pWal->hdr.mxFrame risks reading a corrupted snapshot. So, retry - ** instead. - ** - ** Before checking that the live wal-index header has not changed - ** since it was read, set Wal.minFrame to the first frame in the wal - ** file that has not yet been checkpointed. This client will not need - ** to read any frames earlier than minFrame from the wal file - they - ** can be safely read directly from the database file. - ** - ** Because a ShmBarrier() call is made between taking the copy of - ** nBackfill and checking that the wal-header in shared-memory still - ** matches the one cached in pWal->hdr, it is guaranteed that the - ** checkpointer that set nBackfill was not working with a wal-index - ** header newer than that cached in pWal->hdr. If it were, that could - ** cause a problem. The checkpointer could omit to checkpoint - ** a version of page X that lies before pWal->minFrame (call that version - ** A) on the basis that there is a newer version (version B) of the same - ** page later in the wal file. But if version B happens to like past - ** frame pWal->hdr.mxFrame - then the client would incorrectly assume - ** that it can read version A from the database file. However, since - ** we can guarantee that the checkpointer that set nBackfill could not - ** see any pages past pWal->hdr.mxFrame, this problem does not come up. - */ - pWal->minFrame = pInfo->nBackfill+1; - walShmBarrier(pWal); - if( pInfo->aReadMark[mxI]!=mxReadMark - || memcmp((void *)walIndexHdr(pWal), &pWal->hdr, sizeof(WalIndexHdr)) - ){ - walUnlockShared(pWal, WAL_READ_LOCK(mxI)); - return WAL_RETRY; - }else{ - assert( mxReadMark<=pWal->hdr.mxFrame ); - pWal->readLock = (i16)mxI; - } + rc = walLockShared(pWal, WAL_READ_LOCK(mxI)); + if( rc ){ + return rc==SQLITE_BUSY ? WAL_RETRY : rc; + } + /* Now that the read-lock has been obtained, check that neither the + ** value in the aReadMark[] array or the contents of the wal-index + ** header have changed. + ** + ** It is necessary to check that the wal-index header did not change + ** between the time it was read and when the shared-lock was obtained + ** on WAL_READ_LOCK(mxI) was obtained to account for the possibility + ** that the log file may have been wrapped by a writer, or that frames + ** that occur later in the log than pWal->hdr.mxFrame may have been + ** copied into the database by a checkpointer. If either of these things + ** happened, then reading the database with the current value of + ** pWal->hdr.mxFrame risks reading a corrupted snapshot. So, retry + ** instead. + ** + ** Before checking that the live wal-index header has not changed + ** since it was read, set Wal.minFrame to the first frame in the wal + ** file that has not yet been checkpointed. This client will not need + ** to read any frames earlier than minFrame from the wal file - they + ** can be safely read directly from the database file. + ** + ** Because a ShmBarrier() call is made between taking the copy of + ** nBackfill and checking that the wal-header in shared-memory still + ** matches the one cached in pWal->hdr, it is guaranteed that the + ** checkpointer that set nBackfill was not working with a wal-index + ** header newer than that cached in pWal->hdr. If it were, that could + ** cause a problem. The checkpointer could omit to checkpoint + ** a version of page X that lies before pWal->minFrame (call that version + ** A) on the basis that there is a newer version (version B) of the same + ** page later in the wal file. But if version B happens to like past + ** frame pWal->hdr.mxFrame - then the client would incorrectly assume + ** that it can read version A from the database file. However, since + ** we can guarantee that the checkpointer that set nBackfill could not + ** see any pages past pWal->hdr.mxFrame, this problem does not come up. + */ + pWal->minFrame = pInfo->nBackfill+1; + walShmBarrier(pWal); + if( pInfo->aReadMark[mxI]!=mxReadMark + || memcmp((void *)walIndexHdr(pWal), &pWal->hdr, sizeof(WalIndexHdr)) + ){ + walUnlockShared(pWal, WAL_READ_LOCK(mxI)); + return WAL_RETRY; + }else{ + assert( mxReadMark<=pWal->hdr.mxFrame ); + pWal->readLock = (i16)mxI; } return rc; } @@ -52651,6 +55661,14 @@ SQLITE_PRIVATE int sqlite3WalBeginReadTransaction(Wal *pWal, int *pChanged){ int rc; /* Return code */ int cnt = 0; /* Number of TryBeginRead attempts */ +#ifdef SQLITE_ENABLE_SNAPSHOT + int bChanged = 0; + WalIndexHdr *pSnapshot = pWal->pSnapshot; + if( pSnapshot && memcmp(pSnapshot, &pWal->hdr, sizeof(WalIndexHdr))!=0 ){ + bChanged = 1; + } +#endif + do{ rc = walTryBeginRead(pWal, pChanged, 0, ++cnt); }while( rc==WAL_RETRY ); @@ -52658,6 +55676,66 @@ SQLITE_PRIVATE int sqlite3WalBeginReadTransaction(Wal *pWal, int *pChanged){ testcase( (rc&0xff)==SQLITE_IOERR ); testcase( rc==SQLITE_PROTOCOL ); testcase( rc==SQLITE_OK ); + +#ifdef SQLITE_ENABLE_SNAPSHOT + if( rc==SQLITE_OK ){ + if( pSnapshot && memcmp(pSnapshot, &pWal->hdr, sizeof(WalIndexHdr))!=0 ){ + /* At this point the client has a lock on an aReadMark[] slot holding + ** a value equal to or smaller than pSnapshot->mxFrame, but pWal->hdr + ** is populated with the wal-index header corresponding to the head + ** of the wal file. Verify that pSnapshot is still valid before + ** continuing. Reasons why pSnapshot might no longer be valid: + ** + ** (1) The WAL file has been reset since the snapshot was taken. + ** In this case, the salt will have changed. + ** + ** (2) A checkpoint as been attempted that wrote frames past + ** pSnapshot->mxFrame into the database file. Note that the + ** checkpoint need not have completed for this to cause problems. + */ + volatile WalCkptInfo *pInfo = walCkptInfo(pWal); + + assert( pWal->readLock>0 || pWal->hdr.mxFrame==0 ); + assert( pInfo->aReadMark[pWal->readLock]<=pSnapshot->mxFrame ); + + /* It is possible that there is a checkpointer thread running + ** concurrent with this code. If this is the case, it may be that the + ** checkpointer has already determined that it will checkpoint + ** snapshot X, where X is later in the wal file than pSnapshot, but + ** has not yet set the pInfo->nBackfillAttempted variable to indicate + ** its intent. To avoid the race condition this leads to, ensure that + ** there is no checkpointer process by taking a shared CKPT lock + ** before checking pInfo->nBackfillAttempted. */ + rc = walLockShared(pWal, WAL_CKPT_LOCK); + + if( rc==SQLITE_OK ){ + /* Check that the wal file has not been wrapped. Assuming that it has + ** not, also check that no checkpointer has attempted to checkpoint any + ** frames beyond pSnapshot->mxFrame. If either of these conditions are + ** true, return SQLITE_BUSY_SNAPSHOT. Otherwise, overwrite pWal->hdr + ** with *pSnapshot and set *pChanged as appropriate for opening the + ** snapshot. */ + if( !memcmp(pSnapshot->aSalt, pWal->hdr.aSalt, sizeof(pWal->hdr.aSalt)) + && pSnapshot->mxFrame>=pInfo->nBackfillAttempted + ){ + assert( pWal->readLock>0 ); + memcpy(&pWal->hdr, pSnapshot, sizeof(WalIndexHdr)); + *pChanged = bChanged; + }else{ + rc = SQLITE_BUSY_SNAPSHOT; + } + + /* Release the shared CKPT lock obtained above. */ + walUnlockShared(pWal, WAL_CKPT_LOCK); + } + + + if( rc!=SQLITE_OK ){ + sqlite3WalEndReadTransaction(pWal); + } + } + } +#endif return rc; } @@ -52830,6 +55908,7 @@ SQLITE_PRIVATE int sqlite3WalBeginWriteTransaction(Wal *pWal){ /* Cannot start a write transaction without first holding a read ** transaction. */ assert( pWal->readLock>=0 ); + assert( pWal->writeLock==0 && pWal->iReCksum==0 ); if( pWal->readOnly ){ return SQLITE_READONLY; @@ -52838,7 +55917,7 @@ SQLITE_PRIVATE int sqlite3WalBeginWriteTransaction(Wal *pWal){ /* Only one writer allowed at a time. Get the write lock. Return ** SQLITE_BUSY if unable. */ - rc = walLockExclusive(pWal, WAL_WRITE_LOCK, 1, 0); + rc = walLockExclusive(pWal, WAL_WRITE_LOCK, 1); if( rc ){ return rc; } @@ -52865,6 +55944,7 @@ SQLITE_PRIVATE int sqlite3WalEndWriteTransaction(Wal *pWal){ if( pWal->writeLock ){ walUnlockExclusive(pWal, WAL_WRITE_LOCK, 1); pWal->writeLock = 0; + pWal->iReCksum = 0; pWal->truncateOnCommit = 0; } return SQLITE_OK; @@ -52983,7 +56063,7 @@ static int walRestartLog(Wal *pWal){ if( pInfo->nBackfill>0 ){ u32 salt1; sqlite3_randomness(4, &salt1); - rc = walLockExclusive(pWal, WAL_READ_LOCK(1), WAL_NREADER-1, 0); + rc = walLockExclusive(pWal, WAL_READ_LOCK(1), WAL_NREADER-1); if( rc==SQLITE_OK ){ /* If all readers are using WAL_READ_LOCK(0) (in other words if no ** readers are currently using the WAL), then the transactions @@ -53071,7 +56151,7 @@ static int walWriteOneFrame( void *pData; /* Data actually written */ u8 aFrame[WAL_FRAME_HDRSIZE]; /* Buffer to assemble frame-header in */ #if defined(SQLITE_HAS_CODEC) - if( (pData = sqlite3PagerCodec(pPage))==0 ) return SQLITE_NOMEM; + if( (pData = sqlite3PagerCodec(pPage))==0 ) return SQLITE_NOMEM_BKPT; #else pData = pPage->pData; #endif @@ -53083,6 +56163,59 @@ static int walWriteOneFrame( return rc; } +/* +** This function is called as part of committing a transaction within which +** one or more frames have been overwritten. It updates the checksums for +** all frames written to the wal file by the current transaction starting +** with the earliest to have been overwritten. +** +** SQLITE_OK is returned if successful, or an SQLite error code otherwise. +*/ +static int walRewriteChecksums(Wal *pWal, u32 iLast){ + const int szPage = pWal->szPage;/* Database page size */ + int rc = SQLITE_OK; /* Return code */ + u8 *aBuf; /* Buffer to load data from wal file into */ + u8 aFrame[WAL_FRAME_HDRSIZE]; /* Buffer to assemble frame-headers in */ + u32 iRead; /* Next frame to read from wal file */ + i64 iCksumOff; + + aBuf = sqlite3_malloc(szPage + WAL_FRAME_HDRSIZE); + if( aBuf==0 ) return SQLITE_NOMEM_BKPT; + + /* Find the checksum values to use as input for the recalculating the + ** first checksum. If the first frame is frame 1 (implying that the current + ** transaction restarted the wal file), these values must be read from the + ** wal-file header. Otherwise, read them from the frame header of the + ** previous frame. */ + assert( pWal->iReCksum>0 ); + if( pWal->iReCksum==1 ){ + iCksumOff = 24; + }else{ + iCksumOff = walFrameOffset(pWal->iReCksum-1, szPage) + 16; + } + rc = sqlite3OsRead(pWal->pWalFd, aBuf, sizeof(u32)*2, iCksumOff); + pWal->hdr.aFrameCksum[0] = sqlite3Get4byte(aBuf); + pWal->hdr.aFrameCksum[1] = sqlite3Get4byte(&aBuf[sizeof(u32)]); + + iRead = pWal->iReCksum; + pWal->iReCksum = 0; + for(; rc==SQLITE_OK && iRead<=iLast; iRead++){ + i64 iOff = walFrameOffset(iRead, szPage); + rc = sqlite3OsRead(pWal->pWalFd, aBuf, szPage+WAL_FRAME_HDRSIZE, iOff); + if( rc==SQLITE_OK ){ + u32 iPgno, nDbSize; + iPgno = sqlite3Get4byte(aBuf); + nDbSize = sqlite3Get4byte(&aBuf[4]); + + walEncodeFrame(pWal, iPgno, nDbSize, &aBuf[WAL_FRAME_HDRSIZE], aFrame); + rc = sqlite3OsWrite(pWal->pWalFd, aFrame, sizeof(aFrame), iOff); + } + } + + sqlite3_free(aBuf); + return rc; +} + /* ** Write a set of frames to the log. The caller must hold the write-lock ** on the log file (obtained using sqlite3WalBeginWriteTransaction()). @@ -53103,6 +56236,8 @@ SQLITE_PRIVATE int sqlite3WalFrames( int szFrame; /* The size of a single frame */ i64 iOffset; /* Next byte to write in WAL file */ WalWriter w; /* The writer */ + u32 iFirst = 0; /* First frame that may be overwritten */ + WalIndexHdr *pLive; /* Pointer to shared header */ assert( pList ); assert( pWal->writeLock ); @@ -53118,6 +56253,11 @@ SQLITE_PRIVATE int sqlite3WalFrames( } #endif + pLive = (WalIndexHdr*)walIndexHdr(pWal); + if( memcmp(&pWal->hdr, (void *)pLive, sizeof(WalIndexHdr))!=0 ){ + iFirst = pLive->mxFrame+1; + } + /* See if it is possible to write these frames into the start of the ** log file, instead of appending to it at pWal->hdr.mxFrame. */ @@ -53182,6 +56322,33 @@ SQLITE_PRIVATE int sqlite3WalFrames( /* Write all frames into the log file exactly once */ for(p=pList; p; p=p->pDirty){ int nDbSize; /* 0 normally. Positive == commit flag */ + + /* Check if this page has already been written into the wal file by + ** the current transaction. If so, overwrite the existing frame and + ** set Wal.writeLock to WAL_WRITELOCK_RECKSUM - indicating that + ** checksums must be recomputed when the transaction is committed. */ + if( iFirst && (p->pDirty || isCommit==0) ){ + u32 iWrite = 0; + VVA_ONLY(rc =) sqlite3WalFindFrame(pWal, p->pgno, &iWrite); + assert( rc==SQLITE_OK || iWrite==0 ); + if( iWrite>=iFirst ){ + i64 iOff = walFrameOffset(iWrite, szPage) + WAL_FRAME_HDRSIZE; + void *pData; + if( pWal->iReCksum==0 || iWriteiReCksum ){ + pWal->iReCksum = iWrite; + } +#if defined(SQLITE_HAS_CODEC) + if( (pData = sqlite3PagerCodec(p))==0 ) return SQLITE_NOMEM; +#else + pData = p->pData; +#endif + rc = sqlite3OsWrite(pWal->pWalFd, pData, szPage, iOff); + if( rc ) return rc; + p->flags &= ~PGHDR_WAL_APPEND; + continue; + } + } + iFrame++; assert( iOffset==walFrameOffset(iFrame, szPage) ); nDbSize = (isCommit && p->pDirty==0) ? nTruncate : 0; @@ -53189,6 +56356,13 @@ SQLITE_PRIVATE int sqlite3WalFrames( if( rc ) return rc; pLast = p; iOffset += szFrame; + p->flags |= PGHDR_WAL_APPEND; + } + + /* Recalculate checksums within the wal file if required. */ + if( isCommit && pWal->iReCksum ){ + rc = walRewriteChecksums(pWal, iFrame); + if( rc ) return rc; } /* If this is the end of a transaction, then we might need to pad @@ -53240,6 +56414,7 @@ SQLITE_PRIVATE int sqlite3WalFrames( */ iFrame = pWal->hdr.mxFrame; for(p=pList; p && rc==SQLITE_OK; p=p->pDirty){ + if( (p->flags & PGHDR_WAL_APPEND)==0 ) continue; iFrame++; rc = walIndexAppend(pWal, iFrame, p->pgno); } @@ -53308,7 +56483,7 @@ SQLITE_PRIVATE int sqlite3WalCheckpoint( /* IMPLEMENTATION-OF: R-62028-47212 All calls obtain an exclusive ** "checkpoint" lock on the database file. */ - rc = walLockExclusive(pWal, WAL_CKPT_LOCK, 1, 0); + rc = walLockExclusive(pWal, WAL_CKPT_LOCK, 1); if( rc ){ /* EVIDENCE-OF: R-10421-19736 If any other process is running a ** checkpoint operation at the same time, the lock cannot be obtained and @@ -53352,6 +56527,7 @@ SQLITE_PRIVATE int sqlite3WalCheckpoint( /* Copy data from the log to the database file. */ if( rc==SQLITE_OK ){ + if( pWal->hdr.mxFrame && walPagesize(pWal)!=nBuf ){ rc = SQLITE_CORRUPT_BKPT; }else{ @@ -53467,6 +56643,52 @@ SQLITE_PRIVATE int sqlite3WalHeapMemory(Wal *pWal){ return (pWal && pWal->exclusiveMode==WAL_HEAPMEMORY_MODE ); } +#ifdef SQLITE_ENABLE_SNAPSHOT +/* Create a snapshot object. The content of a snapshot is opaque to +** every other subsystem, so the WAL module can put whatever it needs +** in the object. +*/ +SQLITE_PRIVATE int sqlite3WalSnapshotGet(Wal *pWal, sqlite3_snapshot **ppSnapshot){ + int rc = SQLITE_OK; + WalIndexHdr *pRet; + + assert( pWal->readLock>=0 && pWal->writeLock==0 ); + + pRet = (WalIndexHdr*)sqlite3_malloc(sizeof(WalIndexHdr)); + if( pRet==0 ){ + rc = SQLITE_NOMEM_BKPT; + }else{ + memcpy(pRet, &pWal->hdr, sizeof(WalIndexHdr)); + *ppSnapshot = (sqlite3_snapshot*)pRet; + } + + return rc; +} + +/* Try to open on pSnapshot when the next read-transaction starts +*/ +SQLITE_PRIVATE void sqlite3WalSnapshotOpen(Wal *pWal, sqlite3_snapshot *pSnapshot){ + pWal->pSnapshot = (WalIndexHdr*)pSnapshot; +} + +/* +** Return a +ve value if snapshot p1 is newer than p2. A -ve value if +** p1 is older than p2 and zero if p1 and p2 are the same snapshot. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_snapshot_cmp(sqlite3_snapshot *p1, sqlite3_snapshot *p2){ + WalIndexHdr *pHdr1 = (WalIndexHdr*)p1; + WalIndexHdr *pHdr2 = (WalIndexHdr*)p2; + + /* aSalt[0] is a copy of the value stored in the wal file header. It + ** is incremented each time the wal file is restarted. */ + if( pHdr1->aSalt[0]aSalt[0] ) return -1; + if( pHdr1->aSalt[0]>pHdr2->aSalt[0] ) return +1; + if( pHdr1->mxFramemxFrame ) return -1; + if( pHdr1->mxFrame>pHdr2->mxFrame ) return +1; + return 0; +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ + #ifdef SQLITE_ENABLE_ZIPVFS /* ** If the argument is not NULL, it points to a Wal object that holds a @@ -53479,6 +56701,12 @@ SQLITE_PRIVATE int sqlite3WalFramesize(Wal *pWal){ } #endif +/* Return the sqlite3_file object for the WAL file +*/ +SQLITE_PRIVATE sqlite3_file *sqlite3WalFile(Wal *pWal){ + return pWal->pWalFd; +} + #endif /* #ifndef SQLITE_OMIT_WAL */ /************** End of wal.c *************************************************/ @@ -53780,7 +57008,6 @@ struct MemPage { u8 nOverflow; /* Number of overflow cell bodies in aCell[] */ u8 intKey; /* True if table b-trees. False for index b-trees */ u8 intKeyLeaf; /* True if the leaf of an intKey table */ - u8 noPayload; /* True if internal intKey page (thus w/o data) */ u8 leaf; /* True if a leaf page */ u8 hdrOffset; /* 100 for page 1. 0 otherwise */ u8 childPtrSize; /* 0 if leaf==1. 4 if leaf==0 */ @@ -53974,7 +57201,6 @@ struct CellInfo { u8 *pPayload; /* Pointer to the start of payload */ u32 nPayload; /* Bytes of payload */ u16 nLocal; /* Amount of payload held locally, not on overflow */ - u16 iOverflow; /* Offset to overflow page number. Zero if no overflow */ u16 nSize; /* Size of the cell content on the main b-tree page */ }; @@ -54021,7 +57247,7 @@ struct BtCursor { int skipNext; /* Prev() is noop if negative. Next() is noop if positive. ** Error code if eState==CURSOR_FAULT */ u8 curFlags; /* zero or more BTCF_* flags defined below */ - u8 curPagerFlags; /* Flags to send to sqlite3PagerAcquire() */ + u8 curPagerFlags; /* Flags to send to sqlite3PagerGet() */ u8 eState; /* One of the CURSOR_XXX constants (see below) */ u8 hints; /* As configured by CursorSetHints() */ /* All fields above are zeroed when the cursor is allocated. See @@ -54369,21 +57595,6 @@ SQLITE_PRIVATE int sqlite3BtreeHoldsMutex(Btree *p){ #endif -#ifndef SQLITE_OMIT_INCRBLOB -/* -** Enter and leave a mutex on a Btree given a cursor owned by that -** Btree. These entry points are used by incremental I/O and can be -** omitted if that module is not used. -*/ -SQLITE_PRIVATE void sqlite3BtreeEnterCursor(BtCursor *pCur){ - sqlite3BtreeEnter(pCur->pBtree); -} -SQLITE_PRIVATE void sqlite3BtreeLeaveCursor(BtCursor *pCur){ - sqlite3BtreeLeave(pCur->pBtree); -} -#endif /* SQLITE_OMIT_INCRBLOB */ - - /* ** Enter the mutex on every Btree associated with a database ** connection. This is needed (for example) prior to parsing @@ -54417,14 +57628,6 @@ SQLITE_PRIVATE void sqlite3BtreeLeaveAll(sqlite3 *db){ } } -/* -** Return true if a particular Btree requires a lock. Return FALSE if -** no lock is ever required since it is not sharable. -*/ -SQLITE_PRIVATE int sqlite3BtreeSharable(Btree *p){ - return p->sharable; -} - #ifndef NDEBUG /* ** Return true if the current thread holds the database connection @@ -54498,6 +57701,25 @@ SQLITE_PRIVATE void sqlite3BtreeEnterAll(sqlite3 *db){ } } #endif /* if SQLITE_THREADSAFE */ + +#ifndef SQLITE_OMIT_INCRBLOB +/* +** Enter a mutex on a Btree given a cursor owned by that Btree. +** +** These entry points are used by incremental I/O only. Enter() is required +** any time OMIT_SHARED_CACHE is not defined, regardless of whether or not +** the build is threadsafe. Leave() is only required by threadsafe builds. +*/ +SQLITE_PRIVATE void sqlite3BtreeEnterCursor(BtCursor *pCur){ + sqlite3BtreeEnter(pCur->pBtree); +} +# if SQLITE_THREADSAFE +SQLITE_PRIVATE void sqlite3BtreeLeaveCursor(BtCursor *pCur){ + sqlite3BtreeLeave(pCur->pBtree); +} +# endif +#endif /* ifndef SQLITE_OMIT_INCRBLOB */ + #endif /* ifndef SQLITE_OMIT_SHARED_CACHE */ /************** End of btmutex.c *********************************************/ @@ -54854,7 +58076,7 @@ static int setSharedCacheTableLock(Btree *p, Pgno iTable, u8 eLock){ if( !pLock ){ pLock = (BtLock *)sqlite3MallocZero(sizeof(BtLock)); if( !pLock ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pLock->iTable = iTable; pLock->pBtree = p; @@ -54954,6 +58176,10 @@ static void releasePage(MemPage *pPage); /* Forward reference */ static int cursorHoldsMutex(BtCursor *p){ return sqlite3_mutex_held(p->pBt->mutex); } +static int cursorOwnsBtShared(BtCursor *p){ + assert( cursorHoldsMutex(p) ); + return (p->pBtree->db==p->pBt->db); +} #endif /* @@ -55053,7 +58279,7 @@ static int btreeSetHasContent(BtShared *pBt, Pgno pgno){ assert( pgno<=pBt->nPage ); pBt->pHasContent = sqlite3BitvecCreate(pBt->nPage); if( !pBt->pHasContent ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } } if( rc==SQLITE_OK && pgno<=sqlite3BitvecSize(pBt->pHasContent) ){ @@ -55132,7 +58358,7 @@ static int saveCursorKey(BtCursor *pCur){ sqlite3_free(pKey); } }else{ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } } assert( !pCur->curIntKey || !pCur->pKey ); @@ -55264,7 +58490,7 @@ static int btreeMoveto( pIdxKey = sqlite3VdbeAllocUnpackedRecord( pCur->pKeyInfo, aSpace, sizeof(aSpace), &pFree ); - if( pIdxKey==0 ) return SQLITE_NOMEM; + if( pIdxKey==0 ) return SQLITE_NOMEM_BKPT; sqlite3VdbeRecordUnpack(pCur->pKeyInfo, (int)nKey, pKey, pIdxKey); if( pIdxKey->nField==0 ){ sqlite3DbFree(pCur->pKeyInfo->db, pFree); @@ -55290,7 +58516,7 @@ static int btreeMoveto( static int btreeRestoreCursorPosition(BtCursor *pCur){ int rc; int skipNext; - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pCur->eState>=CURSOR_REQUIRESEEK ); if( pCur->eState==CURSOR_FAULT ){ return pCur->skipNext; @@ -55362,6 +58588,26 @@ SQLITE_PRIVATE int sqlite3BtreeCursorRestore(BtCursor *pCur, int *pDifferentRow) return SQLITE_OK; } +#ifdef SQLITE_ENABLE_CURSOR_HINTS +/* +** Provide hints to the cursor. The particular hint given (and the type +** and number of the varargs parameters) is determined by the eHintType +** parameter. See the definitions of the BTREE_HINT_* macros for details. +*/ +SQLITE_PRIVATE void sqlite3BtreeCursorHint(BtCursor *pCur, int eHintType, ...){ + /* Used only by system that substitute their own storage engine */ +} +#endif + +/* +** Provide flag hints to the cursor. +*/ +SQLITE_PRIVATE void sqlite3BtreeCursorHintFlags(BtCursor *pCur, unsigned x){ + assert( x==BTREE_SEEK_EQ || x==BTREE_BULKLOAD || x==0 ); + pCur->hints = x; +} + + #ifndef SQLITE_OMIT_AUTOVACUUM /* ** Given a page number of a regular database page, return the page @@ -55415,7 +58661,7 @@ static void ptrmapPut(BtShared *pBt, Pgno key, u8 eType, Pgno parent, int *pRC){ return; } iPtrmap = PTRMAP_PAGENO(pBt, key); - rc = sqlite3PagerGet(pBt->pPager, iPtrmap, &pDbPage); + rc = sqlite3PagerGet(pBt->pPager, iPtrmap, &pDbPage, 0); if( rc!=SQLITE_OK ){ *pRC = rc; return; @@ -55458,7 +58704,7 @@ static int ptrmapGet(BtShared *pBt, Pgno key, u8 *pEType, Pgno *pPgno){ assert( sqlite3_mutex_held(pBt->mutex) ); iPtrmap = PTRMAP_PAGENO(pBt, key); - rc = sqlite3PagerGet(pBt->pPager, iPtrmap, &pDbPage); + rc = sqlite3PagerGet(pBt->pPager, iPtrmap, &pDbPage, 0); if( rc!=0 ){ return rc; } @@ -55535,8 +58781,7 @@ static SQLITE_NOINLINE void btreeParseCellAdjustSizeForOverflow( }else{ pInfo->nLocal = (u16)minLocal; } - pInfo->iOverflow = (u16)(&pInfo->pPayload[pInfo->nLocal] - pCell); - pInfo->nSize = pInfo->iOverflow + 4; + pInfo->nSize = (u16)(&pInfo->pPayload[pInfo->nLocal] - pCell) + 4; } /* @@ -55560,7 +58805,6 @@ static void btreeParseCellPtrNoPayload( ){ assert( sqlite3_mutex_held(pPage->pBt->mutex) ); assert( pPage->leaf==0 ); - assert( pPage->noPayload ); assert( pPage->childPtrSize==4 ); #ifndef SQLITE_DEBUG UNUSED_PARAMETER(pPage); @@ -55568,7 +58812,6 @@ static void btreeParseCellPtrNoPayload( pInfo->nSize = 4 + getVarint(&pCell[4], (u64*)&pInfo->nKey); pInfo->nPayload = 0; pInfo->nLocal = 0; - pInfo->iOverflow = 0; pInfo->pPayload = 0; return; } @@ -55583,8 +58826,6 @@ static void btreeParseCellPtr( assert( sqlite3_mutex_held(pPage->pBt->mutex) ); assert( pPage->leaf==0 || pPage->leaf==1 ); - assert( pPage->intKeyLeaf || pPage->noPayload ); - assert( pPage->noPayload==0 ); assert( pPage->intKeyLeaf ); assert( pPage->childPtrSize==0 ); pIter = pCell; @@ -55638,7 +58879,6 @@ static void btreeParseCellPtr( pInfo->nSize = nPayload + (u16)(pIter - pCell); if( pInfo->nSize<4 ) pInfo->nSize = 4; pInfo->nLocal = (u16)nPayload; - pInfo->iOverflow = 0; }else{ btreeParseCellAdjustSizeForOverflow(pPage, pCell, pInfo); } @@ -55654,7 +58894,6 @@ static void btreeParseCellPtrIndex( assert( sqlite3_mutex_held(pPage->pBt->mutex) ); assert( pPage->leaf==0 || pPage->leaf==1 ); assert( pPage->intKeyLeaf==0 ); - assert( pPage->noPayload==0 ); pIter = pCell + pPage->childPtrSize; nPayload = *pIter; if( nPayload>=0x80 ){ @@ -55677,7 +58916,6 @@ static void btreeParseCellPtrIndex( pInfo->nSize = nPayload + (u16)(pIter - pCell); if( pInfo->nSize<4 ) pInfo->nSize = 4; pInfo->nLocal = (u16)nPayload; - pInfo->iOverflow = 0; }else{ btreeParseCellAdjustSizeForOverflow(pPage, pCell, pInfo); } @@ -55716,7 +58954,6 @@ static u16 cellSizePtr(MemPage *pPage, u8 *pCell){ pPage->xParseCell(pPage, pCell, &debuginfo); #endif - assert( pPage->noPayload==0 ); nSize = *pIter; if( nSize>=0x80 ){ pEnd = &pIter[8]; @@ -55793,8 +59030,8 @@ static void ptrmapPutOvflPtr(MemPage *pPage, u8 *pCell, int *pRC){ if( *pRC ) return; assert( pCell!=0 ); pPage->xParseCell(pPage, pCell, &info); - if( info.iOverflow ){ - Pgno ovfl = get4byte(&pCell[info.iOverflow]); + if( info.nLocalpBt, ovfl, PTRMAP_OVERFLOW1, pPage->pgno, pRC); } } @@ -56165,35 +59402,32 @@ static int decodeFlags(MemPage *pPage, int flagByte){ pPage->xCellSize = cellSizePtr; pBt = pPage->pBt; if( flagByte==(PTF_LEAFDATA | PTF_INTKEY) ){ - /* EVIDENCE-OF: R-03640-13415 A value of 5 means the page is an interior - ** table b-tree page. */ + /* EVIDENCE-OF: R-07291-35328 A value of 5 (0x05) means the page is an + ** interior table b-tree page. */ assert( (PTF_LEAFDATA|PTF_INTKEY)==5 ); - /* EVIDENCE-OF: R-20501-61796 A value of 13 means the page is a leaf - ** table b-tree page. */ + /* EVIDENCE-OF: R-26900-09176 A value of 13 (0x0d) means the page is a + ** leaf table b-tree page. */ assert( (PTF_LEAFDATA|PTF_INTKEY|PTF_LEAF)==13 ); pPage->intKey = 1; if( pPage->leaf ){ pPage->intKeyLeaf = 1; - pPage->noPayload = 0; pPage->xParseCell = btreeParseCellPtr; }else{ pPage->intKeyLeaf = 0; - pPage->noPayload = 1; pPage->xCellSize = cellSizePtrNoPayload; pPage->xParseCell = btreeParseCellPtrNoPayload; } pPage->maxLocal = pBt->maxLeaf; pPage->minLocal = pBt->minLeaf; }else if( flagByte==PTF_ZERODATA ){ - /* EVIDENCE-OF: R-27225-53936 A value of 2 means the page is an interior - ** index b-tree page. */ + /* EVIDENCE-OF: R-43316-37308 A value of 2 (0x02) means the page is an + ** interior index b-tree page. */ assert( (PTF_ZERODATA)==2 ); - /* EVIDENCE-OF: R-16571-11615 A value of 10 means the page is a leaf - ** index b-tree page. */ + /* EVIDENCE-OF: R-59615-42828 A value of 10 (0x0a) means the page is a + ** leaf index b-tree page. */ assert( (PTF_ZERODATA|PTF_LEAF)==10 ); pPage->intKey = 0; pPage->intKeyLeaf = 0; - pPage->noPayload = 0; pPage->xParseCell = btreeParseCellPtrIndex; pPage->maxLocal = pBt->maxLocal; pPage->minLocal = pBt->minLocal; @@ -56385,11 +59619,14 @@ static void zeroPage(MemPage *pPage, int flags){ */ static MemPage *btreePageFromDbPage(DbPage *pDbPage, Pgno pgno, BtShared *pBt){ MemPage *pPage = (MemPage*)sqlite3PagerGetExtra(pDbPage); - pPage->aData = sqlite3PagerGetData(pDbPage); - pPage->pDbPage = pDbPage; - pPage->pBt = pBt; - pPage->pgno = pgno; - pPage->hdrOffset = pgno==1 ? 100 : 0; + if( pgno!=pPage->pgno ){ + pPage->aData = sqlite3PagerGetData(pDbPage); + pPage->pDbPage = pDbPage; + pPage->pBt = pBt; + pPage->pgno = pgno; + pPage->hdrOffset = pgno==1 ? 100 : 0; + } + assert( pPage->aData==sqlite3PagerGetData(pDbPage) ); return pPage; } @@ -56415,7 +59652,7 @@ static int btreeGetPage( assert( flags==0 || flags==PAGER_GET_NOCONTENT || flags==PAGER_GET_READONLY ); assert( sqlite3_mutex_held(pBt->mutex) ); - rc = sqlite3PagerAcquire(pBt->pPager, pgno, (DbPage**)&pDbPage, flags); + rc = sqlite3PagerGet(pBt->pPager, pgno, (DbPage**)&pDbPage, flags); if( rc ) return rc; *ppPage = btreePageFromDbPage(pDbPage, pgno, pBt); return SQLITE_OK; @@ -56480,24 +59717,25 @@ static int getAndInitPage( rc = SQLITE_CORRUPT_BKPT; goto getAndInitPage_error; } - rc = sqlite3PagerAcquire(pBt->pPager, pgno, (DbPage**)&pDbPage, bReadOnly); + rc = sqlite3PagerGet(pBt->pPager, pgno, (DbPage**)&pDbPage, bReadOnly); if( rc ){ goto getAndInitPage_error; } - *ppPage = btreePageFromDbPage(pDbPage, pgno, pBt); + *ppPage = (MemPage*)sqlite3PagerGetExtra(pDbPage); if( (*ppPage)->isInit==0 ){ + btreePageFromDbPage(pDbPage, pgno, pBt); rc = btreeInitPage(*ppPage); if( rc!=SQLITE_OK ){ releasePage(*ppPage); goto getAndInitPage_error; } } + assert( (*ppPage)->pgno==pgno ); + assert( (*ppPage)->aData==sqlite3PagerGetData(pDbPage) ); /* If obtaining a child page for a cursor, we must verify that the page is ** compatible with the root page. */ - if( pCur - && ((*ppPage)->nCell<1 || (*ppPage)->intKey!=pCur->curIntKey) - ){ + if( pCur && ((*ppPage)->nCell<1 || (*ppPage)->intKey!=pCur->curIntKey) ){ rc = SQLITE_CORRUPT_BKPT; releasePage(*ppPage); goto getAndInitPage_error; @@ -56664,7 +59902,7 @@ SQLITE_PRIVATE int sqlite3BtreeOpen( } p = sqlite3MallocZero(sizeof(Btree)); if( !p ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } p->inTrans = TRANS_NONE; p->db = db; @@ -56688,7 +59926,7 @@ SQLITE_PRIVATE int sqlite3BtreeOpen( p->sharable = 1; if( !zFullPathname ){ sqlite3_free(p); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } if( isMemdb ){ memcpy(zFullPathname, zFilename, nFilename); @@ -56756,7 +59994,7 @@ SQLITE_PRIVATE int sqlite3BtreeOpen( pBt = sqlite3MallocZero( sizeof(*pBt) ); if( pBt==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto btree_open_out; } rc = sqlite3PagerOpen(pVfs, &pBt->pPager, zFilename, @@ -56825,8 +60063,7 @@ SQLITE_PRIVATE int sqlite3BtreeOpen( if( SQLITE_THREADSAFE && sqlite3GlobalConfig.bCoreMutex ){ pBt->mutex = sqlite3MutexAlloc(SQLITE_MUTEX_FAST); if( pBt->mutex==0 ){ - rc = SQLITE_NOMEM; - db->mallocFailed = 0; + rc = SQLITE_NOMEM_BKPT; goto btree_open_out; } } @@ -56849,12 +60086,12 @@ SQLITE_PRIVATE int sqlite3BtreeOpen( for(i=0; inDb; i++){ if( (pSib = db->aDb[i].pBt)!=0 && pSib->sharable ){ while( pSib->pPrev ){ pSib = pSib->pPrev; } - if( p->pBtpBt ){ + if( (uptr)p->pBt<(uptr)pSib->pBt ){ p->pNext = pSib; p->pPrev = 0; pSib->pPrev = p; }else{ - while( pSib->pNext && pSib->pNext->pBtpBt ){ + while( pSib->pNext && (uptr)pSib->pNext->pBt<(uptr)p->pBt ){ pSib = pSib->pNext; } p->pNext = pSib->pNext; @@ -57036,19 +60273,11 @@ SQLITE_PRIVATE int sqlite3BtreeClose(Btree *p){ } /* -** Change the limit on the number of pages allowed in the cache. -** -** The maximum number of cache pages is set to the absolute -** value of mxPage. If mxPage is negative, the pager will -** operate asynchronously - it will not stop to do fsync()s -** to insure data is written to the disk surface before -** continuing. Transactions still work if synchronous is off, -** and the database cannot be corrupted if this program -** crashes. But if the operating system crashes or there is -** an abrupt power failure when synchronous is off, the database -** could be left in an inconsistent and unrecoverable state. -** Synchronous is on by default so database corruption is not -** normally a worry. +** Change the "soft" limit on the number of pages in the cache. +** Unused and unmodified pages will be recycled when the number of +** pages in the cache exceeds this soft limit. But the size of the +** cache is allowed to grow larger than this limit if it contains +** dirty pages or pages still in active use. */ SQLITE_PRIVATE int sqlite3BtreeSetCacheSize(Btree *p, int mxPage){ BtShared *pBt = p->pBt; @@ -57059,6 +60288,26 @@ SQLITE_PRIVATE int sqlite3BtreeSetCacheSize(Btree *p, int mxPage){ return SQLITE_OK; } +/* +** Change the "spill" limit on the number of pages in the cache. +** If the number of pages exceeds this limit during a write transaction, +** the pager might attempt to "spill" pages to the journal early in +** order to free up memory. +** +** The value returned is the current spill size. If zero is passed +** as an argument, no changes are made to the spill size setting, so +** using mxPage of 0 is a way to query the current spill size. +*/ +SQLITE_PRIVATE int sqlite3BtreeSetSpillSize(Btree *p, int mxPage){ + BtShared *pBt = p->pBt; + int res; + assert( sqlite3_mutex_held(p->db->mutex) ); + sqlite3BtreeEnter(p); + res = sqlite3PagerSetSpillsize(pBt->pPager, mxPage); + sqlite3BtreeLeave(p); + return res; +} + #if SQLITE_MAX_MMAP_SIZE>0 /* ** Change the limit on the amount of the database file that may be @@ -57096,21 +60345,6 @@ SQLITE_PRIVATE int sqlite3BtreeSetPagerFlags( } #endif -/* -** Return TRUE if the given btree is set to safety level 1. In other -** words, return TRUE if no sync() occurs on the disk files. -*/ -SQLITE_PRIVATE int sqlite3BtreeSyncDisabled(Btree *p){ - BtShared *pBt = p->pBt; - int rc; - assert( sqlite3_mutex_held(p->db->mutex) ); - sqlite3BtreeEnter(p); - assert( pBt && pBt->pPager ); - rc = sqlite3PagerNosync(pBt->pPager); - sqlite3BtreeLeave(p); - return rc; -} - /* ** Change the default pages size and the number of reserved bytes per page. ** Or, if the page size has already been fixed, return SQLITE_READONLY @@ -57356,9 +60590,25 @@ static int lockBtree(BtShared *pBt){ rc = sqlite3PagerOpenWal(pBt->pPager, &isOpen); if( rc!=SQLITE_OK ){ goto page1_init_failed; - }else if( isOpen==0 ){ - releasePage(pPage1); - return SQLITE_OK; + }else{ +#if SQLITE_DEFAULT_SYNCHRONOUS!=SQLITE_DEFAULT_WAL_SYNCHRONOUS + sqlite3 *db; + Db *pDb; + if( (db=pBt->db)!=0 && (pDb=db->aDb)!=0 ){ + while( pDb->pBt==0 || pDb->pBt->pBt!=pBt ){ pDb++; } + if( pDb->bSyncSet==0 + && pDb->safety_level==SQLITE_DEFAULT_SYNCHRONOUS+1 + ){ + pDb->safety_level = SQLITE_DEFAULT_WAL_SYNCHRONOUS+1; + sqlite3PagerSetFlags(pBt->pPager, + pDb->safety_level | (db->flags & PAGER_FLAGS_MASK)); + } + } +#endif + if( isOpen==0 ){ + releasePage(pPage1); + return SQLITE_OK; + } } rc = SQLITE_NOTADB; } @@ -57598,7 +60848,6 @@ SQLITE_PRIVATE int sqlite3BtreeNewDb(Btree *p){ ** proceed. */ SQLITE_PRIVATE int sqlite3BtreeBeginTrans(Btree *p, int wrflag){ - sqlite3 *pBlock = 0; BtShared *pBt = p->pBt; int rc = SQLITE_OK; @@ -57621,27 +60870,30 @@ SQLITE_PRIVATE int sqlite3BtreeBeginTrans(Btree *p, int wrflag){ } #ifndef SQLITE_OMIT_SHARED_CACHE - /* If another database handle has already opened a write transaction - ** on this shared-btree structure and a second write transaction is - ** requested, return SQLITE_LOCKED. - */ - if( (wrflag && pBt->inTransaction==TRANS_WRITE) - || (pBt->btsFlags & BTS_PENDING)!=0 - ){ - pBlock = pBt->pWriter->db; - }else if( wrflag>1 ){ - BtLock *pIter; - for(pIter=pBt->pLock; pIter; pIter=pIter->pNext){ - if( pIter->pBtree!=p ){ - pBlock = pIter->pBtree->db; - break; + { + sqlite3 *pBlock = 0; + /* If another database handle has already opened a write transaction + ** on this shared-btree structure and a second write transaction is + ** requested, return SQLITE_LOCKED. + */ + if( (wrflag && pBt->inTransaction==TRANS_WRITE) + || (pBt->btsFlags & BTS_PENDING)!=0 + ){ + pBlock = pBt->pWriter->db; + }else if( wrflag>1 ){ + BtLock *pIter; + for(pIter=pBt->pLock; pIter; pIter=pIter->pNext){ + if( pIter->pBtree!=p ){ + pBlock = pIter->pBtree->db; + break; + } } } - } - if( pBlock ){ - sqlite3ConnectionBlocked(p->db, pBlock); - rc = SQLITE_LOCKED_SHAREDCACHE; - goto trans_begun; + if( pBlock ){ + sqlite3ConnectionBlocked(p->db, pBlock); + rc = SQLITE_LOCKED_SHAREDCACHE; + goto trans_begun; + } } #endif @@ -57816,11 +61068,11 @@ static int modifyPagePointer(MemPage *pPage, Pgno iFrom, Pgno iTo, u8 eType){ if( eType==PTRMAP_OVERFLOW1 ){ CellInfo info; pPage->xParseCell(pPage, pCell, &info); - if( info.iOverflow - && pCell+info.iOverflow+3<=pPage->aData+pPage->maskPage - && iFrom==get4byte(&pCell[info.iOverflow]) + if( info.nLocalaData+pPage->maskPage + && iFrom==get4byte(pCell+info.nSize-4) ){ - put4byte(&pCell[info.iOverflow], iTo); + put4byte(pCell+info.nSize-4, iTo); break; } }else{ @@ -58523,13 +61775,13 @@ SQLITE_PRIVATE int sqlite3BtreeSavepoint(Btree *p, int op, int iSavepoint){ ** on the database already. If a write-cursor is requested, then ** the caller is assumed to have an open write transaction. ** -** If wrFlag==0, then the cursor can only be used for reading. -** If wrFlag==1, then the cursor can be used for reading or for -** writing if other conditions for writing are also met. These -** are the conditions that must be met in order for writing to -** be allowed: +** If the BTREE_WRCSR bit of wrFlag is clear, then the cursor can only +** be used for reading. If the BTREE_WRCSR bit is set, then the cursor +** can be used for reading or for writing if other conditions for writing +** are also met. These are the conditions that must be met in order +** for writing to be allowed: ** -** 1: The cursor must have been opened with wrFlag==1 +** 1: The cursor must have been opened with wrFlag containing BTREE_WRCSR ** ** 2: Other database connections that share the same pager cache ** but which are not in the READ_UNCOMMITTED state may not have @@ -58541,6 +61793,16 @@ SQLITE_PRIVATE int sqlite3BtreeSavepoint(Btree *p, int op, int iSavepoint){ ** ** 4: There must be an active transaction. ** +** The BTREE_FORDELETE bit of wrFlag may optionally be set if BTREE_WRCSR +** is set. If FORDELETE is set, that is a hint to the implementation that +** this cursor will only be used to seek to and delete entries of an index +** as part of a larger DELETE statement. The FORDELETE hint is not used by +** this implementation. But in a hypothetical alternative storage engine +** in which index entries are automatically deleted when corresponding table +** rows are deleted, the FORDELETE flag is a hint that all SEEK and DELETE +** operations on this cursor can be no-ops and all READ operations can +** return a null row (2-bytes: 0x01 0x00). +** ** No checking is done to make sure that page iTable really is the ** root page of a b-tree. If it is not, then the cursor acquired ** will not work correctly. @@ -58559,13 +61821,16 @@ static int btreeCursor( BtCursor *pX; /* Looping over other all cursors */ assert( sqlite3BtreeHoldsMutex(p) ); - assert( wrFlag==0 || wrFlag==1 ); + assert( wrFlag==0 + || wrFlag==BTREE_WRCSR + || wrFlag==(BTREE_WRCSR|BTREE_FORDELETE) + ); /* The following assert statements verify that if this is a sharable ** b-tree database, the connection is holding the required table locks, ** and that no other connection has any open cursor that conflicts with ** this lock. */ - assert( hasSharedCacheTableLock(p, iTable, pKeyInfo!=0, wrFlag+1) ); + assert( hasSharedCacheTableLock(p, iTable, pKeyInfo!=0, (wrFlag?2:1)) ); assert( wrFlag==0 || !hasReadConflicts(p, iTable) ); /* Assert that the caller has opened the required transaction. */ @@ -58576,7 +61841,7 @@ static int btreeCursor( if( wrFlag ){ allocateTempSpace(pBt); - if( pBt->pTmpSpace==0 ) return SQLITE_NOMEM; + if( pBt->pTmpSpace==0 ) return SQLITE_NOMEM_BKPT; } if( iTable==1 && btreePagecount(pBt)==0 ){ assert( wrFlag==0 ); @@ -58590,8 +61855,7 @@ static int btreeCursor( pCur->pKeyInfo = pKeyInfo; pCur->pBtree = p; pCur->pBt = pBt; - assert( wrFlag==0 || wrFlag==BTCF_WriteFlag ); - pCur->curFlags = wrFlag; + pCur->curFlags = wrFlag ? BTCF_WriteFlag : 0; pCur->curPagerFlags = wrFlag ? 0 : PAGER_GET_READONLY; /* If there are two or more cursors on the same btree, then all such ** cursors *must* have the BTCF_Multiple flag set. */ @@ -58756,7 +62020,7 @@ SQLITE_PRIVATE int sqlite3BtreeKeySize(BtCursor *pCur, i64 *pSize){ ** to return an integer result code for historical reasons. */ SQLITE_PRIVATE int sqlite3BtreeDataSize(BtCursor *pCur, u32 *pSize){ - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pCur->eState==CURSOR_VALID ); assert( pCur->iPage>=0 ); assert( pCur->iPageinfo.nPayload ); - if( &aPayload[pCur->info.nLocal] > &pPage->aData[pBt->usableSize] ){ - /* Trying to read or write past the end of the data is an error */ + assert( aPayload > pPage->aData ); + if( (uptr)(aPayload - pPage->aData) > (pBt->usableSize - pCur->info.nLocal) ){ + /* Trying to read or write past the end of the data is an error. The + ** conditional above is really: + ** &aPayload[pCur->info.nLocal] > &pPage->aData[pBt->usableSize] + ** but is recast into its current form to avoid integer overflow problems + */ return SQLITE_CORRUPT_BKPT; } @@ -58975,7 +62244,7 @@ static int accessPayload( pCur->aOverflow, nOvfl*2*sizeof(Pgno) ); if( aNew==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else{ pCur->nOvflAlloc = nOvfl*2; pCur->aOverflow = aNew; @@ -59003,7 +62272,9 @@ static int accessPayload( /* If required, populate the overflow page-list cache. */ if( (pCur->curFlags & BTCF_ValidOvfl)!=0 ){ - assert(!pCur->aOverflow[iIdx] || pCur->aOverflow[iIdx]==nextPage); + assert( pCur->aOverflow[iIdx]==0 + || pCur->aOverflow[iIdx]==nextPage + || CORRUPT_DB ); pCur->aOverflow[iIdx] = nextPage; } @@ -59073,7 +62344,7 @@ static int accessPayload( { DbPage *pDbPage; - rc = sqlite3PagerAcquire(pBt->pPager, nextPage, &pDbPage, + rc = sqlite3PagerGet(pBt->pPager, nextPage, &pDbPage, ((eOp&0x01)==0 ? PAGER_GET_READONLY : 0) ); if( rc==SQLITE_OK ){ @@ -59134,7 +62405,7 @@ SQLITE_PRIVATE int sqlite3BtreeData(BtCursor *pCur, u32 offset, u32 amt, void *p } #endif - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); rc = restoreCursorPosition(pCur); if( rc==SQLITE_OK ){ assert( pCur->eState==CURSOR_VALID ); @@ -59172,7 +62443,7 @@ static const void *fetchPayload( assert( pCur!=0 && pCur->iPage>=0 && pCur->apPage[pCur->iPage]); assert( pCur->eState==CURSOR_VALID ); assert( sqlite3_mutex_held(pCur->pBtree->db->mutex) ); - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pCur->aiIdx[pCur->iPage]apPage[pCur->iPage]->nCell ); assert( pCur->info.nSize>0 ); assert( pCur->info.pPayload>pCur->apPage[pCur->iPage]->aData || CORRUPT_DB ); @@ -59218,7 +62489,7 @@ SQLITE_PRIVATE const void *sqlite3BtreeDataFetch(BtCursor *pCur, u32 *pAmt){ static int moveToChild(BtCursor *pCur, u32 newPgno){ BtShared *pBt = pCur->pBt; - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pCur->eState==CURSOR_VALID ); assert( pCur->iPageiPage>=0 ); @@ -59264,7 +62535,7 @@ static void assertParentIndex(MemPage *pParent, int iIdx, Pgno iChild){ ** the largest cell index. */ static void moveToParent(BtCursor *pCur){ - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pCur->eState==CURSOR_VALID ); assert( pCur->iPage>0 ); assert( pCur->apPage[pCur->iPage] ); @@ -59304,7 +62575,7 @@ static int moveToRoot(BtCursor *pCur){ MemPage *pRoot; int rc = SQLITE_OK; - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( CURSOR_INVALID < CURSOR_REQUIRESEEK ); assert( CURSOR_VALID < CURSOR_REQUIRESEEK ); assert( CURSOR_FAULT > CURSOR_REQUIRESEEK ); @@ -59383,7 +62654,7 @@ static int moveToLeftmost(BtCursor *pCur){ int rc = SQLITE_OK; MemPage *pPage; - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pCur->eState==CURSOR_VALID ); while( rc==SQLITE_OK && !(pPage = pCur->apPage[pCur->iPage])->leaf ){ assert( pCur->aiIdx[pCur->iPage]nCell ); @@ -59408,7 +62679,7 @@ static int moveToRightmost(BtCursor *pCur){ int rc = SQLITE_OK; MemPage *pPage = 0; - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pCur->eState==CURSOR_VALID ); while( !(pPage = pCur->apPage[pCur->iPage])->leaf ){ pgno = get4byte(&pPage->aData[pPage->hdrOffset+8]); @@ -59429,7 +62700,7 @@ static int moveToRightmost(BtCursor *pCur){ SQLITE_PRIVATE int sqlite3BtreeFirst(BtCursor *pCur, int *pRes){ int rc; - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( sqlite3_mutex_held(pCur->pBtree->db->mutex) ); rc = moveToRoot(pCur); if( rc==SQLITE_OK ){ @@ -59452,7 +62723,7 @@ SQLITE_PRIVATE int sqlite3BtreeFirst(BtCursor *pCur, int *pRes){ SQLITE_PRIVATE int sqlite3BtreeLast(BtCursor *pCur, int *pRes){ int rc; - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( sqlite3_mutex_held(pCur->pBtree->db->mutex) ); /* If the cursor already points to the last entry, this is a no-op. */ @@ -59517,6 +62788,8 @@ SQLITE_PRIVATE int sqlite3BtreeLast(BtCursor *pCur, int *pRes){ ** *pRes>0 The cursor is left pointing at an entry that ** is larger than intKey/pIdxKey. ** +** For index tables, the pIdxKey->eqSeen field is set to 1 if there +** exists an entry in the table that exactly matches pIdxKey. */ SQLITE_PRIVATE int sqlite3BtreeMovetoUnpacked( BtCursor *pCur, /* The cursor to be moved */ @@ -59528,7 +62801,7 @@ SQLITE_PRIVATE int sqlite3BtreeMovetoUnpacked( int rc; RecordCompare xRecordCompare; - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( sqlite3_mutex_held(pCur->pBtree->db->mutex) ); assert( pRes ); assert( (pIdxKey==0)==(pCur->pKeyInfo==0) ); @@ -59676,7 +62949,7 @@ SQLITE_PRIVATE int sqlite3BtreeMovetoUnpacked( } pCellKey = sqlite3Malloc( nCell+18 ); if( pCellKey==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto moveto_finish; } pCur->aiIdx[pCur->iPage] = (u16)idx; @@ -59776,7 +63049,7 @@ static SQLITE_NOINLINE int btreeNext(BtCursor *pCur, int *pRes){ int idx; MemPage *pPage; - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pCur->skipNext==0 || pCur->eState!=CURSOR_VALID ); assert( *pRes==0 ); if( pCur->eState!=CURSOR_VALID ){ @@ -59840,7 +63113,7 @@ static SQLITE_NOINLINE int btreeNext(BtCursor *pCur, int *pRes){ } SQLITE_PRIVATE int sqlite3BtreeNext(BtCursor *pCur, int *pRes){ MemPage *pPage; - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pRes!=0 ); assert( *pRes==0 || *pRes==1 ); assert( pCur->skipNext==0 || pCur->eState!=CURSOR_VALID ); @@ -59885,7 +63158,7 @@ static SQLITE_NOINLINE int btreePrevious(BtCursor *pCur, int *pRes){ int rc; MemPage *pPage; - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pRes!=0 ); assert( *pRes==0 ); assert( pCur->skipNext==0 || pCur->eState!=CURSOR_VALID ); @@ -59941,7 +63214,7 @@ static SQLITE_NOINLINE int btreePrevious(BtCursor *pCur, int *pRes){ return rc; } SQLITE_PRIVATE int sqlite3BtreePrevious(BtCursor *pCur, int *pRes){ - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pRes!=0 ); assert( *pRes==0 || *pRes==1 ); assert( pCur->skipNext==0 || pCur->eState!=CURSOR_VALID ); @@ -60456,13 +63729,13 @@ static int clearCell( assert( sqlite3_mutex_held(pPage->pBt->mutex) ); pPage->xParseCell(pPage, pCell, &info); *pnSize = info.nSize; - if( info.iOverflow==0 ){ + if( info.nLocal==info.nPayload ){ return SQLITE_OK; /* No overflow pages. Return without doing anything */ } - if( pCell+info.iOverflow+3 > pPage->aData+pPage->maskPage ){ + if( pCell+info.nSize-1 > pPage->aData+pPage->maskPage ){ return SQLITE_CORRUPT_BKPT; /* Cell extends past end of page */ } - ovflPgno = get4byte(&pCell[info.iOverflow]); + ovflPgno = get4byte(pCell + info.nSize - 4); assert( pBt->usableSize > 4 ); ovflPageSize = pBt->usableSize - 4; nOvfl = (info.nPayload - info.nLocal + ovflPageSize - 1)/ovflPageSize; @@ -60607,11 +63880,10 @@ static int fillInCell( { CellInfo info; pPage->xParseCell(pPage, pCell, &info); - assert( nHeader=(int)(info.pPayload - pCell) ); + assert( nHeader==(int)(info.pPayload - pCell) ); assert( info.nKey==nKey ); assert( *pnSize == info.nSize ); assert( spaceLeft == info.nLocal ); - assert( pPrior == &pCell[info.iOverflow] ); } #endif @@ -60932,7 +64204,7 @@ static int rebuildPage( pData = pEnd; for(i=0; iaData && pCellapCell[i] will never overlap on a well-formed @@ -61043,7 +64315,7 @@ static int pageFreeArray( for(i=iFirst; iapCell[i]; - if( pCell>=pStart && pCellapCell[i+iNew]; int iOff = get2byteAligned(&pPg->aCellIdx[i*2]); - if( pCell>=aData && pCell<&aData[pPg->pBt->usableSize] ){ + if( SQLITE_WITHIN(pCell, aData, &aData[pPg->pBt->usableSize]) ){ pCell = &pTmp[pCell - aData]; } assert( 0==memcmp(pCell, &aData[iOff], @@ -61321,8 +64593,8 @@ static int ptrmapCheckPages(MemPage **apPage, int nPage){ z = findCell(pPage, j); pPage->xParseCell(pPage, z, &info); - if( info.iOverflow ){ - Pgno ovfl = get4byte(&z[info.iOverflow]); + if( info.nLocalpgno && e==PTRMAP_OVERFLOW1 ); } @@ -61440,9 +64712,6 @@ static void copyNodeContent(MemPage *pFrom, MemPage *pTo, int *pRC){ ** If aOvflSpace is set to a null pointer, this function returns ** SQLITE_NOMEM. */ -#if defined(_MSC_VER) && _MSC_VER >= 1700 && defined(_M_ARM) -#pragma optimize("", off) -#endif static int balance_nonroot( MemPage *pParent, /* Parent page of siblings being balanced */ int iParentIdx, /* Index of "the page" in pParent */ @@ -61499,7 +64768,7 @@ static int balance_nonroot( assert( pParent->nOverflow==0 || pParent->aiOvfl[0]==iParentIdx ); if( !aOvflSpace ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } /* Find the sibling pages to balance. Also locate the cells in pParent @@ -61599,7 +64868,7 @@ static int balance_nonroot( assert( szScratch<=6*(int)pBt->pageSize ); b.apCell = sqlite3ScratchMalloc( szScratch ); if( b.apCell==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto balance_cleanup; } b.szCell = (u16*)&b.apCell[nMaxCells]; @@ -61658,9 +64927,8 @@ static int balance_nonroot( ** long be able to find the cells if a pointer to each cell is not saved ** first. */ - memset(&b.szCell[b.nCell], 0, sizeof(b.szCell[0])*limit); + memset(&b.szCell[b.nCell], 0, sizeof(b.szCell[0])*(limit+pOld->nOverflow)); if( pOld->nOverflow>0 ){ - memset(&b.szCell[b.nCell+limit], 0, sizeof(b.szCell[0])*pOld->nOverflow); limit = pOld->aiOvfl[0]; for(j=0; j=nNew || pNew->pgno!=aPgno[iOld] - || pCell=&aOld[usableSize] + || !SQLITE_WITHIN(pCell,aOld,&aOld[usableSize]) ){ if( !leafCorrection ){ ptrmapPut(pBt, get4byte(pCell), PTRMAP_BTREE, pNew->pgno, &rc); @@ -62036,9 +65303,9 @@ static int balance_nonroot( ** any cell). But it is important to pass the correct size to ** insertCell(), so reparse the cell now. ** - ** Note that this can never happen in an SQLite data file, as all - ** cells are at least 4 bytes. It only happens in b-trees used - ** to evaluate "IN (SELECT ...)" and similar clauses. + ** This can only happen for b-trees used to evaluate "IN (SELECT ...)" + ** and WITHOUT ROWID tables with exactly one column which is the + ** primary key. */ if( b.szCell[j]==4 ){ assert(leafCorrection==4); @@ -62188,9 +65455,6 @@ balance_cleanup: return rc; } -#if defined(_MSC_VER) && _MSC_VER >= 1700 && defined(_M_ARM) -#pragma optimize("", on) -#endif /* @@ -62275,8 +65539,8 @@ static int balance(BtCursor *pCur){ u8 aBalanceQuickSpace[13]; u8 *pFree = 0; - TESTONLY( int balance_quick_called = 0 ); - TESTONLY( int balance_deeper_called = 0 ); + VVA_ONLY( int balance_quick_called = 0 ); + VVA_ONLY( int balance_deeper_called = 0 ); do { int iPage = pCur->iPage; @@ -62289,7 +65553,8 @@ static int balance(BtCursor *pCur){ ** and copy the current contents of the root-page to it. The ** next iteration of the do-loop will balance the child page. */ - assert( (balance_deeper_called++)==0 ); + assert( balance_deeper_called==0 ); + VVA_ONLY( balance_deeper_called++ ); rc = balance_deeper(pPage, &pCur->apPage[1]); if( rc==SQLITE_OK ){ pCur->iPage = 1; @@ -62328,7 +65593,8 @@ static int balance(BtCursor *pCur){ ** function. If this were not verified, a subtle bug involving reuse ** of the aBalanceQuickSpace[] might sneak in. */ - assert( (balance_quick_called++)==0 ); + assert( balance_quick_called==0 ); + VVA_ONLY( balance_quick_called++ ); rc = balance_quick(pParent, pPage, aBalanceQuickSpace); }else #endif @@ -62429,7 +65695,7 @@ SQLITE_PRIVATE int sqlite3BtreeInsert( return pCur->skipNext; } - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( (pCur->curFlags & BTCF_WriteFlag)!=0 && pBt->inTransaction==TRANS_WRITE && (pBt->btsFlags & BTS_READ_ONLY)==0 ); @@ -62559,13 +65825,21 @@ end_insert: /* ** Delete the entry that the cursor is pointing to. ** -** If the second parameter is zero, then the cursor is left pointing at an -** arbitrary location after the delete. If it is non-zero, then the cursor -** is left in a state such that the next call to BtreeNext() or BtreePrev() -** moves it to the same row as it would if the call to BtreeDelete() had -** been omitted. +** If the BTREE_SAVEPOSITION bit of the flags parameter is zero, then +** the cursor is left pointing at an arbitrary location after the delete. +** But if that bit is set, then the cursor is left in a state such that +** the next call to BtreeNext() or BtreePrev() moves it to the same row +** as it would have been on if the call to BtreeDelete() had been omitted. +** +** The BTREE_AUXDELETE bit of flags indicates that is one of several deletes +** associated with a single table entry and its indexes. Only one of those +** deletes is considered the "primary" delete. The primary delete occurs +** on a cursor that is not a BTREE_FORDELETE cursor. All but one delete +** operation on non-FORDELETE cursors is tagged with the AUXDELETE flag. +** The BTREE_AUXDELETE bit is a hint that is not used by this implementation, +** but which might be used by alternative storage engines. */ -SQLITE_PRIVATE int sqlite3BtreeDelete(BtCursor *pCur, int bPreserve){ +SQLITE_PRIVATE int sqlite3BtreeDelete(BtCursor *pCur, u8 flags){ Btree *p = pCur->pBtree; BtShared *pBt = p->pBt; int rc; /* Return code */ @@ -62575,8 +65849,9 @@ SQLITE_PRIVATE int sqlite3BtreeDelete(BtCursor *pCur, int bPreserve){ int iCellDepth; /* Depth of node containing pCell */ u16 szCell; /* Size of the cell being deleted */ int bSkipnext = 0; /* Leaf cursor in SKIPNEXT state */ + u8 bPreserve = flags & BTREE_SAVEPOSITION; /* Keep cursor valid */ - assert( cursorHoldsMutex(pCur) ); + assert( cursorOwnsBtShared(pCur) ); assert( pBt->inTransaction==TRANS_WRITE ); assert( (pBt->btsFlags & BTS_READ_ONLY)==0 ); assert( pCur->curFlags & BTCF_WriteFlag ); @@ -62584,12 +65859,35 @@ SQLITE_PRIVATE int sqlite3BtreeDelete(BtCursor *pCur, int bPreserve){ assert( !hasReadConflicts(p, pCur->pgnoRoot) ); assert( pCur->aiIdx[pCur->iPage]apPage[pCur->iPage]->nCell ); assert( pCur->eState==CURSOR_VALID ); + assert( (flags & ~(BTREE_SAVEPOSITION | BTREE_AUXDELETE))==0 ); iCellDepth = pCur->iPage; iCellIdx = pCur->aiIdx[iCellDepth]; pPage = pCur->apPage[iCellDepth]; pCell = findCell(pPage, iCellIdx); + /* If the bPreserve flag is set to true, then the cursor position must + ** be preserved following this delete operation. If the current delete + ** will cause a b-tree rebalance, then this is done by saving the cursor + ** key and leaving the cursor in CURSOR_REQUIRESEEK state before + ** returning. + ** + ** Or, if the current delete will not cause a rebalance, then the cursor + ** will be left in CURSOR_SKIPNEXT state pointing to the entry immediately + ** before or after the deleted entry. In this case set bSkipnext to true. */ + if( bPreserve ){ + if( !pPage->leaf + || (pPage->nFree+cellSizePtr(pPage,pCell)+2)>(int)(pBt->usableSize*2/3) + ){ + /* A b-tree rebalance will be required after deleting this entry. + ** Save the cursor key. */ + rc = saveCursorKey(pCur); + if( rc ) return rc; + }else{ + bSkipnext = 1; + } + } + /* If the page containing the entry to delete is not a leaf page, move ** the cursor to the largest entry in the tree that is smaller than ** the entry being deleted. This cell will replace the cell being deleted @@ -62616,28 +65914,6 @@ SQLITE_PRIVATE int sqlite3BtreeDelete(BtCursor *pCur, int bPreserve){ invalidateIncrblobCursors(p, pCur->info.nKey, 0); } - /* If the bPreserve flag is set to true, then the cursor position must - ** be preserved following this delete operation. If the current delete - ** will cause a b-tree rebalance, then this is done by saving the cursor - ** key and leaving the cursor in CURSOR_REQUIRESEEK state before - ** returning. - ** - ** Or, if the current delete will not cause a rebalance, then the cursor - ** will be left in CURSOR_SKIPNEXT state pointing to the entry immediately - ** before or after the deleted entry. In this case set bSkipnext to true. */ - if( bPreserve ){ - if( !pPage->leaf - || (pPage->nFree+cellSizePtr(pPage,pCell)+2)>(int)(pBt->usableSize*2/3) - ){ - /* A b-tree rebalance will be required after deleting this entry. - ** Save the cursor key. */ - rc = saveCursorKey(pCur); - if( rc ) return rc; - }else{ - bSkipnext = 1; - } - } - /* Make the page containing the entry to be deleted writable. Then free any ** overflow pages associated with the entry and finally remove the cell ** itself from within the page. */ @@ -62695,8 +65971,8 @@ SQLITE_PRIVATE int sqlite3BtreeDelete(BtCursor *pCur, int bPreserve){ if( rc==SQLITE_OK ){ if( bSkipnext ){ - assert( bPreserve && pCur->iPage==iCellDepth ); - assert( pPage==pCur->apPage[pCur->iPage] ); + assert( bPreserve && (pCur->iPage==iCellDepth || CORRUPT_DB) ); + assert( pPage==pCur->apPage[pCur->iPage] || CORRUPT_DB ); assert( (pPage->nCell>0 || CORRUPT_DB) && iCellIdx<=pPage->nCell ); pCur->eState = CURSOR_SKIPNEXT; if( iCellIdx>=pPage->nCell ){ @@ -63017,6 +66293,14 @@ static int btreeDropTable(Btree *p, Pgno iTable, int *piMoved){ return SQLITE_LOCKED_SHAREDCACHE; } + /* + ** It is illegal to drop the sqlite_master table on page 1. But again, + ** this error is caught long before reaching this point. + */ + if( NEVER(iTable<2) ){ + return SQLITE_CORRUPT_BKPT; + } + rc = btreeGetPage(pBt, (Pgno)iTable, &pPage, 0); if( rc ) return rc; rc = sqlite3BtreeClearTable(p, iTable, 0); @@ -63027,76 +66311,67 @@ static int btreeDropTable(Btree *p, Pgno iTable, int *piMoved){ *piMoved = 0; - if( iTable>1 ){ #ifdef SQLITE_OMIT_AUTOVACUUM - freePage(pPage, &rc); - releasePage(pPage); + freePage(pPage, &rc); + releasePage(pPage); #else - if( pBt->autoVacuum ){ - Pgno maxRootPgno; - sqlite3BtreeGetMeta(p, BTREE_LARGEST_ROOT_PAGE, &maxRootPgno); + if( pBt->autoVacuum ){ + Pgno maxRootPgno; + sqlite3BtreeGetMeta(p, BTREE_LARGEST_ROOT_PAGE, &maxRootPgno); - if( iTable==maxRootPgno ){ - /* If the table being dropped is the table with the largest root-page - ** number in the database, put the root page on the free list. - */ - freePage(pPage, &rc); - releasePage(pPage); - if( rc!=SQLITE_OK ){ - return rc; - } - }else{ - /* The table being dropped does not have the largest root-page - ** number in the database. So move the page that does into the - ** gap left by the deleted root-page. - */ - MemPage *pMove; - releasePage(pPage); - rc = btreeGetPage(pBt, maxRootPgno, &pMove, 0); - if( rc!=SQLITE_OK ){ - return rc; - } - rc = relocatePage(pBt, pMove, PTRMAP_ROOTPAGE, 0, iTable, 0); - releasePage(pMove); - if( rc!=SQLITE_OK ){ - return rc; - } - pMove = 0; - rc = btreeGetPage(pBt, maxRootPgno, &pMove, 0); - freePage(pMove, &rc); - releasePage(pMove); - if( rc!=SQLITE_OK ){ - return rc; - } - *piMoved = maxRootPgno; - } - - /* Set the new 'max-root-page' value in the database header. This - ** is the old value less one, less one more if that happens to - ** be a root-page number, less one again if that is the - ** PENDING_BYTE_PAGE. + if( iTable==maxRootPgno ){ + /* If the table being dropped is the table with the largest root-page + ** number in the database, put the root page on the free list. */ - maxRootPgno--; - while( maxRootPgno==PENDING_BYTE_PAGE(pBt) - || PTRMAP_ISPAGE(pBt, maxRootPgno) ){ - maxRootPgno--; - } - assert( maxRootPgno!=PENDING_BYTE_PAGE(pBt) ); - - rc = sqlite3BtreeUpdateMeta(p, 4, maxRootPgno); - }else{ freePage(pPage, &rc); releasePage(pPage); + if( rc!=SQLITE_OK ){ + return rc; + } + }else{ + /* The table being dropped does not have the largest root-page + ** number in the database. So move the page that does into the + ** gap left by the deleted root-page. + */ + MemPage *pMove; + releasePage(pPage); + rc = btreeGetPage(pBt, maxRootPgno, &pMove, 0); + if( rc!=SQLITE_OK ){ + return rc; + } + rc = relocatePage(pBt, pMove, PTRMAP_ROOTPAGE, 0, iTable, 0); + releasePage(pMove); + if( rc!=SQLITE_OK ){ + return rc; + } + pMove = 0; + rc = btreeGetPage(pBt, maxRootPgno, &pMove, 0); + freePage(pMove, &rc); + releasePage(pMove); + if( rc!=SQLITE_OK ){ + return rc; + } + *piMoved = maxRootPgno; } -#endif - }else{ - /* If sqlite3BtreeDropTable was called on page 1. - ** This really never should happen except in a corrupt - ** database. + + /* Set the new 'max-root-page' value in the database header. This + ** is the old value less one, less one more if that happens to + ** be a root-page number, less one again if that is the + ** PENDING_BYTE_PAGE. */ - zeroPage(pPage, PTF_INTKEY|PTF_LEAF ); + maxRootPgno--; + while( maxRootPgno==PENDING_BYTE_PAGE(pBt) + || PTRMAP_ISPAGE(pBt, maxRootPgno) ){ + maxRootPgno--; + } + assert( maxRootPgno!=PENDING_BYTE_PAGE(pBt) ); + + rc = sqlite3BtreeUpdateMeta(p, 4, maxRootPgno); + }else{ + freePage(pPage, &rc); releasePage(pPage); } +#endif return rc; } SQLITE_PRIVATE int sqlite3BtreeDropTable(Btree *p, int iTable, int *piMoved){ @@ -63283,9 +66558,9 @@ static void checkAppendMsg( sqlite3StrAccumAppend(&pCheck->errMsg, "\n", 1); } if( pCheck->zPfx ){ - sqlite3XPrintf(&pCheck->errMsg, 0, pCheck->zPfx, pCheck->v1, pCheck->v2); + sqlite3XPrintf(&pCheck->errMsg, pCheck->zPfx, pCheck->v1, pCheck->v2); } - sqlite3VXPrintf(&pCheck->errMsg, 1, zFormat, ap); + sqlite3VXPrintf(&pCheck->errMsg, zFormat, ap); va_end(ap); if( pCheck->errMsg.accError==STRACCUM_NOMEM ){ pCheck->mallocFailed = 1; @@ -63389,7 +66664,7 @@ static void checkList( break; } if( checkRef(pCheck, iPage) ) break; - if( sqlite3PagerGet(pCheck->pPager, (Pgno)iPage, &pOvflPage) ){ + if( sqlite3PagerGet(pCheck->pPager, (Pgno)iPage, &pOvflPage, 0) ){ checkAppendMsg(pCheck, "failed to get page %d", iPage); break; } @@ -63635,9 +66910,9 @@ static int checkTreePage( if( info.nPayload>info.nLocal ){ int nPage; /* Number of pages on the overflow chain */ Pgno pgnoOvfl; /* First page of the overflow chain */ - assert( pc + info.iOverflow <= usableSize ); + assert( pc + info.nSize - 4 <= usableSize ); nPage = (info.nPayload - info.nLocal + usableSize - 5)/(usableSize - 4); - pgnoOvfl = get4byte(&pCell[info.iOverflow]); + pgnoOvfl = get4byte(&pCell[info.nSize - 4]); #ifndef SQLITE_OMIT_AUTOVACUUM if( pBt->autoVacuum ){ checkPtrmap(pCheck, pgnoOvfl, PTRMAP_OVERFLOW1, iPage); @@ -63786,7 +67061,8 @@ SQLITE_PRIVATE char *sqlite3BtreeIntegrityCheck( sqlite3BtreeEnter(p); assert( p->inTrans>TRANS_NONE && pBt->inTransaction>TRANS_NONE ); - assert( (nRef = sqlite3PagerRefcount(pBt->pPager))>=0 ); + VVA_ONLY( nRef = sqlite3PagerRefcount(pBt->pPager) ); + assert( nRef>=0 ); sCheck.pBt = pBt; sCheck.pPager = pBt->pPager; sCheck.nPage = btreePagecount(sCheck.pBt); @@ -63799,6 +67075,7 @@ SQLITE_PRIVATE char *sqlite3BtreeIntegrityCheck( sCheck.aPgRef = 0; sCheck.heap = 0; sqlite3StrAccumInit(&sCheck.errMsg, 0, zErr, sizeof(zErr), SQLITE_MAX_LENGTH); + sCheck.errMsg.printfFlags = SQLITE_PRINTF_INTERNAL; if( sCheck.nPage==0 ){ goto integrity_ck_cleanup; } @@ -64038,7 +67315,7 @@ SQLITE_PRIVATE int sqlite3BtreeLockTable(Btree *p, int iTab, u8 isWriteLock){ */ SQLITE_PRIVATE int sqlite3BtreePutData(BtCursor *pCsr, u32 offset, u32 amt, void *z){ int rc; - assert( cursorHoldsMutex(pCsr) ); + assert( cursorOwnsBtShared(pCsr) ); assert( sqlite3_mutex_held(pCsr->pBtree->db->mutex) ); assert( pCsr->curFlags & BTCF_Incrblob ); @@ -64126,15 +67403,6 @@ SQLITE_PRIVATE int sqlite3BtreeSetVersion(Btree *pBtree, int iVersion){ return rc; } -/* -** set the mask of hint flags for cursor pCsr. -*/ -SQLITE_PRIVATE void sqlite3BtreeCursorHints(BtCursor *pCsr, unsigned int mask){ - assert( mask==BTREE_BULKLOAD || mask==BTREE_SEEK_EQ || mask==0 ); - pCsr->hints = mask; -} - -#ifdef SQLITE_DEBUG /* ** Return true if the cursor has a hint specified. This routine is ** only used from within assert() statements @@ -64142,7 +67410,6 @@ SQLITE_PRIVATE void sqlite3BtreeCursorHints(BtCursor *pCsr, unsigned int mask){ SQLITE_PRIVATE int sqlite3BtreeCursorHasHint(BtCursor *pCsr, unsigned int mask){ return (pCsr->hints & mask)!=0; } -#endif /* ** Return true if the given Btree is read-only. @@ -64156,6 +67423,15 @@ SQLITE_PRIVATE int sqlite3BtreeIsReadonly(Btree *p){ */ SQLITE_PRIVATE int sqlite3HeaderSizeBtree(void){ return ROUND8(sizeof(MemPage)); } +#if !defined(SQLITE_OMIT_SHARED_CACHE) +/* +** Return true if the Btree passed as the only argument is sharable. +*/ +SQLITE_PRIVATE int sqlite3BtreeSharable(Btree *p){ + return p->sharable; +} +#endif + /************** End of btree.c ***********************************************/ /************** Begin file backup.c ******************************************/ /* @@ -64248,7 +67524,7 @@ static Btree *findBtree(sqlite3 *pErrorDb, sqlite3 *pDb, const char *zDb){ pParse = sqlite3StackAllocZero(pErrorDb, sizeof(*pParse)); if( pParse==0 ){ sqlite3ErrorWithMsg(pErrorDb, SQLITE_NOMEM, "out of memory"); - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else{ pParse->db = pDb; if( sqlite3OpenTempDatabase(pParse) ){ @@ -64342,7 +67618,7 @@ SQLITE_API sqlite3_backup *SQLITE_STDCALL sqlite3_backup_init( ** sqlite3_backup_finish(). */ p = (sqlite3_backup *)sqlite3MallocZero(sizeof(sqlite3_backup)); if( !p ){ - sqlite3Error(pDestDb, SQLITE_NOMEM); + sqlite3Error(pDestDb, SQLITE_NOMEM_BKPT); } } @@ -64453,7 +67729,7 @@ static int backupOnePage( DbPage *pDestPg = 0; Pgno iDest = (Pgno)(iOff/nDestPgsz)+1; if( iDest==PENDING_BYTE_PAGE(p->pDest->pBt) ) continue; - if( SQLITE_OK==(rc = sqlite3PagerGet(pDestPager, iDest, &pDestPg)) + if( SQLITE_OK==(rc = sqlite3PagerGet(pDestPager, iDest, &pDestPg, 0)) && SQLITE_OK==(rc = sqlite3PagerWrite(pDestPg)) ){ const u8 *zIn = &zSrcData[iOff%nSrcPgsz]; @@ -64579,8 +67855,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_backup_step(sqlite3_backup *p, int nPage){ const Pgno iSrcPg = p->iNext; /* Source page number */ if( iSrcPg!=PENDING_BYTE_PAGE(p->pSrc->pBt) ){ DbPage *pSrcPg; /* Source page object */ - rc = sqlite3PagerAcquire(pSrcPager, iSrcPg, &pSrcPg, - PAGER_GET_READONLY); + rc = sqlite3PagerGet(pSrcPager, iSrcPg, &pSrcPg,PAGER_GET_READONLY); if( rc==SQLITE_OK ){ rc = backupOnePage(p, iSrcPg, sqlite3PagerGetData(pSrcPg), 0); sqlite3PagerUnref(pSrcPg); @@ -64680,7 +67955,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_backup_step(sqlite3_backup *p, int nPage){ for(iPg=nDestTruncate; rc==SQLITE_OK && iPg<=(Pgno)nDstPage; iPg++){ if( iPg!=PENDING_BYTE_PAGE(p->pDest->pBt) ){ DbPage *pPg; - rc = sqlite3PagerGet(pDestPager, iPg, &pPg); + rc = sqlite3PagerGet(pDestPager, iPg, &pPg, 0); if( rc==SQLITE_OK ){ rc = sqlite3PagerWrite(pPg); sqlite3PagerUnref(pPg); @@ -64700,7 +67975,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_backup_step(sqlite3_backup *p, int nPage){ ){ PgHdr *pSrcPg = 0; const Pgno iSrcPg = (Pgno)((iOff/pgszSrc)+1); - rc = sqlite3PagerGet(pSrcPager, iSrcPg, &pSrcPg); + rc = sqlite3PagerGet(pSrcPager, iSrcPg, &pSrcPg, 0); if( rc==SQLITE_OK ){ u8 *zData = sqlite3PagerGetData(pSrcPg); rc = sqlite3OsWrite(pFile, zData, pgszSrc, iOff); @@ -64742,7 +68017,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_backup_step(sqlite3_backup *p, int nPage){ } if( rc==SQLITE_IOERR_NOMEM ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } p->rc = rc; } @@ -65077,6 +68352,7 @@ SQLITE_PRIVATE int sqlite3VdbeChangeEncoding(Mem *pMem, int desiredEnc){ SQLITE_PRIVATE SQLITE_NOINLINE int sqlite3VdbeMemGrow(Mem *pMem, int n, int bPreserve){ assert( sqlite3VdbeCheckMemInvariants(pMem) ); assert( (pMem->flags&MEM_RowSet)==0 ); + testcase( pMem->db==0 ); /* If the bPreserve flag is set to true, then the memory cell must already ** contain a valid string or blob value. */ @@ -65098,7 +68374,7 @@ SQLITE_PRIVATE SQLITE_NOINLINE int sqlite3VdbeMemGrow(Mem *pMem, int n, int bPre sqlite3VdbeMemSetNull(pMem); pMem->z = 0; pMem->szMalloc = 0; - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; }else{ pMem->szMalloc = sqlite3DbMallocSize(pMem->db, pMem->zMalloc); } @@ -65156,7 +68432,7 @@ SQLITE_PRIVATE int sqlite3VdbeMemMakeWriteable(Mem *pMem){ f = pMem->flags; if( (f&(MEM_Str|MEM_Blob)) && (pMem->szMalloc==0 || pMem->z!=pMem->zMalloc) ){ if( sqlite3VdbeMemGrow(pMem, pMem->n + 2, 1) ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pMem->z[pMem->n] = 0; pMem->z[pMem->n+1] = 0; @@ -65188,7 +68464,7 @@ SQLITE_PRIVATE int sqlite3VdbeMemExpandBlob(Mem *pMem){ nByte = 1; } if( sqlite3VdbeMemGrow(pMem, nByte, 1) ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } memset(&pMem->z[pMem->n], 0, pMem->u.nZero); @@ -65205,7 +68481,7 @@ SQLITE_PRIVATE int sqlite3VdbeMemExpandBlob(Mem *pMem){ */ static SQLITE_NOINLINE int vdbeMemAddTerminator(Mem *pMem){ if( sqlite3VdbeMemGrow(pMem, pMem->n+2, 1) ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pMem->z[pMem->n] = 0; pMem->z[pMem->n+1] = 0; @@ -65254,7 +68530,7 @@ SQLITE_PRIVATE int sqlite3VdbeMemStringify(Mem *pMem, u8 enc, u8 bForce){ if( sqlite3VdbeMemClearAndResize(pMem, nByte) ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } /* For a Real or Integer, use sqlite3_snprintf() to produce the UTF-8 @@ -65680,7 +68956,7 @@ SQLITE_PRIVATE void sqlite3VdbeMemSetRowSet(Mem *pMem){ assert( db!=0 ); assert( (pMem->flags & MEM_RowSet)==0 ); sqlite3VdbeMemRelease(pMem); - pMem->zMalloc = sqlite3DbMallocRaw(db, 64); + pMem->zMalloc = sqlite3DbMallocRawNN(db, 64); if( db->mallocFailed ){ pMem->flags = MEM_Null; pMem->szMalloc = 0; @@ -65721,7 +68997,7 @@ SQLITE_PRIVATE int sqlite3VdbeMemTooBig(Mem *p){ SQLITE_PRIVATE void sqlite3VdbeMemAboutToChange(Vdbe *pVdbe, Mem *pMem){ int i; Mem *pX; - for(i=1, pX=&pVdbe->aMem[1]; i<=pVdbe->nMem; i++, pX++){ + for(i=0, pX=pVdbe->aMem; inMem; i++, pX++){ if( pX->pScopyFrom==pMem ){ pX->flags |= MEM_Undefined; pX->pScopyFrom = 0; @@ -65762,10 +69038,6 @@ SQLITE_PRIVATE void sqlite3VdbeMemShallowCopy(Mem *pTo, const Mem *pFrom, int sr SQLITE_PRIVATE int sqlite3VdbeMemCopy(Mem *pTo, const Mem *pFrom){ int rc = SQLITE_OK; - /* The pFrom==0 case in the following assert() is when an sqlite3_value - ** from sqlite3_value_dup() is used as the argument - ** to sqlite3_result_value(). */ - assert( pTo->db==pFrom->db || pFrom->db==0 ); assert( (pFrom->flags & MEM_RowSet)==0 ); if( VdbeMemDynamic(pTo) ) vdbeMemClearExternAndSetNull(pTo); memcpy(pTo, pFrom, MEMCELLSIZE); @@ -65865,7 +69137,7 @@ SQLITE_PRIVATE int sqlite3VdbeMemSetStr( testcase( nAlloc==31 ); testcase( nAlloc==32 ); if( sqlite3VdbeMemClearAndResize(pMem, MAX(nAlloc,32)) ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } memcpy(pMem->z, z, nAlloc); }else if( xDel==SQLITE_DYNAMIC ){ @@ -65885,7 +69157,7 @@ SQLITE_PRIVATE int sqlite3VdbeMemSetStr( #ifndef SQLITE_OMIT_UTF16 if( pMem->enc!=SQLITE_UTF8 && sqlite3VdbeMemHandleBom(pMem) ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } #endif @@ -66146,7 +69418,6 @@ static int valueFromFunction( FuncDef *pFunc = 0; /* Function definition */ sqlite3_value *pVal = 0; /* New value */ int rc = SQLITE_OK; /* Return code */ - int nName; /* Size of function name in bytes */ ExprList *pList = 0; /* Function arguments */ int i; /* Iterator variable */ @@ -66154,8 +69425,7 @@ static int valueFromFunction( assert( (p->flags & EP_TokenOnly)==0 ); pList = p->x.pList; if( pList ) nVal = pList->nExpr; - nName = sqlite3Strlen30(p->u.zToken); - pFunc = sqlite3FindFunction(db, p->u.zToken, nName, nVal, enc, 0); + pFunc = sqlite3FindFunction(db, p->u.zToken, nVal, enc, 0); assert( pFunc ); if( (pFunc->funcFlags & (SQLITE_FUNC_CONSTANT|SQLITE_FUNC_SLOCHNG))==0 || (pFunc->funcFlags & SQLITE_FUNC_NEEDCOLL) @@ -66166,7 +69436,7 @@ static int valueFromFunction( if( pList ){ apVal = (sqlite3_value**)sqlite3DbMallocZero(db, sizeof(apVal[0]) * nVal); if( apVal==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto value_from_function_out; } for(i=0; ixFunc(&ctx, nVal, apVal); + pFunc->xSFunc(&ctx, nVal, apVal); if( ctx.isError ){ rc = ctx.isError; sqlite3ErrorMsg(pCtx->pParse, "%s", sqlite3_value_text(pVal)); @@ -66247,7 +69517,7 @@ static int valueFromExpr( *ppVal = 0; return SQLITE_OK; } - while( (op = pExpr->op)==TK_UPLUS ) pExpr = pExpr->pLeft; + while( (op = pExpr->op)==TK_UPLUS || op==TK_SPAN ) pExpr = pExpr->pLeft; if( NEVER(op==TK_REGISTER) ) op = pExpr->op2; /* Compressed expressions only appear when parsing the DEFAULT clause @@ -66342,7 +69612,7 @@ static int valueFromExpr( return rc; no_mem: - db->mallocFailed = 1; + sqlite3OomFault(db); sqlite3DbFree(db, zVal); assert( *ppVal==0 ); #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 @@ -66350,7 +69620,7 @@ no_mem: #else assert( pCtx==0 ); sqlite3ValueFree(pVal); #endif - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } /* @@ -66388,21 +69658,20 @@ static void recordFunc( sqlite3_value **argv ){ const int file_format = 1; - int iSerial; /* Serial type */ + u32 iSerial; /* Serial type */ int nSerial; /* Bytes of space for iSerial as varint */ - int nVal; /* Bytes of space required for argv[0] */ + u32 nVal; /* Bytes of space required for argv[0] */ int nRet; sqlite3 *db; u8 *aRet; UNUSED_PARAMETER( argc ); - iSerial = sqlite3VdbeSerialType(argv[0], file_format); + iSerial = sqlite3VdbeSerialType(argv[0], file_format, &nVal); nSerial = sqlite3VarintLen(iSerial); - nVal = sqlite3VdbeSerialTypeLen(iSerial); db = sqlite3_context_db_handle(context); nRet = 1 + nSerial + nVal; - aRet = sqlite3DbMallocRaw(db, nRet); + aRet = sqlite3DbMallocRawNN(db, nRet); if( aRet==0 ){ sqlite3_result_error_nomem(context); }else{ @@ -66418,15 +69687,10 @@ static void recordFunc( ** Register built-in functions used to help read ANALYZE data. */ SQLITE_PRIVATE void sqlite3AnalyzeFunctions(void){ - static SQLITE_WSD FuncDef aAnalyzeTableFuncs[] = { + static FuncDef aAnalyzeTableFuncs[] = { FUNCTION(sqlite_record, 1, 0, 0, recordFunc), }; - int i; - FuncDefHash *pHash = &GLOBAL(FuncDefHash, sqlite3GlobalFunctions); - FuncDef *aFunc = (FuncDef*)&GLOBAL(FuncDef, aAnalyzeTableFuncs); - for(i=0; inRec ) return SQLITE_CORRUPT_BKPT; if( pMem==0 ){ pMem = *ppVal = sqlite3ValueNew(db); - if( pMem==0 ) return SQLITE_NOMEM; + if( pMem==0 ) return SQLITE_NOMEM_BKPT; } sqlite3VdbeSerialGet(&a[iField-szField], t, pMem); pMem->enc = ENC(db); @@ -66718,6 +69982,7 @@ SQLITE_PRIVATE Vdbe *sqlite3VdbeCreate(Parse *pParse){ assert( pParse->aLabel==0 ); assert( pParse->nLabel==0 ); assert( pParse->nOpAlloc==0 ); + assert( pParse->szOpAlloc==0 ); return p; } @@ -66760,6 +70025,7 @@ SQLITE_API const char *SQLITE_STDCALL sqlite3_sql(sqlite3_stmt *pStmt){ SQLITE_PRIVATE void sqlite3VdbeSwap(Vdbe *pA, Vdbe *pB){ Vdbe tmp, *pTmp; char *zTmp; + assert( pA->db==pB->db ); tmp = *pA; *pA = *pB; *pB = tmp; @@ -66807,10 +70073,11 @@ static int growOpArray(Vdbe *v, int nOp){ assert( nNew>=(p->nOpAlloc+nOp) ); pNew = sqlite3DbRealloc(p->db, v->aOp, nNew*sizeof(Op)); if( pNew ){ - p->nOpAlloc = sqlite3DbMallocSize(p->db, pNew)/sizeof(Op); + p->szOpAlloc = sqlite3DbMallocSize(p->db, pNew); + p->nOpAlloc = p->szOpAlloc/sizeof(Op); v->aOp = pNew; } - return (pNew ? SQLITE_OK : SQLITE_NOMEM); + return (pNew ? SQLITE_OK : SQLITE_NOMEM_BKPT); } #ifdef SQLITE_DEBUG @@ -66840,17 +70107,21 @@ static void test_addop_breakpoint(void){ ** the sqlite3VdbeChangeP4() function to change the value of the P4 ** operand. */ +static SQLITE_NOINLINE int growOp3(Vdbe *p, int op, int p1, int p2, int p3){ + assert( p->pParse->nOpAlloc<=p->nOp ); + if( growOpArray(p, 1) ) return 1; + assert( p->pParse->nOpAlloc>p->nOp ); + return sqlite3VdbeAddOp3(p, op, p1, p2, p3); +} SQLITE_PRIVATE int sqlite3VdbeAddOp3(Vdbe *p, int op, int p1, int p2, int p3){ int i; VdbeOp *pOp; i = p->nOp; assert( p->magic==VDBE_MAGIC_INIT ); - assert( op>0 && op<0xff ); + assert( op>=0 && op<0xff ); if( p->pParse->nOpAlloc<=i ){ - if( growOpArray(p, 1) ){ - return 1; - } + return growOp3(p, op, p1, p2, p3); } p->nOp++; pOp = &p->aOp[i]; @@ -66927,8 +70198,7 @@ SQLITE_PRIVATE void sqlite3VdbeMultiLoad(Vdbe *p, int iDest, const char *zTypes, for(i=0; (c = zTypes[i])!=0; i++){ if( c=='s' ){ const char *z = va_arg(ap, const char*); - int addr = sqlite3VdbeAddOp2(p, z==0 ? OP_Null : OP_String8, 0, iDest++); - if( z ) sqlite3VdbeChangeP4(p, addr, z, 0); + sqlite3VdbeAddOp4(p, z==0 ? OP_Null : OP_String8, 0, iDest++, 0, z, 0); }else{ assert( c=='i' ); sqlite3VdbeAddOp2(p, OP_Integer, va_arg(ap, int), iDest++); @@ -66967,7 +70237,7 @@ SQLITE_PRIVATE int sqlite3VdbeAddOp4Dup8( const u8 *zP4, /* The P4 operand */ int p4type /* P4 operand type */ ){ - char *p4copy = sqlite3DbMallocRaw(sqlite3VdbeDb(p), 8); + char *p4copy = sqlite3DbMallocRawNN(sqlite3VdbeDb(p), 8); if( p4copy ) memcpy(p4copy, zP4, 8); return sqlite3VdbeAddOp4(p, op, p1, p2, p3, p4copy, p4type); } @@ -66982,8 +70252,7 @@ SQLITE_PRIVATE int sqlite3VdbeAddOp4Dup8( */ SQLITE_PRIVATE void sqlite3VdbeAddParseSchemaOp(Vdbe *p, int iDb, char *zWhere){ int j; - int addr = sqlite3VdbeAddOp3(p, OP_ParseSchema, iDb, 0, 0); - sqlite3VdbeChangeP4(p, addr, zWhere, P4_DYNAMIC); + sqlite3VdbeAddOp4(p, OP_ParseSchema, iDb, 0, 0, zWhere, P4_DYNAMIC); for(j=0; jdb->nDb; j++) sqlite3VdbeUsesBtree(p, j); } @@ -67003,6 +70272,21 @@ SQLITE_PRIVATE int sqlite3VdbeAddOp4Int( return addr; } +/* Insert the end of a co-routine +*/ +SQLITE_PRIVATE void sqlite3VdbeEndCoroutine(Vdbe *v, int regYield){ + sqlite3VdbeAddOp1(v, OP_EndCoroutine, regYield); + + /* Clear the temporary register cache, thereby ensuring that each + ** co-routine has its own independent set of registers, because co-routines + ** might expect their registers to be preserved across an OP_Yield, and + ** that could cause problems if two or more co-routines are using the same + ** temporary register. + */ + v->pParse->nTempReg = 0; + v->pParse->nRangeReg = 0; +} + /* ** Create a new symbolic label for an instruction that has yet to be ** coded. The symbolic label is really just a negative number. The @@ -67028,7 +70312,7 @@ SQLITE_PRIVATE int sqlite3VdbeMakeLabel(Vdbe *v){ if( p->aLabel ){ p->aLabel[i] = -1; } - return -1-i; + return ADDR(i); } /* @@ -67038,7 +70322,7 @@ SQLITE_PRIVATE int sqlite3VdbeMakeLabel(Vdbe *v){ */ SQLITE_PRIVATE void sqlite3VdbeResolveLabel(Vdbe *v, int x){ Parse *p = v->pParse; - int j = -1-x; + int j = ADDR(x); assert( v->magic==VDBE_MAGIC_INIT ); assert( jnLabel ); assert( j>=0 ); @@ -67055,6 +70339,13 @@ SQLITE_PRIVATE void sqlite3VdbeRunOnlyOnce(Vdbe *p){ p->runOnlyOnce = 1; } +/* +** Mark the VDBE as one that can only be run multiple times. +*/ +SQLITE_PRIVATE void sqlite3VdbeReusable(Vdbe *p){ + p->runOnlyOnce = 0; +} + #ifdef SQLITE_DEBUG /* sqlite3AssertMayAbort() logic */ /* @@ -67201,73 +70492,84 @@ SQLITE_PRIVATE int sqlite3VdbeAssertMayAbort(Vdbe *v, int mayAbort){ ** (4) Initialize the p4.xAdvance pointer on opcodes that use it. ** ** (5) Reclaim the memory allocated for storing labels. +** +** This routine will only function correctly if the mkopcodeh.tcl generator +** script numbers the opcodes correctly. Changes to this routine must be +** coordinated with changes to mkopcodeh.tcl. */ static void resolveP2Values(Vdbe *p, int *pMaxFuncArgs){ - int i; int nMaxArgs = *pMaxFuncArgs; Op *pOp; Parse *pParse = p->pParse; int *aLabel = pParse->aLabel; p->readOnly = 1; p->bIsReader = 0; - for(pOp=p->aOp, i=p->nOp-1; i>=0; i--, pOp++){ - u8 opcode = pOp->opcode; + pOp = &p->aOp[p->nOp-1]; + while(1){ - /* NOTE: Be sure to update mkopcodeh.awk when adding or removing - ** cases from this switch! */ - switch( opcode ){ - case OP_Transaction: { - if( pOp->p2!=0 ) p->readOnly = 0; - /* fall thru */ - } - case OP_AutoCommit: - case OP_Savepoint: { - p->bIsReader = 1; - break; - } + /* Only JUMP opcodes and the short list of special opcodes in the switch + ** below need to be considered. The mkopcodeh.tcl generator script groups + ** all these opcodes together near the front of the opcode list. Skip + ** any opcode that does not need processing by virtual of the fact that + ** it is larger than SQLITE_MX_JUMP_OPCODE, as a performance optimization. + */ + if( pOp->opcode<=SQLITE_MX_JUMP_OPCODE ){ + /* NOTE: Be sure to update mkopcodeh.tcl when adding or removing + ** cases from this switch! */ + switch( pOp->opcode ){ + case OP_Transaction: { + if( pOp->p2!=0 ) p->readOnly = 0; + /* fall thru */ + } + case OP_AutoCommit: + case OP_Savepoint: { + p->bIsReader = 1; + break; + } #ifndef SQLITE_OMIT_WAL - case OP_Checkpoint: + case OP_Checkpoint: #endif - case OP_Vacuum: - case OP_JournalMode: { - p->readOnly = 0; - p->bIsReader = 1; - break; - } + case OP_Vacuum: + case OP_JournalMode: { + p->readOnly = 0; + p->bIsReader = 1; + break; + } #ifndef SQLITE_OMIT_VIRTUALTABLE - case OP_VUpdate: { - if( pOp->p2>nMaxArgs ) nMaxArgs = pOp->p2; - break; - } - case OP_VFilter: { - int n; - assert( p->nOp - i >= 3 ); - assert( pOp[-1].opcode==OP_Integer ); - n = pOp[-1].p1; - if( n>nMaxArgs ) nMaxArgs = n; - break; - } + case OP_VUpdate: { + if( pOp->p2>nMaxArgs ) nMaxArgs = pOp->p2; + break; + } + case OP_VFilter: { + int n; + assert( (pOp - p->aOp) >= 3 ); + assert( pOp[-1].opcode==OP_Integer ); + n = pOp[-1].p1; + if( n>nMaxArgs ) nMaxArgs = n; + break; + } #endif - case OP_Next: - case OP_NextIfOpen: - case OP_SorterNext: { - pOp->p4.xAdvance = sqlite3BtreeNext; - pOp->p4type = P4_ADVANCE; - break; + case OP_Next: + case OP_NextIfOpen: + case OP_SorterNext: { + pOp->p4.xAdvance = sqlite3BtreeNext; + pOp->p4type = P4_ADVANCE; + break; + } + case OP_Prev: + case OP_PrevIfOpen: { + pOp->p4.xAdvance = sqlite3BtreePrevious; + pOp->p4type = P4_ADVANCE; + break; + } } - case OP_Prev: - case OP_PrevIfOpen: { - pOp->p4.xAdvance = sqlite3BtreePrevious; - pOp->p4type = P4_ADVANCE; - break; + if( (sqlite3OpcodeProperty[pOp->opcode] & OPFLG_JUMP)!=0 && pOp->p2<0 ){ + assert( ADDR(pOp->p2)nLabel ); + pOp->p2 = aLabel[ADDR(pOp->p2)]; } } - - pOp->opflags = sqlite3OpcodeProperty[opcode]; - if( (pOp->opflags & OPFLG_JUMP)!=0 && pOp->p2<0 ){ - assert( -1-pOp->p2nLabel ); - pOp->p2 = aLabel[-1-pOp->p2]; - } + if( pOp==p->aOp ) break; + pOp--; } sqlite3DbFree(p->db, pParse->aLabel); pParse->aLabel = 0; @@ -67284,6 +70586,20 @@ SQLITE_PRIVATE int sqlite3VdbeCurrentAddr(Vdbe *p){ return p->nOp; } +/* +** Verify that at least N opcode slots are available in p without +** having to malloc for more space (except when compiled using +** SQLITE_TEST_REALLOC_STRESS). This interface is used during testing +** to verify that certain calls to sqlite3VdbeAddOpList() can never +** fail due to a OOM fault and hence that the return value from +** sqlite3VdbeAddOpList() will always be non-NULL. +*/ +#if defined(SQLITE_DEBUG) && !defined(SQLITE_TEST_REALLOC_STRESS) +SQLITE_PRIVATE void sqlite3VdbeVerifyNoMallocRequired(Vdbe *p, int N){ + assert( p->nOp + N <= p->pParse->nOpAlloc ); +} +#endif + /* ** This function returns a pointer to the array of opcodes associated with ** the Vdbe passed as the first argument. It is the callers responsibility @@ -67309,28 +70625,33 @@ SQLITE_PRIVATE VdbeOp *sqlite3VdbeTakeOpArray(Vdbe *p, int *pnOp, int *pnMaxArg) } /* -** Add a whole list of operations to the operation stack. Return the -** address of the first operation added. +** Add a whole list of operations to the operation stack. Return a +** pointer to the first operation inserted. +** +** Non-zero P2 arguments to jump instructions are automatically adjusted +** so that the jump target is relative to the first operation inserted. */ -SQLITE_PRIVATE int sqlite3VdbeAddOpList(Vdbe *p, int nOp, VdbeOpList const *aOp, int iLineno){ - int addr, i; - VdbeOp *pOut; +SQLITE_PRIVATE VdbeOp *sqlite3VdbeAddOpList( + Vdbe *p, /* Add opcodes to the prepared statement */ + int nOp, /* Number of opcodes to add */ + VdbeOpList const *aOp, /* The opcodes to be added */ + int iLineno /* Source-file line number of first opcode */ +){ + int i; + VdbeOp *pOut, *pFirst; assert( nOp>0 ); assert( p->magic==VDBE_MAGIC_INIT ); if( p->nOp + nOp > p->pParse->nOpAlloc && growOpArray(p, nOp) ){ return 0; } - addr = p->nOp; - pOut = &p->aOp[addr]; + pFirst = pOut = &p->aOp[p->nOp]; for(i=0; ip2; pOut->opcode = aOp->opcode; pOut->p1 = aOp->p1; - if( p2<0 ){ - assert( sqlite3OpcodeProperty[pOut->opcode] & OPFLG_JUMP ); - pOut->p2 = addr + ADDR(p2); - }else{ - pOut->p2 = p2; + pOut->p2 = aOp->p2; + assert( aOp->p2>=0 ); + if( (sqlite3OpcodeProperty[aOp->opcode] & OPFLG_JUMP)!=0 && aOp->p2>0 ){ + pOut->p2 += p->nOp; } pOut->p3 = aOp->p3; pOut->p4type = P4_NOTUSED; @@ -67346,12 +70667,12 @@ SQLITE_PRIVATE int sqlite3VdbeAddOpList(Vdbe *p, int nOp, VdbeOpList const *aOp, #endif #ifdef SQLITE_DEBUG if( p->db->flags & SQLITE_VdbeAddopTrace ){ - sqlite3VdbePrintOp(0, i+addr, &p->aOp[i+addr]); + sqlite3VdbePrintOp(0, i+p->nOp, &p->aOp[i+p->nOp]); } #endif } p->nOp += nOp; - return addr; + return pFirst; } #if defined(SQLITE_ENABLE_STMT_SCANSTATUS) @@ -67399,7 +70720,7 @@ SQLITE_PRIVATE void sqlite3VdbeChangeP3(Vdbe *p, u32 addr, int val){ sqlite3VdbeGetOp(p,addr)->p3 = val; } SQLITE_PRIVATE void sqlite3VdbeChangeP5(Vdbe *p, u8 p5){ - sqlite3VdbeGetOp(p,-1)->p5 = p5; + if( !p->db->mallocFailed ) p->aOp[p->nOp-1].p5 = p5; } /* @@ -67428,46 +70749,50 @@ static void vdbeFreeOpArray(sqlite3 *, Op *, int); ** Delete a P4 value if necessary. */ static void freeP4(sqlite3 *db, int p4type, void *p4){ - if( p4 ){ - assert( db ); - switch( p4type ){ - case P4_FUNCCTX: { - freeEphemeralFunction(db, ((sqlite3_context*)p4)->pFunc); - /* Fall through into the next case */ - } - case P4_REAL: - case P4_INT64: - case P4_DYNAMIC: - case P4_INTARRAY: { - sqlite3DbFree(db, p4); - break; - } - case P4_KEYINFO: { - if( db->pnBytesFreed==0 ) sqlite3KeyInfoUnref((KeyInfo*)p4); - break; - } - case P4_MPRINTF: { - if( db->pnBytesFreed==0 ) sqlite3_free(p4); - break; - } - case P4_FUNCDEF: { - freeEphemeralFunction(db, (FuncDef*)p4); - break; - } - case P4_MEM: { - if( db->pnBytesFreed==0 ){ - sqlite3ValueFree((sqlite3_value*)p4); - }else{ - Mem *p = (Mem*)p4; - if( p->szMalloc ) sqlite3DbFree(db, p->zMalloc); - sqlite3DbFree(db, p); - } - break; - } - case P4_VTAB : { - if( db->pnBytesFreed==0 ) sqlite3VtabUnlock((VTable *)p4); - break; + assert( db ); + switch( p4type ){ + case P4_FUNCCTX: { + freeEphemeralFunction(db, ((sqlite3_context*)p4)->pFunc); + /* Fall through into the next case */ + } + case P4_REAL: + case P4_INT64: + case P4_DYNAMIC: + case P4_INTARRAY: { + sqlite3DbFree(db, p4); + break; + } + case P4_KEYINFO: { + if( db->pnBytesFreed==0 ) sqlite3KeyInfoUnref((KeyInfo*)p4); + break; + } +#ifdef SQLITE_ENABLE_CURSOR_HINTS + case P4_EXPR: { + sqlite3ExprDelete(db, (Expr*)p4); + break; + } +#endif + case P4_MPRINTF: { + if( db->pnBytesFreed==0 ) sqlite3_free(p4); + break; + } + case P4_FUNCDEF: { + freeEphemeralFunction(db, (FuncDef*)p4); + break; + } + case P4_MEM: { + if( db->pnBytesFreed==0 ){ + sqlite3ValueFree((sqlite3_value*)p4); + }else{ + Mem *p = (Mem*)p4; + if( p->szMalloc ) sqlite3DbFree(db, p->zMalloc); + sqlite3DbFree(db, p); } + break; + } + case P4_VTAB : { + if( db->pnBytesFreed==0 ) sqlite3VtabUnlock((VTable *)p4); + break; } } } @@ -67481,7 +70806,7 @@ static void vdbeFreeOpArray(sqlite3 *db, Op *aOp, int nOp){ if( aOp ){ Op *pOp; for(pOp=aOp; pOp<&aOp[nOp]; pOp++){ - freeP4(db, pOp->p4type, pOp->p4.p); + if( pOp->p4type ) freeP4(db, pOp->p4type, pOp->p4.p); #ifdef SQLITE_ENABLE_EXPLAIN_COMMENTS sqlite3DbFree(db, pOp->zComment); #endif @@ -67503,15 +70828,16 @@ SQLITE_PRIVATE void sqlite3VdbeLinkSubProgram(Vdbe *pVdbe, SubProgram *p){ /* ** Change the opcode at addr into OP_Noop */ -SQLITE_PRIVATE void sqlite3VdbeChangeToNoop(Vdbe *p, int addr){ - if( addrnOp ){ - VdbeOp *pOp = &p->aOp[addr]; - sqlite3 *db = p->db; - freeP4(db, pOp->p4type, pOp->p4.p); - memset(pOp, 0, sizeof(pOp[0])); - pOp->opcode = OP_Noop; - if( addr==p->nOp-1 ) p->nOp--; - } +SQLITE_PRIVATE int sqlite3VdbeChangeToNoop(Vdbe *p, int addr){ + VdbeOp *pOp; + if( p->db->mallocFailed ) return 0; + assert( addr>=0 && addrnOp ); + pOp = &p->aOp[addr]; + freeP4(p->db, pOp->p4type, pOp->p4.p); + pOp->p4type = P4_NOTUSED; + pOp->p4.z = 0; + pOp->opcode = OP_Noop; + return 1; } /* @@ -67520,8 +70846,7 @@ SQLITE_PRIVATE void sqlite3VdbeChangeToNoop(Vdbe *p, int addr){ */ SQLITE_PRIVATE int sqlite3VdbeDeletePriorOpcode(Vdbe *p, u8 op){ if( (p->nOp-1)>(p->pParse->iFixedOp) && p->aOp[p->nOp-1].opcode==op ){ - sqlite3VdbeChangeToNoop(p, p->nOp-1); - return 1; + return sqlite3VdbeChangeToNoop(p, p->nOp-1); }else{ return 0; } @@ -67544,16 +70869,34 @@ SQLITE_PRIVATE int sqlite3VdbeDeletePriorOpcode(Vdbe *p, u8 op){ ** ** If addr<0 then change P4 on the most recently inserted instruction. */ +static void SQLITE_NOINLINE vdbeChangeP4Full( + Vdbe *p, + Op *pOp, + const char *zP4, + int n +){ + if( pOp->p4type ){ + freeP4(p->db, pOp->p4type, pOp->p4.p); + pOp->p4type = 0; + pOp->p4.p = 0; + } + if( n<0 ){ + sqlite3VdbeChangeP4(p, (int)(pOp - p->aOp), zP4, n); + }else{ + if( n==0 ) n = sqlite3Strlen30(zP4); + pOp->p4.z = sqlite3DbStrNDup(p->db, zP4, n); + pOp->p4type = P4_DYNAMIC; + } +} SQLITE_PRIVATE void sqlite3VdbeChangeP4(Vdbe *p, int addr, const char *zP4, int n){ Op *pOp; sqlite3 *db; assert( p!=0 ); db = p->db; assert( p->magic==VDBE_MAGIC_INIT ); - if( p->aOp==0 || db->mallocFailed ){ - if( n!=P4_VTAB ){ - freeP4(db, n, (void*)*(char**)&zP4); - } + assert( p->aOp!=0 || db->mallocFailed ); + if( db->mallocFailed ){ + if( n!=P4_VTAB ) freeP4(db, n, (void*)*(char**)&zP4); return; } assert( p->nOp>0 ); @@ -67562,34 +70905,20 @@ SQLITE_PRIVATE void sqlite3VdbeChangeP4(Vdbe *p, int addr, const char *zP4, int addr = p->nOp - 1; } pOp = &p->aOp[addr]; - assert( pOp->p4type==P4_NOTUSED - || pOp->p4type==P4_INT32 - || pOp->p4type==P4_KEYINFO ); - freeP4(db, pOp->p4type, pOp->p4.p); - pOp->p4.p = 0; + if( n>=0 || pOp->p4type ){ + vdbeChangeP4Full(p, pOp, zP4, n); + return; + } if( n==P4_INT32 ){ /* Note: this cast is safe, because the origin data point was an int ** that was cast to a (const char *). */ pOp->p4.i = SQLITE_PTR_TO_INT(zP4); pOp->p4type = P4_INT32; - }else if( zP4==0 ){ - pOp->p4.p = 0; - pOp->p4type = P4_NOTUSED; - }else if( n==P4_KEYINFO ){ - pOp->p4.p = (void*)zP4; - pOp->p4type = P4_KEYINFO; - }else if( n==P4_VTAB ){ - pOp->p4.p = (void*)zP4; - pOp->p4type = P4_VTAB; - sqlite3VtabLock((VTable *)zP4); - assert( ((VTable *)zP4)->db==p->db ); - }else if( n<0 ){ + }else if( zP4!=0 ){ + assert( n<0 ); pOp->p4.p = (void*)zP4; pOp->p4type = (signed char)n; - }else{ - if( n==0 ) n = sqlite3Strlen30(zP4); - pOp->p4.z = sqlite3DbStrNDup(p->db, zP4, n); - pOp->p4type = P4_DYNAMIC; + if( n==P4_VTAB ) sqlite3VtabLock((VTable*)zP4); } } @@ -67768,75 +71097,138 @@ static int displayComment( } #endif /* SQLITE_DEBUG */ +#if VDBE_DISPLAY_P4 && defined(SQLITE_ENABLE_CURSOR_HINTS) +/* +** Translate the P4.pExpr value for an OP_CursorHint opcode into text +** that can be displayed in the P4 column of EXPLAIN output. +*/ +static void displayP4Expr(StrAccum *p, Expr *pExpr){ + const char *zOp = 0; + switch( pExpr->op ){ + case TK_STRING: + sqlite3XPrintf(p, "%Q", pExpr->u.zToken); + break; + case TK_INTEGER: + sqlite3XPrintf(p, "%d", pExpr->u.iValue); + break; + case TK_NULL: + sqlite3XPrintf(p, "NULL"); + break; + case TK_REGISTER: { + sqlite3XPrintf(p, "r[%d]", pExpr->iTable); + break; + } + case TK_COLUMN: { + if( pExpr->iColumn<0 ){ + sqlite3XPrintf(p, "rowid"); + }else{ + sqlite3XPrintf(p, "c%d", (int)pExpr->iColumn); + } + break; + } + case TK_LT: zOp = "LT"; break; + case TK_LE: zOp = "LE"; break; + case TK_GT: zOp = "GT"; break; + case TK_GE: zOp = "GE"; break; + case TK_NE: zOp = "NE"; break; + case TK_EQ: zOp = "EQ"; break; + case TK_IS: zOp = "IS"; break; + case TK_ISNOT: zOp = "ISNOT"; break; + case TK_AND: zOp = "AND"; break; + case TK_OR: zOp = "OR"; break; + case TK_PLUS: zOp = "ADD"; break; + case TK_STAR: zOp = "MUL"; break; + case TK_MINUS: zOp = "SUB"; break; + case TK_REM: zOp = "REM"; break; + case TK_BITAND: zOp = "BITAND"; break; + case TK_BITOR: zOp = "BITOR"; break; + case TK_SLASH: zOp = "DIV"; break; + case TK_LSHIFT: zOp = "LSHIFT"; break; + case TK_RSHIFT: zOp = "RSHIFT"; break; + case TK_CONCAT: zOp = "CONCAT"; break; + case TK_UMINUS: zOp = "MINUS"; break; + case TK_UPLUS: zOp = "PLUS"; break; + case TK_BITNOT: zOp = "BITNOT"; break; + case TK_NOT: zOp = "NOT"; break; + case TK_ISNULL: zOp = "ISNULL"; break; + case TK_NOTNULL: zOp = "NOTNULL"; break; -#if !defined(SQLITE_OMIT_EXPLAIN) || !defined(NDEBUG) \ - || defined(VDBE_PROFILE) || defined(SQLITE_DEBUG) + default: + sqlite3XPrintf(p, "%s", "expr"); + break; + } + + if( zOp ){ + sqlite3XPrintf(p, "%s(", zOp); + displayP4Expr(p, pExpr->pLeft); + if( pExpr->pRight ){ + sqlite3StrAccumAppend(p, ",", 1); + displayP4Expr(p, pExpr->pRight); + } + sqlite3StrAccumAppend(p, ")", 1); + } +} +#endif /* VDBE_DISPLAY_P4 && defined(SQLITE_ENABLE_CURSOR_HINTS) */ + + +#if VDBE_DISPLAY_P4 /* ** Compute a string that describes the P4 parameter for an opcode. ** Use zTemp for any required temporary buffer space. */ static char *displayP4(Op *pOp, char *zTemp, int nTemp){ char *zP4 = zTemp; + StrAccum x; assert( nTemp>=20 ); + sqlite3StrAccumInit(&x, 0, zTemp, nTemp, 0); switch( pOp->p4type ){ case P4_KEYINFO: { - int i, j; + int j; KeyInfo *pKeyInfo = pOp->p4.pKeyInfo; assert( pKeyInfo->aSortOrder!=0 ); - sqlite3_snprintf(nTemp, zTemp, "k(%d", pKeyInfo->nField); - i = sqlite3Strlen30(zTemp); + sqlite3XPrintf(&x, "k(%d", pKeyInfo->nField); for(j=0; jnField; j++){ CollSeq *pColl = pKeyInfo->aColl[j]; - const char *zColl = pColl ? pColl->zName : "nil"; - int n = sqlite3Strlen30(zColl); - if( n==6 && memcmp(zColl,"BINARY",6)==0 ){ - zColl = "B"; - n = 1; - } - if( i+n>nTemp-7 ){ - memcpy(&zTemp[i],",...",4); - i += 4; - break; - } - zTemp[i++] = ','; - if( pKeyInfo->aSortOrder[j] ){ - zTemp[i++] = '-'; - } - memcpy(&zTemp[i], zColl, n+1); - i += n; + const char *zColl = pColl ? pColl->zName : ""; + if( strcmp(zColl, "BINARY")==0 ) zColl = "B"; + sqlite3XPrintf(&x, ",%s%s", pKeyInfo->aSortOrder[j] ? "-" : "", zColl); } - zTemp[i++] = ')'; - zTemp[i] = 0; - assert( ip4.pExpr); + break; + } +#endif case P4_COLLSEQ: { CollSeq *pColl = pOp->p4.pColl; - sqlite3_snprintf(nTemp, zTemp, "(%.20s)", pColl->zName); + sqlite3XPrintf(&x, "(%.20s)", pColl->zName); break; } case P4_FUNCDEF: { FuncDef *pDef = pOp->p4.pFunc; - sqlite3_snprintf(nTemp, zTemp, "%s(%d)", pDef->zName, pDef->nArg); + sqlite3XPrintf(&x, "%s(%d)", pDef->zName, pDef->nArg); break; } #ifdef SQLITE_DEBUG case P4_FUNCCTX: { FuncDef *pDef = pOp->p4.pCtx->pFunc; - sqlite3_snprintf(nTemp, zTemp, "%s(%d)", pDef->zName, pDef->nArg); + sqlite3XPrintf(&x, "%s(%d)", pDef->zName, pDef->nArg); break; } #endif case P4_INT64: { - sqlite3_snprintf(nTemp, zTemp, "%lld", *pOp->p4.pI64); + sqlite3XPrintf(&x, "%lld", *pOp->p4.pI64); break; } case P4_INT32: { - sqlite3_snprintf(nTemp, zTemp, "%d", pOp->p4.i); + sqlite3XPrintf(&x, "%d", pOp->p4.i); break; } case P4_REAL: { - sqlite3_snprintf(nTemp, zTemp, "%.16g", *pOp->p4.pReal); + sqlite3XPrintf(&x, "%.16g", *pOp->p4.pReal); break; } case P4_MEM: { @@ -67844,11 +71236,11 @@ static char *displayP4(Op *pOp, char *zTemp, int nTemp){ if( pMem->flags & MEM_Str ){ zP4 = pMem->z; }else if( pMem->flags & MEM_Int ){ - sqlite3_snprintf(nTemp, zTemp, "%lld", pMem->u.i); + sqlite3XPrintf(&x, "%lld", pMem->u.i); }else if( pMem->flags & MEM_Real ){ - sqlite3_snprintf(nTemp, zTemp, "%.16g", pMem->u.r); + sqlite3XPrintf(&x, "%.16g", pMem->u.r); }else if( pMem->flags & MEM_Null ){ - sqlite3_snprintf(nTemp, zTemp, "NULL"); + zP4 = "NULL"; }else{ assert( pMem->flags & MEM_Blob ); zP4 = "(blob)"; @@ -67858,22 +71250,34 @@ static char *displayP4(Op *pOp, char *zTemp, int nTemp){ #ifndef SQLITE_OMIT_VIRTUALTABLE case P4_VTAB: { sqlite3_vtab *pVtab = pOp->p4.pVtab->pVtab; - sqlite3_snprintf(nTemp, zTemp, "vtab:%p", pVtab); + sqlite3XPrintf(&x, "vtab:%p", pVtab); break; } #endif case P4_INTARRAY: { - sqlite3_snprintf(nTemp, zTemp, "intarray"); + int i; + int *ai = pOp->p4.ai; + int n = ai[0]; /* The first element of an INTARRAY is always the + ** count of the number of elements to follow */ + for(i=1; ip4.pTab->zName); + break; + } default: { zP4 = pOp->p4.z; if( zP4==0 ){ @@ -67882,10 +71286,11 @@ static char *displayP4(Op *pOp, char *zTemp, int nTemp){ } } } + sqlite3StrAccumFinish(&x); assert( zP4!=0 ); return zP4; } -#endif +#endif /* VDBE_DISPLAY_P4 */ /* ** Declare to the Vdbe that the BTree object at db->aDb[i] is used. @@ -67904,7 +71309,7 @@ SQLITE_PRIVATE void sqlite3VdbeUsesBtree(Vdbe *p, int i){ } } -#if !defined(SQLITE_OMIT_SHARED_CACHE) && SQLITE_THREADSAFE>0 +#if !defined(SQLITE_OMIT_SHARED_CACHE) /* ** If SQLite is compiled to support shared-cache mode and to be threadsafe, ** this routine obtains the mutex associated with each BtShared structure @@ -68001,7 +71406,6 @@ static void releaseMemArray(Mem *p, int N){ if( p && N ){ Mem *pEnd = &p[N]; sqlite3 *db = p->db; - u8 malloc_failed = db->mallocFailed; if( db->pnBytesFreed ){ do{ if( p->szMalloc ) sqlite3DbFree(db, p->zMalloc); @@ -68037,7 +71441,6 @@ static void releaseMemArray(Mem *p, int N){ p->flags = MEM_Undefined; }while( (++p)mallocFailed = malloc_failed; } } @@ -68053,6 +71456,7 @@ SQLITE_PRIVATE void sqlite3VdbeFrameDelete(VdbeFrame *p){ sqlite3VdbeFreeCursor(p->v, apCsr[i]); } releaseMemArray(aMem, p->nChildMem); + sqlite3VdbeDeleteAuxData(p->v->db, &p->pAuxData, -1, 0); sqlite3DbFree(p->v->db, p); } @@ -68095,10 +71499,10 @@ SQLITE_PRIVATE int sqlite3VdbeList( releaseMemArray(pMem, 8); p->pResultSet = 0; - if( p->rc==SQLITE_NOMEM ){ + if( p->rc==SQLITE_NOMEM_BKPT ){ /* This happens if a malloc() inside a call to sqlite3_column_text() or ** sqlite3_column_text16() failed. */ - db->mallocFailed = 1; + sqlite3OomFault(db); return SQLITE_ERROR; } @@ -68199,12 +71603,12 @@ SQLITE_PRIVATE int sqlite3VdbeList( pMem->u.i = pOp->p3; /* P3 */ pMem++; - if( sqlite3VdbeMemClearAndResize(pMem, 32) ){ /* P4 */ + if( sqlite3VdbeMemClearAndResize(pMem, 100) ){ /* P4 */ assert( p->db->mallocFailed ); return SQLITE_ERROR; } pMem->flags = MEM_Str|MEM_Term; - zP4 = displayP4(pOp, pMem->z, 32); + zP4 = displayP4(pOp, pMem->z, pMem->szMalloc); if( zP4!=pMem->z ){ sqlite3VdbeMemSetStr(pMem, zP4, -1, SQLITE_UTF8, 0); }else{ @@ -68296,43 +71700,46 @@ SQLITE_PRIVATE void sqlite3VdbeIOTraceSql(Vdbe *p){ } #endif /* !SQLITE_OMIT_TRACE && SQLITE_ENABLE_IOTRACE */ -/* -** Allocate space from a fixed size buffer and return a pointer to -** that space. If insufficient space is available, return NULL. +/* An instance of this object describes bulk memory available for use +** by subcomponents of a prepared statement. Space is allocated out +** of a ReusableSpace object by the allocSpace() routine below. +*/ +struct ReusableSpace { + u8 *pSpace; /* Available memory */ + int nFree; /* Bytes of available memory */ + int nNeeded; /* Total bytes that could not be allocated */ +}; + +/* Try to allocate nByte bytes of 8-byte aligned bulk memory for pBuf +** from the ReusableSpace object. Return a pointer to the allocated +** memory on success. If insufficient memory is available in the +** ReusableSpace object, increase the ReusableSpace.nNeeded +** value by the amount needed and return NULL. ** -** The pBuf parameter is the initial value of a pointer which will -** receive the new memory. pBuf is normally NULL. If pBuf is not -** NULL, it means that memory space has already been allocated and that -** this routine should not allocate any new memory. When pBuf is not -** NULL simply return pBuf. Only allocate new memory space when pBuf -** is NULL. +** If pBuf is not initially NULL, that means that the memory has already +** been allocated by a prior call to this routine, so just return a copy +** of pBuf and leave ReusableSpace unchanged. ** -** nByte is the number of bytes of space needed. -** -** *ppFrom points to available space and pEnd points to the end of the -** available space. When space is allocated, *ppFrom is advanced past -** the end of the allocated space. -** -** *pnByte is a counter of the number of bytes of space that have failed -** to allocate. If there is insufficient space in *ppFrom to satisfy the -** request, then increment *pnByte by the amount of the request. +** This allocator is employed to repurpose unused slots at the end of the +** opcode array of prepared state for other memory needs of the prepared +** statement. */ static void *allocSpace( - void *pBuf, /* Where return pointer will be stored */ - int nByte, /* Number of bytes to allocate */ - u8 **ppFrom, /* IN/OUT: Allocate from *ppFrom */ - u8 *pEnd, /* Pointer to 1 byte past the end of *ppFrom buffer */ - int *pnByte /* If allocation cannot be made, increment *pnByte */ + struct ReusableSpace *p, /* Bulk memory available for allocation */ + void *pBuf, /* Pointer to a prior allocation */ + int nByte /* Bytes of memory needed */ ){ - assert( EIGHT_BYTE_ALIGNMENT(*ppFrom) ); - if( pBuf ) return pBuf; - nByte = ROUND8(nByte); - if( &(*ppFrom)[nByte] <= pEnd ){ - pBuf = (void*)*ppFrom; - *ppFrom += nByte; - }else{ - *pnByte += nByte; + assert( EIGHT_BYTE_ALIGNMENT(p->pSpace) ); + if( pBuf==0 ){ + nByte = ROUND8(nByte); + if( nByte <= p->nFree ){ + p->nFree -= nByte; + pBuf = &p->pSpace[p->nFree]; + }else{ + p->nNeeded += nByte; + } } + assert( EIGHT_BYTE_ALIGNMENT(pBuf) ); return pBuf; } @@ -68355,14 +71762,13 @@ SQLITE_PRIVATE void sqlite3VdbeRewind(Vdbe *p){ p->magic = VDBE_MAGIC_RUN; #ifdef SQLITE_DEBUG - for(i=1; inMem; i++){ + for(i=0; inMem; i++){ assert( p->aMem[i].db==p->db ); } #endif p->pc = -1; p->rc = SQLITE_OK; p->errorAction = OE_Abort; - p->magic = VDBE_MAGIC_RUN; p->nChange = 0; p->cacheCtr = 1; p->minWriteFileFormat = 255; @@ -68405,9 +71811,7 @@ SQLITE_PRIVATE void sqlite3VdbeMakeReady( int nArg; /* Number of arguments in subprograms */ int nOnce; /* Number of OP_Once instructions */ int n; /* Loop counter */ - u8 *zCsr; /* Memory available for allocation */ - u8 *zEnd; /* First byte past allocated memory */ - int nByte; /* How much extra memory is needed */ + struct ReusableSpace x; /* Reusable bulk memory */ assert( p!=0 ); assert( p->nOp>0 ); @@ -68423,61 +71827,59 @@ SQLITE_PRIVATE void sqlite3VdbeMakeReady( nOnce = pParse->nOnce; if( nOnce==0 ) nOnce = 1; /* Ensure at least one byte in p->aOnceFlag[] */ - /* For each cursor required, also allocate a memory cell. Memory - ** cells (nMem+1-nCursor)..nMem, inclusive, will never be used by - ** the vdbe program. Instead they are used to allocate space for - ** VdbeCursor/BtCursor structures. The blob of memory associated with - ** cursor 0 is stored in memory cell nMem. Memory cell (nMem-1) - ** stores the blob of memory associated with cursor 1, etc. - ** + /* Each cursor uses a memory cell. The first cursor (cursor 0) can + ** use aMem[0] which is not otherwise used by the VDBE program. Allocate + ** space at the end of aMem[] for cursors 1 and greater. ** See also: allocateCursor(). */ nMem += nCursor; + if( nCursor==0 && nMem>0 ) nMem++; /* Space for aMem[0] even if not used */ - /* Allocate space for memory registers, SQL variables, VDBE cursors and - ** an array to marshal SQL function arguments in. + /* Figure out how much reusable memory is available at the end of the + ** opcode array. This extra memory will be reallocated for other elements + ** of the prepared statement. */ - zCsr = (u8*)&p->aOp[p->nOp]; /* Memory avaliable for allocation */ - zEnd = (u8*)&p->aOp[pParse->nOpAlloc]; /* First byte past end of zCsr[] */ + n = ROUND8(sizeof(Op)*p->nOp); /* Bytes of opcode memory used */ + x.pSpace = &((u8*)p->aOp)[n]; /* Unused opcode memory */ + assert( EIGHT_BYTE_ALIGNMENT(x.pSpace) ); + x.nFree = ROUNDDOWN8(pParse->szOpAlloc - n); /* Bytes of unused memory */ + assert( x.nFree>=0 ); + if( x.nFree>0 ){ + memset(x.pSpace, 0, x.nFree); + assert( EIGHT_BYTE_ALIGNMENT(&x.pSpace[x.nFree]) ); + } resolveP2Values(p, &nArg); p->usesStmtJournal = (u8)(pParse->isMultiWrite && pParse->mayAbort); if( pParse->explain && nMem<10 ){ nMem = 10; } - memset(zCsr, 0, zEnd-zCsr); - zCsr += (zCsr - (u8*)0)&7; - assert( EIGHT_BYTE_ALIGNMENT(zCsr) ); p->expired = 0; - /* Memory for registers, parameters, cursor, etc, is allocated in two - ** passes. On the first pass, we try to reuse unused space at the + /* Memory for registers, parameters, cursor, etc, is allocated in one or two + ** passes. On the first pass, we try to reuse unused memory at the ** end of the opcode array. If we are unable to satisfy all memory ** requirements by reusing the opcode array tail, then the second - ** pass will fill in the rest using a fresh allocation. + ** pass will fill in the remainder using a fresh memory allocation. ** ** This two-pass approach that reuses as much memory as possible from - ** the leftover space at the end of the opcode array can significantly + ** the leftover memory at the end of the opcode array. This can significantly ** reduce the amount of memory held by a prepared statement. */ do { - nByte = 0; - p->aMem = allocSpace(p->aMem, nMem*sizeof(Mem), &zCsr, zEnd, &nByte); - p->aVar = allocSpace(p->aVar, nVar*sizeof(Mem), &zCsr, zEnd, &nByte); - p->apArg = allocSpace(p->apArg, nArg*sizeof(Mem*), &zCsr, zEnd, &nByte); - p->azVar = allocSpace(p->azVar, nVar*sizeof(char*), &zCsr, zEnd, &nByte); - p->apCsr = allocSpace(p->apCsr, nCursor*sizeof(VdbeCursor*), - &zCsr, zEnd, &nByte); - p->aOnceFlag = allocSpace(p->aOnceFlag, nOnce, &zCsr, zEnd, &nByte); + x.nNeeded = 0; + p->aMem = allocSpace(&x, p->aMem, nMem*sizeof(Mem)); + p->aVar = allocSpace(&x, p->aVar, nVar*sizeof(Mem)); + p->apArg = allocSpace(&x, p->apArg, nArg*sizeof(Mem*)); + p->apCsr = allocSpace(&x, p->apCsr, nCursor*sizeof(VdbeCursor*)); + p->aOnceFlag = allocSpace(&x, p->aOnceFlag, nOnce); #ifdef SQLITE_ENABLE_STMT_SCANSTATUS - p->anExec = allocSpace(p->anExec, p->nOp*sizeof(i64), &zCsr, zEnd, &nByte); + p->anExec = allocSpace(&x, p->anExec, p->nOp*sizeof(i64)); #endif - if( nByte ){ - p->pFree = sqlite3DbMallocZero(db, nByte); - } - zCsr = p->pFree; - zEnd = &zCsr[nByte]; - }while( nByte && !db->mallocFailed ); + if( x.nNeeded==0 ) break; + x.pSpace = p->pFree = sqlite3DbMallocZero(db, x.nNeeded); + x.nFree = x.nNeeded; + }while( !db->mallocFailed ); p->nCursor = nCursor; p->nOnceFlag = nOnce; @@ -68488,15 +71890,13 @@ SQLITE_PRIVATE void sqlite3VdbeMakeReady( p->aVar[n].db = db; } } - if( p->azVar && pParse->nzVar>0 ){ - p->nzVar = pParse->nzVar; - memcpy(p->azVar, pParse->azVar, p->nzVar*sizeof(p->azVar[0])); - memset(pParse->azVar, 0, pParse->nzVar*sizeof(pParse->azVar[0])); - } + p->nzVar = pParse->nzVar; + p->azVar = pParse->azVar; + pParse->nzVar = 0; + pParse->azVar = 0; if( p->aMem ){ - p->aMem--; /* aMem[] goes from 1..nMem */ - p->nMem = nMem; /* not from 0..nMem-1 */ - for(n=1; n<=nMem; n++){ + p->nMem = nMem; + for(n=0; naMem[n].flags = MEM_Undefined; p->aMem[n].db = db; } @@ -68513,23 +71913,34 @@ SQLITE_PRIVATE void sqlite3VdbeFreeCursor(Vdbe *p, VdbeCursor *pCx){ if( pCx==0 ){ return; } - sqlite3VdbeSorterClose(p->db, pCx); - if( pCx->pBt ){ - sqlite3BtreeClose(pCx->pBt); - /* The pCx->pCursor will be close automatically, if it exists, by - ** the call above. */ - }else if( pCx->pCursor ){ - sqlite3BtreeCloseCursor(pCx->pCursor); - } + assert( pCx->pBt==0 || pCx->eCurType==CURTYPE_BTREE ); + switch( pCx->eCurType ){ + case CURTYPE_SORTER: { + sqlite3VdbeSorterClose(p->db, pCx); + break; + } + case CURTYPE_BTREE: { + if( pCx->pBt ){ + sqlite3BtreeClose(pCx->pBt); + /* The pCx->pCursor will be close automatically, if it exists, by + ** the call above. */ + }else{ + assert( pCx->uc.pCursor!=0 ); + sqlite3BtreeCloseCursor(pCx->uc.pCursor); + } + break; + } #ifndef SQLITE_OMIT_VIRTUALTABLE - else if( pCx->pVtabCursor ){ - sqlite3_vtab_cursor *pVtabCursor = pCx->pVtabCursor; - const sqlite3_module *pModule = pVtabCursor->pVtab->pModule; - assert( pVtabCursor->pVtab->nRef>0 ); - pVtabCursor->pVtab->nRef--; - pModule->xClose(pVtabCursor); - } + case CURTYPE_VTAB: { + sqlite3_vtab_cursor *pVCur = pCx->uc.pVCur; + const sqlite3_module *pModule = pVCur->pVtab->pModule; + assert( pVCur->pVtab->nRef>0 ); + pVCur->pVtab->nRef--; + pModule->xClose(pVCur); + break; + } #endif + } } /* @@ -68570,6 +71981,9 @@ SQLITE_PRIVATE int sqlite3VdbeFrameRestore(VdbeFrame *pFrame){ v->db->lastRowid = pFrame->lastRowid; v->nChange = pFrame->nChange; v->db->nChange = pFrame->nDbChange; + sqlite3VdbeDeleteAuxData(v->db, &v->pAuxData, -1, 0); + v->pAuxData = pFrame->pAuxData; + pFrame->pAuxData = 0; return pFrame->pc; } @@ -68592,7 +72006,7 @@ static void closeAllCursors(Vdbe *p){ assert( p->nFrame==0 ); closeCursorsInFrame(p); if( p->aMem ){ - releaseMemArray(&p->aMem[1], p->nMem); + releaseMemArray(p->aMem, p->nMem); } while( p->pDelFrame ){ VdbeFrame *pDel = p->pDelFrame; @@ -68601,7 +72015,7 @@ static void closeAllCursors(Vdbe *p){ } /* Delete any auxdata allocations made by the VM */ - if( p->pAuxData ) sqlite3VdbeDeleteAuxData(p, -1, 0); + if( p->pAuxData ) sqlite3VdbeDeleteAuxData(p->db, &p->pAuxData, -1, 0); assert( p->pAuxData==0 ); } @@ -68617,7 +72031,7 @@ static void Cleanup(Vdbe *p){ int i; if( p->apCsr ) for(i=0; inCursor; i++) assert( p->apCsr[i]==0 ); if( p->aMem ){ - for(i=1; i<=p->nMem; i++) assert( p->aMem[i].flags==MEM_Undefined ); + for(i=0; inMem; i++) assert( p->aMem[i].flags==MEM_Undefined ); } #endif @@ -68673,7 +72087,7 @@ SQLITE_PRIVATE int sqlite3VdbeSetColName( assert( vardb->mallocFailed ){ assert( !zName || xDel!=SQLITE_DYNAMIC ); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } assert( p->aColName!=0 ); pColName = &(p->aColName[idx+var*p->nResColumn]); @@ -68690,7 +72104,9 @@ SQLITE_PRIVATE int sqlite3VdbeSetColName( */ static int vdbeCommit(sqlite3 *db, Vdbe *p){ int i; - int nTrans = 0; /* Number of databases with an active write-transaction */ + int nTrans = 0; /* Number of databases with an active write-transaction + ** that are candidates for a two-phase commit using a + ** master-journal */ int rc = SQLITE_OK; int needXcommit = 0; @@ -68718,10 +72134,28 @@ static int vdbeCommit(sqlite3 *db, Vdbe *p){ for(i=0; rc==SQLITE_OK && inDb; i++){ Btree *pBt = db->aDb[i].pBt; if( sqlite3BtreeIsInTrans(pBt) ){ + /* Whether or not a database might need a master journal depends upon + ** its journal mode (among other things). This matrix determines which + ** journal modes use a master journal and which do not */ + static const u8 aMJNeeded[] = { + /* DELETE */ 1, + /* PERSIST */ 1, + /* OFF */ 0, + /* TRUNCATE */ 1, + /* MEMORY */ 0, + /* WAL */ 0 + }; + Pager *pPager; /* Pager associated with pBt */ needXcommit = 1; - if( i!=1 ) nTrans++; sqlite3BtreeEnter(pBt); - rc = sqlite3PagerExclusiveLock(sqlite3BtreePager(pBt)); + pPager = sqlite3BtreePager(pBt); + if( db->aDb[i].safety_level!=PAGER_SYNCHRONOUS_OFF + && aMJNeeded[sqlite3PagerGetJournalMode(pPager)] + ){ + assert( i!=1 ); + nTrans++; + } + rc = sqlite3PagerExclusiveLock(pPager); sqlite3BtreeLeave(pBt); } } @@ -68779,7 +72213,6 @@ static int vdbeCommit(sqlite3 *db, Vdbe *p){ #ifndef SQLITE_OMIT_DISKIO else{ sqlite3_vfs *pVfs = db->pVfs; - int needSync = 0; char *zMaster = 0; /* File-name for the master journal */ char const *zMainFile = sqlite3BtreeGetFilename(db->aDb[0].pBt); sqlite3_file *pMaster = 0; @@ -68791,7 +72224,7 @@ static int vdbeCommit(sqlite3 *db, Vdbe *p){ /* Select a master journal file name */ nMainFile = sqlite3Strlen30(zMainFile); zMaster = sqlite3MPrintf(db, "%s-mjXXXXXX9XXz", zMainFile); - if( zMaster==0 ) return SQLITE_NOMEM; + if( zMaster==0 ) return SQLITE_NOMEM_BKPT; do { u32 iRandom; if( retryCount ){ @@ -68839,9 +72272,6 @@ static int vdbeCommit(sqlite3 *db, Vdbe *p){ continue; /* Ignore TEMP and :memory: databases */ } assert( zFile[0]!=0 ); - if( !needSync && !sqlite3BtreeSyncDisabled(pBt) ){ - needSync = 1; - } rc = sqlite3OsWrite(pMaster, zFile, sqlite3Strlen30(zFile)+1, offset); offset += sqlite3Strlen30(zFile)+1; if( rc!=SQLITE_OK ){ @@ -68856,8 +72286,7 @@ static int vdbeCommit(sqlite3 *db, Vdbe *p){ /* Sync the master journal file. If the IOCAP_SEQUENTIAL device ** flag is set this is not required. */ - if( needSync - && 0==(sqlite3OsDeviceCharacteristics(pMaster)&SQLITE_IOCAP_SEQUENTIAL) + if( 0==(sqlite3OsDeviceCharacteristics(pMaster)&SQLITE_IOCAP_SEQUENTIAL) && SQLITE_OK!=(rc = sqlite3OsSync(pMaster, SQLITE_SYNC_NORMAL)) ){ sqlite3OsCloseFree(pMaster); @@ -68893,7 +72322,7 @@ static int vdbeCommit(sqlite3 *db, Vdbe *p){ ** doing this the directory is synced again before any individual ** transaction files are deleted. */ - rc = sqlite3OsDelete(pVfs, zMaster, needSync); + rc = sqlite3OsDelete(pVfs, zMaster, 1); sqlite3DbFree(db, zMaster); zMaster = 0; if( rc ){ @@ -69080,8 +72509,8 @@ SQLITE_PRIVATE int sqlite3VdbeHalt(Vdbe *p){ ** one, or the complete transaction if there is no statement transaction. */ - if( p->db->mallocFailed ){ - p->rc = SQLITE_NOMEM; + if( db->mallocFailed ){ + p->rc = SQLITE_NOMEM_BKPT; } if( p->aOnceFlag ) memset(p->aOnceFlag, 0, p->nOnceFlag); closeAllCursors(p); @@ -69241,8 +72670,8 @@ SQLITE_PRIVATE int sqlite3VdbeHalt(Vdbe *p){ } p->magic = VDBE_MAGIC_HALT; checkActiveVdbeCnt(db); - if( p->db->mallocFailed ){ - p->rc = SQLITE_NOMEM; + if( db->mallocFailed ){ + p->rc = SQLITE_NOMEM_BKPT; } /* If the auto-commit flag is set to true, then any locks that were held @@ -69278,12 +72707,12 @@ SQLITE_PRIVATE int sqlite3VdbeTransferError(Vdbe *p){ sqlite3 *db = p->db; int rc = p->rc; if( p->zErrMsg ){ - u8 mallocFailed = db->mallocFailed; + db->bBenignMalloc++; sqlite3BeginBenignMalloc(); if( db->pErr==0 ) db->pErr = sqlite3ValueNew(db); sqlite3ValueSetStr(db->pErr, -1, p->zErrMsg, SQLITE_UTF8, SQLITE_TRANSIENT); sqlite3EndBenignMalloc(); - db->mallocFailed = mallocFailed; + db->bBenignMalloc--; db->errCode = rc; }else{ sqlite3Error(db, rc); @@ -69429,8 +72858,7 @@ SQLITE_PRIVATE int sqlite3VdbeFinalize(Vdbe *p){ ** * the corresponding bit in argument mask is clear (where the first ** function parameter corresponds to bit 0 etc.). */ -SQLITE_PRIVATE void sqlite3VdbeDeleteAuxData(Vdbe *pVdbe, int iOp, int mask){ - AuxData **pp = &pVdbe->pAuxData; +SQLITE_PRIVATE void sqlite3VdbeDeleteAuxData(sqlite3 *db, AuxData **pp, int iOp, int mask){ while( *pp ){ AuxData *pAux = *pp; if( (iOp<0) @@ -69441,7 +72869,7 @@ SQLITE_PRIVATE void sqlite3VdbeDeleteAuxData(Vdbe *pVdbe, int iOp, int mask){ pAux->xDelete(pAux->pAux); } *pp = pAux->pNext; - sqlite3DbFree(pVdbe->db, pAux); + sqlite3DbFree(db, pAux); }else{ pp= &pAux->pNext; } @@ -69468,6 +72896,7 @@ SQLITE_PRIVATE void sqlite3VdbeClearObject(sqlite3 *db, Vdbe *p){ sqlite3DbFree(db, pSub); } for(i=p->nzVar-1; i>=0; i--) sqlite3DbFree(db, p->azVar[i]); + sqlite3DbFree(db, p->azVar); vdbeFreeOpArray(db, p->aOp, p->nOp); sqlite3DbFree(db, p->aColName); sqlite3DbFree(db, p->zSql); @@ -69516,7 +72945,8 @@ static int SQLITE_NOINLINE handleDeferredMoveto(VdbeCursor *p){ #endif assert( p->deferredMoveto ); assert( p->isTable ); - rc = sqlite3BtreeMovetoUnpacked(p->pCursor, 0, p->movetoTarget, 0, &res); + assert( p->eCurType==CURTYPE_BTREE ); + rc = sqlite3BtreeMovetoUnpacked(p->uc.pCursor, 0, p->movetoTarget, 0, &res); if( rc ) return rc; if( res!=0 ) return SQLITE_CORRUPT_BKPT; #ifdef SQLITE_TEST @@ -69536,9 +72966,10 @@ static int SQLITE_NOINLINE handleDeferredMoveto(VdbeCursor *p){ */ static int SQLITE_NOINLINE handleMovedCursor(VdbeCursor *p){ int isDifferentRow, rc; - assert( p->pCursor!=0 ); - assert( sqlite3BtreeCursorHasMoved(p->pCursor) ); - rc = sqlite3BtreeCursorRestore(p->pCursor, &isDifferentRow); + assert( p->eCurType==CURTYPE_BTREE ); + assert( p->uc.pCursor!=0 ); + assert( sqlite3BtreeCursorHasMoved(p->uc.pCursor) ); + rc = sqlite3BtreeCursorRestore(p->uc.pCursor, &isDifferentRow); p->cacheStatus = CACHE_STALE; if( isDifferentRow ) p->nullRow = 1; return rc; @@ -69549,7 +72980,8 @@ static int SQLITE_NOINLINE handleMovedCursor(VdbeCursor *p){ ** if need be. Return any I/O error from the restore operation. */ SQLITE_PRIVATE int sqlite3VdbeCursorRestore(VdbeCursor *p){ - if( sqlite3BtreeCursorHasMoved(p->pCursor) ){ + assert( p->eCurType==CURTYPE_BTREE ); + if( sqlite3BtreeCursorHasMoved(p->uc.pCursor) ){ return handleMovedCursor(p); } return SQLITE_OK; @@ -69568,12 +73000,21 @@ SQLITE_PRIVATE int sqlite3VdbeCursorRestore(VdbeCursor *p){ ** If the cursor is already pointing to the correct row and that row has ** not been deleted out from under the cursor, then this routine is a no-op. */ -SQLITE_PRIVATE int sqlite3VdbeCursorMoveto(VdbeCursor *p){ - if( p->deferredMoveto ){ - return handleDeferredMoveto(p); - } - if( p->pCursor && sqlite3BtreeCursorHasMoved(p->pCursor) ){ - return handleMovedCursor(p); +SQLITE_PRIVATE int sqlite3VdbeCursorMoveto(VdbeCursor **pp, int *piCol){ + VdbeCursor *p = *pp; + if( p->eCurType==CURTYPE_BTREE ){ + if( p->deferredMoveto ){ + int iMap; + if( p->aAltMap && (iMap = p->aAltMap[1+*piCol])>0 ){ + *pp = p->pAltCursor; + *piCol = iMap - 1; + return SQLITE_OK; + } + return handleDeferredMoveto(p); + } + if( sqlite3BtreeCursorHasMoved(p->uc.pCursor) ){ + return handleMovedCursor(p); + } } return SQLITE_OK; } @@ -69623,11 +73064,13 @@ SQLITE_PRIVATE int sqlite3VdbeCursorMoveto(VdbeCursor *p){ /* ** Return the serial-type for the value stored in pMem. */ -SQLITE_PRIVATE u32 sqlite3VdbeSerialType(Mem *pMem, int file_format){ +SQLITE_PRIVATE u32 sqlite3VdbeSerialType(Mem *pMem, int file_format, u32 *pLen){ int flags = pMem->flags; u32 n; + assert( pLen!=0 ); if( flags&MEM_Null ){ + *pLen = 0; return 0; } if( flags&MEM_Int ){ @@ -69641,15 +73084,23 @@ SQLITE_PRIVATE u32 sqlite3VdbeSerialType(Mem *pMem, int file_format){ u = i; } if( u<=127 ){ - return ((i&1)==i && file_format>=4) ? 8+(u32)u : 1; + if( (i&1)==i && file_format>=4 ){ + *pLen = 0; + return 8+(u32)u; + }else{ + *pLen = 1; + return 1; + } } - if( u<=32767 ) return 2; - if( u<=8388607 ) return 3; - if( u<=2147483647 ) return 4; - if( u<=MAX_6BYTE ) return 5; + if( u<=32767 ){ *pLen = 2; return 2; } + if( u<=8388607 ){ *pLen = 3; return 3; } + if( u<=2147483647 ){ *pLen = 4; return 4; } + if( u<=MAX_6BYTE ){ *pLen = 6; return 5; } + *pLen = 8; return 6; } if( flags&MEM_Real ){ + *pLen = 8; return 7; } assert( pMem->db->mallocFailed || flags&(MEM_Str|MEM_Blob) ); @@ -69658,26 +73109,46 @@ SQLITE_PRIVATE u32 sqlite3VdbeSerialType(Mem *pMem, int file_format){ if( flags & MEM_Zero ){ n += pMem->u.nZero; } + *pLen = n; return ((n*2) + 12 + ((flags&MEM_Str)!=0)); } /* -** The sizes for serial types less than 12 +** The sizes for serial types less than 128 */ static const u8 sqlite3SmallTypeSizes[] = { - 0, 1, 2, 3, 4, 6, 8, 8, 0, 0, 0, 0 + /* 0 1 2 3 4 5 6 7 8 9 */ +/* 0 */ 0, 1, 2, 3, 4, 6, 8, 8, 0, 0, +/* 10 */ 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, +/* 20 */ 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, +/* 30 */ 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, +/* 40 */ 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, +/* 50 */ 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, +/* 60 */ 24, 24, 25, 25, 26, 26, 27, 27, 28, 28, +/* 70 */ 29, 29, 30, 30, 31, 31, 32, 32, 33, 33, +/* 80 */ 34, 34, 35, 35, 36, 36, 37, 37, 38, 38, +/* 90 */ 39, 39, 40, 40, 41, 41, 42, 42, 43, 43, +/* 100 */ 44, 44, 45, 45, 46, 46, 47, 47, 48, 48, +/* 110 */ 49, 49, 50, 50, 51, 51, 52, 52, 53, 53, +/* 120 */ 54, 54, 55, 55, 56, 56, 57, 57 }; /* ** Return the length of the data corresponding to the supplied serial-type. */ SQLITE_PRIVATE u32 sqlite3VdbeSerialTypeLen(u32 serial_type){ - if( serial_type>=12 ){ + if( serial_type>=128 ){ return (serial_type-12)/2; }else{ + assert( serial_type<12 + || sqlite3SmallTypeSizes[serial_type]==(serial_type - 12)/2 ); return sqlite3SmallTypeSizes[serial_type]; } } +SQLITE_PRIVATE u8 sqlite3VdbeOneByteSerialTypeLen(u8 serial_type){ + assert( serial_type<128 ); + return sqlite3SmallTypeSizes[serial_type]; +} /* ** If we are on an architecture with mixed-endian floating @@ -69773,7 +73244,7 @@ SQLITE_PRIVATE u32 sqlite3VdbeSerialPut(u8 *buf, Mem *pMem, u32 serial_type){ assert( pMem->n + ((pMem->flags & MEM_Zero)?pMem->u.nZero:0) == (int)sqlite3VdbeSerialTypeLen(serial_type) ); len = pMem->n; - memcpy(buf, pMem->z, len); + if( len>0 ) memcpy(buf, pMem->z, len); return len; } @@ -69876,6 +73347,10 @@ SQLITE_PRIVATE u32 sqlite3VdbeSerialGet( /* EVIDENCE-OF: R-01849-26079 Value is a big-endian 32-bit ** twos-complement integer. */ pMem->u.i = FOUR_BYTE_INT(buf); +#ifdef __HP_cc + /* Work around a sign-extension bug in the HP compiler for HP/UX */ + if( buf[0]&0x80 ) pMem->u.i |= 0xffffffff80000000LL; +#endif pMem->flags = MEM_Int; testcase( pMem->u.i<0 ); return 4; @@ -69993,6 +73468,7 @@ SQLITE_PRIVATE void sqlite3VdbeRecordUnpack( pMem->db = pKeyInfo->db; /* pMem->flags = 0; // sqlite3VdbeSerialGet() will set this for us */ pMem->szMalloc = 0; + pMem->z = 0; d += sqlite3VdbeSerialGet(&aKey[d], serial_type, pMem); pMem++; if( (++u)>=p->nField ) break; @@ -70173,9 +73649,9 @@ static int vdbeCompareMemString( v2 = sqlite3ValueText((sqlite3_value*)&c2, pColl->enc); n2 = v2==0 ? 0 : c2.n; rc = pColl->xCmp(pColl->pUser, n1, v1, n2, v2); + if( (v1==0 || v2==0) && prcErr ) *prcErr = SQLITE_NOMEM_BKPT; sqlite3VdbeMemRelease(&c1); sqlite3VdbeMemRelease(&c2); - if( (v1==0 || v2==0) && prcErr ) *prcErr = SQLITE_NOMEM; return rc; } } @@ -70191,6 +73667,34 @@ static SQLITE_NOINLINE int sqlite3BlobCompare(const Mem *pB1, const Mem *pB2){ return pB1->n - pB2->n; } +/* +** Do a comparison between a 64-bit signed integer and a 64-bit floating-point +** number. Return negative, zero, or positive if the first (i64) is less than, +** equal to, or greater than the second (double). +*/ +static int sqlite3IntFloatCompare(i64 i, double r){ + if( sizeof(LONGDOUBLE_TYPE)>8 ){ + LONGDOUBLE_TYPE x = (LONGDOUBLE_TYPE)i; + if( xr ) return +1; + return 0; + }else{ + i64 y; + double s; + if( r<-9223372036854775808.0 ) return +1; + if( r>9223372036854775807.0 ) return -1; + y = (i64)r; + if( iy ){ + if( y==SMALLEST_INT64 && r>0.0 ) return -1; + return +1; + } + s = (double)i; + if( sr ) return +1; + return 0; + } +} /* ** Compare the values contained by the two memory cells, returning @@ -70217,34 +73721,34 @@ SQLITE_PRIVATE int sqlite3MemCompare(const Mem *pMem1, const Mem *pMem2, const C return (f2&MEM_Null) - (f1&MEM_Null); } - /* If one value is a number and the other is not, the number is less. - ** If both are numbers, compare as reals if one is a real, or as integers - ** if both values are integers. + /* At least one of the two values is a number */ if( combined_flags&(MEM_Int|MEM_Real) ){ - double r1, r2; if( (f1 & f2 & MEM_Int)!=0 ){ if( pMem1->u.i < pMem2->u.i ) return -1; - if( pMem1->u.i > pMem2->u.i ) return 1; + if( pMem1->u.i > pMem2->u.i ) return +1; return 0; } + if( (f1 & f2 & MEM_Real)!=0 ){ + if( pMem1->u.r < pMem2->u.r ) return -1; + if( pMem1->u.r > pMem2->u.r ) return +1; + return 0; + } + if( (f1&MEM_Int)!=0 ){ + if( (f2&MEM_Real)!=0 ){ + return sqlite3IntFloatCompare(pMem1->u.i, pMem2->u.r); + }else{ + return -1; + } + } if( (f1&MEM_Real)!=0 ){ - r1 = pMem1->u.r; - }else if( (f1&MEM_Int)!=0 ){ - r1 = (double)pMem1->u.i; - }else{ - return 1; + if( (f2&MEM_Int)!=0 ){ + return -sqlite3IntFloatCompare(pMem2->u.i, pMem1->u.r); + }else{ + return -1; + } } - if( (f2&MEM_Real)!=0 ){ - r2 = pMem2->u.r; - }else if( (f2&MEM_Int)!=0 ){ - r2 = (double)pMem2->u.i; - }else{ - return -1; - } - if( r1r2 ) return 1; - return 0; + return +1; } /* If one value is a string and the other is a blob, the string is less. @@ -70258,7 +73762,7 @@ SQLITE_PRIVATE int sqlite3MemCompare(const Mem *pMem1, const Mem *pMem2, const C return -1; } - assert( pMem1->enc==pMem2->enc ); + assert( pMem1->enc==pMem2->enc || pMem1->db->mallocFailed ); assert( pMem1->enc==SQLITE_UTF8 || pMem1->enc==SQLITE_UTF16LE || pMem1->enc==SQLITE_UTF16BE ); @@ -70395,13 +73899,8 @@ SQLITE_PRIVATE int sqlite3VdbeRecordCompareWithSkip( }else if( serial_type==0 ){ rc = -1; }else if( serial_type==7 ){ - double rhs = (double)pRhs->u.i; sqlite3VdbeSerialGet(&aKey1[d1], serial_type, &mem1); - if( mem1.u.rrhs ){ - rc = +1; - } + rc = -sqlite3IntFloatCompare(pRhs->u.i, mem1.u.r); }else{ i64 lhs = vdbeRecordDecodeInt(serial_type, &aKey1[d1]); i64 rhs = pRhs->u.i; @@ -70425,18 +73924,15 @@ SQLITE_PRIVATE int sqlite3VdbeRecordCompareWithSkip( }else if( serial_type==0 ){ rc = -1; }else{ - double rhs = pRhs->u.r; - double lhs; sqlite3VdbeSerialGet(&aKey1[d1], serial_type, &mem1); if( serial_type==7 ){ - lhs = mem1.u.r; + if( mem1.u.ru.r ){ + rc = -1; + }else if( mem1.u.r>pRhs->u.r ){ + rc = +1; + } }else{ - lhs = (double)mem1.u.i; - } - if( lhsrhs ){ - rc = +1; + rc = sqlite3IntFloatCompare(mem1.u.i, pRhs->u.r); } } } @@ -70526,6 +74022,7 @@ SQLITE_PRIVATE int sqlite3VdbeRecordCompareWithSkip( || vdbeRecordCompareDebug(nKey1, pKey1, pPKey2, pPKey2->default_rc) || pKeyInfo->db->mallocFailed ); + pPKey2->eqSeen = 1; return pPKey2->default_rc; } SQLITE_PRIVATE int sqlite3VdbeRecordCompare( @@ -70625,6 +74122,7 @@ static int vdbeRecordCompareInt( /* The first fields of the two keys are equal and there are no trailing ** fields. Return pPKey2->default_rc in this case. */ res = pPKey2->default_rc; + pPKey2->eqSeen = 1; } assert( vdbeRecordCompareDebug(nKey1, pKey1, pPKey2, res) ); @@ -70645,6 +74143,7 @@ static int vdbeRecordCompareString( int serial_type; int res; + assert( pPKey2->aMem[0].flags & MEM_Str ); vdbeAssertFieldCountWithinLimits(nKey1, pKey1, pPKey2->pKeyInfo); getVarint32(&aKey1[1], serial_type); if( serial_type<12 ){ @@ -70671,6 +74170,7 @@ static int vdbeRecordCompareString( res = sqlite3VdbeRecordCompareWithSkip(nKey1, pKey1, pPKey2, 1); }else{ res = pPKey2->default_rc; + pPKey2->eqSeen = 1; } }else if( res>0 ){ res = pPKey2->r2; @@ -70828,9 +74328,11 @@ SQLITE_PRIVATE int sqlite3VdbeIdxKeyCompare( ){ i64 nCellKey = 0; int rc; - BtCursor *pCur = pC->pCursor; + BtCursor *pCur; Mem m; + assert( pC->eCurType==CURTYPE_BTREE ); + pCur = pC->uc.pCursor; assert( sqlite3BtreeCursorIsValid(pCur) ); VVA_ONLY(rc =) sqlite3BtreeKeySize(pCur, &nCellKey); assert( rc==SQLITE_OK ); /* pCur is always valid so KeySize cannot fail */ @@ -70841,7 +74343,7 @@ SQLITE_PRIVATE int sqlite3VdbeIdxKeyCompare( return SQLITE_CORRUPT_BKPT; } sqlite3VdbeMemInit(&m, db, 0); - rc = sqlite3VdbeMemFromBtree(pC->pCursor, 0, (u32)nCellKey, 1, &m); + rc = sqlite3VdbeMemFromBtree(pCur, 0, (u32)nCellKey, 1, &m); if( rc ){ return rc; } @@ -70937,14 +74439,100 @@ SQLITE_PRIVATE void sqlite3VdbeSetVarmask(Vdbe *v, int iVar){ ** in memory obtained from sqlite3DbMalloc). */ SQLITE_PRIVATE void sqlite3VtabImportErrmsg(Vdbe *p, sqlite3_vtab *pVtab){ - sqlite3 *db = p->db; - sqlite3DbFree(db, p->zErrMsg); - p->zErrMsg = sqlite3DbStrDup(db, pVtab->zErrMsg); - sqlite3_free(pVtab->zErrMsg); - pVtab->zErrMsg = 0; + if( pVtab->zErrMsg ){ + sqlite3 *db = p->db; + sqlite3DbFree(db, p->zErrMsg); + p->zErrMsg = sqlite3DbStrDup(db, pVtab->zErrMsg); + sqlite3_free(pVtab->zErrMsg); + pVtab->zErrMsg = 0; + } } #endif /* SQLITE_OMIT_VIRTUALTABLE */ +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + +/* +** If the second argument is not NULL, release any allocations associated +** with the memory cells in the p->aMem[] array. Also free the UnpackedRecord +** structure itself, using sqlite3DbFree(). +** +** This function is used to free UnpackedRecord structures allocated by +** the vdbeUnpackRecord() function found in vdbeapi.c. +*/ +static void vdbeFreeUnpacked(sqlite3 *db, UnpackedRecord *p){ + if( p ){ + int i; + for(i=0; inField; i++){ + Mem *pMem = &p->aMem[i]; + if( pMem->zMalloc ) sqlite3VdbeMemRelease(pMem); + } + sqlite3DbFree(db, p); + } +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** Invoke the pre-update hook. If this is an UPDATE or DELETE pre-update call, +** then cursor passed as the second argument should point to the row about +** to be update or deleted. If the application calls sqlite3_preupdate_old(), +** the required value will be read from the row the cursor points to. +*/ +SQLITE_PRIVATE void sqlite3VdbePreUpdateHook( + Vdbe *v, /* Vdbe pre-update hook is invoked by */ + VdbeCursor *pCsr, /* Cursor to grab old.* values from */ + int op, /* SQLITE_INSERT, UPDATE or DELETE */ + const char *zDb, /* Database name */ + Table *pTab, /* Modified table */ + i64 iKey1, /* Initial key value */ + int iReg /* Register for new.* record */ +){ + sqlite3 *db = v->db; + i64 iKey2; + PreUpdate preupdate; + const char *zTbl = pTab->zName; + static const u8 fakeSortOrder = 0; + + assert( db->pPreUpdate==0 ); + memset(&preupdate, 0, sizeof(PreUpdate)); + if( op==SQLITE_UPDATE ){ + iKey2 = v->aMem[iReg].u.i; + }else{ + iKey2 = iKey1; + } + + assert( pCsr->nField==pTab->nCol + || (pCsr->nField==pTab->nCol+1 && op==SQLITE_DELETE && iReg==-1) + ); + + preupdate.v = v; + preupdate.pCsr = pCsr; + preupdate.op = op; + preupdate.iNewReg = iReg; + preupdate.keyinfo.db = db; + preupdate.keyinfo.enc = ENC(db); + preupdate.keyinfo.nField = pTab->nCol; + preupdate.keyinfo.aSortOrder = (u8*)&fakeSortOrder; + preupdate.iKey1 = iKey1; + preupdate.iKey2 = iKey2; + preupdate.iPKey = pTab->iPKey; + + db->pPreUpdate = &preupdate; + db->xPreUpdateCallback(db->pPreUpdateArg, db, op, zDb, zTbl, iKey1, iKey2); + db->pPreUpdate = 0; + sqlite3DbFree(db, preupdate.aRecord); + vdbeFreeUnpacked(db, preupdate.pUnpacked); + vdbeFreeUnpacked(db, preupdate.pNewUnpacked); + if( preupdate.aNew ){ + int i; + for(i=0; inField; i++){ + sqlite3VdbeMemRelease(&preupdate.aNew[i]); + } + sqlite3DbFree(db, preupdate.aNew); + } +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + /************** End of vdbeaux.c *********************************************/ /************** Begin file vdbeapi.c *****************************************/ /* @@ -71137,7 +74725,8 @@ SQLITE_API sqlite_int64 SQLITE_STDCALL sqlite3_value_int64(sqlite3_value *pVal){ return sqlite3VdbeIntValue((Mem*)pVal); } SQLITE_API unsigned int SQLITE_STDCALL sqlite3_value_subtype(sqlite3_value *pVal){ - return ((Mem*)pVal)->eSubtype; + Mem *pMem = (Mem*)pVal; + return ((pMem->flags & MEM_Subtype) ? pMem->eSubtype : 0); } SQLITE_API const unsigned char *SQLITE_STDCALL sqlite3_value_text(sqlite3_value *pVal){ return (const unsigned char *)sqlite3ValueText(pVal, SQLITE_UTF8); @@ -71318,8 +74907,10 @@ SQLITE_API void SQLITE_STDCALL sqlite3_result_null(sqlite3_context *pCtx){ sqlite3VdbeMemSetNull(pCtx->pOut); } SQLITE_API void SQLITE_STDCALL sqlite3_result_subtype(sqlite3_context *pCtx, unsigned int eSubtype){ - assert( sqlite3_mutex_held(pCtx->pOut->db->mutex) ); - pCtx->pOut->eSubtype = eSubtype & 0xff; + Mem *pOut = pCtx->pOut; + assert( sqlite3_mutex_held(pOut->db->mutex) ); + pOut->eSubtype = eSubtype & 0xff; + pOut->flags |= MEM_Subtype; } SQLITE_API void SQLITE_STDCALL sqlite3_result_text( sqlite3_context *pCtx, @@ -71417,9 +75008,9 @@ SQLITE_API void SQLITE_STDCALL sqlite3_result_error_toobig(sqlite3_context *pCtx SQLITE_API void SQLITE_STDCALL sqlite3_result_error_nomem(sqlite3_context *pCtx){ assert( sqlite3_mutex_held(pCtx->pOut->db->mutex) ); sqlite3VdbeMemSetNull(pCtx->pOut); - pCtx->isError = SQLITE_NOMEM; + pCtx->isError = SQLITE_NOMEM_BKPT; pCtx->fErrorOrAux = 1; - pCtx->pOut->db->mallocFailed = 1; + sqlite3OomFault(pCtx->pOut->db); } /* @@ -71493,7 +75084,7 @@ static int sqlite3Step(Vdbe *p){ db = p->db; if( db->mallocFailed ){ p->rc = SQLITE_NOMEM; - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } if( p->pc<=0 && p->expired ){ @@ -71556,7 +75147,7 @@ static int sqlite3Step(Vdbe *p){ db->errCode = rc; if( SQLITE_NOMEM==sqlite3ApiExit(p->db, p->rc) ){ - p->rc = SQLITE_NOMEM; + p->rc = SQLITE_NOMEM_BKPT; } end_of_step: /* At this point local variable rc holds the value that should be @@ -71623,7 +75214,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_step(sqlite3_stmt *pStmt){ v->rc = rc2; } else { v->zErrMsg = 0; - v->rc = rc = SQLITE_NOMEM; + v->rc = rc = SQLITE_NOMEM_BKPT; } } rc = sqlite3ApiExit(db, rc); @@ -71728,7 +75319,7 @@ static SQLITE_NOINLINE void *createAggContext(sqlite3_context *p, int nByte){ ** same context that was returned on prior calls. */ SQLITE_API void *SQLITE_STDCALL sqlite3_aggregate_context(sqlite3_context *p, int nByte){ - assert( p && p->pFunc && p->pFunc->xStep ); + assert( p && p->pFunc && p->pFunc->xFinalize ); assert( sqlite3_mutex_held(p->pOut->db->mutex) ); testcase( nByte<0 ); if( (p->pMem->flags & MEM_Agg)==0 ){ @@ -71819,7 +75410,7 @@ failed: ** context. */ SQLITE_API int SQLITE_STDCALL sqlite3_aggregate_count(sqlite3_context *p){ - assert( p && p->pMem && p->pFunc && p->pFunc->xStep ); + assert( p && p->pMem && p->pFunc && p->pFunc->xFinalize ); return p->pMem->n; } #endif @@ -72047,7 +75638,7 @@ static const void *columnName( ** is the case, clear the mallocFailed flag and return NULL. */ if( db->mallocFailed ){ - db->mallocFailed = 0; + sqlite3OomClear(db); ret = 0; } sqlite3_mutex_leave(db->mutex); @@ -72247,6 +75838,9 @@ SQLITE_API int SQLITE_STDCALL sqlite3_bind_blob( int nData, void (*xDel)(void*) ){ +#ifdef SQLITE_ENABLE_API_ARMOR + if( nData<0 ) return SQLITE_MISUSE_BKPT; +#endif return bindText(pStmt, i, zData, nData, xDel, 0); } SQLITE_API int SQLITE_STDCALL sqlite3_bind_blob64( @@ -72547,6 +76141,187 @@ SQLITE_API int SQLITE_STDCALL sqlite3_stmt_status(sqlite3_stmt *pStmt, int op, i return (int)v; } +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** Allocate and populate an UnpackedRecord structure based on the serialized +** record in nKey/pKey. Return a pointer to the new UnpackedRecord structure +** if successful, or a NULL pointer if an OOM error is encountered. +*/ +static UnpackedRecord *vdbeUnpackRecord( + KeyInfo *pKeyInfo, + int nKey, + const void *pKey +){ + char *dummy; /* Dummy argument for AllocUnpackedRecord() */ + UnpackedRecord *pRet; /* Return value */ + + pRet = sqlite3VdbeAllocUnpackedRecord(pKeyInfo, 0, 0, &dummy); + if( pRet ){ + memset(pRet->aMem, 0, sizeof(Mem)*(pKeyInfo->nField+1)); + sqlite3VdbeRecordUnpack(pKeyInfo, nKey, pKey, pRet); + } + return pRet; +} + +/* +** This function is called from within a pre-update callback to retrieve +** a field of the row currently being updated or deleted. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_preupdate_old(sqlite3 *db, int iIdx, sqlite3_value **ppValue){ + PreUpdate *p = db->pPreUpdate; + int rc = SQLITE_OK; + + /* Test that this call is being made from within an SQLITE_DELETE or + ** SQLITE_UPDATE pre-update callback, and that iIdx is within range. */ + if( !p || p->op==SQLITE_INSERT ){ + rc = SQLITE_MISUSE_BKPT; + goto preupdate_old_out; + } + if( iIdx>=p->pCsr->nField || iIdx<0 ){ + rc = SQLITE_RANGE; + goto preupdate_old_out; + } + + /* If the old.* record has not yet been loaded into memory, do so now. */ + if( p->pUnpacked==0 ){ + u32 nRec; + u8 *aRec; + + rc = sqlite3BtreeDataSize(p->pCsr->uc.pCursor, &nRec); + if( rc!=SQLITE_OK ) goto preupdate_old_out; + aRec = sqlite3DbMallocRaw(db, nRec); + if( !aRec ) goto preupdate_old_out; + rc = sqlite3BtreeData(p->pCsr->uc.pCursor, 0, nRec, aRec); + if( rc==SQLITE_OK ){ + p->pUnpacked = vdbeUnpackRecord(&p->keyinfo, nRec, aRec); + if( !p->pUnpacked ) rc = SQLITE_NOMEM; + } + if( rc!=SQLITE_OK ){ + sqlite3DbFree(db, aRec); + goto preupdate_old_out; + } + p->aRecord = aRec; + } + + if( iIdx>=p->pUnpacked->nField ){ + *ppValue = (sqlite3_value *)columnNullValue(); + }else{ + *ppValue = &p->pUnpacked->aMem[iIdx]; + if( iIdx==p->iPKey ){ + sqlite3VdbeMemSetInt64(*ppValue, p->iKey1); + } + } + + preupdate_old_out: + sqlite3Error(db, rc); + return sqlite3ApiExit(db, rc); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** This function is called from within a pre-update callback to retrieve +** the number of columns in the row being updated, deleted or inserted. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_preupdate_count(sqlite3 *db){ + PreUpdate *p = db->pPreUpdate; + return (p ? p->keyinfo.nField : 0); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** This function is designed to be called from within a pre-update callback +** only. It returns zero if the change that caused the callback was made +** immediately by a user SQL statement. Or, if the change was made by a +** trigger program, it returns the number of trigger programs currently +** on the stack (1 for a top-level trigger, 2 for a trigger fired by a +** top-level trigger etc.). +** +** For the purposes of the previous paragraph, a foreign key CASCADE, SET NULL +** or SET DEFAULT action is considered a trigger. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_preupdate_depth(sqlite3 *db){ + PreUpdate *p = db->pPreUpdate; + return (p ? p->v->nFrame : 0); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** This function is called from within a pre-update callback to retrieve +** a field of the row currently being updated or inserted. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_preupdate_new(sqlite3 *db, int iIdx, sqlite3_value **ppValue){ + PreUpdate *p = db->pPreUpdate; + int rc = SQLITE_OK; + Mem *pMem; + + if( !p || p->op==SQLITE_DELETE ){ + rc = SQLITE_MISUSE_BKPT; + goto preupdate_new_out; + } + if( iIdx>=p->pCsr->nField || iIdx<0 ){ + rc = SQLITE_RANGE; + goto preupdate_new_out; + } + + if( p->op==SQLITE_INSERT ){ + /* For an INSERT, memory cell p->iNewReg contains the serialized record + ** that is being inserted. Deserialize it. */ + UnpackedRecord *pUnpack = p->pNewUnpacked; + if( !pUnpack ){ + Mem *pData = &p->v->aMem[p->iNewReg]; + rc = sqlite3VdbeMemExpandBlob(pData); + if( rc!=SQLITE_OK ) goto preupdate_new_out; + pUnpack = vdbeUnpackRecord(&p->keyinfo, pData->n, pData->z); + if( !pUnpack ){ + rc = SQLITE_NOMEM; + goto preupdate_new_out; + } + p->pNewUnpacked = pUnpack; + } + if( iIdx>=pUnpack->nField ){ + pMem = (sqlite3_value *)columnNullValue(); + }else{ + pMem = &pUnpack->aMem[iIdx]; + if( iIdx==p->iPKey ){ + sqlite3VdbeMemSetInt64(pMem, p->iKey2); + } + } + }else{ + /* For an UPDATE, memory cell (p->iNewReg+1+iIdx) contains the required + ** value. Make a copy of the cell contents and return a pointer to it. + ** It is not safe to return a pointer to the memory cell itself as the + ** caller may modify the value text encoding. + */ + assert( p->op==SQLITE_UPDATE ); + if( !p->aNew ){ + p->aNew = (Mem *)sqlite3DbMallocZero(db, sizeof(Mem) * p->pCsr->nField); + if( !p->aNew ){ + rc = SQLITE_NOMEM; + goto preupdate_new_out; + } + } + assert( iIdx>=0 && iIdxpCsr->nField ); + pMem = &p->aNew[iIdx]; + if( pMem->flags==0 ){ + if( iIdx==p->iPKey ){ + sqlite3VdbeMemSetInt64(pMem, p->iKey2); + }else{ + rc = sqlite3VdbeMemCopy(pMem, &p->v->aMem[p->iNewReg+1+iIdx]); + if( rc!=SQLITE_OK ) goto preupdate_new_out; + } + } + } + *ppValue = pMem; + + preupdate_new_out: + sqlite3Error(db, rc); + return sqlite3ApiExit(db, rc); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + #ifdef SQLITE_ENABLE_STMT_SCANSTATUS /* ** Return status data for a single loop within query pStmt. @@ -72748,9 +76523,9 @@ SQLITE_PRIVATE char *sqlite3VdbeExpandSql( if( pVar->flags & MEM_Null ){ sqlite3StrAccumAppend(&out, "NULL", 4); }else if( pVar->flags & MEM_Int ){ - sqlite3XPrintf(&out, 0, "%lld", pVar->u.i); + sqlite3XPrintf(&out, "%lld", pVar->u.i); }else if( pVar->flags & MEM_Real ){ - sqlite3XPrintf(&out, 0, "%!.15g", pVar->u.r); + sqlite3XPrintf(&out, "%!.15g", pVar->u.r); }else if( pVar->flags & MEM_Str ){ int nOut; /* Number of bytes of the string text to include in output */ #ifndef SQLITE_OMIT_UTF16 @@ -72771,17 +76546,17 @@ SQLITE_PRIVATE char *sqlite3VdbeExpandSql( while( nOutn && (pVar->z[nOut]&0xc0)==0x80 ){ nOut++; } } #endif - sqlite3XPrintf(&out, 0, "'%.*q'", nOut, pVar->z); + sqlite3XPrintf(&out, "'%.*q'", nOut, pVar->z); #ifdef SQLITE_TRACE_SIZE_LIMIT if( nOutn ){ - sqlite3XPrintf(&out, 0, "/*+%d bytes*/", pVar->n-nOut); + sqlite3XPrintf(&out, "/*+%d bytes*/", pVar->n-nOut); } #endif #ifndef SQLITE_OMIT_UTF16 if( enc!=SQLITE_UTF8 ) sqlite3VdbeMemRelease(&utf8); #endif }else if( pVar->flags & MEM_Zero ){ - sqlite3XPrintf(&out, 0, "zeroblob(%d)", pVar->u.nZero); + sqlite3XPrintf(&out, "zeroblob(%d)", pVar->u.nZero); }else{ int nOut; /* Number of bytes of the blob to include in output */ assert( pVar->flags & MEM_Blob ); @@ -72791,12 +76566,12 @@ SQLITE_PRIVATE char *sqlite3VdbeExpandSql( if( nOut>SQLITE_TRACE_SIZE_LIMIT ) nOut = SQLITE_TRACE_SIZE_LIMIT; #endif for(i=0; iz[i]&0xff); + sqlite3XPrintf(&out, "%02x", pVar->z[i]&0xff); } sqlite3StrAccumAppend(&out, "'", 1); #ifdef SQLITE_TRACE_SIZE_LIMIT if( nOutn ){ - sqlite3XPrintf(&out, 0, "/*+%d bytes*/", pVar->n-nOut); + sqlite3XPrintf(&out, "/*+%d bytes*/", pVar->n-nOut); } #endif } @@ -72897,6 +76672,16 @@ static void updateMaxBlobsize(Mem *p){ } #endif +/* +** This macro evaluates to true if either the update hook or the preupdate +** hook are enabled for database connect DB. +*/ +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +# define HAS_UPDATE_HOOK(DB) ((DB)->xPreUpdateCallback||(DB)->xUpdateCallback) +#else +# define HAS_UPDATE_HOOK(DB) ((DB)->xUpdateCallback) +#endif + /* ** The next global variable is incremented each time the OP_Found opcode ** is executed. This is used to test whether or not the foreign key @@ -72976,7 +76761,7 @@ SQLITE_API int sqlite3_found_count = 0; && sqlite3VdbeMemMakeWriteable(P) ){ goto no_mem;} /* Return true if the cursor was opened using the OP_OpenSorter opcode. */ -#define isSorter(x) ((x)->pSorter!=0) +#define isSorter(x) ((x)->eCurType==CURTYPE_SORTER) /* ** Allocate VdbeCursor number iCur. Return a pointer to it. Return NULL @@ -72987,7 +76772,7 @@ static VdbeCursor *allocateCursor( int iCur, /* Index of the new VdbeCursor */ int nField, /* Number of fields in the table or index */ int iDb, /* Database the cursor belongs to, or -1 */ - int isBtreeCursor /* True for B-Tree. False for pseudo-table or vtab */ + u8 eCurType /* Type of the new cursor */ ){ /* Find the memory cell that will be used to store the blob of memory ** required for this VdbeCursor structure. It is convenient to use a @@ -73003,33 +76788,34 @@ static VdbeCursor *allocateCursor( ** be freed lazily via the sqlite3_release_memory() API. This ** minimizes the number of malloc calls made by the system. ** - ** Memory cells for cursors are allocated at the top of the address - ** space. Memory cell (p->nMem) corresponds to cursor 0. Space for - ** cursor 1 is managed by memory cell (p->nMem-1), etc. + ** The memory cell for cursor 0 is aMem[0]. The rest are allocated from + ** the top of the register space. Cursor 1 is at Mem[p->nMem-1]. + ** Cursor 2 is at Mem[p->nMem-2]. And so forth. */ - Mem *pMem = &p->aMem[p->nMem-iCur]; + Mem *pMem = iCur>0 ? &p->aMem[p->nMem-iCur] : p->aMem; int nByte; VdbeCursor *pCx = 0; nByte = ROUND8(sizeof(VdbeCursor)) + 2*sizeof(u32)*nField + - (isBtreeCursor?sqlite3BtreeCursorSize():0); + (eCurType==CURTYPE_BTREE?sqlite3BtreeCursorSize():0); - assert( iCurnCursor ); - if( p->apCsr[iCur] ){ + assert( iCur>=0 && iCurnCursor ); + if( p->apCsr[iCur] ){ /*OPTIMIZATION-IF-FALSE*/ sqlite3VdbeFreeCursor(p, p->apCsr[iCur]); p->apCsr[iCur] = 0; } if( SQLITE_OK==sqlite3VdbeMemClearAndResize(pMem, nByte) ){ p->apCsr[iCur] = pCx = (VdbeCursor*)pMem->z; memset(pCx, 0, sizeof(VdbeCursor)); + pCx->eCurType = eCurType; pCx->iDb = iDb; pCx->nField = nField; pCx->aOffset = &pCx->aType[nField]; - if( isBtreeCursor ){ - pCx->pCursor = (BtCursor*) + if( eCurType==CURTYPE_BTREE ){ + pCx->uc.pCursor = (BtCursor*) &pMem->z[ROUND8(sizeof(VdbeCursor))+2*sizeof(u32)*nField]; - sqlite3BtreeCursorZero(pCx->pCursor); + sqlite3BtreeCursorZero(pCx->uc.pCursor); } } return pCx; @@ -73092,7 +76878,7 @@ static void applyAffinity( if( affinity>=SQLITE_AFF_NUMERIC ){ assert( affinity==SQLITE_AFF_INTEGER || affinity==SQLITE_AFF_REAL || affinity==SQLITE_AFF_NUMERIC ); - if( (pRec->flags & MEM_Int)==0 ){ + if( (pRec->flags & MEM_Int)==0 ){ /*OPTIMIZATION-IF-FALSE*/ if( (pRec->flags & MEM_Real)==0 ){ if( pRec->flags & MEM_Str ) applyNumericAffinity(pRec,1); }else{ @@ -73102,10 +76888,13 @@ static void applyAffinity( }else if( affinity==SQLITE_AFF_TEXT ){ /* Only attempt the conversion to TEXT if there is an integer or real ** representation (blob and NULL do not get converted) but no string - ** representation. - */ - if( 0==(pRec->flags&MEM_Str) && (pRec->flags&(MEM_Real|MEM_Int)) ){ - sqlite3VdbeMemStringify(pRec, enc, 1); + ** representation. It would be harmless to repeat the conversion if + ** there is already a string rep, but it is pointless to waste those + ** CPU cycles. */ + if( 0==(pRec->flags&MEM_Str) ){ /*OPTIMIZATION-IF-FALSE*/ + if( (pRec->flags&(MEM_Real|MEM_Int)) ){ + sqlite3VdbeMemStringify(pRec, enc, 1); + } } pRec->flags &= ~(MEM_Real|MEM_Int); } @@ -73281,6 +77070,7 @@ static void memTracePrint(Mem *p){ sqlite3VdbeMemPrettyPrint(p, zBuf); printf(" %s", zBuf); } + if( p->flags & MEM_Subtype ) printf(" subtype=0x%02x", p->eSubtype); } static void registerTrace(int iReg, Mem *p){ printf("REG[%d] = ", iReg); @@ -73418,16 +77208,24 @@ static int checkSavepointCount(sqlite3 *db){ /* ** Return the register of pOp->p2 after first preparing it to be ** overwritten with an integer value. -*/ +*/ +static SQLITE_NOINLINE Mem *out2PrereleaseWithClear(Mem *pOut){ + sqlite3VdbeMemSetNull(pOut); + pOut->flags = MEM_Int; + return pOut; +} static Mem *out2Prerelease(Vdbe *p, VdbeOp *pOp){ Mem *pOut; assert( pOp->p2>0 ); - assert( pOp->p2<=(p->nMem-p->nCursor) ); + assert( pOp->p2<=(p->nMem+1 - p->nCursor) ); pOut = &p->aMem[pOp->p2]; memAboutToChange(p, pOut); - if( VdbeMemDynamic(pOut) ) sqlite3VdbeMemSetNull(pOut); - pOut->flags = MEM_Int; - return pOut; + if( VdbeMemDynamic(pOut) ){ /*OPTIMIZATION-IF-FALSE*/ + return out2PrereleaseWithClear(pOut); + }else{ + pOut->flags = MEM_Int; + return pOut; + } } @@ -73442,6 +77240,9 @@ SQLITE_PRIVATE int sqlite3VdbeExec( Op *pOp = aOp; /* Current operation */ #if defined(SQLITE_DEBUG) || defined(VDBE_PROFILE) Op *pOrigOp; /* Value of pOp at the top of the loop */ +#endif +#ifdef SQLITE_DEBUG + int nExtraDelete = 0; /* Verifies FORDELETE and AUXDELETE flags */ #endif int rc = SQLITE_OK; /* Value to return */ sqlite3 *db = p->db; /* The database */ @@ -73514,9 +77315,12 @@ SQLITE_PRIVATE int sqlite3VdbeExec( } sqlite3EndBenignMalloc(); #endif - for(pOp=&aOp[p->pc]; rc==SQLITE_OK; pOp++){ + for(pOp=&aOp[p->pc]; 1; pOp++){ + /* Errors are detected by individual opcodes, with an immediate + ** jumps to abort_due_to_error. */ + assert( rc==SQLITE_OK ); + assert( pOp>=aOp && pOp<&aOp[p->nOp]); - if( db->mallocFailed ) goto no_mem; #ifdef VDBE_PROFILE start = sqlite3Hwtime(); #endif @@ -73548,37 +77352,39 @@ SQLITE_PRIVATE int sqlite3VdbeExec( /* Sanity checking on other operands */ #ifdef SQLITE_DEBUG - assert( pOp->opflags==sqlite3OpcodeProperty[pOp->opcode] ); - if( (pOp->opflags & OPFLG_IN1)!=0 ){ - assert( pOp->p1>0 ); - assert( pOp->p1<=(p->nMem-p->nCursor) ); - assert( memIsValid(&aMem[pOp->p1]) ); - assert( sqlite3VdbeCheckMemInvariants(&aMem[pOp->p1]) ); - REGISTER_TRACE(pOp->p1, &aMem[pOp->p1]); - } - if( (pOp->opflags & OPFLG_IN2)!=0 ){ - assert( pOp->p2>0 ); - assert( pOp->p2<=(p->nMem-p->nCursor) ); - assert( memIsValid(&aMem[pOp->p2]) ); - assert( sqlite3VdbeCheckMemInvariants(&aMem[pOp->p2]) ); - REGISTER_TRACE(pOp->p2, &aMem[pOp->p2]); - } - if( (pOp->opflags & OPFLG_IN3)!=0 ){ - assert( pOp->p3>0 ); - assert( pOp->p3<=(p->nMem-p->nCursor) ); - assert( memIsValid(&aMem[pOp->p3]) ); - assert( sqlite3VdbeCheckMemInvariants(&aMem[pOp->p3]) ); - REGISTER_TRACE(pOp->p3, &aMem[pOp->p3]); - } - if( (pOp->opflags & OPFLG_OUT2)!=0 ){ - assert( pOp->p2>0 ); - assert( pOp->p2<=(p->nMem-p->nCursor) ); - memAboutToChange(p, &aMem[pOp->p2]); - } - if( (pOp->opflags & OPFLG_OUT3)!=0 ){ - assert( pOp->p3>0 ); - assert( pOp->p3<=(p->nMem-p->nCursor) ); - memAboutToChange(p, &aMem[pOp->p3]); + { + u8 opProperty = sqlite3OpcodeProperty[pOp->opcode]; + if( (opProperty & OPFLG_IN1)!=0 ){ + assert( pOp->p1>0 ); + assert( pOp->p1<=(p->nMem+1 - p->nCursor) ); + assert( memIsValid(&aMem[pOp->p1]) ); + assert( sqlite3VdbeCheckMemInvariants(&aMem[pOp->p1]) ); + REGISTER_TRACE(pOp->p1, &aMem[pOp->p1]); + } + if( (opProperty & OPFLG_IN2)!=0 ){ + assert( pOp->p2>0 ); + assert( pOp->p2<=(p->nMem+1 - p->nCursor) ); + assert( memIsValid(&aMem[pOp->p2]) ); + assert( sqlite3VdbeCheckMemInvariants(&aMem[pOp->p2]) ); + REGISTER_TRACE(pOp->p2, &aMem[pOp->p2]); + } + if( (opProperty & OPFLG_IN3)!=0 ){ + assert( pOp->p3>0 ); + assert( pOp->p3<=(p->nMem+1 - p->nCursor) ); + assert( memIsValid(&aMem[pOp->p3]) ); + assert( sqlite3VdbeCheckMemInvariants(&aMem[pOp->p3]) ); + REGISTER_TRACE(pOp->p3, &aMem[pOp->p3]); + } + if( (opProperty & OPFLG_OUT2)!=0 ){ + assert( pOp->p2>0 ); + assert( pOp->p2<=(p->nMem+1 - p->nCursor) ); + memAboutToChange(p, &aMem[pOp->p2]); + } + if( (opProperty & OPFLG_OUT3)!=0 ){ + assert( pOp->p3>0 ); + assert( pOp->p3<=(p->nMem+1 - p->nCursor) ); + memAboutToChange(p, &aMem[pOp->p3]); + } } #endif #if defined(SQLITE_DEBUG) || defined(VDBE_PROFILE) @@ -73662,7 +77468,7 @@ check_for_interrupt: nProgressLimit = nVmStep + db->nProgressOps - (nVmStep%db->nProgressOps); if( db->xProgress(db->pProgressArg) ){ rc = SQLITE_INTERRUPT; - goto vdbe_error_halt; + goto abort_due_to_error; } } #endif @@ -73676,7 +77482,7 @@ check_for_interrupt: ** and then jump to address P2. */ case OP_Gosub: { /* jump */ - assert( pOp->p1>0 && pOp->p1<=(p->nMem-p->nCursor) ); + assert( pOp->p1>0 && pOp->p1<=(p->nMem+1 - p->nCursor) ); pIn1 = &aMem[pOp->p1]; assert( VdbeMemDynamic(pIn1)==0 ); memAboutToChange(p, pIn1); @@ -73716,7 +77522,7 @@ case OP_Return: { /* in1 */ ** See also: EndCoroutine */ case OP_InitCoroutine: { /* jump */ - assert( pOp->p1>0 && pOp->p1<=(p->nMem-p->nCursor) ); + assert( pOp->p1>0 && pOp->p1<=(p->nMem+1 - p->nCursor) ); assert( pOp->p2>=0 && pOp->p2nOp ); assert( pOp->p3>=0 && pOp->p3nOp ); pOut = &aMem[pOp->p1]; @@ -73818,8 +77624,6 @@ case OP_HaltIfNull: { /* in3 */ ** is the same as executing Halt. */ case OP_Halt: { - const char *zType; - const char *zLogFmt; VdbeFrame *pFrame; int pcx; @@ -73848,34 +77652,28 @@ case OP_Halt: { p->rc = pOp->p1; p->errorAction = (u8)pOp->p2; p->pc = pcx; + assert( pOp->p5>=0 && pOp->p5<=4 ); if( p->rc ){ if( pOp->p5 ){ static const char * const azType[] = { "NOT NULL", "UNIQUE", "CHECK", "FOREIGN KEY" }; - assert( pOp->p5>=1 && pOp->p5<=4 ); testcase( pOp->p5==1 ); testcase( pOp->p5==2 ); testcase( pOp->p5==3 ); testcase( pOp->p5==4 ); - zType = azType[pOp->p5-1]; + sqlite3VdbeError(p, "%s constraint failed", azType[pOp->p5-1]); + if( pOp->p4.z ){ + p->zErrMsg = sqlite3MPrintf(db, "%z: %s", p->zErrMsg, pOp->p4.z); + } }else{ - zType = 0; - } - assert( zType!=0 || pOp->p4.z!=0 ); - zLogFmt = "abort at %d in [%s]: %s"; - if( zType && pOp->p4.z ){ - sqlite3VdbeError(p, "%s constraint failed: %s", zType, pOp->p4.z); - }else if( pOp->p4.z ){ sqlite3VdbeError(p, "%s", pOp->p4.z); - }else{ - sqlite3VdbeError(p, "%s constraint failed", zType); } - sqlite3_log(pOp->p1, zLogFmt, pcx, p->zSql, p->zErrMsg); + sqlite3_log(pOp->p1, "abort at %d in [%s]: %s", pcx, p->zSql, p->zErrMsg); } rc = sqlite3VdbeHalt(p); assert( rc==SQLITE_BUSY || rc==SQLITE_OK || rc==SQLITE_ERROR ); if( rc==SQLITE_BUSY ){ - p->rc = rc = SQLITE_BUSY; + p->rc = SQLITE_BUSY; }else{ assert( rc==SQLITE_OK || (p->rc&0xff)==SQLITE_CONSTRAINT ); assert( rc==SQLITE_OK || db->nDeferredCons>0 || db->nDeferredImmCons>0 ); @@ -73941,7 +77739,7 @@ case OP_String8: { /* same as TK_STRING, out2 */ #ifndef SQLITE_OMIT_UTF16 if( encoding!=SQLITE_UTF8 ){ rc = sqlite3VdbeMemSetStr(pOut, pOp->p4.z, -1, SQLITE_UTF8, SQLITE_STATIC); - if( rc==SQLITE_TOOBIG ) goto too_big; + assert( rc==SQLITE_OK || rc==SQLITE_TOOBIG ); if( SQLITE_OK!=sqlite3VdbeChangeEncoding(pOut, encoding) ) goto no_mem; assert( pOut->szMalloc>0 && pOut->zMalloc==pOut->z ); assert( VdbeMemDynamic(pOut)==0 ); @@ -73954,10 +77752,12 @@ case OP_String8: { /* same as TK_STRING, out2 */ pOp->p4.z = pOut->z; pOp->p1 = pOut->n; } + testcase( rc==SQLITE_TOOBIG ); #endif if( pOp->p1>db->aLimit[SQLITE_LIMIT_LENGTH] ){ goto too_big; } + assert( rc==SQLITE_OK ); /* Fall through to the next case, OP_String */ } @@ -73966,10 +77766,12 @@ case OP_String8: { /* same as TK_STRING, out2 */ ** ** The string value P4 of length P1 (bytes) is stored in register P2. ** -** If P5!=0 and the content of register P3 is greater than zero, then +** If P3 is not zero and the content of register P3 is equal to P5, then ** the datatype of the register P2 is converted to BLOB. The content is ** the same sequence of bytes, it is merely interpreted as a BLOB instead -** of a string, as if it had been CAST. +** of a string, as if it had been CAST. In other words: +** +** if( P3!=0 and reg[P3]==P5 ) reg[P2] := CAST(reg[P2] as BLOB) */ case OP_String: { /* out2 */ assert( pOp->p4.z!=0 ); @@ -73979,13 +77781,14 @@ case OP_String: { /* out2 */ pOut->n = pOp->p1; pOut->enc = encoding; UPDATE_MAX_BLOBSIZE(pOut); - if( pOp->p5 ){ - assert( pOp->p3>0 ); - assert( pOp->p3<=(p->nMem-p->nCursor) ); +#ifndef SQLITE_LIKE_DOESNT_MATCH_BLOBS + if( pOp->p3>0 ){ + assert( pOp->p3<=(p->nMem+1 - p->nCursor) ); pIn3 = &aMem[pOp->p3]; assert( pIn3->flags & MEM_Int ); - if( pIn3->u.i ) pOut->flags = MEM_Blob|MEM_Static|MEM_Term; + if( pIn3->u.i==pOp->p5 ) pOut->flags = MEM_Blob|MEM_Static|MEM_Term; } +#endif break; } @@ -74006,7 +77809,7 @@ case OP_Null: { /* out2 */ u16 nullFlag; pOut = out2Prerelease(p, pOp); cnt = pOp->p3-pOp->p2; - assert( pOp->p3<=(p->nMem-p->nCursor) ); + assert( pOp->p3<=(p->nMem+1 - p->nCursor) ); pOut->flags = nullFlag = pOp->p1 ? (MEM_Null|MEM_Cleared) : MEM_Null; while( cnt>0 ){ pOut++; @@ -74027,7 +77830,7 @@ case OP_Null: { /* out2 */ ** previously copied using OP_SCopy, the copies will continue to be valid. */ case OP_SoftNull: { - assert( pOp->p1>0 && pOp->p1<=(p->nMem-p->nCursor) ); + assert( pOp->p1>0 && pOp->p1<=(p->nMem+1 - p->nCursor) ); pOut = &aMem[pOp->p1]; pOut->flags = (pOut->flags|MEM_Null)&~MEM_Undefined; break; @@ -74094,8 +77897,8 @@ case OP_Move: { pIn1 = &aMem[p1]; pOut = &aMem[p2]; do{ - assert( pOut<=&aMem[(p->nMem-p->nCursor)] ); - assert( pIn1<=&aMem[(p->nMem-p->nCursor)] ); + assert( pOut<=&aMem[(p->nMem+1 - p->nCursor)] ); + assert( pIn1<=&aMem[(p->nMem+1 - p->nCursor)] ); assert( memIsValid(pIn1) ); memAboutToChange(p, pOut); sqlite3VdbeMemMove(pOut, pIn1); @@ -74165,6 +77968,22 @@ case OP_SCopy: { /* out2 */ break; } +/* Opcode: IntCopy P1 P2 * * * +** Synopsis: r[P2]=r[P1] +** +** Transfer the integer value held in register P1 into register P2. +** +** This is an optimized version of SCopy that works only for integer +** values. +*/ +case OP_IntCopy: { /* out2 */ + pIn1 = &aMem[pOp->p1]; + assert( (pIn1->flags & MEM_Int)!=0 ); + pOut = &aMem[pOp->p2]; + sqlite3VdbeMemSetInt64(pOut, pIn1->u.i); + break; +} + /* Opcode: ResultRow P1 P2 * * * ** Synopsis: output=r[P1@P2] ** @@ -74179,7 +77998,7 @@ case OP_ResultRow: { int i; assert( p->nResColumn==pOp->p2 ); assert( pOp->p1>0 ); - assert( pOp->p1+pOp->p2<=(p->nMem-p->nCursor)+1 ); + assert( pOp->p1+pOp->p2<=(p->nMem+1 - p->nCursor)+1 ); #ifndef SQLITE_OMIT_PROGRESS_CALLBACK /* Run the progress counter just before returning. @@ -74189,7 +78008,7 @@ case OP_ResultRow: { && db->xProgress(db->pProgressArg)!=0 ){ rc = SQLITE_INTERRUPT; - goto vdbe_error_halt; + goto abort_due_to_error; } #endif @@ -74199,7 +78018,7 @@ case OP_ResultRow: { if( SQLITE_OK!=(rc = sqlite3VdbeCheckFk(p, 0)) ){ assert( db->flags&SQLITE_CountRows ); assert( p->usesStmtJournal ); - break; + goto abort_due_to_error; } /* If the SQLITE_CountRows flag is set in sqlite3.flags mask, then @@ -74219,9 +78038,7 @@ case OP_ResultRow: { */ assert( p->iStatement==0 || db->flags&SQLITE_CountRows ); rc = sqlite3VdbeCloseStatement(p, SAVEPOINT_RELEASE); - if( NEVER(rc!=SQLITE_OK) ){ - break; - } + assert( rc==SQLITE_OK ); /* Invalidate all ephemeral cursor row caches */ p->cacheCtr = (p->cacheCtr + 2)|1; @@ -74493,10 +78310,10 @@ case OP_Function0: { assert( pOp->p4type==P4_FUNCDEF ); n = pOp->p5; - assert( pOp->p3>0 && pOp->p3<=(p->nMem-p->nCursor) ); - assert( n==0 || (pOp->p2>0 && pOp->p2+n<=(p->nMem-p->nCursor)+1) ); + assert( pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor) ); + assert( n==0 || (pOp->p2>0 && pOp->p2+n<=(p->nMem+1 - p->nCursor)+1) ); assert( pOp->p3p2 || pOp->p3>=pOp->p2+n ); - pCtx = sqlite3DbMallocRaw(db, sizeof(*pCtx) + (n-1)*sizeof(sqlite3_value*)); + pCtx = sqlite3DbMallocRawNN(db, sizeof(*pCtx) + (n-1)*sizeof(sqlite3_value*)); if( pCtx==0 ) goto no_mem; pCtx->pOut = 0; pCtx->pFunc = pOp->p4.pFunc; @@ -74535,8 +78352,8 @@ case OP_Function: { MemSetTypeFlag(pCtx->pOut, MEM_Null); pCtx->fErrorOrAux = 0; db->lastRowid = lastRowid; - (*pCtx->pFunc->xFunc)(pCtx, pCtx->argc, pCtx->argv); /* IMP: R-24505-23230 */ - lastRowid = db->lastRowid; /* Remember rowid changes made by xFunc */ + (*pCtx->pFunc->xSFunc)(pCtx, pCtx->argc, pCtx->argv);/* IMP: R-24505-23230 */ + lastRowid = db->lastRowid; /* Remember rowid changes made by xSFunc */ /* If the function returned an error, throw an exception */ if( pCtx->fErrorOrAux ){ @@ -74544,7 +78361,8 @@ case OP_Function: { sqlite3VdbeError(p, "%s", sqlite3_value_text(pCtx->pOut)); rc = pCtx->isError; } - sqlite3VdbeDeleteAuxData(p, pCtx->iOp, pOp->p1); + sqlite3VdbeDeleteAuxData(db, &p->pAuxData, pCtx->iOp, pOp->p1); + if( rc ) goto abort_due_to_error; } /* Copy the result of the function into register P3 */ @@ -74728,6 +78546,7 @@ case OP_Cast: { /* in1 */ rc = ExpandBlob(pIn1); sqlite3VdbeMemCast(pIn1, pOp->p2, encoding); UPDATE_MAX_BLOBSIZE(pIn1); + if( rc ) goto abort_due_to_error; break; } #endif /* SQLITE_OMIT_CAST */ @@ -74854,6 +78673,7 @@ case OP_Ge: { /* same as TK_GE, jump, in1, in3 */ */ if( pOp->p5 & SQLITE_STOREP2 ){ pOut = &aMem[pOp->p2]; + memAboutToChange(p, pOut); MemSetTypeFlag(pOut, MEM_Null); REGISTER_TRACE(pOp->p2, pOut); }else{ @@ -74868,21 +78688,23 @@ case OP_Ge: { /* same as TK_GE, jump, in1, in3 */ /* Neither operand is NULL. Do a comparison. */ affinity = pOp->p5 & SQLITE_AFF_MASK; if( affinity>=SQLITE_AFF_NUMERIC ){ - if( (pIn1->flags & (MEM_Int|MEM_Real|MEM_Str))==MEM_Str ){ - applyNumericAffinity(pIn1,0); - } - if( (pIn3->flags & (MEM_Int|MEM_Real|MEM_Str))==MEM_Str ){ - applyNumericAffinity(pIn3,0); + if( (flags1 | flags3)&MEM_Str ){ + if( (flags1 & (MEM_Int|MEM_Real|MEM_Str))==MEM_Str ){ + applyNumericAffinity(pIn1,0); + } + if( (flags3 & (MEM_Int|MEM_Real|MEM_Str))==MEM_Str ){ + applyNumericAffinity(pIn3,0); + } } }else if( affinity==SQLITE_AFF_TEXT ){ - if( (pIn1->flags & MEM_Str)==0 && (pIn1->flags & (MEM_Int|MEM_Real))!=0 ){ + if( (flags1 & MEM_Str)==0 && (flags1 & (MEM_Int|MEM_Real))!=0 ){ testcase( pIn1->flags & MEM_Int ); testcase( pIn1->flags & MEM_Real ); sqlite3VdbeMemStringify(pIn1, encoding, 1); testcase( (flags1&MEM_Dyn) != (pIn1->flags&MEM_Dyn) ); flags1 = (pIn1->flags & ~MEM_TypeMask) | (flags1 & MEM_TypeMask); } - if( (pIn3->flags & MEM_Str)==0 && (pIn3->flags & (MEM_Int|MEM_Real))!=0 ){ + if( (flags3 & MEM_Str)==0 && (flags3 & (MEM_Int|MEM_Real))!=0 ){ testcase( pIn3->flags & MEM_Int ); testcase( pIn3->flags & MEM_Real ); sqlite3VdbeMemStringify(pIn3, encoding, 1); @@ -74891,15 +78713,14 @@ case OP_Ge: { /* same as TK_GE, jump, in1, in3 */ } } assert( pOp->p4type==P4_COLLSEQ || pOp->p4.pColl==0 ); - if( pIn1->flags & MEM_Zero ){ + if( flags1 & MEM_Zero ){ sqlite3VdbeMemExpandBlob(pIn1); flags1 &= ~MEM_Zero; } - if( pIn3->flags & MEM_Zero ){ + if( flags3 & MEM_Zero ){ sqlite3VdbeMemExpandBlob(pIn3); flags3 &= ~MEM_Zero; } - if( db->mallocFailed ) goto no_mem; res = sqlite3MemCompare(pIn3, pIn1, pOp->p4.pColl); } switch( pOp->opcode ){ @@ -74940,11 +78761,14 @@ case OP_Ge: { /* same as TK_GE, jump, in1, in3 */ ** The permutation is only valid until the next OP_Compare that has ** the OPFLAG_PERMUTE bit set in P5. Typically the OP_Permutation should ** occur immediately prior to the OP_Compare. +** +** The first integer in the P4 integer array is the length of the array +** and does not become part of the permutation. */ case OP_Permutation: { assert( pOp->p4type==P4_INTARRAY ); assert( pOp->p4.ai ); - aPermute = pOp->p4.ai; + aPermute = pOp->p4.ai + 1; break; } @@ -74989,11 +78813,11 @@ case OP_Compare: { if( aPermute ){ int k, mx = 0; for(k=0; kmx ) mx = aPermute[k]; - assert( p1>0 && p1+mx<=(p->nMem-p->nCursor)+1 ); - assert( p2>0 && p2+mx<=(p->nMem-p->nCursor)+1 ); + assert( p1>0 && p1+mx<=(p->nMem+1 - p->nCursor)+1 ); + assert( p2>0 && p2+mx<=(p->nMem+1 - p->nCursor)+1 ); }else{ - assert( p1>0 && p1+n<=(p->nMem-p->nCursor)+1 ); - assert( p2>0 && p2+n<=(p->nMem-p->nCursor)+1 ); + assert( p1>0 && p1+n<=(p->nMem+1 - p->nCursor)+1 ); + assert( p2>0 && p2+n<=(p->nMem+1 - p->nCursor)+1 ); } #endif /* SQLITE_DEBUG */ for(i=0; iflags value */ Mem *pReg; /* PseudoTable input register */ + pC = p->apCsr[pOp->p1]; p2 = pOp->p2; - assert( pOp->p3>0 && pOp->p3<=(p->nMem-p->nCursor) ); + + /* If the cursor cache is stale, bring it up-to-date */ + rc = sqlite3VdbeCursorMoveto(&pC, &p2); + + assert( pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor) ); pDest = &aMem[pOp->p3]; memAboutToChange(p, pDest); assert( pOp->p1>=0 && pOp->p1nCursor ); - pC = p->apCsr[pOp->p1]; assert( pC!=0 ); assert( p2nField ); aOffset = pC->aOffset; -#ifndef SQLITE_OMIT_VIRTUALTABLE - assert( pC->pVtabCursor==0 ); /* OP_Column never called on virtual table */ -#endif - pCrsr = pC->pCursor; - assert( pCrsr!=0 || pC->pseudoTableReg>0 ); /* pCrsr NULL on PseudoTables */ - assert( pCrsr!=0 || pC->nullRow ); /* pC->nullRow on PseudoTables */ + assert( pC->eCurType!=CURTYPE_VTAB ); + assert( pC->eCurType!=CURTYPE_PSEUDO || pC->nullRow ); + assert( pC->eCurType!=CURTYPE_SORTER ); + pCrsr = pC->uc.pCursor; - /* If the cursor cache is stale, bring it up-to-date */ - rc = sqlite3VdbeCursorMoveto(pC); if( rc ) goto abort_due_to_error; if( pC->cacheStatus!=p->cacheCtr ){ if( pC->nullRow ){ - if( pCrsr==0 ){ - assert( pC->pseudoTableReg>0 ); - pReg = &aMem[pC->pseudoTableReg]; + if( pC->eCurType==CURTYPE_PSEUDO ){ + assert( pC->uc.pseudoTableReg>0 ); + pReg = &aMem[pC->uc.pseudoTableReg]; assert( pReg->flags & MEM_Blob ); assert( memIsValid(pReg) ); pC->payloadSize = pC->szRow = avail = pReg->n; @@ -75283,6 +79106,7 @@ case OP_Column: { goto op_column_out; } }else{ + assert( pC->eCurType==CURTYPE_BTREE ); assert( pCrsr ); if( pC->isTable==0 ){ assert( sqlite3BtreeCursorIsValid(pCrsr) ); @@ -75303,31 +79127,17 @@ case OP_Column: { assert( avail<=65536 ); /* Maximum page size is 64KiB */ if( pC->payloadSize <= (u32)avail ){ pC->szRow = pC->payloadSize; + }else if( pC->payloadSize > (u32)db->aLimit[SQLITE_LIMIT_LENGTH] ){ + goto too_big; }else{ pC->szRow = avail; } - if( pC->payloadSize > (u32)db->aLimit[SQLITE_LIMIT_LENGTH] ){ - goto too_big; - } } pC->cacheStatus = p->cacheCtr; pC->iHdrOffset = getVarint32(pC->aRow, offset); pC->nHdrParsed = 0; aOffset[0] = offset; - /* Make sure a corrupt database has not given us an oversize header. - ** Do this now to avoid an oversize memory allocation. - ** - ** Type entries can be between 1 and 5 bytes each. But 4 and 5 byte - ** types use so much data space that there can only be 4096 and 32 of - ** them, respectively. So the maximum header length results from a - ** 3-byte type for each of the maximum of 32768 columns plus three - ** extra bytes for the header length itself. 32768*3 + 3 = 98307. - */ - if( offset > 98307 || offset > pC->payloadSize ){ - rc = SQLITE_CORRUPT_BKPT; - goto op_column_error; - } if( availaRow does not have to hold the entire row, but it does at least @@ -75336,6 +79146,20 @@ case OP_Column: { ** dynamically allocated. */ pC->aRow = 0; pC->szRow = 0; + + /* Make sure a corrupt database has not given us an oversize header. + ** Do this now to avoid an oversize memory allocation. + ** + ** Type entries can be between 1 and 5 bytes each. But 4 and 5 byte + ** types use so much data space that there can only be 4096 and 32 of + ** them, respectively. So the maximum header length results from a + ** 3-byte type for each of the maximum of 32768 columns plus three + ** extra bytes for the header length itself. 32768*3 + 3 = 98307. + */ + if( offset > 98307 || offset > pC->payloadSize ){ + rc = SQLITE_CORRUPT_BKPT; + goto abort_due_to_error; + } } /* The following goto is an optimization. It can be omitted and @@ -75358,11 +79182,8 @@ case OP_Column: { /* Make sure zData points to enough of the record to cover the header. */ if( pC->aRow==0 ){ memset(&sMem, 0, sizeof(sMem)); - rc = sqlite3VdbeMemFromBtree(pCrsr, 0, aOffset[0], - !pC->isTable, &sMem); - if( rc!=SQLITE_OK ){ - goto op_column_error; - } + rc = sqlite3VdbeMemFromBtree(pCrsr, 0, aOffset[0], !pC->isTable, &sMem); + if( rc!=SQLITE_OK ) goto abort_due_to_error; zData = (u8*)sMem.z; }else{ zData = pC->aRow; @@ -75370,48 +79191,40 @@ case OP_Column: { /* Fill in pC->aType[i] and aOffset[i] values through the p2-th field. */ i = pC->nHdrParsed; - offset = aOffset[i]; + offset64 = aOffset[i]; zHdr = zData + pC->iHdrOffset; zEndHdr = zData + aOffset[0]; assert( i<=p2 && zHdraType[i] = t; - szField = sqlite3VdbeSerialTypeLen(t); - offset += szField; - if( offsetaType[i++] = t; + aOffset[i] = (u32)(offset64 & 0xffffffff); }while( i<=p2 && zHdrnHdrParsed = i; pC->iHdrOffset = (u32)(zHdr - zData); - if( pC->aRow==0 ){ - sqlite3VdbeMemRelease(&sMem); - sMem.flags = MEM_Null; - } /* The record is corrupt if any of the following are true: ** (1) the bytes of the header extend past the declared header size - ** (zHdr>zEndHdr) ** (2) the entire header was used but not all data was used - ** (zHdr==zEndHdr && offset!=pC->payloadSize) ** (3) the end of the data extends beyond the end of the record. - ** (offset > pC->payloadSize) */ - if( (zHdr>=zEndHdr && (zHdr>zEndHdr || offset!=pC->payloadSize)) - || (offset > pC->payloadSize) + if( (zHdr>=zEndHdr && (zHdr>zEndHdr || offset64!=pC->payloadSize)) + || (offset64 > pC->payloadSize) ){ + if( pC->aRow==0 ) sqlite3VdbeMemRelease(&sMem); rc = SQLITE_CORRUPT_BKPT; - goto op_column_error; + goto abort_due_to_error; } + if( pC->aRow==0 ) sqlite3VdbeMemRelease(&sMem); + + }else{ + t = 0; } /* If after trying to extract new entries from the header, nHdrParsed is @@ -75426,6 +79239,8 @@ case OP_Column: { } goto op_column_out; } + }else{ + t = pC->aType[p2]; } /* Extract the content for the p2+1-th column. Control can only @@ -75436,11 +79251,32 @@ case OP_Column: { assert( rc==SQLITE_OK ); assert( sqlite3VdbeCheckMemInvariants(pDest) ); if( VdbeMemDynamic(pDest) ) sqlite3VdbeMemSetNull(pDest); - t = pC->aType[p2]; + assert( t==pC->aType[p2] ); + pDest->enc = encoding; if( pC->szRow>=aOffset[p2+1] ){ /* This is the common case where the desired content fits on the original ** page - where the content is not on an overflow page */ - sqlite3VdbeSerialGet(pC->aRow+aOffset[p2], t, pDest); + zData = pC->aRow + aOffset[p2]; + if( t<12 ){ + sqlite3VdbeSerialGet(zData, t, pDest); + }else{ + /* If the column value is a string, we need a persistent value, not + ** a MEM_Ephem value. This branch is a fast short-cut that is equivalent + ** to calling sqlite3VdbeSerialGet() and sqlite3VdbeDeephemeralize(). + */ + static const u16 aFlag[] = { MEM_Blob, MEM_Str|MEM_Term }; + pDest->n = len = (t-12)/2; + if( pDest->szMalloc < len+2 ){ + pDest->flags = MEM_Null; + if( sqlite3VdbeMemGrow(pDest, len+2, 0) ) goto no_mem; + }else{ + pDest->z = pDest->zMalloc; + } + memcpy(pDest->z, zData, len); + pDest->z[len] = 0; + pDest->z[len+1] = 0; + pDest->flags = aFlag[t&1]; + } }else{ /* This branch happens only when content is on overflow pages */ if( ((pOp->p5 & (OPFLAG_LENGTHARG|OPFLAG_TYPEOFARG))!=0 @@ -75452,39 +79288,19 @@ case OP_Column: { ** 2. the length(X) function if X is a blob, and ** 3. if the content length is zero. ** So we might as well use bogus content rather than reading - ** content from disk. NULL will work for the value for strings - ** and blobs and whatever is in the payloadSize64 variable - ** will work for everything else. */ - sqlite3VdbeSerialGet(t<=13 ? (u8*)&payloadSize64 : 0, t, pDest); + ** content from disk. */ + static u8 aZero[8]; /* This is the bogus content */ + sqlite3VdbeSerialGet(aZero, t, pDest); }else{ rc = sqlite3VdbeMemFromBtree(pCrsr, aOffset[p2], len, !pC->isTable, pDest); - if( rc!=SQLITE_OK ){ - goto op_column_error; - } + if( rc!=SQLITE_OK ) goto abort_due_to_error; sqlite3VdbeSerialGet((const u8*)pDest->z, t, pDest); pDest->flags &= ~MEM_Ephem; } } - pDest->enc = encoding; op_column_out: - /* If the column value is an ephemeral string, go ahead and persist - ** that string in case the cursor moves before the column value is - ** used. The following code does the equivalent of Deephemeralize() - ** but does it faster. */ - if( (pDest->flags & MEM_Ephem)!=0 && pDest->z ){ - fx = pDest->flags & (MEM_Str|MEM_Blob); - assert( fx!=0 ); - zData = (const u8*)pDest->z; - len = pDest->n; - if( sqlite3VdbeMemClearAndResize(pDest, len+2) ) goto no_mem; - memcpy(pDest->z, zData, len); - pDest->z[len] = 0; - pDest->z[len+1] = 0; - pDest->flags = fx|MEM_Term; - } -op_column_error: UPDATE_MAX_BLOBSIZE(pDest); REGISTER_TRACE(pOp->p3, pDest); break; @@ -75508,7 +79324,7 @@ case OP_Affinity: { assert( zAffinity[pOp->p2]==0 ); pIn1 = &aMem[pOp->p1]; while( (cAff = *(zAffinity++))!=0 ){ - assert( pIn1 <= &p->aMem[(p->nMem-p->nCursor)] ); + assert( pIn1 <= &p->aMem[(p->nMem+1 - p->nCursor)] ); assert( memIsValid(pIn1) ); applyAffinity(pIn1, cAff, encoding); pIn1++; @@ -75548,7 +79364,7 @@ case OP_MakeRecord: { int file_format; /* File format to use for encoding */ int i; /* Space used in zNewRecord[] header */ int j; /* Space used in zNewRecord[] content */ - int len; /* Length of a field */ + u32 len; /* Length of a field */ /* Assuming the record contains N fields, the record format looks ** like this: @@ -75570,7 +79386,7 @@ case OP_MakeRecord: { nZero = 0; /* Number of zero bytes at the end of the record */ nField = pOp->p1; zAffinity = pOp->p4.z; - assert( nField>0 && pOp->p2>0 && pOp->p2+nField<=(p->nMem-p->nCursor)+1 ); + assert( nField>0 && pOp->p2>0 && pOp->p2+nField<=(p->nMem+1 - p->nCursor)+1 ); pData0 = &aMem[nField]; nField = pOp->p2; pLast = &pData0[nField-1]; @@ -75598,8 +79414,7 @@ case OP_MakeRecord: { pRec = pLast; do{ assert( memIsValid(pRec) ); - pRec->uTemp = serial_type = sqlite3VdbeSerialType(pRec, file_format); - len = sqlite3VdbeSerialTypeLen(serial_type); + pRec->uTemp = serial_type = sqlite3VdbeSerialType(pRec, file_format, &len); if( pRec->flags & MEM_Zero ){ if( nData ){ if( sqlite3VdbeMemExpandBlob(pRec) ) goto no_mem; @@ -75612,7 +79427,9 @@ case OP_MakeRecord: { testcase( serial_type==127 ); testcase( serial_type==128 ); nHdr += serial_type<=127 ? 1 : sqlite3VarintLen(serial_type); - }while( (--pRec)>=pData0 ); + if( pRec==pData0 ) break; + pRec--; + }while(1); /* EVIDENCE-OF: R-22564-11647 The header begins with a single varint ** which determines the total number of bytes in the header. The varint @@ -75661,7 +79478,7 @@ case OP_MakeRecord: { assert( i==nHdr ); assert( j==nByte ); - assert( pOp->p3>0 && pOp->p3<=(p->nMem-p->nCursor) ); + assert( pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor) ); pOut->n = (int)nByte; pOut->flags = MEM_Blob; if( nZero ){ @@ -75685,10 +79502,12 @@ case OP_Count: { /* out2 */ i64 nEntry; BtCursor *pCrsr; - pCrsr = p->apCsr[pOp->p1]->pCursor; + assert( p->apCsr[pOp->p1]->eCurType==CURTYPE_BTREE ); + pCrsr = p->apCsr[pOp->p1]->uc.pCursor; assert( pCrsr ); nEntry = 0; /* Not needed. Only used to silence a warning. */ rc = sqlite3BtreeCount(pCrsr, &nEntry); + if( rc ) goto abort_due_to_error; pOut = out2Prerelease(p, pOp); pOut->u.i = nEntry; break; @@ -75745,7 +79564,7 @@ case OP_Savepoint: { #endif /* Create a new savepoint structure. */ - pNew = sqlite3DbMallocRaw(db, sizeof(Savepoint)+nName+1); + pNew = sqlite3DbMallocRawNN(db, sizeof(Savepoint)+nName+1); if( pNew ){ pNew->zName = (char *)&pNew[1]; memcpy(pNew->zName, zName, nName+1); @@ -75758,7 +79577,7 @@ case OP_Savepoint: { }else{ db->nSavepoint++; } - + /* Link the new savepoint into the database handle's list. */ pNew->pNext = db->pSavepoint; db->pSavepoint = pNew; @@ -75866,6 +79685,7 @@ case OP_Savepoint: { } } } + if( rc ) goto abort_due_to_error; break; } @@ -75882,28 +79702,27 @@ case OP_Savepoint: { case OP_AutoCommit: { int desiredAutoCommit; int iRollback; - int turnOnAC; desiredAutoCommit = pOp->p1; iRollback = pOp->p2; - turnOnAC = desiredAutoCommit && !db->autoCommit; assert( desiredAutoCommit==1 || desiredAutoCommit==0 ); assert( desiredAutoCommit==1 || iRollback==0 ); assert( db->nVdbeActive>0 ); /* At least this one VM is active */ assert( p->bIsReader ); - if( turnOnAC && !iRollback && db->nVdbeWrite>0 ){ - /* If this instruction implements a COMMIT and other VMs are writing - ** return an error indicating that the other VMs must complete first. - */ - sqlite3VdbeError(p, "cannot commit transaction - " - "SQL statements in progress"); - rc = SQLITE_BUSY; - }else if( desiredAutoCommit!=db->autoCommit ){ + if( desiredAutoCommit!=db->autoCommit ){ if( iRollback ){ assert( desiredAutoCommit==1 ); sqlite3RollbackAll(db, SQLITE_ABORT_ROLLBACK); db->autoCommit = 1; + }else if( desiredAutoCommit && db->nVdbeWrite>0 ){ + /* If this instruction implements a COMMIT and other VMs are writing + ** return an error indicating that the other VMs must complete first. + */ + sqlite3VdbeError(p, "cannot commit transaction - " + "SQL statements in progress"); + rc = SQLITE_BUSY; + goto abort_due_to_error; }else if( (rc = sqlite3VdbeCheckFk(p, 1))!=SQLITE_OK ){ goto vdbe_return; }else{ @@ -75930,6 +79749,7 @@ case OP_AutoCommit: { "cannot commit - no transaction is active")); rc = SQLITE_ERROR; + goto abort_due_to_error; } break; } @@ -76052,6 +79872,7 @@ case OP_Transaction: { p->expired = 1; rc = SQLITE_SCHEMA; } + if( rc ) goto abort_due_to_error; break; } @@ -76088,15 +79909,15 @@ case OP_ReadCookie: { /* out2 */ /* Opcode: SetCookie P1 P2 P3 * * ** -** Write the content of register P3 (interpreted as an integer) -** into cookie number P2 of database P1. P2==1 is the schema version. -** P2==2 is the database format. P2==3 is the recommended pager cache +** Write the integer value P3 into cookie number P2 of database P1. +** P2==1 is the schema version. P2==2 is the database format. +** P2==3 is the recommended pager cache ** size, and so forth. P1==0 is the main database file and P1==1 is the ** database file used to store temporary tables. ** ** A transaction must be started before executing this opcode. */ -case OP_SetCookie: { /* in3 */ +case OP_SetCookie: { Db *pDb; assert( pOp->p2p1>=0 && pOp->p1nDb ); @@ -76105,17 +79926,15 @@ case OP_SetCookie: { /* in3 */ pDb = &db->aDb[pOp->p1]; assert( pDb->pBt!=0 ); assert( sqlite3SchemaMutexHeld(db, pOp->p1, 0) ); - pIn3 = &aMem[pOp->p3]; - sqlite3VdbeMemIntegerify(pIn3); /* See note about index shifting on OP_ReadCookie */ - rc = sqlite3BtreeUpdateMeta(pDb->pBt, pOp->p2, (int)pIn3->u.i); + rc = sqlite3BtreeUpdateMeta(pDb->pBt, pOp->p2, pOp->p3); if( pOp->p2==BTREE_SCHEMA_VERSION ){ /* When the schema cookie changes, record the new cookie internally */ - pDb->pSchema->schema_cookie = (int)pIn3->u.i; + pDb->pSchema->schema_cookie = pOp->p3; db->flags |= SQLITE_InternChanges; }else if( pOp->p2==BTREE_FILE_FORMAT ){ /* Record changes in the file format */ - pDb->pSchema->file_format = (u8)pIn3->u.i; + pDb->pSchema->file_format = pOp->p3; } if( pOp->p1==1 ){ /* Invalidate all prepared statements whenever the TEMP database @@ -76123,6 +79942,7 @@ case OP_SetCookie: { /* in3 */ sqlite3ExpirePreparedStatements(db); p->expired = 0; } + if( rc ) goto abort_due_to_error; break; } @@ -76213,7 +80033,6 @@ case OP_ReopenIdx: { case OP_OpenRead: case OP_OpenWrite: - assert( (pOp->p5&(OPFLAG_P2ISREG|OPFLAG_BULKCSR|OPFLAG_SEEKEQ))==pOp->p5 ); assert( pOp->opcode==OP_OpenWrite || pOp->p5==0 || pOp->p5==OPFLAG_SEEKEQ ); assert( p->bIsReader ); assert( pOp->opcode==OP_OpenRead || pOp->opcode==OP_ReopenIdx @@ -76221,7 +80040,7 @@ case OP_OpenWrite: if( p->expired ){ rc = SQLITE_ABORT_ROLLBACK; - break; + goto abort_due_to_error; } nField = 0; @@ -76234,7 +80053,8 @@ case OP_OpenWrite: pX = pDb->pBt; assert( pX!=0 ); if( pOp->opcode==OP_OpenWrite ){ - wrFlag = 1; + assert( OPFLAG_FORDELETE==BTREE_FORDELETE ); + wrFlag = BTREE_WRCSR | (pOp->p5 & OPFLAG_FORDELETE); assert( sqlite3SchemaMutexHeld(db, iDb, 0) ); if( pDb->pSchema->file_format < p->minWriteFileFormat ){ p->minWriteFileFormat = pDb->pSchema->file_format; @@ -76244,7 +80064,7 @@ case OP_OpenWrite: } if( pOp->p5 & OPFLAG_P2ISREG ){ assert( p2>0 ); - assert( p2<=(p->nMem-p->nCursor) ); + assert( p2<=(p->nMem+1 - p->nCursor) ); pIn2 = &aMem[p2]; assert( memIsValid(pIn2) ); assert( (pIn2->flags & MEM_Int)!=0 ); @@ -76254,10 +80074,7 @@ case OP_OpenWrite: ** that opcode will always set the p2 value to 2 or more or else fail. ** If there were a failure, the prepared statement would have halted ** before reaching this instruction. */ - if( NEVER(p2<2) ) { - rc = SQLITE_CORRUPT_BKPT; - goto abort_due_to_error; - } + assert( p2>=2 ); } if( pOp->p4type==P4_KEYINFO ){ pKeyInfo = pOp->p4.pKeyInfo; @@ -76270,12 +80087,15 @@ case OP_OpenWrite: assert( pOp->p1>=0 ); assert( nField>=0 ); testcase( nField==0 ); /* Table with INTEGER PRIMARY KEY and nothing else */ - pCur = allocateCursor(p, pOp->p1, nField, iDb, 1); + pCur = allocateCursor(p, pOp->p1, nField, iDb, CURTYPE_BTREE); if( pCur==0 ) goto no_mem; pCur->nullRow = 1; pCur->isOrdered = 1; pCur->pgnoRoot = p2; - rc = sqlite3BtreeCursor(pX, p2, wrFlag, pKeyInfo, pCur->pCursor); +#ifdef SQLITE_DEBUG + pCur->wrFlag = wrFlag; +#endif + rc = sqlite3BtreeCursor(pX, p2, wrFlag, pKeyInfo, pCur->uc.pCursor); pCur->pKeyInfo = pKeyInfo; /* Set the VdbeCursor.isTable variable. Previous versions of ** SQLite used to check if the root-page flags were sane at this point @@ -76286,8 +80106,13 @@ case OP_OpenWrite: open_cursor_set_hints: assert( OPFLAG_BULKCSR==BTREE_BULKLOAD ); assert( OPFLAG_SEEKEQ==BTREE_SEEK_EQ ); - sqlite3BtreeCursorHints(pCur->pCursor, - (pOp->p5 & (OPFLAG_BULKCSR|OPFLAG_SEEKEQ))); + testcase( pOp->p5 & OPFLAG_BULKCSR ); +#ifdef SQLITE_ENABLE_CURSOR_HINTS + testcase( pOp->p2 & OPFLAG_SEEKEQ ); +#endif + sqlite3BtreeCursorHintFlags(pCur->uc.pCursor, + (pOp->p5 & (OPFLAG_BULKCSR|OPFLAG_SEEKEQ))); + if( rc ) goto abort_due_to_error; break; } @@ -76330,7 +80155,7 @@ case OP_OpenEphemeral: { SQLITE_OPEN_TRANSIENT_DB; assert( pOp->p1>=0 ); assert( pOp->p2>=0 ); - pCx = allocateCursor(p, pOp->p1, pOp->p2, -1, 1); + pCx = allocateCursor(p, pOp->p1, pOp->p2, -1, CURTYPE_BTREE); if( pCx==0 ) goto no_mem; pCx->nullRow = 1; pCx->isEphemeral = 1; @@ -76354,14 +80179,17 @@ case OP_OpenEphemeral: { assert( pKeyInfo->db==db ); assert( pKeyInfo->enc==ENC(db) ); pCx->pKeyInfo = pKeyInfo; - rc = sqlite3BtreeCursor(pCx->pBt, pgno, 1, pKeyInfo, pCx->pCursor); + rc = sqlite3BtreeCursor(pCx->pBt, pgno, BTREE_WRCSR, + pKeyInfo, pCx->uc.pCursor); } pCx->isTable = 0; }else{ - rc = sqlite3BtreeCursor(pCx->pBt, MASTER_ROOT, 1, 0, pCx->pCursor); + rc = sqlite3BtreeCursor(pCx->pBt, MASTER_ROOT, BTREE_WRCSR, + 0, pCx->uc.pCursor); pCx->isTable = 1; } } + if( rc ) goto abort_due_to_error; pCx->isOrdered = (pOp->p5!=BTREE_UNORDERED); break; } @@ -76381,12 +80209,13 @@ case OP_SorterOpen: { assert( pOp->p1>=0 ); assert( pOp->p2>=0 ); - pCx = allocateCursor(p, pOp->p1, pOp->p2, -1, 1); + pCx = allocateCursor(p, pOp->p1, pOp->p2, -1, CURTYPE_SORTER); if( pCx==0 ) goto no_mem; pCx->pKeyInfo = pOp->p4.pKeyInfo; assert( pCx->pKeyInfo->db==db ); assert( pCx->pKeyInfo->enc==ENC(db) ); rc = sqlite3VdbeSorterInit(db, pOp->p3, pCx); + if( rc ) goto abort_due_to_error; break; } @@ -76401,7 +80230,7 @@ case OP_SequenceTest: { VdbeCursor *pC; assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; - assert( pC->pSorter ); + assert( isSorter(pC) ); if( (pC->seqCount++)==0 ){ goto jump_to_p2; } @@ -76429,10 +80258,10 @@ case OP_OpenPseudo: { assert( pOp->p1>=0 ); assert( pOp->p3>=0 ); - pCx = allocateCursor(p, pOp->p1, pOp->p3, -1, 0); + pCx = allocateCursor(p, pOp->p1, pOp->p3, -1, CURTYPE_PSEUDO); if( pCx==0 ) goto no_mem; pCx->nullRow = 1; - pCx->pseudoTableReg = pOp->p2; + pCx->uc.pseudoTableReg = pOp->p2; pCx->isTable = 1; assert( pOp->p5==0 ); break; @@ -76464,7 +80293,7 @@ case OP_Close: { case OP_ColumnsUsed: { VdbeCursor *pC; pC = p->apCsr[pOp->p1]; - assert( pC->pCursor ); + assert( pC->eCurType==CURTYPE_BTREE ); pC->maskUsed = *(u64*)pOp->p4.pI64; break; } @@ -76482,6 +80311,13 @@ case OP_ColumnsUsed: { ** is greater than or equal to the key value. If there are no records ** greater than or equal to the key and P2 is not zero, then jump to P2. ** +** If the cursor P1 was opened using the OPFLAG_SEEKEQ flag, then this +** opcode will always land on a record that equally equals the key, or +** else jump immediately to P2. When the cursor is OPFLAG_SEEKEQ, this +** opcode must be followed by an IdxLE opcode with the same arguments. +** The IdxLE opcode will be skipped if this opcode succeeds, but the +** IdxLE opcode will be used on subsequent loop iterations. +** ** This opcode leaves the cursor configured to move in forward order, ** from the beginning toward the end. In other words, the cursor is ** configured to use Next, not Prev. @@ -76540,51 +80376,48 @@ case OP_ColumnsUsed: { ** from the end toward the beginning. In other words, the cursor is ** configured to use Prev, not Next. ** +** If the cursor P1 was opened using the OPFLAG_SEEKEQ flag, then this +** opcode will always land on a record that equally equals the key, or +** else jump immediately to P2. When the cursor is OPFLAG_SEEKEQ, this +** opcode must be followed by an IdxGE opcode with the same arguments. +** The IdxGE opcode will be skipped if this opcode succeeds, but the +** IdxGE opcode will be used on subsequent loop iterations. +** ** See also: Found, NotFound, SeekGt, SeekGe, SeekLt */ case OP_SeekLT: /* jump, in3 */ case OP_SeekLE: /* jump, in3 */ case OP_SeekGE: /* jump, in3 */ case OP_SeekGT: { /* jump, in3 */ - int res; - int oc; - VdbeCursor *pC; - UnpackedRecord r; - int nField; - i64 iKey; /* The rowid we are to seek to */ + int res; /* Comparison result */ + int oc; /* Opcode */ + VdbeCursor *pC; /* The cursor to seek */ + UnpackedRecord r; /* The key to seek for */ + int nField; /* Number of columns or fields in the key */ + i64 iKey; /* The rowid we are to seek to */ + int eqOnly; /* Only interested in == results */ assert( pOp->p1>=0 && pOp->p1nCursor ); assert( pOp->p2!=0 ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); - assert( pC->pseudoTableReg==0 ); + assert( pC->eCurType==CURTYPE_BTREE ); assert( OP_SeekLE == OP_SeekLT+1 ); assert( OP_SeekGE == OP_SeekLT+2 ); assert( OP_SeekGT == OP_SeekLT+3 ); assert( pC->isOrdered ); - assert( pC->pCursor!=0 ); + assert( pC->uc.pCursor!=0 ); oc = pOp->opcode; + eqOnly = 0; pC->nullRow = 0; #ifdef SQLITE_DEBUG pC->seekOp = pOp->opcode; #endif - /* For a cursor with the BTREE_SEEK_EQ hint, only the OP_SeekGE and - ** OP_SeekLE opcodes are allowed, and these must be immediately followed - ** by an OP_IdxGT or OP_IdxLT opcode, respectively, with the same key. - */ -#ifdef SQLITE_DEBUG - if( sqlite3BtreeCursorHasHint(pC->pCursor, BTREE_SEEK_EQ) ){ - assert( pOp->opcode==OP_SeekGE || pOp->opcode==OP_SeekLE ); - assert( pOp[1].opcode==OP_IdxLT || pOp[1].opcode==OP_IdxGT ); - assert( pOp[1].p1==pOp[0].p1 ); - assert( pOp[1].p2==pOp[0].p2 ); - assert( pOp[1].p3==pOp[0].p3 ); - assert( pOp[1].p4.i==pOp[0].p4.i ); - } -#endif - if( pC->isTable ){ + /* The BTREE_SEEK_EQ flag is only set on index cursors */ + assert( sqlite3BtreeCursorHasHint(pC->uc.pCursor, BTREE_SEEK_EQ)==0 ); + /* The input value in P3 might be of any type: integer, real, string, ** blob, or NULL. But it needs to be an integer before we can do ** the seek, so convert it. */ @@ -76627,12 +80460,26 @@ case OP_SeekGT: { /* jump, in3 */ if( (oc & 0x0001)==(OP_SeekLT & 0x0001) ) oc++; } } - rc = sqlite3BtreeMovetoUnpacked(pC->pCursor, 0, (u64)iKey, 0, &res); + rc = sqlite3BtreeMovetoUnpacked(pC->uc.pCursor, 0, (u64)iKey, 0, &res); pC->movetoTarget = iKey; /* Used by OP_Delete */ if( rc!=SQLITE_OK ){ goto abort_due_to_error; } }else{ + /* For a cursor with the BTREE_SEEK_EQ hint, only the OP_SeekGE and + ** OP_SeekLE opcodes are allowed, and these must be immediately followed + ** by an OP_IdxGT or OP_IdxLT opcode, respectively, with the same key. + */ + if( sqlite3BtreeCursorHasHint(pC->uc.pCursor, BTREE_SEEK_EQ) ){ + eqOnly = 1; + assert( pOp->opcode==OP_SeekGE || pOp->opcode==OP_SeekLE ); + assert( pOp[1].opcode==OP_IdxLT || pOp[1].opcode==OP_IdxGT ); + assert( pOp[1].p1==pOp[0].p1 ); + assert( pOp[1].p2==pOp[0].p2 ); + assert( pOp[1].p3==pOp[0].p3 ); + assert( pOp[1].p4.i==pOp[0].p4.i ); + } + nField = pOp->p4.i; assert( pOp->p4type==P4_INT32 ); assert( nField>0 ); @@ -76657,10 +80504,15 @@ case OP_SeekGT: { /* jump, in3 */ { int i; for(i=0; ipCursor, &r, 0, 0, &res); + r.eqSeen = 0; + rc = sqlite3BtreeMovetoUnpacked(pC->uc.pCursor, &r, 0, 0, &res); if( rc!=SQLITE_OK ){ goto abort_due_to_error; } + if( eqOnly && r.eqSeen==0 ){ + assert( res!=0 ); + goto seek_not_found; + } } pC->deferredMoveto = 0; pC->cacheStatus = CACHE_STALE; @@ -76670,7 +80522,7 @@ case OP_SeekGT: { /* jump, in3 */ if( oc>=OP_SeekGE ){ assert( oc==OP_SeekGE || oc==OP_SeekGT ); if( res<0 || (res==0 && oc==OP_SeekGT) ){ res = 0; - rc = sqlite3BtreeNext(pC->pCursor, &res); + rc = sqlite3BtreeNext(pC->uc.pCursor, &res); if( rc!=SQLITE_OK ) goto abort_due_to_error; }else{ res = 0; @@ -76679,47 +80531,26 @@ case OP_SeekGT: { /* jump, in3 */ assert( oc==OP_SeekLT || oc==OP_SeekLE ); if( res>0 || (res==0 && oc==OP_SeekLT) ){ res = 0; - rc = sqlite3BtreePrevious(pC->pCursor, &res); + rc = sqlite3BtreePrevious(pC->uc.pCursor, &res); if( rc!=SQLITE_OK ) goto abort_due_to_error; }else{ /* res might be negative because the table is empty. Check to ** see if this is the case. */ - res = sqlite3BtreeEof(pC->pCursor); + res = sqlite3BtreeEof(pC->uc.pCursor); } } +seek_not_found: assert( pOp->p2>0 ); VdbeBranchTaken(res!=0,2); if( res ){ goto jump_to_p2; + }else if( eqOnly ){ + assert( pOp[1].opcode==OP_IdxLT || pOp[1].opcode==OP_IdxGT ); + pOp++; /* Skip the OP_IdxLt or OP_IdxGT that follows */ } break; } - -/* Opcode: Seek P1 P2 * * * -** Synopsis: intkey=r[P2] -** -** P1 is an open table cursor and P2 is a rowid integer. Arrange -** for P1 to move so that it points to the rowid given by P2. -** -** This is actually a deferred seek. Nothing actually happens until -** the cursor is used to read a record. That way, if no reads -** occur, no unnecessary I/O happens. -*/ -case OP_Seek: { /* in2 */ - VdbeCursor *pC; - - assert( pOp->p1>=0 && pOp->p1nCursor ); - pC = p->apCsr[pOp->p1]; - assert( pC!=0 ); - assert( pC->pCursor!=0 ); - assert( pC->isTable ); - pC->nullRow = 0; - pIn2 = &aMem[pOp->p2]; - pC->movetoTarget = sqlite3VdbeIntValue(pIn2); - pC->deferredMoveto = 1; - break; -} /* Opcode: Found P1 P2 P3 P4 * @@ -76806,7 +80637,8 @@ case OP_Found: { /* jump, in3 */ pC->seekOp = pOp->opcode; #endif pIn3 = &aMem[pOp->p3]; - assert( pC->pCursor!=0 ); + assert( pC->eCurType==CURTYPE_BTREE ); + assert( pC->uc.pCursor!=0 ); assert( pC->isTable==0 ); pFree = 0; if( pOp->p4.i>0 ){ @@ -76843,10 +80675,10 @@ case OP_Found: { /* jump, in3 */ } } } - rc = sqlite3BtreeMovetoUnpacked(pC->pCursor, pIdxKey, 0, 0, &res); + rc = sqlite3BtreeMovetoUnpacked(pC->uc.pCursor, pIdxKey, 0, 0, &res); sqlite3DbFree(db, pFree); if( rc!=SQLITE_OK ){ - break; + goto abort_due_to_error; } pC->seekResult = res; alreadyExists = (res==0); @@ -76897,8 +80729,8 @@ case OP_NotExists: { /* jump, in3 */ pC->seekOp = 0; #endif assert( pC->isTable ); - assert( pC->pseudoTableReg==0 ); - pCrsr = pC->pCursor; + assert( pC->eCurType==CURTYPE_BTREE ); + pCrsr = pC->uc.pCursor; assert( pCrsr!=0 ); res = 0; iKey = pIn3->u.i; @@ -76918,6 +80750,7 @@ case OP_NotExists: { /* jump, in3 */ goto jump_to_p2; } } + if( rc ) goto abort_due_to_error; break; } @@ -76932,6 +80765,7 @@ case OP_NotExists: { /* jump, in3 */ case OP_Sequence: { /* out2 */ assert( pOp->p1>=0 && pOp->p1nCursor ); assert( p->apCsr[pOp->p1]!=0 ); + assert( p->apCsr[pOp->p1]->eCurType!=CURTYPE_VTAB ); pOut = out2Prerelease(p, pOp); pOut->u.i = p->apCsr[pOp->p1]->seqCount++; break; @@ -76967,7 +80801,8 @@ case OP_NewRowid: { /* out2 */ assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); - assert( pC->pCursor!=0 ); + assert( pC->eCurType==CURTYPE_BTREE ); + assert( pC->uc.pCursor!=0 ); { /* The next rowid or record number (different terms for the same ** thing) is obtained in a two-step algorithm. @@ -76995,15 +80830,15 @@ case OP_NewRowid: { /* out2 */ #endif if( !pC->useRandomRowid ){ - rc = sqlite3BtreeLast(pC->pCursor, &res); + rc = sqlite3BtreeLast(pC->uc.pCursor, &res); if( rc!=SQLITE_OK ){ goto abort_due_to_error; } if( res ){ v = 1; /* IMP: R-61914-48074 */ }else{ - assert( sqlite3BtreeCursorIsValid(pC->pCursor) ); - rc = sqlite3BtreeKeySize(pC->pCursor, &v); + assert( sqlite3BtreeCursorIsValid(pC->uc.pCursor) ); + rc = sqlite3BtreeKeySize(pC->uc.pCursor, &v); assert( rc==SQLITE_OK ); /* Cannot fail following BtreeLast() */ if( v>=MAX_ROWID ){ pC->useRandomRowid = 1; @@ -77024,7 +80859,7 @@ case OP_NewRowid: { /* out2 */ pMem = &pFrame->aMem[pOp->p3]; }else{ /* Assert that P3 is a valid memory cell. */ - assert( pOp->p3<=(p->nMem-p->nCursor) ); + assert( pOp->p3<=(p->nMem+1 - p->nCursor) ); pMem = &aMem[pOp->p3]; memAboutToChange(p, pMem); } @@ -77054,11 +80889,12 @@ case OP_NewRowid: { /* out2 */ do{ sqlite3_randomness(sizeof(v), &v); v &= (MAX_ROWID>>1); v++; /* Ensure that v is greater than zero */ - }while( ((rc = sqlite3BtreeMovetoUnpacked(pC->pCursor, 0, (u64)v, + }while( ((rc = sqlite3BtreeMovetoUnpacked(pC->uc.pCursor, 0, (u64)v, 0, &res))==SQLITE_OK) && (res==0) && (++cnt<100)); - if( rc==SQLITE_OK && res==0 ){ + if( rc ) goto abort_due_to_error; + if( res==0 ){ rc = SQLITE_FULL; /* IMP: R-38219-53002 */ goto abort_due_to_error; } @@ -77098,9 +80934,9 @@ case OP_NewRowid: { /* out2 */ ** is part of an INSERT operation. The difference is only important to ** the update hook. ** -** Parameter P4 may point to a string containing the table-name, or -** may be NULL. If it is not NULL, then the update-hook -** (sqlite3.xUpdateCallback) is invoked following a successful insert. +** Parameter P4 may point to a Table structure, or may be NULL. If it is +** not NULL, then the update-hook (sqlite3.xUpdateCallback) is invoked +** following a successful insert. ** ** (WARNING/TODO: If P1 is a pseudo-cursor and P2 is dynamically ** allocated, then ownership of P2 is transferred to the pseudo-cursor @@ -77126,17 +80962,19 @@ case OP_InsertInt: { int nZero; /* Number of zero-bytes to append */ int seekResult; /* Result of prior seek or 0 if no USESEEKRESULT flag */ const char *zDb; /* database name - used by the update hook */ - const char *zTbl; /* Table name - used by the opdate hook */ + Table *pTab; /* Table structure - used by update and pre-update hooks */ int op; /* Opcode for update hook: SQLITE_UPDATE or SQLITE_INSERT */ + op = 0; pData = &aMem[pOp->p2]; assert( pOp->p1>=0 && pOp->p1nCursor ); assert( memIsValid(pData) ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); - assert( pC->pCursor!=0 ); - assert( pC->pseudoTableReg==0 ); + assert( pC->eCurType==CURTYPE_BTREE ); + assert( pC->uc.pCursor!=0 ); assert( pC->isTable ); + assert( pOp->p4type==P4_TABLE || pOp->p4type>=P4_STATIC ); REGISTER_TRACE(pOp->p2, pData); if( pOp->opcode==OP_Insert ){ @@ -77150,6 +80988,28 @@ case OP_InsertInt: { iKey = pOp->p3; } + if( pOp->p4type==P4_TABLE && HAS_UPDATE_HOOK(db) ){ + assert( pC->isTable ); + assert( pC->iDb>=0 ); + zDb = db->aDb[pC->iDb].zName; + pTab = pOp->p4.pTab; + assert( HasRowid(pTab) ); + op = ((pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT); + }else{ + pTab = 0; /* Not needed. Silence a comiler warning. */ + zDb = 0; /* Not needed. Silence a compiler warning. */ + } + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + /* Invoke the pre-update hook, if any */ + if( db->xPreUpdateCallback + && pOp->p4type==P4_TABLE + && !(pOp->p5 & OPFLAG_ISUPDATE) + ){ + sqlite3VdbePreUpdateHook(p, pC, SQLITE_INSERT, zDb, pTab, iKey, pOp->p2); + } +#endif + if( pOp->p5 & OPFLAG_NCHANGE ) p->nChange++; if( pOp->p5 & OPFLAG_LASTROWID ) db->lastRowid = lastRowid = iKey; if( pData->flags & MEM_Null ){ @@ -77164,7 +81024,7 @@ case OP_InsertInt: { }else{ nZero = 0; } - rc = sqlite3BtreeInsert(pC->pCursor, 0, iKey, + rc = sqlite3BtreeInsert(pC->uc.pCursor, 0, iKey, pData->z, pData->n, nZero, (pOp->p5 & OPFLAG_APPEND)!=0, seekResult ); @@ -77172,74 +81032,137 @@ case OP_InsertInt: { pC->cacheStatus = CACHE_STALE; /* Invoke the update-hook if required. */ - if( rc==SQLITE_OK && db->xUpdateCallback && pOp->p4.z ){ - zDb = db->aDb[pC->iDb].zName; - zTbl = pOp->p4.z; - op = ((pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT); - assert( pC->isTable ); - db->xUpdateCallback(db->pUpdateArg, op, zDb, zTbl, iKey); - assert( pC->iDb>=0 ); + if( rc ) goto abort_due_to_error; + if( db->xUpdateCallback && op ){ + db->xUpdateCallback(db->pUpdateArg, op, zDb, pTab->zName, iKey); } break; } -/* Opcode: Delete P1 P2 * P4 P5 +/* Opcode: Delete P1 P2 P3 P4 P5 ** ** Delete the record at which the P1 cursor is currently pointing. ** -** If the P5 parameter is non-zero, the cursor will be left pointing at -** either the next or the previous record in the table. If it is left -** pointing at the next record, then the next Next instruction will be a -** no-op. As a result, in this case it is OK to delete a record from within a -** Next loop. If P5 is zero, then the cursor is left in an undefined state. +** If the OPFLAG_SAVEPOSITION bit of the P5 parameter is set, then +** the cursor will be left pointing at either the next or the previous +** record in the table. If it is left pointing at the next record, then +** the next Next instruction will be a no-op. As a result, in this case +** it is ok to delete a record from within a Next loop. If +** OPFLAG_SAVEPOSITION bit of P5 is clear, then the cursor will be +** left in an undefined state. ** -** If the OPFLAG_NCHANGE flag of P2 is set, then the row change count is -** incremented (otherwise not). +** If the OPFLAG_AUXDELETE bit is set on P5, that indicates that this +** delete one of several associated with deleting a table row and all its +** associated index entries. Exactly one of those deletes is the "primary" +** delete. The others are all on OPFLAG_FORDELETE cursors or else are +** marked with the AUXDELETE flag. +** +** If the OPFLAG_NCHANGE flag of P2 (NB: P2 not P5) is set, then the row +** change count is incremented (otherwise not). ** ** P1 must not be pseudo-table. It has to be a real table with ** multiple rows. ** -** If P4 is not NULL, then it is the name of the table that P1 is -** pointing to. The update hook will be invoked, if it exists. -** If P4 is not NULL then the P1 cursor must have been positioned -** using OP_NotFound prior to invoking this opcode. +** If P4 is not NULL then it points to a Table struture. In this case either +** the update or pre-update hook, or both, may be invoked. The P1 cursor must +** have been positioned using OP_NotFound prior to invoking this opcode in +** this case. Specifically, if one is configured, the pre-update hook is +** invoked if P4 is not NULL. The update-hook is invoked if one is configured, +** P4 is not NULL, and the OPFLAG_NCHANGE flag is set in P2. +** +** If the OPFLAG_ISUPDATE flag is set in P2, then P3 contains the address +** of the memory cell that contains the value that the rowid of the row will +** be set to by the update. */ case OP_Delete: { VdbeCursor *pC; - u8 hasUpdateCallback; + const char *zDb; + Table *pTab; + int opflags; + opflags = pOp->p2; assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); - assert( pC->pCursor!=0 ); /* Only valid for real tables, no pseudotables */ + assert( pC->eCurType==CURTYPE_BTREE ); + assert( pC->uc.pCursor!=0 ); assert( pC->deferredMoveto==0 ); - hasUpdateCallback = db->xUpdateCallback && pOp->p4.z && pC->isTable; - if( pOp->p5 && hasUpdateCallback ){ - sqlite3BtreeKeySize(pC->pCursor, &pC->movetoTarget); - } - #ifdef SQLITE_DEBUG - /* The seek operation that positioned the cursor prior to OP_Delete will - ** have also set the pC->movetoTarget field to the rowid of the row that - ** is being deleted */ - if( pOp->p4.z && pC->isTable && pOp->p5==0 ){ + if( pOp->p4type==P4_TABLE && HasRowid(pOp->p4.pTab) && pOp->p5==0 ){ + /* If p5 is zero, the seek operation that positioned the cursor prior to + ** OP_Delete will have also set the pC->movetoTarget field to the rowid of + ** the row that is being deleted */ i64 iKey = 0; - sqlite3BtreeKeySize(pC->pCursor, &iKey); - assert( pC->movetoTarget==iKey ); + sqlite3BtreeKeySize(pC->uc.pCursor, &iKey); + assert( pC->movetoTarget==iKey ); } #endif + + /* If the update-hook or pre-update-hook will be invoked, set zDb to + ** the name of the db to pass as to it. Also set local pTab to a copy + ** of p4.pTab. Finally, if p5 is true, indicating that this cursor was + ** last moved with OP_Next or OP_Prev, not Seek or NotFound, set + ** VdbeCursor.movetoTarget to the current rowid. */ + if( pOp->p4type==P4_TABLE && HAS_UPDATE_HOOK(db) ){ + assert( pC->iDb>=0 ); + assert( pOp->p4.pTab!=0 ); + zDb = db->aDb[pC->iDb].zName; + pTab = pOp->p4.pTab; + if( (pOp->p5 & OPFLAG_SAVEPOSITION)!=0 && pC->isTable ){ + sqlite3BtreeKeySize(pC->uc.pCursor, &pC->movetoTarget); + } + }else{ + zDb = 0; /* Not needed. Silence a compiler warning. */ + pTab = 0; /* Not needed. Silence a compiler warning. */ + } + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + /* Invoke the pre-update-hook if required. */ + if( db->xPreUpdateCallback && pOp->p4.pTab && HasRowid(pTab) ){ + assert( !(opflags & OPFLAG_ISUPDATE) || (aMem[pOp->p3].flags & MEM_Int) ); + sqlite3VdbePreUpdateHook(p, pC, + (opflags & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_DELETE, + zDb, pTab, pC->movetoTarget, + pOp->p3 + ); + } + if( opflags & OPFLAG_ISNOOP ) break; +#endif - rc = sqlite3BtreeDelete(pC->pCursor, pOp->p5); + /* Only flags that can be set are SAVEPOISTION and AUXDELETE */ + assert( (pOp->p5 & ~(OPFLAG_SAVEPOSITION|OPFLAG_AUXDELETE))==0 ); + assert( OPFLAG_SAVEPOSITION==BTREE_SAVEPOSITION ); + assert( OPFLAG_AUXDELETE==BTREE_AUXDELETE ); + +#ifdef SQLITE_DEBUG + if( p->pFrame==0 ){ + if( pC->isEphemeral==0 + && (pOp->p5 & OPFLAG_AUXDELETE)==0 + && (pC->wrFlag & OPFLAG_FORDELETE)==0 + ){ + nExtraDelete++; + } + if( pOp->p2 & OPFLAG_NCHANGE ){ + nExtraDelete--; + } + } +#endif + + rc = sqlite3BtreeDelete(pC->uc.pCursor, pOp->p5); pC->cacheStatus = CACHE_STALE; + if( rc ) goto abort_due_to_error; /* Invoke the update-hook if required. */ - if( rc==SQLITE_OK && hasUpdateCallback ){ - db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, - db->aDb[pC->iDb].zName, pOp->p4.z, pC->movetoTarget); - assert( pC->iDb>=0 ); + if( opflags & OPFLAG_NCHANGE ){ + p->nChange++; + if( db->xUpdateCallback && HasRowid(pTab) ){ + db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, zDb, pTab->zName, + pC->movetoTarget); + assert( pC->iDb>=0 ); + } } - if( pOp->p2 & OPFLAG_NCHANGE ) p->nChange++; + break; } /* Opcode: ResetCount * * * * * @@ -77283,6 +81206,7 @@ case OP_SorterCompare: { res = 0; rc = sqlite3VdbeSorterCompare(pC, pIn3, nKeyCol, &res); VdbeBranchTaken(res!=0,2); + if( rc ) goto abort_due_to_error; if( res ) goto jump_to_p2; break; }; @@ -77308,6 +81232,7 @@ case OP_SorterData: { rc = sqlite3VdbeSorterRowkey(pC, pOut); assert( rc!=SQLITE_OK || (pOut->flags & MEM_Blob) ); assert( pOp->p1>=0 && pOp->p1nCursor ); + if( rc ) goto abort_due_to_error; p->apCsr[pOp->p3]->cacheStatus = CACHE_STALE; break; } @@ -77347,14 +81272,14 @@ case OP_RowData: { /* Note that RowKey and RowData are really exactly the same instruction */ assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; + assert( pC!=0 ); + assert( pC->eCurType==CURTYPE_BTREE ); assert( isSorter(pC)==0 ); assert( pC->isTable || pOp->opcode!=OP_RowData ); assert( pC->isTable==0 || pOp->opcode==OP_RowData ); - assert( pC!=0 ); assert( pC->nullRow==0 ); - assert( pC->pseudoTableReg==0 ); - assert( pC->pCursor!=0 ); - pCrsr = pC->pCursor; + assert( pC->uc.pCursor!=0 ); + pCrsr = pC->uc.pCursor; /* The OP_RowKey and OP_RowData opcodes always follow OP_NotExists or ** OP_Rewind/Op_Next with no intervening instructions that might invalidate @@ -77396,6 +81321,7 @@ case OP_RowData: { }else{ rc = sqlite3BtreeData(pCrsr, 0, n, pOut->z); } + if( rc ) goto abort_due_to_error; pOut->enc = SQLITE_UTF8; /* In case the blob is ever cast to text */ UPDATE_MAX_BLOBSIZE(pOut); REGISTER_TRACE(pOp->p2, pOut); @@ -77422,29 +81348,32 @@ case OP_Rowid: { /* out2 */ assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); - assert( pC->pseudoTableReg==0 || pC->nullRow ); + assert( pC->eCurType!=CURTYPE_PSEUDO || pC->nullRow ); if( pC->nullRow ){ pOut->flags = MEM_Null; break; }else if( pC->deferredMoveto ){ v = pC->movetoTarget; #ifndef SQLITE_OMIT_VIRTUALTABLE - }else if( pC->pVtabCursor ){ - pVtab = pC->pVtabCursor->pVtab; + }else if( pC->eCurType==CURTYPE_VTAB ){ + assert( pC->uc.pVCur!=0 ); + pVtab = pC->uc.pVCur->pVtab; pModule = pVtab->pModule; assert( pModule->xRowid ); - rc = pModule->xRowid(pC->pVtabCursor, &v); + rc = pModule->xRowid(pC->uc.pVCur, &v); sqlite3VtabImportErrmsg(p, pVtab); + if( rc ) goto abort_due_to_error; #endif /* SQLITE_OMIT_VIRTUALTABLE */ }else{ - assert( pC->pCursor!=0 ); + assert( pC->eCurType==CURTYPE_BTREE ); + assert( pC->uc.pCursor!=0 ); rc = sqlite3VdbeCursorRestore(pC); if( rc ) goto abort_due_to_error; if( pC->nullRow ){ pOut->flags = MEM_Null; break; } - rc = sqlite3BtreeKeySize(pC->pCursor, &v); + rc = sqlite3BtreeKeySize(pC->uc.pCursor, &v); assert( rc==SQLITE_OK ); /* Always so because of CursorRestore() above */ } pOut->u.i = v; @@ -77465,8 +81394,9 @@ case OP_NullRow: { assert( pC!=0 ); pC->nullRow = 1; pC->cacheStatus = CACHE_STALE; - if( pC->pCursor ){ - sqlite3BtreeClearCursor(pC->pCursor); + if( pC->eCurType==CURTYPE_BTREE ){ + assert( pC->uc.pCursor!=0 ); + sqlite3BtreeClearCursor(pC->uc.pCursor); } break; } @@ -77491,7 +81421,8 @@ case OP_Last: { /* jump */ assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); - pCrsr = pC->pCursor; + assert( pC->eCurType==CURTYPE_BTREE ); + pCrsr = pC->uc.pCursor; res = 0; assert( pCrsr!=0 ); rc = sqlite3BtreeLast(pCrsr, &res); @@ -77502,6 +81433,7 @@ case OP_Last: { /* jump */ #ifdef SQLITE_DEBUG pC->seekOp = OP_Last; #endif + if( rc ) goto abort_due_to_error; if( pOp->p2>0 ){ VdbeBranchTaken(res!=0,2); if( res ) goto jump_to_p2; @@ -77559,12 +81491,14 @@ case OP_Rewind: { /* jump */ if( isSorter(pC) ){ rc = sqlite3VdbeSorterRewind(pC, &res); }else{ - pCrsr = pC->pCursor; + assert( pC->eCurType==CURTYPE_BTREE ); + pCrsr = pC->uc.pCursor; assert( pCrsr ); rc = sqlite3BtreeFirst(pCrsr, &res); pC->deferredMoveto = 0; pC->cacheStatus = CACHE_STALE; } + if( rc ) goto abort_due_to_error; pC->nullRow = (u8)res; assert( pOp->p2>0 && pOp->p2nOp ); VdbeBranchTaken(res!=0,2); @@ -77656,7 +81590,7 @@ case OP_Next: /* jump */ res = pOp->p3; assert( pC!=0 ); assert( pC->deferredMoveto==0 ); - assert( pC->pCursor ); + assert( pC->eCurType==CURTYPE_BTREE ); assert( res==0 || (res==1 && pC->isTable==0) ); testcase( res==1 ); assert( pOp->opcode!=OP_Next || pOp->p4.xAdvance==sqlite3BtreeNext ); @@ -77673,10 +81607,11 @@ case OP_Next: /* jump */ || pC->seekOp==OP_SeekLT || pC->seekOp==OP_SeekLE || pC->seekOp==OP_Last ); - rc = pOp->p4.xAdvance(pC->pCursor, &res); + rc = pOp->p4.xAdvance(pC->uc.pCursor, &res); next_tail: pC->cacheStatus = CACHE_STALE; VdbeBranchTaken(res==0,2); + if( rc ) goto abort_due_to_error; if( res==0 ){ pC->nullRow = 0; p->aCounter[pOp->p5]++; @@ -77724,22 +81659,22 @@ case OP_IdxInsert: { /* in2 */ pIn2 = &aMem[pOp->p2]; assert( pIn2->flags & MEM_Blob ); if( pOp->p5 & OPFLAG_NCHANGE ) p->nChange++; - assert( pC->pCursor!=0 ); + assert( pC->eCurType==CURTYPE_BTREE || pOp->opcode==OP_SorterInsert ); assert( pC->isTable==0 ); rc = ExpandBlob(pIn2); - if( rc==SQLITE_OK ){ - if( pOp->opcode==OP_SorterInsert ){ - rc = sqlite3VdbeSorterWrite(pC, pIn2); - }else{ - nKey = pIn2->n; - zKey = pIn2->z; - rc = sqlite3BtreeInsert(pC->pCursor, zKey, nKey, "", 0, 0, pOp->p3, - ((pOp->p5 & OPFLAG_USESEEKRESULT) ? pC->seekResult : 0) - ); - assert( pC->deferredMoveto==0 ); - pC->cacheStatus = CACHE_STALE; - } + if( rc ) goto abort_due_to_error; + if( pOp->opcode==OP_SorterInsert ){ + rc = sqlite3VdbeSorterWrite(pC, pIn2); + }else{ + nKey = pIn2->n; + zKey = pIn2->z; + rc = sqlite3BtreeInsert(pC->uc.pCursor, zKey, nKey, "", 0, 0, pOp->p3, + ((pOp->p5 & OPFLAG_USESEEKRESULT) ? pC->seekResult : 0) + ); + assert( pC->deferredMoveto==0 ); + pC->cacheStatus = CACHE_STALE; } + if( rc) goto abort_due_to_error; break; } @@ -77757,29 +81692,48 @@ case OP_IdxDelete: { UnpackedRecord r; assert( pOp->p3>0 ); - assert( pOp->p2>0 && pOp->p2+pOp->p3<=(p->nMem-p->nCursor)+1 ); + assert( pOp->p2>0 && pOp->p2+pOp->p3<=(p->nMem+1 - p->nCursor)+1 ); assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); - pCrsr = pC->pCursor; + assert( pC->eCurType==CURTYPE_BTREE ); + pCrsr = pC->uc.pCursor; assert( pCrsr!=0 ); assert( pOp->p5==0 ); r.pKeyInfo = pC->pKeyInfo; r.nField = (u16)pOp->p3; r.default_rc = 0; r.aMem = &aMem[pOp->p2]; -#ifdef SQLITE_DEBUG - { int i; for(i=0; ideferredMoveto==0 ); pC->cacheStatus = CACHE_STALE; break; } +/* Opcode: Seek P1 * P3 P4 * +** Synopsis: Move P3 to P1.rowid +** +** P1 is an open index cursor and P3 is a cursor on the corresponding +** table. This opcode does a deferred seek of the P3 table cursor +** to the row that corresponds to the current row of P1. +** +** This is a deferred seek. Nothing actually happens until +** the cursor is used to read a record. That way, if no reads +** occur, no unnecessary I/O happens. +** +** P4 may be an array of integers (type P4_INTARRAY) containing +** one entry for each column in the P3 table. If array entry a(i) +** is non-zero, then reading column a(i)-1 from cursor P3 is +** equivalent to performing the deferred seek and then reading column i +** from P1. This information is stored in P3 and used to redirect +** reads against P3 over to P1, thus possibly avoiding the need to +** seek and read cursor P3. +*/ /* Opcode: IdxRowid P1 P2 * * * ** Synopsis: r[P2]=rowid ** @@ -77789,36 +81743,57 @@ case OP_IdxDelete: { ** ** See also: Rowid, MakeRecord. */ +case OP_Seek: case OP_IdxRowid: { /* out2 */ - BtCursor *pCrsr; - VdbeCursor *pC; - i64 rowid; + VdbeCursor *pC; /* The P1 index cursor */ + VdbeCursor *pTabCur; /* The P2 table cursor (OP_Seek only) */ + i64 rowid; /* Rowid that P1 current points to */ - pOut = out2Prerelease(p, pOp); assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); - pCrsr = pC->pCursor; - assert( pCrsr!=0 ); - pOut->flags = MEM_Null; + assert( pC->eCurType==CURTYPE_BTREE ); + assert( pC->uc.pCursor!=0 ); assert( pC->isTable==0 ); assert( pC->deferredMoveto==0 ); + assert( !pC->nullRow || pOp->opcode==OP_IdxRowid ); + + /* The IdxRowid and Seek opcodes are combined because of the commonality + ** of sqlite3VdbeCursorRestore() and sqlite3VdbeIdxRowid(). */ + rc = sqlite3VdbeCursorRestore(pC); /* sqlite3VbeCursorRestore() can only fail if the record has been deleted - ** out from under the cursor. That will never happend for an IdxRowid - ** opcode, hence the NEVER() arround the check of the return value. - */ - rc = sqlite3VdbeCursorRestore(pC); + ** out from under the cursor. That will never happens for an IdxRowid + ** or Seek opcode */ if( NEVER(rc!=SQLITE_OK) ) goto abort_due_to_error; if( !pC->nullRow ){ rowid = 0; /* Not needed. Only used to silence a warning. */ - rc = sqlite3VdbeIdxRowid(db, pCrsr, &rowid); + rc = sqlite3VdbeIdxRowid(db, pC->uc.pCursor, &rowid); if( rc!=SQLITE_OK ){ goto abort_due_to_error; } - pOut->u.i = rowid; - pOut->flags = MEM_Int; + if( pOp->opcode==OP_Seek ){ + assert( pOp->p3>=0 && pOp->p3nCursor ); + pTabCur = p->apCsr[pOp->p3]; + assert( pTabCur!=0 ); + assert( pTabCur->eCurType==CURTYPE_BTREE ); + assert( pTabCur->uc.pCursor!=0 ); + assert( pTabCur->isTable ); + pTabCur->nullRow = 0; + pTabCur->movetoTarget = rowid; + pTabCur->deferredMoveto = 1; + assert( pOp->p4type==P4_INTARRAY || pOp->p4.ai==0 ); + pTabCur->aAltMap = pOp->p4.ai; + pTabCur->pAltCursor = pC; + }else{ + pOut = out2Prerelease(p, pOp); + pOut->u.i = rowid; + pOut->flags = MEM_Int; + } + }else{ + assert( pOp->opcode==OP_IdxRowid ); + sqlite3VdbeMemSetNull(&aMem[pOp->p2]); } break; } @@ -77879,7 +81854,8 @@ case OP_IdxGE: { /* jump */ pC = p->apCsr[pOp->p1]; assert( pC!=0 ); assert( pC->isOrdered ); - assert( pC->pCursor!=0); + assert( pC->eCurType==CURTYPE_BTREE ); + assert( pC->uc.pCursor!=0); assert( pC->deferredMoveto==0 ); assert( pOp->p5==0 || pOp->p5==1 ); assert( pOp->p4type==P4_INT32 ); @@ -77907,6 +81883,7 @@ case OP_IdxGE: { /* jump */ res++; } VdbeBranchTaken(res>0,2); + if( rc ) goto abort_due_to_error; if( res>0 ) goto jump_to_p2; break; } @@ -77936,11 +81913,13 @@ case OP_Destroy: { /* out2 */ int iDb; assert( p->readOnly==0 ); + assert( pOp->p1>1 ); pOut = out2Prerelease(p, pOp); pOut->flags = MEM_Null; if( db->nVdbeRead > db->nVDestroy+1 ){ rc = SQLITE_LOCKED; p->errorAction = OE_Abort; + goto abort_due_to_error; }else{ iDb = pOp->p3; assert( DbMaskTest(p->btreeMask, iDb) ); @@ -77948,8 +81927,9 @@ case OP_Destroy: { /* out2 */ rc = sqlite3BtreeDropTable(db->aDb[iDb].pBt, pOp->p1, &iMoved); pOut->flags = MEM_Int; pOut->u.i = iMoved; + if( rc ) goto abort_due_to_error; #ifndef SQLITE_OMIT_AUTOVACUUM - if( rc==SQLITE_OK && iMoved!=0 ){ + if( iMoved!=0 ){ sqlite3RootPageMoved(db, iDb, iMoved, pOp->p1); /* All OP_Destroy operations occur on the same btree */ assert( resetSchemaOnFault==0 || resetSchemaOnFault==iDb+1 ); @@ -77995,6 +81975,7 @@ case OP_Clear: { aMem[pOp->p3].u.i += nChange; } } + if( rc ) goto abort_due_to_error; break; } @@ -78012,11 +81993,13 @@ case OP_ResetSorter: { assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); - if( pC->pSorter ){ - sqlite3VdbeSorterReset(db, pC->pSorter); + if( isSorter(pC) ){ + sqlite3VdbeSorterReset(db, pC->uc.pSorter); }else{ + assert( pC->eCurType==CURTYPE_BTREE ); assert( pC->isEphemeral ); - rc = sqlite3BtreeClearTableOfCursor(pC->pCursor); + rc = sqlite3BtreeClearTableOfCursor(pC->uc.pCursor); + if( rc ) goto abort_due_to_error; } break; } @@ -78065,6 +82048,7 @@ case OP_CreateTable: { /* out2 */ flags = BTREE_BLOBKEY; } rc = sqlite3BtreeCreateTable(pDb->pBt, &pgno, flags); + if( rc ) goto abort_due_to_error; pOut->u.i = pgno; break; } @@ -78105,7 +82089,7 @@ case OP_ParseSchema: { "SELECT name, rootpage, sql FROM '%q'.%s WHERE %s ORDER BY rowid", db->aDb[iDb].zName, zMaster, pOp->p4.z); if( zSql==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else{ assert( db->init.busy==0 ); db->init.busy = 1; @@ -78117,9 +82101,12 @@ case OP_ParseSchema: { db->init.busy = 0; } } - if( rc ) sqlite3ResetAllSchemasOfConnection(db); - if( rc==SQLITE_NOMEM ){ - goto no_mem; + if( rc ){ + sqlite3ResetAllSchemasOfConnection(db); + if( rc==SQLITE_NOMEM ){ + goto no_mem; + } + goto abort_due_to_error; } break; } @@ -78134,6 +82121,7 @@ case OP_ParseSchema: { case OP_LoadAnalysis: { assert( pOp->p1>=0 && pOp->p1nDb ); rc = sqlite3AnalysisLoad(db, pOp->p1); + if( rc ) goto abort_due_to_error; break; } #endif /* !defined(SQLITE_OMIT_ANALYZE) */ @@ -78179,7 +82167,7 @@ case OP_DropTrigger: { #ifndef SQLITE_OMIT_INTEGRITY_CHECK -/* Opcode: IntegrityCk P1 P2 P3 * P5 +/* Opcode: IntegrityCk P1 P2 P3 P4 P5 ** ** Do an analysis of the currently open database. Store in ** register P1 the text of an error message describing any problems. @@ -78190,9 +82178,8 @@ case OP_DropTrigger: { ** In other words, the analysis stops as soon as reg(P1) errors are ** seen. Reg(P1) is updated with the number of errors remaining. ** -** The root page numbers of all tables in the database are integer -** stored in reg(P1), reg(P1+1), reg(P1+2), .... There are P2 tables -** total. +** The root page numbers of all tables in the database are integers +** stored in P4_INTARRAY argument. ** ** If P5 is not zero, the check is done on the auxiliary database ** file, not the main database file. @@ -78202,30 +82189,24 @@ case OP_DropTrigger: { case OP_IntegrityCk: { int nRoot; /* Number of tables to check. (Number of root pages.) */ int *aRoot; /* Array of rootpage numbers for tables to be checked */ - int j; /* Loop counter */ int nErr; /* Number of errors reported */ char *z; /* Text of the error report */ Mem *pnErr; /* Register keeping track of errors remaining */ assert( p->bIsReader ); nRoot = pOp->p2; + aRoot = pOp->p4.ai; assert( nRoot>0 ); - aRoot = sqlite3DbMallocRaw(db, sizeof(int)*(nRoot+1) ); - if( aRoot==0 ) goto no_mem; - assert( pOp->p3>0 && pOp->p3<=(p->nMem-p->nCursor) ); + assert( aRoot[nRoot]==0 ); + assert( pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor) ); pnErr = &aMem[pOp->p3]; assert( (pnErr->flags & MEM_Int)!=0 ); assert( (pnErr->flags & (MEM_Str|MEM_Blob))==0 ); pIn1 = &aMem[pOp->p1]; - for(j=0; jp5nDb ); assert( DbMaskTest(p->btreeMask, pOp->p5) ); z = sqlite3BtreeIntegrityCheck(db->aDb[pOp->p5].pBt, aRoot, nRoot, (int)pnErr->u.i, &nErr); - sqlite3DbFree(db, aRoot); pnErr->u.i -= nErr; sqlite3VdbeMemSetNull(pIn1); if( nErr==0 ){ @@ -78393,7 +82374,7 @@ case OP_Program: { /* jump */ if( p->nFrame>=db->aLimit[SQLITE_LIMIT_TRIGGER_DEPTH] ){ rc = SQLITE_ERROR; sqlite3VdbeError(p, "too many levels of trigger recursion"); - break; + goto abort_due_to_error; } /* Register pRt is used to store the memory required to save the state @@ -78407,6 +82388,8 @@ case OP_Program: { /* jump */ ** variable nMem (and later, VdbeFrame.nChildMem) to this value. */ nMem = pProgram->nMem + pProgram->nCsr; + assert( nMem>0 ); + if( pProgram->nCsr==0 ) nMem++; nByte = ROUND8(sizeof(VdbeFrame)) + nMem * sizeof(Mem) + pProgram->nCsr * sizeof(VdbeCursor *) @@ -78443,7 +82426,8 @@ case OP_Program: { /* jump */ } }else{ pFrame = pRt->u.pFrame; - assert( pProgram->nMem+pProgram->nCsr==pFrame->nChildMem ); + assert( pProgram->nMem+pProgram->nCsr==pFrame->nChildMem + || (pProgram->nCsr==0 && pProgram->nMem+1==pFrame->nChildMem) ); assert( pProgram->nCsr==pFrame->nChildCsr ); assert( (int)(pOp - aOp)==pFrame->pc ); } @@ -78453,12 +82437,15 @@ case OP_Program: { /* jump */ pFrame->lastRowid = lastRowid; pFrame->nChange = p->nChange; pFrame->nDbChange = p->db->nChange; + assert( pFrame->pAuxData==0 ); + pFrame->pAuxData = p->pAuxData; + p->pAuxData = 0; p->nChange = 0; p->pFrame = pFrame; - p->aMem = aMem = &VdbeFrameMem(pFrame)[-1]; + p->aMem = aMem = VdbeFrameMem(pFrame); p->nMem = pFrame->nChildMem; p->nCursor = (u16)pFrame->nChildCsr; - p->apCsr = (VdbeCursor **)&aMem[p->nMem+1]; + p->apCsr = (VdbeCursor **)&aMem[p->nMem]; p->aOp = aOp = pProgram->aOp; p->nOp = pProgram->nOp; p->aOnceFlag = (u8 *)&p->apCsr[p->nCursor]; @@ -78592,20 +82579,31 @@ case OP_IfPos: { /* jump, in1 */ break; } -/* Opcode: SetIfNotPos P1 P2 P3 * * -** Synopsis: if r[P1]<=0 then r[P2]=P3 +/* Opcode: OffsetLimit P1 P2 P3 * * +** Synopsis: if r[P1]>0 then r[P2]=r[P1]+max(0,r[P3]) else r[P2]=(-1) ** -** Register P1 must contain an integer. -** If the value of register P1 is not positive (if it is less than 1) then -** set the value of register P2 to be the integer P3. +** This opcode performs a commonly used computation associated with +** LIMIT and OFFSET process. r[P1] holds the limit counter. r[P3] +** holds the offset counter. The opcode computes the combined value +** of the LIMIT and OFFSET and stores that value in r[P2]. The r[P2] +** value computed is the total number of rows that will need to be +** visited in order to complete the query. +** +** If r[P3] is zero or negative, that means there is no OFFSET +** and r[P2] is set to be the value of the LIMIT, r[P1]. +** +** if r[P1] is zero or negative, that means there is no LIMIT +** and r[P2] is set to -1. +** +** Otherwise, r[P2] is set to the sum of r[P1] and r[P3]. */ -case OP_SetIfNotPos: { /* in1, in2 */ +case OP_OffsetLimit: { /* in1, out2, in3 */ pIn1 = &aMem[pOp->p1]; - assert( pIn1->flags&MEM_Int ); - if( pIn1->u.i<=0 ){ - pOut = out2Prerelease(p, pOp); - pOut->u.i = pOp->p3; - } + pIn3 = &aMem[pOp->p3]; + pOut = out2Prerelease(p, pOp); + assert( pIn1->flags & MEM_Int ); + assert( pIn3->flags & MEM_Int ); + pOut->u.i = pIn1->u.i<=0 ? -1 : pIn1->u.i+(pIn3->u.i>0?pIn3->u.i:0); break; } @@ -78644,21 +82642,6 @@ case OP_DecrJumpZero: { /* jump, in1 */ } -/* Opcode: JumpZeroIncr P1 P2 * * * -** Synopsis: if (r[P1]++)==0 ) goto P2 -** -** The register P1 must contain an integer. If register P1 is initially -** zero, then jump to P2. Increment register P1 regardless of whether or -** not the jump is taken. -*/ -case OP_JumpZeroIncr: { /* jump, in1 */ - pIn1 = &aMem[pOp->p1]; - assert( pIn1->flags&MEM_Int ); - VdbeBranchTaken(pIn1->u.i==0, 2); - if( (pIn1->u.i++)==0 ) goto jump_to_p2; - break; -} - /* Opcode: AggStep0 * P2 P3 P4 P5 ** Synopsis: accum=r[P3] step(r[P2@P5]) ** @@ -78693,10 +82676,10 @@ case OP_AggStep0: { assert( pOp->p4type==P4_FUNCDEF ); n = pOp->p5; - assert( pOp->p3>0 && pOp->p3<=(p->nMem-p->nCursor) ); - assert( n==0 || (pOp->p2>0 && pOp->p2+n<=(p->nMem-p->nCursor)+1) ); + assert( pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor) ); + assert( n==0 || (pOp->p2>0 && pOp->p2+n<=(p->nMem+1 - p->nCursor)+1) ); assert( pOp->p3p2 || pOp->p3>=pOp->p2+n ); - pCtx = sqlite3DbMallocRaw(db, sizeof(*pCtx) + (n-1)*sizeof(sqlite3_value*)); + pCtx = sqlite3DbMallocRawNN(db, sizeof(*pCtx) + (n-1)*sizeof(sqlite3_value*)); if( pCtx==0 ) goto no_mem; pCtx->pMem = 0; pCtx->pFunc = pOp->p4.pFunc; @@ -78739,13 +82722,14 @@ case OP_AggStep: { pCtx->pOut = &t; pCtx->fErrorOrAux = 0; pCtx->skipFlag = 0; - (pCtx->pFunc->xStep)(pCtx,pCtx->argc,pCtx->argv); /* IMP: R-24505-23230 */ + (pCtx->pFunc->xSFunc)(pCtx,pCtx->argc,pCtx->argv); /* IMP: R-24505-23230 */ if( pCtx->fErrorOrAux ){ if( pCtx->isError ){ sqlite3VdbeError(p, "%s", sqlite3_value_text(&t)); rc = pCtx->isError; } sqlite3VdbeMemRelease(&t); + if( rc ) goto abort_due_to_error; }else{ assert( t.flags==MEM_Null ); } @@ -78772,12 +82756,13 @@ case OP_AggStep: { */ case OP_AggFinal: { Mem *pMem; - assert( pOp->p1>0 && pOp->p1<=(p->nMem-p->nCursor) ); + assert( pOp->p1>0 && pOp->p1<=(p->nMem+1 - p->nCursor) ); pMem = &aMem[pOp->p1]; assert( (pMem->flags & ~(MEM_Null|MEM_Agg))==0 ); rc = sqlite3VdbeMemFinalize(pMem, pOp->p4.pFunc); if( rc ){ sqlite3VdbeError(p, "%s", sqlite3_value_text(pMem)); + goto abort_due_to_error; } sqlite3VdbeChangeEncoding(pMem, encoding); UPDATE_MAX_BLOBSIZE(pMem); @@ -78813,7 +82798,8 @@ case OP_Checkpoint: { || pOp->p2==SQLITE_CHECKPOINT_TRUNCATE ); rc = sqlite3Checkpoint(db, pOp->p1, pOp->p2, &aRes[1], &aRes[2]); - if( rc==SQLITE_BUSY ){ + if( rc ){ + if( rc!=SQLITE_BUSY ) goto abort_due_to_error; rc = SQLITE_OK; aRes[0] = 1; } @@ -78886,7 +82872,7 @@ case OP_JournalMode: { /* out2 */ "cannot change %s wal mode from within a transaction", (eNew==PAGER_JOURNALMODE_WAL ? "into" : "out of") ); - break; + goto abort_due_to_error; }else{ if( eOld==PAGER_JOURNALMODE_WAL ){ @@ -78916,9 +82902,7 @@ case OP_JournalMode: { /* out2 */ } #endif /* ifndef SQLITE_OMIT_WAL */ - if( rc ){ - eNew = eOld; - } + if( rc ) eNew = eOld; eNew = sqlite3PagerSetJournalMode(pPager, eNew); pOut->flags = MEM_Str|MEM_Static|MEM_Term; @@ -78926,6 +82910,7 @@ case OP_JournalMode: { /* out2 */ pOut->n = sqlite3Strlen30(pOut->z); pOut->enc = SQLITE_UTF8; sqlite3VdbeChangeEncoding(pOut, encoding); + if( rc ) goto abort_due_to_error; break; }; #endif /* SQLITE_OMIT_PRAGMA */ @@ -78940,6 +82925,7 @@ case OP_JournalMode: { /* out2 */ case OP_Vacuum: { assert( p->readOnly==0 ); rc = sqlite3RunVacuum(&p->zErrMsg, db); + if( rc ) goto abort_due_to_error; break; } #endif @@ -78960,7 +82946,8 @@ case OP_IncrVacuum: { /* jump */ pBt = db->aDb[pOp->p1].pBt; rc = sqlite3BtreeIncrVacuum(pBt); VdbeBranchTaken(rc==SQLITE_DONE,2); - if( rc==SQLITE_DONE ){ + if( rc ){ + if( rc!=SQLITE_DONE ) goto abort_due_to_error; rc = SQLITE_OK; goto jump_to_p2; } @@ -79011,9 +82998,12 @@ case OP_TableLock: { assert( DbMaskTest(p->btreeMask, p1) ); assert( isWriteLock==0 || isWriteLock==1 ); rc = sqlite3BtreeLockTable(db->aDb[p1].pBt, pOp->p2, isWriteLock); - if( (rc&0xFF)==SQLITE_LOCKED ){ - const char *z = pOp->p4.z; - sqlite3VdbeError(p, "database table is locked: %s", z); + if( rc ){ + if( (rc&0xFF)==SQLITE_LOCKED ){ + const char *z = pOp->p4.z; + sqlite3VdbeError(p, "database table is locked: %s", z); + } + goto abort_due_to_error; } } break; @@ -79035,6 +83025,7 @@ case OP_VBegin: { pVTab = pOp->p4.pVtab; rc = sqlite3VtabBegin(db, pVTab); if( pVTab ) sqlite3VtabImportErrmsg(p, pVTab->pVtab); + if( rc ) goto abort_due_to_error; break; } #endif /* SQLITE_OMIT_VIRTUALTABLE */ @@ -79063,6 +83054,7 @@ case OP_VCreate: { rc = sqlite3VtabCallCreate(db, pOp->p1, zTab, &p->zErrMsg); } sqlite3VdbeMemRelease(&sMem); + if( rc ) goto abort_due_to_error; break; } #endif /* SQLITE_OMIT_VIRTUALTABLE */ @@ -79077,6 +83069,7 @@ case OP_VDestroy: { db->nVDestroy++; rc = sqlite3VtabCallDestroy(db, pOp->p1, pOp->p4.z); db->nVDestroy--; + if( rc ) goto abort_due_to_error; break; } #endif /* SQLITE_OMIT_VIRTUALTABLE */ @@ -79090,35 +83083,35 @@ case OP_VDestroy: { */ case OP_VOpen: { VdbeCursor *pCur; - sqlite3_vtab_cursor *pVtabCursor; + sqlite3_vtab_cursor *pVCur; sqlite3_vtab *pVtab; const sqlite3_module *pModule; assert( p->bIsReader ); pCur = 0; - pVtabCursor = 0; + pVCur = 0; pVtab = pOp->p4.pVtab->pVtab; if( pVtab==0 || NEVER(pVtab->pModule==0) ){ rc = SQLITE_LOCKED; - break; + goto abort_due_to_error; } pModule = pVtab->pModule; - rc = pModule->xOpen(pVtab, &pVtabCursor); + rc = pModule->xOpen(pVtab, &pVCur); sqlite3VtabImportErrmsg(p, pVtab); - if( SQLITE_OK==rc ){ - /* Initialize sqlite3_vtab_cursor base class */ - pVtabCursor->pVtab = pVtab; + if( rc ) goto abort_due_to_error; - /* Initialize vdbe cursor object */ - pCur = allocateCursor(p, pOp->p1, 0, -1, 0); - if( pCur ){ - pCur->pVtabCursor = pVtabCursor; - pVtab->nRef++; - }else{ - assert( db->mallocFailed ); - pModule->xClose(pVtabCursor); - goto no_mem; - } + /* Initialize sqlite3_vtab_cursor base class */ + pVCur->pVtab = pVtab; + + /* Initialize vdbe cursor object */ + pCur = allocateCursor(p, pOp->p1, 0, -1, CURTYPE_VTAB); + if( pCur ){ + pCur->uc.pVCur = pVCur; + pVtab->nRef++; + }else{ + assert( db->mallocFailed ); + pModule->xClose(pVCur); + goto no_mem; } break; } @@ -79150,7 +83143,7 @@ case OP_VFilter: { /* jump */ const sqlite3_module *pModule; Mem *pQuery; Mem *pArgc; - sqlite3_vtab_cursor *pVtabCursor; + sqlite3_vtab_cursor *pVCur; sqlite3_vtab *pVtab; VdbeCursor *pCur; int res; @@ -79162,9 +83155,9 @@ case OP_VFilter: { /* jump */ pCur = p->apCsr[pOp->p1]; assert( memIsValid(pQuery) ); REGISTER_TRACE(pOp->p3, pQuery); - assert( pCur->pVtabCursor ); - pVtabCursor = pCur->pVtabCursor; - pVtab = pVtabCursor->pVtab; + assert( pCur->eCurType==CURTYPE_VTAB ); + pVCur = pCur->uc.pVCur; + pVtab = pVCur->pVtab; pModule = pVtab->pModule; /* Grab the index number and argc parameters */ @@ -79178,11 +83171,10 @@ case OP_VFilter: { /* jump */ for(i = 0; ixFilter(pVtabCursor, iQuery, pOp->p4.z, nArg, apArg); + rc = pModule->xFilter(pVCur, iQuery, pOp->p4.z, nArg, apArg); sqlite3VtabImportErrmsg(p, pVtab); - if( rc==SQLITE_OK ){ - res = pModule->xEof(pVtabCursor); - } + if( rc ) goto abort_due_to_error; + res = pModule->xEof(pVCur); pCur->nullRow = 0; VdbeBranchTaken(res!=0,2); if( res ) goto jump_to_p2; @@ -79205,21 +83197,21 @@ case OP_VColumn: { sqlite3_context sContext; VdbeCursor *pCur = p->apCsr[pOp->p1]; - assert( pCur->pVtabCursor ); - assert( pOp->p3>0 && pOp->p3<=(p->nMem-p->nCursor) ); + assert( pCur->eCurType==CURTYPE_VTAB ); + assert( pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor) ); pDest = &aMem[pOp->p3]; memAboutToChange(p, pDest); if( pCur->nullRow ){ sqlite3VdbeMemSetNull(pDest); break; } - pVtab = pCur->pVtabCursor->pVtab; + pVtab = pCur->uc.pVCur->pVtab; pModule = pVtab->pModule; assert( pModule->xColumn ); memset(&sContext, 0, sizeof(sContext)); sContext.pOut = pDest; MemSetTypeFlag(pDest, MEM_Null); - rc = pModule->xColumn(pCur->pVtabCursor, &sContext, pOp->p2); + rc = pModule->xColumn(pCur->uc.pVCur, &sContext, pOp->p2); sqlite3VtabImportErrmsg(p, pVtab); if( sContext.isError ){ rc = sContext.isError; @@ -79231,6 +83223,7 @@ case OP_VColumn: { if( sqlite3VdbeMemTooBig(pDest) ){ goto too_big; } + if( rc ) goto abort_due_to_error; break; } #endif /* SQLITE_OMIT_VIRTUALTABLE */ @@ -79250,11 +83243,11 @@ case OP_VNext: { /* jump */ res = 0; pCur = p->apCsr[pOp->p1]; - assert( pCur->pVtabCursor ); + assert( pCur->eCurType==CURTYPE_VTAB ); if( pCur->nullRow ){ break; } - pVtab = pCur->pVtabCursor->pVtab; + pVtab = pCur->uc.pVCur->pVtab; pModule = pVtab->pModule; assert( pModule->xNext ); @@ -79264,11 +83257,10 @@ case OP_VNext: { /* jump */ ** data is available) and the error code returned when xColumn or ** some other method is next invoked on the save virtual table cursor. */ - rc = pModule->xNext(pCur->pVtabCursor); + rc = pModule->xNext(pCur->uc.pVCur); sqlite3VtabImportErrmsg(p, pVtab); - if( rc==SQLITE_OK ){ - res = pModule->xEof(pCur->pVtabCursor); - } + if( rc ) goto abort_due_to_error; + res = pModule->xEof(pCur->uc.pVCur); VdbeBranchTaken(!res,2); if( !res ){ /* If there is data, jump to P2 */ @@ -79300,11 +83292,11 @@ case OP_VRename: { testcase( pName->enc==SQLITE_UTF16BE ); testcase( pName->enc==SQLITE_UTF16LE ); rc = sqlite3VdbeChangeEncoding(pName, SQLITE_UTF8); - if( rc==SQLITE_OK ){ - rc = pVtab->pModule->xRename(pVtab, pName->z); - sqlite3VtabImportErrmsg(p, pVtab); - p->expired = 0; - } + if( rc ) goto abort_due_to_error; + rc = pVtab->pModule->xRename(pVtab, pName->z); + sqlite3VtabImportErrmsg(p, pVtab); + p->expired = 0; + if( rc ) goto abort_due_to_error; break; } #endif @@ -79353,7 +83345,7 @@ case OP_VUpdate: { pVtab = pOp->p4.pVtab->pVtab; if( pVtab==0 || NEVER(pVtab->pModule==0) ){ rc = SQLITE_LOCKED; - break; + goto abort_due_to_error; } pModule = pVtab->pModule; nArg = pOp->p2; @@ -79385,6 +83377,7 @@ case OP_VUpdate: { }else{ p->nChange++; } + if( rc ) goto abort_due_to_error; } break; } @@ -79476,6 +83469,28 @@ case OP_Init: { /* jump */ break; } +#ifdef SQLITE_ENABLE_CURSOR_HINTS +/* Opcode: CursorHint P1 * * P4 * +** +** Provide a hint to cursor P1 that it only needs to return rows that +** satisfy the Expr in P4. TK_REGISTER terms in the P4 expression refer +** to values currently held in registers. TK_COLUMN terms in the P4 +** expression refer to columns in the b-tree to which cursor P1 is pointing. +*/ +case OP_CursorHint: { + VdbeCursor *pC; + + assert( pOp->p1>=0 && pOp->p1nCursor ); + assert( pOp->p4type==P4_EXPR ); + pC = p->apCsr[pOp->p1]; + if( pC ){ + assert( pC->eCurType==CURTYPE_BTREE ); + sqlite3BtreeCursorHint(pC->uc.pCursor, BTREE_HINT_RANGE, + pOp->p4.pExpr, aMem); + } + break; +} +#endif /* SQLITE_ENABLE_CURSOR_HINTS */ /* Opcode: Noop * * * * * ** @@ -79519,11 +83534,12 @@ default: { /* This is really OP_Noop and OP_Explain */ #ifdef SQLITE_DEBUG if( db->flags & SQLITE_VdbeTrace ){ + u8 opProperty = sqlite3OpcodeProperty[pOrigOp->opcode]; if( rc!=0 ) printf("rc=%d\n",rc); - if( pOrigOp->opflags & (OPFLG_OUT2) ){ + if( opProperty & (OPFLG_OUT2) ){ registerTrace(pOrigOp->p2, &aMem[pOrigOp->p2]); } - if( pOrigOp->opflags & OPFLG_OUT3 ){ + if( opProperty & OPFLG_OUT3 ){ registerTrace(pOrigOp->p3, &aMem[pOrigOp->p3]); } } @@ -79534,14 +83550,19 @@ default: { /* This is really OP_Noop and OP_Explain */ /* If we reach this point, it means that execution is finished with ** an error of some kind. */ -vdbe_error_halt: +abort_due_to_error: + if( db->mallocFailed ) rc = SQLITE_NOMEM_BKPT; assert( rc ); + if( p->zErrMsg==0 && rc!=SQLITE_IOERR_NOMEM ){ + sqlite3VdbeError(p, "%s", sqlite3ErrStr(rc)); + } p->rc = rc; + sqlite3SystemError(db, rc); testcase( sqlite3GlobalConfig.xLog!=0 ); sqlite3_log(rc, "statement aborts at %d: [%s] %s", (int)(pOp - aOp), p->zSql, p->zErrMsg); sqlite3VdbeHalt(p); - if( rc==SQLITE_IOERR_NOMEM ) db->mallocFailed = 1; + if( rc==SQLITE_IOERR_NOMEM ) sqlite3OomFault(db); rc = SQLITE_ERROR; if( resetSchemaOnFault>0 ){ sqlite3ResetOneSchema(db, resetSchemaOnFault-1); @@ -79555,6 +83576,9 @@ vdbe_return: testcase( nVmStep>0 ); p->aCounter[SQLITE_STMTSTATUS_VM_STEP] += (int)nVmStep; sqlite3VdbeLeave(p); + assert( rc!=SQLITE_OK || nExtraDelete==0 + || sqlite3_strlike("DELETE%",p->zSql,0)!=0 + ); return rc; /* Jump to here if a string or blob larger than SQLITE_MAX_LENGTH @@ -79563,36 +83587,25 @@ vdbe_return: too_big: sqlite3VdbeError(p, "string or blob too big"); rc = SQLITE_TOOBIG; - goto vdbe_error_halt; + goto abort_due_to_error; /* Jump to here if a malloc() fails. */ no_mem: - db->mallocFailed = 1; + sqlite3OomFault(db); sqlite3VdbeError(p, "out of memory"); - rc = SQLITE_NOMEM; - goto vdbe_error_halt; - - /* Jump to here for any other kind of fatal error. The "rc" variable - ** should hold the error number. - */ -abort_due_to_error: - assert( p->zErrMsg==0 ); - if( db->mallocFailed ) rc = SQLITE_NOMEM; - if( rc!=SQLITE_IOERR_NOMEM ){ - sqlite3VdbeError(p, "%s", sqlite3ErrStr(rc)); - } - goto vdbe_error_halt; + rc = SQLITE_NOMEM_BKPT; + goto abort_due_to_error; /* Jump to here if the sqlite3_interrupt() API sets the interrupt ** flag. */ abort_due_to_interrupt: assert( db->u1.isInterrupted ); - rc = SQLITE_INTERRUPT; + rc = db->mallocFailed ? SQLITE_NOMEM_BKPT : SQLITE_INTERRUPT; p->rc = rc; sqlite3VdbeError(p, "%s", sqlite3ErrStr(rc)); - goto vdbe_error_halt; + goto abort_due_to_error; } @@ -79630,6 +83643,8 @@ struct Incrblob { BtCursor *pCsr; /* Cursor pointing at blob row */ sqlite3_stmt *pStmt; /* Statement holding cursor open */ sqlite3 *db; /* The associated database */ + char *zDb; /* Database name */ + Table *pTab; /* Table object */ }; @@ -79676,7 +83691,7 @@ static int blobSeekToRow(Incrblob *p, sqlite3_int64 iRow, char **pzErr){ }else{ p->iOffset = pC->aType[p->iCol + pC->nField]; p->nByte = sqlite3VdbeSerialTypeLen(type); - p->pCsr = pC->pCursor; + p->pCsr = pC->uc.pCursor; sqlite3BtreeIncrblobCursor(p->pCsr); } } @@ -79715,38 +83730,6 @@ SQLITE_API int SQLITE_STDCALL sqlite3_blob_open( ){ int nAttempt = 0; int iCol; /* Index of zColumn in row-record */ - - /* This VDBE program seeks a btree cursor to the identified - ** db/table/row entry. The reason for using a vdbe program instead - ** of writing code to use the b-tree layer directly is that the - ** vdbe program will take advantage of the various transaction, - ** locking and error handling infrastructure built into the vdbe. - ** - ** After seeking the cursor, the vdbe executes an OP_ResultRow. - ** Code external to the Vdbe then "borrows" the b-tree cursor and - ** uses it to implement the blob_read(), blob_write() and - ** blob_bytes() functions. - ** - ** The sqlite3_blob_close() function finalizes the vdbe program, - ** which closes the b-tree cursor and (possibly) commits the - ** transaction. - */ - static const int iLn = VDBE_OFFSET_LINENO(4); - static const VdbeOpList openBlob[] = { - /* {OP_Transaction, 0, 0, 0}, // 0: Inserted separately */ - {OP_TableLock, 0, 0, 0}, /* 1: Acquire a read or write lock */ - /* One of the following two instructions is replaced by an OP_Noop. */ - {OP_OpenRead, 0, 0, 0}, /* 2: Open cursor 0 for reading */ - {OP_OpenWrite, 0, 0, 0}, /* 3: Open cursor 0 for read/write */ - {OP_Variable, 1, 1, 1}, /* 4: Push the rowid to the stack */ - {OP_NotExists, 0, 10, 1}, /* 5: Seek the cursor */ - {OP_Column, 0, 0, 1}, /* 6 */ - {OP_ResultRow, 1, 0, 0}, /* 7 */ - {OP_Goto, 0, 4, 0}, /* 8 */ - {OP_Close, 0, 0, 0}, /* 9 */ - {OP_Halt, 0, 0, 0}, /* 10 */ - }; - int rc = SQLITE_OK; char *zErr = 0; Table *pTab; @@ -79805,6 +83788,8 @@ SQLITE_API int SQLITE_STDCALL sqlite3_blob_open( sqlite3BtreeLeaveAll(db); goto blob_open_out; } + pBlob->pTab = pTab; + pBlob->zDb = db->aDb[sqlite3SchemaToIndex(db, pTab->pSchema)].zName; /* Now search pTab for the exact column. */ for(iCol=0; iColnCol; iCol++) { @@ -79865,45 +83850,78 @@ SQLITE_API int SQLITE_STDCALL sqlite3_blob_open( pBlob->pStmt = (sqlite3_stmt *)sqlite3VdbeCreate(pParse); assert( pBlob->pStmt || db->mallocFailed ); if( pBlob->pStmt ){ + + /* This VDBE program seeks a btree cursor to the identified + ** db/table/row entry. The reason for using a vdbe program instead + ** of writing code to use the b-tree layer directly is that the + ** vdbe program will take advantage of the various transaction, + ** locking and error handling infrastructure built into the vdbe. + ** + ** After seeking the cursor, the vdbe executes an OP_ResultRow. + ** Code external to the Vdbe then "borrows" the b-tree cursor and + ** uses it to implement the blob_read(), blob_write() and + ** blob_bytes() functions. + ** + ** The sqlite3_blob_close() function finalizes the vdbe program, + ** which closes the b-tree cursor and (possibly) commits the + ** transaction. + */ + static const int iLn = VDBE_OFFSET_LINENO(2); + static const VdbeOpList openBlob[] = { + {OP_TableLock, 0, 0, 0}, /* 0: Acquire a read or write lock */ + {OP_OpenRead, 0, 0, 0}, /* 1: Open a cursor */ + {OP_Variable, 1, 1, 0}, /* 2: Move ?1 into reg[1] */ + {OP_NotExists, 0, 7, 1}, /* 3: Seek the cursor */ + {OP_Column, 0, 0, 1}, /* 4 */ + {OP_ResultRow, 1, 0, 0}, /* 5 */ + {OP_Goto, 0, 2, 0}, /* 6 */ + {OP_Close, 0, 0, 0}, /* 7 */ + {OP_Halt, 0, 0, 0}, /* 8 */ + }; Vdbe *v = (Vdbe *)pBlob->pStmt; int iDb = sqlite3SchemaToIndex(db, pTab->pSchema); - + VdbeOp *aOp; sqlite3VdbeAddOp4Int(v, OP_Transaction, iDb, flags, pTab->pSchema->schema_cookie, pTab->pSchema->iGeneration); sqlite3VdbeChangeP5(v, 1); - sqlite3VdbeAddOpList(v, ArraySize(openBlob), openBlob, iLn); + aOp = sqlite3VdbeAddOpList(v, ArraySize(openBlob), openBlob, iLn); /* Make sure a mutex is held on the table to be accessed */ sqlite3VdbeUsesBtree(v, iDb); - /* Configure the OP_TableLock instruction */ + if( db->mallocFailed==0 ){ + assert( aOp!=0 ); + /* Configure the OP_TableLock instruction */ #ifdef SQLITE_OMIT_SHARED_CACHE - sqlite3VdbeChangeToNoop(v, 1); + aOp[0].opcode = OP_Noop; #else - sqlite3VdbeChangeP1(v, 1, iDb); - sqlite3VdbeChangeP2(v, 1, pTab->tnum); - sqlite3VdbeChangeP3(v, 1, flags); - sqlite3VdbeChangeP4(v, 1, pTab->zName, P4_TRANSIENT); + aOp[0].p1 = iDb; + aOp[0].p2 = pTab->tnum; + aOp[0].p3 = flags; + sqlite3VdbeChangeP4(v, 1, pTab->zName, P4_TRANSIENT); + } + if( db->mallocFailed==0 ){ #endif - /* Remove either the OP_OpenWrite or OpenRead. Set the P2 - ** parameter of the other to pTab->tnum. */ - sqlite3VdbeChangeToNoop(v, 3 - flags); - sqlite3VdbeChangeP2(v, 2 + flags, pTab->tnum); - sqlite3VdbeChangeP3(v, 2 + flags, iDb); + /* Remove either the OP_OpenWrite or OpenRead. Set the P2 + ** parameter of the other to pTab->tnum. */ + if( flags ) aOp[1].opcode = OP_OpenWrite; + aOp[1].p2 = pTab->tnum; + aOp[1].p3 = iDb; + + /* Configure the number of columns. Configure the cursor to + ** think that the table has one more column than it really + ** does. An OP_Column to retrieve this imaginary column will + ** always return an SQL NULL. This is useful because it means + ** we can invoke OP_Column to fill in the vdbe cursors type + ** and offset cache without causing any IO. + */ + aOp[1].p4type = P4_INT32; + aOp[1].p4.i = pTab->nCol+1; + aOp[4].p2 = pTab->nCol; - /* Configure the number of columns. Configure the cursor to - ** think that the table has one more column than it really - ** does. An OP_Column to retrieve this imaginary column will - ** always return an SQL NULL. This is useful because it means - ** we can invoke OP_Column to fill in the vdbe cursors type - ** and offset cache without causing any IO. - */ - sqlite3VdbeChangeP4(v, 2+flags, SQLITE_INT_TO_PTR(pTab->nCol+1),P4_INT32); - sqlite3VdbeChangeP2(v, 6, pTab->nCol); - if( !db->mallocFailed ){ pParse->nVar = 1; pParse->nMem = 1; pParse->nTab = 1; @@ -79993,6 +84011,30 @@ static int blobReadWrite( */ assert( db == v->db ); sqlite3BtreeEnterCursor(p->pCsr); + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + if( xCall==sqlite3BtreePutData && db->xPreUpdateCallback ){ + /* If a pre-update hook is registered and this is a write cursor, + ** invoke it here. + ** + ** TODO: The preupdate-hook is passed SQLITE_DELETE, even though this + ** operation should really be an SQLITE_UPDATE. This is probably + ** incorrect, but is convenient because at this point the new.* values + ** are not easily obtainable. And for the sessions module, an + ** SQLITE_UPDATE where the PK columns do not change is handled in the + ** same way as an SQLITE_DELETE (the SQLITE_DELETE code is actually + ** slightly more efficient). Since you cannot write to a PK column + ** using the incremental-blob API, this works. For the sessions module + ** anyhow. + */ + sqlite3_int64 iKey; + sqlite3BtreeKeySize(p->pCsr, &iKey); + sqlite3VdbePreUpdateHook( + v, v->apCsr[0], SQLITE_DELETE, p->zDb, p->pTab, iKey, -1 + ); + } +#endif + rc = xCall(p->pCsr, iOffset+p->iOffset, n, z); sqlite3BtreeLeaveCursor(p->pCsr); if( rc==SQLITE_ABORT ){ @@ -80619,7 +84661,7 @@ static int vdbePmaReadBlob( int nNew = MAX(128, p->nAlloc*2); while( nByte>nNew ) nNew = nNew*2; aNew = sqlite3Realloc(p->aAlloc, nNew); - if( !aNew ) return SQLITE_NOMEM; + if( !aNew ) return SQLITE_NOMEM_BKPT; p->nAlloc = nNew; p->aAlloc = aNew; } @@ -80731,7 +84773,7 @@ static int vdbePmaReaderSeek( int iBuf = pReadr->iReadOff % pgsz; if( pReadr->aBuffer==0 ){ pReadr->aBuffer = (u8*)sqlite3Malloc(pgsz); - if( pReadr->aBuffer==0 ) rc = SQLITE_NOMEM; + if( pReadr->aBuffer==0 ) rc = SQLITE_NOMEM_BKPT; pReadr->nBuffer = pgsz; } if( rc==SQLITE_OK && iBuf ){ @@ -80816,7 +84858,7 @@ static int vdbePmaReaderInit( rc = vdbePmaReaderSeek(pTask, pReadr, pFile, iStart); if( rc==SQLITE_OK ){ - u64 nByte; /* Size of PMA in bytes */ + u64 nByte = 0; /* Size of PMA in bytes */ rc = vdbePmaReadVarint(pReadr, &nByte); pReadr->iEof = pReadr->iReadOff + nByte; *pnByte += nByte; @@ -81010,7 +85052,6 @@ SQLITE_PRIVATE int sqlite3VdbeSorterInit( ){ int pgsz; /* Page size of main database */ int i; /* Used to iterate through aTask[] */ - int mxCache; /* Cache size */ VdbeSorter *pSorter; /* The new sorter */ KeyInfo *pKeyInfo; /* Copy of pCsr->pKeyInfo with db==0 */ int szKeyInfo; /* Size of pCsr->pKeyInfo in bytes */ @@ -81040,13 +85081,14 @@ SQLITE_PRIVATE int sqlite3VdbeSorterInit( #endif assert( pCsr->pKeyInfo && pCsr->pBt==0 ); + assert( pCsr->eCurType==CURTYPE_SORTER ); szKeyInfo = sizeof(KeyInfo) + (pCsr->pKeyInfo->nField-1)*sizeof(CollSeq*); sz = sizeof(VdbeSorter) + nWorker * sizeof(SortSubtask); pSorter = (VdbeSorter*)sqlite3DbMallocZero(db, sz + szKeyInfo); - pCsr->pSorter = pSorter; + pCsr->uc.pSorter = pSorter; if( pSorter==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else{ pSorter->pKeyInfo = pKeyInfo = (KeyInfo*)((u8*)pSorter + sz); memcpy(pKeyInfo, pCsr->pKeyInfo, szKeyInfo); @@ -81057,7 +85099,7 @@ SQLITE_PRIVATE int sqlite3VdbeSorterInit( } pSorter->pgsz = pgsz = sqlite3BtreeGetPageSize(db->aDb[0].pBt); pSorter->nTask = nWorker + 1; - pSorter->iPrev = nWorker-1; + pSorter->iPrev = (u8)(nWorker - 1); pSorter->bUseThreads = (pSorter->nTask>1); pSorter->db = db; for(i=0; inTask; i++){ @@ -81066,11 +85108,20 @@ SQLITE_PRIVATE int sqlite3VdbeSorterInit( } if( !sqlite3TempInMemory(db) ){ + i64 mxCache; /* Cache size in bytes*/ u32 szPma = sqlite3GlobalConfig.szPma; pSorter->mnPmaSize = szPma * pgsz; + mxCache = db->aDb[0].pSchema->cache_size; - if( mxCache<(int)szPma ) mxCache = (int)szPma; - pSorter->mxPmaSize = MIN((i64)mxCache*pgsz, SQLITE_MAX_PMASZ); + if( mxCache<0 ){ + /* A negative cache-size value C indicates that the cache is abs(C) + ** KiB in size. */ + mxCache = mxCache * -1024; + }else{ + mxCache = mxCache * pgsz; + } + mxCache = MIN(mxCache, SQLITE_MAX_PMASZ); + pSorter->mxPmaSize = MAX(pSorter->mnPmaSize, (int)mxCache); /* EVIDENCE-OF: R-26747-61719 When the application provides any amount of ** scratch memory using SQLITE_CONFIG_SCRATCH, SQLite avoids unnecessary @@ -81080,7 +85131,7 @@ SQLITE_PRIVATE int sqlite3VdbeSorterInit( assert( pSorter->iMemory==0 ); pSorter->nMemory = pgsz; pSorter->list.aMemory = (u8*)sqlite3Malloc(pgsz); - if( !pSorter->list.aMemory ) rc = SQLITE_NOMEM; + if( !pSorter->list.aMemory ) rc = SQLITE_NOMEM_BKPT; } } @@ -81328,12 +85379,14 @@ SQLITE_PRIVATE void sqlite3VdbeSorterReset(sqlite3 *db, VdbeSorter *pSorter){ ** Free any cursor components allocated by sqlite3VdbeSorterXXX routines. */ SQLITE_PRIVATE void sqlite3VdbeSorterClose(sqlite3 *db, VdbeCursor *pCsr){ - VdbeSorter *pSorter = pCsr->pSorter; + VdbeSorter *pSorter; + assert( pCsr->eCurType==CURTYPE_SORTER ); + pSorter = pCsr->uc.pSorter; if( pSorter ){ sqlite3VdbeSorterReset(db, pSorter); sqlite3_free(pSorter->list.aMemory); sqlite3DbFree(db, pSorter); - pCsr->pSorter = 0; + pCsr->uc.pSorter = 0; } } @@ -81400,7 +85453,7 @@ static int vdbeSortAllocUnpacked(SortSubtask *pTask){ pTask->pSorter->pKeyInfo, 0, 0, &pFree ); assert( pTask->pUnpacked==(UnpackedRecord*)pFree ); - if( pFree==0 ) return SQLITE_NOMEM; + if( pFree==0 ) return SQLITE_NOMEM_BKPT; pTask->pUnpacked->nField = pTask->pSorter->pKeyInfo->nField; pTask->pUnpacked->errCode = 0; } @@ -81475,7 +85528,7 @@ static int vdbeSorterSort(SortSubtask *pTask, SorterList *pList){ aSlot = (SorterRecord **)sqlite3MallocZero(64 * sizeof(SorterRecord *)); if( !aSlot ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } while( p ){ @@ -81525,7 +85578,7 @@ static void vdbePmaWriterInit( memset(p, 0, sizeof(PmaWriter)); p->aBuffer = (u8*)sqlite3Malloc(nBuf); if( !p->aBuffer ){ - p->eFWErr = SQLITE_NOMEM; + p->eFWErr = SQLITE_NOMEM_BKPT; }else{ p->iBufEnd = p->iBufStart = (iStart % nBuf); p->iWriteOff = iStart - p->iBufStart; @@ -81813,7 +85866,7 @@ static int vdbeSorterFlushPMA(VdbeSorter *pSorter){ pSorter->nMemory = sqlite3MallocSize(aMem); }else if( pSorter->list.aMemory ){ pSorter->list.aMemory = sqlite3Malloc(pSorter->nMemory); - if( !pSorter->list.aMemory ) return SQLITE_NOMEM; + if( !pSorter->list.aMemory ) return SQLITE_NOMEM_BKPT; } rc = vdbeSorterCreateThread(pTask, vdbeSorterFlushThread, pCtx); @@ -81831,15 +85884,16 @@ SQLITE_PRIVATE int sqlite3VdbeSorterWrite( const VdbeCursor *pCsr, /* Sorter cursor */ Mem *pVal /* Memory cell containing record */ ){ - VdbeSorter *pSorter = pCsr->pSorter; + VdbeSorter *pSorter; int rc = SQLITE_OK; /* Return Code */ SorterRecord *pNew; /* New list element */ - int bFlush; /* True to flush contents of memory to PMA */ int nReq; /* Bytes of memory required */ int nPMA; /* Bytes of PMA space required */ int t; /* serial type of first record field */ + assert( pCsr->eCurType==CURTYPE_SORTER ); + pSorter = pCsr->uc.pSorter; getVarint32((const u8*)&pVal->z[1], t); if( t>0 && t<10 && t!=7 ){ pSorter->typeMask &= SORTER_TYPE_INTEGER; @@ -81896,27 +85950,28 @@ SQLITE_PRIVATE int sqlite3VdbeSorterWrite( if( nMin>pSorter->nMemory ){ u8 *aNew; + int iListOff = (u8*)pSorter->list.pList - pSorter->list.aMemory; int nNew = pSorter->nMemory * 2; while( nNew < nMin ) nNew = nNew*2; if( nNew > pSorter->mxPmaSize ) nNew = pSorter->mxPmaSize; if( nNew < nMin ) nNew = nMin; aNew = sqlite3Realloc(pSorter->list.aMemory, nNew); - if( !aNew ) return SQLITE_NOMEM; - pSorter->list.pList = (SorterRecord*)( - aNew + ((u8*)pSorter->list.pList - pSorter->list.aMemory) - ); + if( !aNew ) return SQLITE_NOMEM_BKPT; + pSorter->list.pList = (SorterRecord*)&aNew[iListOff]; pSorter->list.aMemory = aNew; pSorter->nMemory = nNew; } pNew = (SorterRecord*)&pSorter->list.aMemory[pSorter->iMemory]; pSorter->iMemory += ROUND8(nReq); - pNew->u.iNext = (int)((u8*)(pSorter->list.pList) - pSorter->list.aMemory); + if( pSorter->list.pList ){ + pNew->u.iNext = (int)((u8*)(pSorter->list.pList) - pSorter->list.aMemory); + } }else{ pNew = (SorterRecord *)sqlite3Malloc(nReq); if( pNew==0 ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pNew->u.pNext = pSorter->list.pList; } @@ -82063,7 +86118,7 @@ static int vdbeIncrMergerNew( pTask->file2.iEof += pIncr->mxSz; }else{ vdbeMergeEngineFree(pMerger); - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } return rc; } @@ -82368,10 +86423,10 @@ static int vdbeMergeEngineLevel0( int rc = SQLITE_OK; *ppOut = pNew = vdbeMergeEngineNew(nPMA); - if( pNew==0 ) rc = SQLITE_NOMEM; + if( pNew==0 ) rc = SQLITE_NOMEM_BKPT; for(i=0; iaReadr[i]; rc = vdbePmaReaderInit(pTask, &pTask->file, iOff, pReadr, &nDummy); iOff = pReadr->iEof; @@ -82439,7 +86494,7 @@ static int vdbeSorterAddToTree( if( pReadr->pIncr==0 ){ MergeEngine *pNew = vdbeMergeEngineNew(SORTER_MAX_MERGE_COUNT); if( pNew==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else{ rc = vdbeIncrMergerNew(pTask, pNew, &pReadr->pIncr); } @@ -82484,7 +86539,7 @@ static int vdbeSorterMergeTreeBuild( assert( pSorter->bUseThreads || pSorter->nTask==1 ); if( pSorter->nTask>1 ){ pMain = vdbeMergeEngineNew(pSorter->nTask); - if( pMain==0 ) rc = SQLITE_NOMEM; + if( pMain==0 ) rc = SQLITE_NOMEM_BKPT; } #endif @@ -82502,7 +86557,7 @@ static int vdbeSorterMergeTreeBuild( int i; int iSeq = 0; pRoot = vdbeMergeEngineNew(SORTER_MAX_MERGE_COUNT); - if( pRoot==0 ) rc = SQLITE_NOMEM; + if( pRoot==0 ) rc = SQLITE_NOMEM_BKPT; for(i=0; inPMA && rc==SQLITE_OK; i += SORTER_MAX_MERGE_COUNT){ MergeEngine *pMerger = 0; /* New level-0 PMA merger */ int nReader; /* Number of level-0 PMAs to merge */ @@ -82573,7 +86628,7 @@ static int vdbeSorterSetupMerge(VdbeSorter *pSorter){ if( rc==SQLITE_OK ){ pReadr = (PmaReader*)sqlite3DbMallocZero(db, sizeof(PmaReader)); pSorter->pReader = pReadr; - if( pReadr==0 ) rc = SQLITE_NOMEM; + if( pReadr==0 ) rc = SQLITE_NOMEM_BKPT; } if( rc==SQLITE_OK ){ rc = vdbeIncrMergerNew(pLast, pMain, &pReadr->pIncr); @@ -82631,9 +86686,11 @@ static int vdbeSorterSetupMerge(VdbeSorter *pSorter){ ** in sorted order. */ SQLITE_PRIVATE int sqlite3VdbeSorterRewind(const VdbeCursor *pCsr, int *pbEof){ - VdbeSorter *pSorter = pCsr->pSorter; + VdbeSorter *pSorter; int rc = SQLITE_OK; /* Return code */ + assert( pCsr->eCurType==CURTYPE_SORTER ); + pSorter = pCsr->uc.pSorter; assert( pSorter ); /* If no data has been written to disk, then do not do so now. Instead, @@ -82677,9 +86734,11 @@ SQLITE_PRIVATE int sqlite3VdbeSorterRewind(const VdbeCursor *pCsr, int *pbEof){ ** Advance to the next element in the sorter. */ SQLITE_PRIVATE int sqlite3VdbeSorterNext(sqlite3 *db, const VdbeCursor *pCsr, int *pbEof){ - VdbeSorter *pSorter = pCsr->pSorter; + VdbeSorter *pSorter; int rc; /* Return code */ + assert( pCsr->eCurType==CURTYPE_SORTER ); + pSorter = pCsr->uc.pSorter; assert( pSorter->bUsePMA || (pSorter->pReader==0 && pSorter->pMerger==0) ); if( pSorter->bUsePMA ){ assert( pSorter->pReader==0 || pSorter->pMerger==0 ); @@ -82739,12 +86798,14 @@ static void *vdbeSorterRowkey( ** Copy the current sorter key into the memory cell pOut. */ SQLITE_PRIVATE int sqlite3VdbeSorterRowkey(const VdbeCursor *pCsr, Mem *pOut){ - VdbeSorter *pSorter = pCsr->pSorter; + VdbeSorter *pSorter; void *pKey; int nKey; /* Sorter key to copy into pOut */ + assert( pCsr->eCurType==CURTYPE_SORTER ); + pSorter = pCsr->uc.pSorter; pKey = vdbeSorterRowkey(pSorter, &nKey); if( sqlite3VdbeMemClearAndResize(pOut, nKey) ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pOut->n = nKey; MemSetTypeFlag(pOut, MEM_Blob); @@ -82775,17 +86836,21 @@ SQLITE_PRIVATE int sqlite3VdbeSorterCompare( int nKeyCol, /* Compare this many columns */ int *pRes /* OUT: Result of comparison */ ){ - VdbeSorter *pSorter = pCsr->pSorter; - UnpackedRecord *r2 = pSorter->pUnpacked; - KeyInfo *pKeyInfo = pCsr->pKeyInfo; + VdbeSorter *pSorter; + UnpackedRecord *r2; + KeyInfo *pKeyInfo; int i; void *pKey; int nKey; /* Sorter key to compare pVal with */ + assert( pCsr->eCurType==CURTYPE_SORTER ); + pSorter = pCsr->uc.pSorter; + r2 = pSorter->pUnpacked; + pKeyInfo = pCsr->pKeyInfo; if( r2==0 ){ char *p; r2 = pSorter->pUnpacked = sqlite3VdbeAllocUnpackedRecord(pKeyInfo,0,0,&p); assert( pSorter->pUnpacked==(UnpackedRecord*)p ); - if( r2==0 ) return SQLITE_NOMEM; + if( r2==0 ) return SQLITE_NOMEM_BKPT; r2->nField = nKeyCol; } assert( r2->nField==nKeyCol ); @@ -82804,265 +86869,6 @@ SQLITE_PRIVATE int sqlite3VdbeSorterCompare( } /************** End of vdbesort.c ********************************************/ -/************** Begin file journal.c *****************************************/ -/* -** 2007 August 22 -** -** The author disclaims copyright to this source code. In place of -** a legal notice, here is a blessing: -** -** May you do good and not evil. -** May you find forgiveness for yourself and forgive others. -** May you share freely, never taking more than you give. -** -************************************************************************* -** -** This file implements a special kind of sqlite3_file object used -** by SQLite to create journal files if the atomic-write optimization -** is enabled. -** -** The distinctive characteristic of this sqlite3_file is that the -** actual on disk file is created lazily. When the file is created, -** the caller specifies a buffer size for an in-memory buffer to -** be used to service read() and write() requests. The actual file -** on disk is not created or populated until either: -** -** 1) The in-memory representation grows too large for the allocated -** buffer, or -** 2) The sqlite3JournalCreate() function is called. -*/ -#ifdef SQLITE_ENABLE_ATOMIC_WRITE -/* #include "sqliteInt.h" */ - - -/* -** A JournalFile object is a subclass of sqlite3_file used by -** as an open file handle for journal files. -*/ -struct JournalFile { - sqlite3_io_methods *pMethod; /* I/O methods on journal files */ - int nBuf; /* Size of zBuf[] in bytes */ - char *zBuf; /* Space to buffer journal writes */ - int iSize; /* Amount of zBuf[] currently used */ - int flags; /* xOpen flags */ - sqlite3_vfs *pVfs; /* The "real" underlying VFS */ - sqlite3_file *pReal; /* The "real" underlying file descriptor */ - const char *zJournal; /* Name of the journal file */ -}; -typedef struct JournalFile JournalFile; - -/* -** If it does not already exists, create and populate the on-disk file -** for JournalFile p. -*/ -static int createFile(JournalFile *p){ - int rc = SQLITE_OK; - if( !p->pReal ){ - sqlite3_file *pReal = (sqlite3_file *)&p[1]; - rc = sqlite3OsOpen(p->pVfs, p->zJournal, pReal, p->flags, 0); - if( rc==SQLITE_OK ){ - p->pReal = pReal; - if( p->iSize>0 ){ - assert(p->iSize<=p->nBuf); - rc = sqlite3OsWrite(p->pReal, p->zBuf, p->iSize, 0); - } - if( rc!=SQLITE_OK ){ - /* If an error occurred while writing to the file, close it before - ** returning. This way, SQLite uses the in-memory journal data to - ** roll back changes made to the internal page-cache before this - ** function was called. */ - sqlite3OsClose(pReal); - p->pReal = 0; - } - } - } - return rc; -} - -/* -** Close the file. -*/ -static int jrnlClose(sqlite3_file *pJfd){ - JournalFile *p = (JournalFile *)pJfd; - if( p->pReal ){ - sqlite3OsClose(p->pReal); - } - sqlite3_free(p->zBuf); - return SQLITE_OK; -} - -/* -** Read data from the file. -*/ -static int jrnlRead( - sqlite3_file *pJfd, /* The journal file from which to read */ - void *zBuf, /* Put the results here */ - int iAmt, /* Number of bytes to read */ - sqlite_int64 iOfst /* Begin reading at this offset */ -){ - int rc = SQLITE_OK; - JournalFile *p = (JournalFile *)pJfd; - if( p->pReal ){ - rc = sqlite3OsRead(p->pReal, zBuf, iAmt, iOfst); - }else if( (iAmt+iOfst)>p->iSize ){ - rc = SQLITE_IOERR_SHORT_READ; - }else{ - memcpy(zBuf, &p->zBuf[iOfst], iAmt); - } - return rc; -} - -/* -** Write data to the file. -*/ -static int jrnlWrite( - sqlite3_file *pJfd, /* The journal file into which to write */ - const void *zBuf, /* Take data to be written from here */ - int iAmt, /* Number of bytes to write */ - sqlite_int64 iOfst /* Begin writing at this offset into the file */ -){ - int rc = SQLITE_OK; - JournalFile *p = (JournalFile *)pJfd; - if( !p->pReal && (iOfst+iAmt)>p->nBuf ){ - rc = createFile(p); - } - if( rc==SQLITE_OK ){ - if( p->pReal ){ - rc = sqlite3OsWrite(p->pReal, zBuf, iAmt, iOfst); - }else{ - memcpy(&p->zBuf[iOfst], zBuf, iAmt); - if( p->iSize<(iOfst+iAmt) ){ - p->iSize = (iOfst+iAmt); - } - } - } - return rc; -} - -/* -** Truncate the file. -*/ -static int jrnlTruncate(sqlite3_file *pJfd, sqlite_int64 size){ - int rc = SQLITE_OK; - JournalFile *p = (JournalFile *)pJfd; - if( p->pReal ){ - rc = sqlite3OsTruncate(p->pReal, size); - }else if( sizeiSize ){ - p->iSize = size; - } - return rc; -} - -/* -** Sync the file. -*/ -static int jrnlSync(sqlite3_file *pJfd, int flags){ - int rc; - JournalFile *p = (JournalFile *)pJfd; - if( p->pReal ){ - rc = sqlite3OsSync(p->pReal, flags); - }else{ - rc = SQLITE_OK; - } - return rc; -} - -/* -** Query the size of the file in bytes. -*/ -static int jrnlFileSize(sqlite3_file *pJfd, sqlite_int64 *pSize){ - int rc = SQLITE_OK; - JournalFile *p = (JournalFile *)pJfd; - if( p->pReal ){ - rc = sqlite3OsFileSize(p->pReal, pSize); - }else{ - *pSize = (sqlite_int64) p->iSize; - } - return rc; -} - -/* -** Table of methods for JournalFile sqlite3_file object. -*/ -static struct sqlite3_io_methods JournalFileMethods = { - 1, /* iVersion */ - jrnlClose, /* xClose */ - jrnlRead, /* xRead */ - jrnlWrite, /* xWrite */ - jrnlTruncate, /* xTruncate */ - jrnlSync, /* xSync */ - jrnlFileSize, /* xFileSize */ - 0, /* xLock */ - 0, /* xUnlock */ - 0, /* xCheckReservedLock */ - 0, /* xFileControl */ - 0, /* xSectorSize */ - 0, /* xDeviceCharacteristics */ - 0, /* xShmMap */ - 0, /* xShmLock */ - 0, /* xShmBarrier */ - 0 /* xShmUnmap */ -}; - -/* -** Open a journal file. -*/ -SQLITE_PRIVATE int sqlite3JournalOpen( - sqlite3_vfs *pVfs, /* The VFS to use for actual file I/O */ - const char *zName, /* Name of the journal file */ - sqlite3_file *pJfd, /* Preallocated, blank file handle */ - int flags, /* Opening flags */ - int nBuf /* Bytes buffered before opening the file */ -){ - JournalFile *p = (JournalFile *)pJfd; - memset(p, 0, sqlite3JournalSize(pVfs)); - if( nBuf>0 ){ - p->zBuf = sqlite3MallocZero(nBuf); - if( !p->zBuf ){ - return SQLITE_NOMEM; - } - }else{ - return sqlite3OsOpen(pVfs, zName, pJfd, flags, 0); - } - p->pMethod = &JournalFileMethods; - p->nBuf = nBuf; - p->flags = flags; - p->zJournal = zName; - p->pVfs = pVfs; - return SQLITE_OK; -} - -/* -** If the argument p points to a JournalFile structure, and the underlying -** file has not yet been created, create it now. -*/ -SQLITE_PRIVATE int sqlite3JournalCreate(sqlite3_file *p){ - if( p->pMethods!=&JournalFileMethods ){ - return SQLITE_OK; - } - return createFile((JournalFile *)p); -} - -/* -** The file-handle passed as the only argument is guaranteed to be an open -** file. It may or may not be of class JournalFile. If the file is a -** JournalFile, and the underlying file on disk has not yet been opened, -** return 0. Otherwise, return 1. -*/ -SQLITE_PRIVATE int sqlite3JournalExists(sqlite3_file *p){ - return (p->pMethods!=&JournalFileMethods || ((JournalFile *)p)->pReal!=0); -} - -/* -** Return the number of bytes required to store a JournalFile that uses vfs -** pVfs to create the underlying on-disk files. -*/ -SQLITE_PRIVATE int sqlite3JournalSize(sqlite3_vfs *pVfs){ - return (pVfs->szOsFile+sizeof(JournalFile)); -} -#endif - -/************** End of journal.c *********************************************/ /************** Begin file memjournal.c **************************************/ /* ** 2008 October 7 @@ -83079,6 +86885,15 @@ SQLITE_PRIVATE int sqlite3JournalSize(sqlite3_vfs *pVfs){ ** This file contains code use to implement an in-memory rollback journal. ** The in-memory rollback journal is used to journal transactions for ** ":memory:" databases and when the journal_mode=MEMORY pragma is used. +** +** Update: The in-memory journal is also used to temporarily cache +** smaller journals that are not critical for power-loss recovery. +** For example, statement journals that are not too big will be held +** entirely in memory, thus reducing the number of file I/O calls, and +** more importantly, reducing temporary file creation events. If these +** journals become too large for memory, they are spilled to disk. But +** in the common case, they are usually small and no file I/O needs to +** occur. */ /* #include "sqliteInt.h" */ @@ -83087,24 +86902,28 @@ typedef struct MemJournal MemJournal; typedef struct FilePoint FilePoint; typedef struct FileChunk FileChunk; -/* Space to hold the rollback journal is allocated in increments of -** this many bytes. -** -** The size chosen is a little less than a power of two. That way, -** the FileChunk object will have a size that almost exactly fills -** a power-of-two allocation. This minimizes wasted space in power-of-two -** memory allocators. -*/ -#define JOURNAL_CHUNKSIZE ((int)(1024-sizeof(FileChunk*))) - /* ** The rollback journal is composed of a linked list of these structures. +** +** The zChunk array is always at least 8 bytes in size - usually much more. +** Its actual size is stored in the MemJournal.nChunkSize variable. */ struct FileChunk { FileChunk *pNext; /* Next chunk in the journal */ - u8 zChunk[JOURNAL_CHUNKSIZE]; /* Content of this chunk */ + u8 zChunk[8]; /* Content of this chunk */ }; +/* +** By default, allocate this many bytes of memory for each FileChunk object. +*/ +#define MEMJOURNAL_DFLT_FILECHUNKSIZE 1024 + +/* +** For chunk size nChunkSize, return the number of bytes that should +** be allocated for each FileChunk structure. +*/ +#define fileChunkSize(nChunkSize) (sizeof(FileChunk) + ((nChunkSize)-8)) + /* ** An instance of this object serves as a cursor into the rollback journal. ** The cursor can be either for reading or writing. @@ -83115,14 +86934,22 @@ struct FilePoint { }; /* -** This subclass is a subclass of sqlite3_file. Each open memory-journal +** This structure is a subclass of sqlite3_file. Each open memory-journal ** is an instance of this class. */ struct MemJournal { - sqlite3_io_methods *pMethod; /* Parent class. MUST BE FIRST */ + const sqlite3_io_methods *pMethod; /* Parent class. MUST BE FIRST */ + int nChunkSize; /* In-memory chunk-size */ + + int nSpill; /* Bytes of data before flushing */ + int nSize; /* Bytes of data currently in memory */ FileChunk *pFirst; /* Head of in-memory chunk-list */ FilePoint endpoint; /* Pointer to the end of the file */ FilePoint readpoint; /* Pointer to the end of the last xRead() */ + + int flags; /* xOpen flags */ + sqlite3_vfs *pVfs; /* The "real" underlying VFS */ + const char *zJournal; /* Name of the journal file */ }; /* @@ -83141,36 +86968,94 @@ static int memjrnlRead( int iChunkOffset; FileChunk *pChunk; - /* SQLite never tries to read past the end of a rollback journal file */ - assert( iOfst+iAmt<=p->endpoint.iOffset ); +#ifdef SQLITE_ENABLE_ATOMIC_WRITE + if( (iAmt+iOfst)>p->endpoint.iOffset ){ + return SQLITE_IOERR_SHORT_READ; + } +#endif + assert( (iAmt+iOfst)<=p->endpoint.iOffset ); + assert( p->readpoint.iOffset==0 || p->readpoint.pChunk!=0 ); if( p->readpoint.iOffset!=iOfst || iOfst==0 ){ sqlite3_int64 iOff = 0; for(pChunk=p->pFirst; - ALWAYS(pChunk) && (iOff+JOURNAL_CHUNKSIZE)<=iOfst; + ALWAYS(pChunk) && (iOff+p->nChunkSize)<=iOfst; pChunk=pChunk->pNext ){ - iOff += JOURNAL_CHUNKSIZE; + iOff += p->nChunkSize; } }else{ pChunk = p->readpoint.pChunk; + assert( pChunk!=0 ); } - iChunkOffset = (int)(iOfst%JOURNAL_CHUNKSIZE); + iChunkOffset = (int)(iOfst%p->nChunkSize); do { - int iSpace = JOURNAL_CHUNKSIZE - iChunkOffset; - int nCopy = MIN(nRead, (JOURNAL_CHUNKSIZE - iChunkOffset)); - memcpy(zOut, &pChunk->zChunk[iChunkOffset], nCopy); + int iSpace = p->nChunkSize - iChunkOffset; + int nCopy = MIN(nRead, (p->nChunkSize - iChunkOffset)); + memcpy(zOut, (u8*)pChunk->zChunk + iChunkOffset, nCopy); zOut += nCopy; nRead -= iSpace; iChunkOffset = 0; } while( nRead>=0 && (pChunk=pChunk->pNext)!=0 && nRead>0 ); - p->readpoint.iOffset = iOfst+iAmt; + p->readpoint.iOffset = pChunk ? iOfst+iAmt : 0; p->readpoint.pChunk = pChunk; return SQLITE_OK; } +/* +** Free the list of FileChunk structures headed at MemJournal.pFirst. +*/ +static void memjrnlFreeChunks(MemJournal *p){ + FileChunk *pIter; + FileChunk *pNext; + for(pIter=p->pFirst; pIter; pIter=pNext){ + pNext = pIter->pNext; + sqlite3_free(pIter); + } + p->pFirst = 0; +} + +/* +** Flush the contents of memory to a real file on disk. +*/ +static int memjrnlCreateFile(MemJournal *p){ + int rc; + sqlite3_file *pReal = (sqlite3_file*)p; + MemJournal copy = *p; + + memset(p, 0, sizeof(MemJournal)); + rc = sqlite3OsOpen(copy.pVfs, copy.zJournal, pReal, copy.flags, 0); + if( rc==SQLITE_OK ){ + int nChunk = copy.nChunkSize; + i64 iOff = 0; + FileChunk *pIter; + for(pIter=copy.pFirst; pIter; pIter=pIter->pNext){ + if( iOff + nChunk > copy.endpoint.iOffset ){ + nChunk = copy.endpoint.iOffset - iOff; + } + rc = sqlite3OsWrite(pReal, (u8*)pIter->zChunk, nChunk, iOff); + if( rc ) break; + iOff += nChunk; + } + if( rc==SQLITE_OK ){ + /* No error has occurred. Free the in-memory buffers. */ + memjrnlFreeChunks(©); + } + } + if( rc!=SQLITE_OK ){ + /* If an error occurred while creating or writing to the file, restore + ** the original before returning. This way, SQLite uses the in-memory + ** journal data to roll back changes made to the internal page-cache + ** before this function was called. */ + sqlite3OsClose(pReal); + *p = copy; + } + return rc; +} + + /* ** Write data to the file. */ @@ -83184,38 +87069,62 @@ static int memjrnlWrite( int nWrite = iAmt; u8 *zWrite = (u8 *)zBuf; - /* An in-memory journal file should only ever be appended to. Random - ** access writes are not required by sqlite. - */ - assert( iOfst==p->endpoint.iOffset ); - UNUSED_PARAMETER(iOfst); - - while( nWrite>0 ){ - FileChunk *pChunk = p->endpoint.pChunk; - int iChunkOffset = (int)(p->endpoint.iOffset%JOURNAL_CHUNKSIZE); - int iSpace = MIN(nWrite, JOURNAL_CHUNKSIZE - iChunkOffset); - - if( iChunkOffset==0 ){ - /* New chunk is required to extend the file. */ - FileChunk *pNew = sqlite3_malloc(sizeof(FileChunk)); - if( !pNew ){ - return SQLITE_IOERR_NOMEM; - } - pNew->pNext = 0; - if( pChunk ){ - assert( p->pFirst ); - pChunk->pNext = pNew; - }else{ - assert( !p->pFirst ); - p->pFirst = pNew; - } - p->endpoint.pChunk = pNew; + /* If the file should be created now, create it and write the new data + ** into the file on disk. */ + if( p->nSpill>0 && (iAmt+iOfst)>p->nSpill ){ + int rc = memjrnlCreateFile(p); + if( rc==SQLITE_OK ){ + rc = sqlite3OsWrite(pJfd, zBuf, iAmt, iOfst); } + return rc; + } - memcpy(&p->endpoint.pChunk->zChunk[iChunkOffset], zWrite, iSpace); - zWrite += iSpace; - nWrite -= iSpace; - p->endpoint.iOffset += iSpace; + /* If the contents of this write should be stored in memory */ + else{ + /* An in-memory journal file should only ever be appended to. Random + ** access writes are not required. The only exception to this is when + ** the in-memory journal is being used by a connection using the + ** atomic-write optimization. In this case the first 28 bytes of the + ** journal file may be written as part of committing the transaction. */ + assert( iOfst==p->endpoint.iOffset || iOfst==0 ); +#ifdef SQLITE_ENABLE_ATOMIC_WRITE + if( iOfst==0 && p->pFirst ){ + assert( p->nChunkSize>iAmt ); + memcpy((u8*)p->pFirst->zChunk, zBuf, iAmt); + }else +#else + assert( iOfst>0 || p->pFirst==0 ); +#endif + { + while( nWrite>0 ){ + FileChunk *pChunk = p->endpoint.pChunk; + int iChunkOffset = (int)(p->endpoint.iOffset%p->nChunkSize); + int iSpace = MIN(nWrite, p->nChunkSize - iChunkOffset); + + if( iChunkOffset==0 ){ + /* New chunk is required to extend the file. */ + FileChunk *pNew = sqlite3_malloc(fileChunkSize(p->nChunkSize)); + if( !pNew ){ + return SQLITE_IOERR_NOMEM_BKPT; + } + pNew->pNext = 0; + if( pChunk ){ + assert( p->pFirst ); + pChunk->pNext = pNew; + }else{ + assert( !p->pFirst ); + p->pFirst = pNew; + } + p->endpoint.pChunk = pNew; + } + + memcpy((u8*)p->endpoint.pChunk->zChunk + iChunkOffset, zWrite, iSpace); + zWrite += iSpace; + nWrite -= iSpace; + p->endpoint.iOffset += iSpace; + } + p->nSize = iAmt + iOfst; + } } return SQLITE_OK; @@ -83223,19 +87132,21 @@ static int memjrnlWrite( /* ** Truncate the file. +** +** If the journal file is already on disk, truncate it there. Or, if it +** is still in main memory but is being truncated to zero bytes in size, +** ignore */ static int memjrnlTruncate(sqlite3_file *pJfd, sqlite_int64 size){ MemJournal *p = (MemJournal *)pJfd; - FileChunk *pChunk; - assert(size==0); - UNUSED_PARAMETER(size); - pChunk = p->pFirst; - while( pChunk ){ - FileChunk *pTmp = pChunk; - pChunk = pChunk->pNext; - sqlite3_free(pTmp); + if( ALWAYS(size==0) ){ + memjrnlFreeChunks(p); + p->nSize = 0; + p->endpoint.pChunk = 0; + p->endpoint.iOffset = 0; + p->readpoint.pChunk = 0; + p->readpoint.iOffset = 0; } - sqlite3MemJournalOpen(pJfd); return SQLITE_OK; } @@ -83243,21 +87154,19 @@ static int memjrnlTruncate(sqlite3_file *pJfd, sqlite_int64 size){ ** Close the file. */ static int memjrnlClose(sqlite3_file *pJfd){ - memjrnlTruncate(pJfd, 0); + MemJournal *p = (MemJournal *)pJfd; + memjrnlFreeChunks(p); return SQLITE_OK; } - /* ** Sync the file. ** -** Syncing an in-memory journal is a no-op. And, in fact, this routine -** is never called in a working implementation. This implementation -** exists purely as a contingency, in case some malfunction in some other -** part of SQLite causes Sync to be called by mistake. +** If the real file has been created, call its xSync method. Otherwise, +** syncing an in-memory journal is a no-op. */ -static int memjrnlSync(sqlite3_file *NotUsed, int NotUsed2){ - UNUSED_PARAMETER2(NotUsed, NotUsed2); +static int memjrnlSync(sqlite3_file *pJfd, int flags){ + UNUSED_PARAMETER2(pJfd, flags); return SQLITE_OK; } @@ -83296,28 +87205,88 @@ static const struct sqlite3_io_methods MemJournalMethods = { }; /* -** Open a journal file. +** Open a journal file. +** +** The behaviour of the journal file depends on the value of parameter +** nSpill. If nSpill is 0, then the journal file is always create and +** accessed using the underlying VFS. If nSpill is less than zero, then +** all content is always stored in main-memory. Finally, if nSpill is a +** positive value, then the journal file is initially created in-memory +** but may be flushed to disk later on. In this case the journal file is +** flushed to disk either when it grows larger than nSpill bytes in size, +** or when sqlite3JournalCreate() is called. */ -SQLITE_PRIVATE void sqlite3MemJournalOpen(sqlite3_file *pJfd){ - MemJournal *p = (MemJournal *)pJfd; - assert( EIGHT_BYTE_ALIGNMENT(p) ); - memset(p, 0, sqlite3MemJournalSize()); - p->pMethod = (sqlite3_io_methods*)&MemJournalMethods; +SQLITE_PRIVATE int sqlite3JournalOpen( + sqlite3_vfs *pVfs, /* The VFS to use for actual file I/O */ + const char *zName, /* Name of the journal file */ + sqlite3_file *pJfd, /* Preallocated, blank file handle */ + int flags, /* Opening flags */ + int nSpill /* Bytes buffered before opening the file */ +){ + MemJournal *p = (MemJournal*)pJfd; + + /* Zero the file-handle object. If nSpill was passed zero, initialize + ** it using the sqlite3OsOpen() function of the underlying VFS. In this + ** case none of the code in this module is executed as a result of calls + ** made on the journal file-handle. */ + memset(p, 0, sizeof(MemJournal)); + if( nSpill==0 ){ + return sqlite3OsOpen(pVfs, zName, pJfd, flags, 0); + } + + if( nSpill>0 ){ + p->nChunkSize = nSpill; + }else{ + p->nChunkSize = 8 + MEMJOURNAL_DFLT_FILECHUNKSIZE - sizeof(FileChunk); + assert( MEMJOURNAL_DFLT_FILECHUNKSIZE==fileChunkSize(p->nChunkSize) ); + } + + p->pMethod = (const sqlite3_io_methods*)&MemJournalMethods; + p->nSpill = nSpill; + p->flags = flags; + p->zJournal = zName; + p->pVfs = pVfs; + return SQLITE_OK; } /* -** Return true if the file-handle passed as an argument is -** an in-memory journal +** Open an in-memory journal file. */ -SQLITE_PRIVATE int sqlite3IsMemJournal(sqlite3_file *pJfd){ - return pJfd->pMethods==&MemJournalMethods; +SQLITE_PRIVATE void sqlite3MemJournalOpen(sqlite3_file *pJfd){ + sqlite3JournalOpen(0, 0, pJfd, 0, -1); +} + +#ifdef SQLITE_ENABLE_ATOMIC_WRITE +/* +** If the argument p points to a MemJournal structure that is not an +** in-memory-only journal file (i.e. is one that was opened with a +ve +** nSpill parameter), and the underlying file has not yet been created, +** create it now. +*/ +SQLITE_PRIVATE int sqlite3JournalCreate(sqlite3_file *p){ + int rc = SQLITE_OK; + if( p->pMethods==&MemJournalMethods && ((MemJournal*)p)->nSpill>0 ){ + rc = memjrnlCreateFile((MemJournal*)p); + } + return rc; +} +#endif + +/* +** The file-handle passed as the only argument is open on a journal file. +** Return true if this "journal file" is currently stored in heap memory, +** or false otherwise. +*/ +SQLITE_PRIVATE int sqlite3JournalIsInMemory(sqlite3_file *p){ + return p->pMethods==&MemJournalMethods; } /* -** Return the number of bytes required to store a MemJournal file descriptor. +** Return the number of bytes required to store a JournalFile that uses vfs +** pVfs to create the underlying on-disk files. */ -SQLITE_PRIVATE int sqlite3MemJournalSize(void){ - return sizeof(MemJournal); +SQLITE_PRIVATE int sqlite3JournalSize(sqlite3_vfs *pVfs){ + return MAX(pVfs->szOsFile, (int)sizeof(MemJournal)); } /************** End of memjournal.c ******************************************/ @@ -83360,9 +87329,8 @@ SQLITE_PRIVATE int sqlite3MemJournalSize(void){ ** The return value from this routine is WRC_Abort to abandon the tree walk ** and WRC_Continue to continue. */ -SQLITE_PRIVATE int sqlite3WalkExpr(Walker *pWalker, Expr *pExpr){ +static SQLITE_NOINLINE int walkExpr(Walker *pWalker, Expr *pExpr){ int rc; - if( pExpr==0 ) return WRC_Continue; testcase( ExprHasProperty(pExpr, EP_TokenOnly) ); testcase( ExprHasProperty(pExpr, EP_Reduced) ); rc = pWalker->xExprCallback(pWalker, pExpr); @@ -83378,6 +87346,9 @@ SQLITE_PRIVATE int sqlite3WalkExpr(Walker *pWalker, Expr *pExpr){ } return rc & WRC_Abort; } +SQLITE_PRIVATE int sqlite3WalkExpr(Walker *pWalker, Expr *pExpr){ + return pExpr ? walkExpr(pWalker,pExpr) : WRC_Continue; +} /* ** Call sqlite3WalkExpr() for every expression in list p or until @@ -83814,7 +87785,6 @@ static int lookupName( } if( iCol>=pTab->nCol && sqlite3IsRowid(zCol) && VisibleRowid(pTab) ){ /* IMP: R-51414-32910 */ - /* IMP: R-44911-55124 */ iCol = -1; } if( iColnCol ){ @@ -83849,7 +87819,7 @@ static int lookupName( && VisibleRowid(pMatch->pTab) ){ cnt = 1; - pExpr->iColumn = -1; /* IMP: R-44911-55124 */ + pExpr->iColumn = -1; pExpr->affinity = SQLITE_AFF_INTEGER; } @@ -84143,16 +88113,16 @@ static int resolveExprStep(Walker *pWalker, Expr *pExpr){ notValid(pParse, pNC, "functions", NC_PartIdx); zId = pExpr->u.zToken; nId = sqlite3Strlen30(zId); - pDef = sqlite3FindFunction(pParse->db, zId, nId, n, enc, 0); + pDef = sqlite3FindFunction(pParse->db, zId, n, enc, 0); if( pDef==0 ){ - pDef = sqlite3FindFunction(pParse->db, zId, nId, -2, enc, 0); + pDef = sqlite3FindFunction(pParse->db, zId, -2, enc, 0); if( pDef==0 ){ no_such_func = 1; }else{ wrong_num_args = 1; } }else{ - is_agg = pDef->xFunc==0; + is_agg = pDef->xFinalize!=0; if( pDef->funcFlags & SQLITE_FUNC_UNLIKELY ){ ExprSetProperty(pExpr, EP_Unlikely|EP_Skip); if( n==2 ){ @@ -84250,6 +88220,7 @@ static int resolveExprStep(Walker *pWalker, Expr *pExpr){ assert( pNC->nRef>=nRef ); if( nRef!=pNC->nRef ){ ExprSetProperty(pExpr, EP_VarSelect); + pNC->ncFlags |= NC_VarSelect; } } break; @@ -84880,10 +88851,12 @@ SQLITE_PRIVATE int sqlite3ResolveExprNames( #endif savedHasAgg = pNC->ncFlags & (NC_HasAgg|NC_MinMaxAgg); pNC->ncFlags &= ~(NC_HasAgg|NC_MinMaxAgg); - memset(&w, 0, sizeof(w)); + w.pParse = pNC->pParse; w.xExprCallback = resolveExprStep; w.xSelectCallback = resolveSelectStep; - w.pParse = pNC->pParse; + w.xSelectCallback2 = 0; + w.walkerDepth = 0; + w.eCode = 0; w.u.pNC = pNC; sqlite3WalkExpr(&w, pExpr); #if SQLITE_MAX_EXPR_DEPTH>0 @@ -84909,9 +88882,10 @@ SQLITE_PRIVATE int sqlite3ResolveExprListNames( ExprList *pList /* The expression list to be analyzed. */ ){ int i; - assert( pList!=0 ); - for(i=0; inExpr; i++){ - if( sqlite3ResolveExprNames(pNC, pList->a[i].pExpr) ) return WRC_Abort; + if( pList ){ + for(i=0; inExpr; i++){ + if( sqlite3ResolveExprNames(pNC, pList->a[i].pExpr) ) return WRC_Abort; + } } return WRC_Continue; } @@ -85068,8 +89042,7 @@ SQLITE_PRIVATE Expr *sqlite3ExprAddCollateToken( SQLITE_PRIVATE Expr *sqlite3ExprAddCollateString(Parse *pParse, Expr *pExpr, const char *zC){ Token s; assert( zC!=0 ); - s.z = zC; - s.n = sqlite3Strlen30(s.z); + sqlite3TokenInit(&s, (char*)zC); return sqlite3ExprAddCollateToken(pParse, pExpr, &s, 0); } @@ -85437,6 +89410,7 @@ SQLITE_PRIVATE Expr *sqlite3ExprAlloc( int nExtra = 0; int iValue = 0; + assert( db!=0 ); if( pToken ){ if( op!=TK_INTEGER || pToken->z==0 || sqlite3GetInt32(pToken->z, &iValue)==0 ){ @@ -85444,8 +89418,9 @@ SQLITE_PRIVATE Expr *sqlite3ExprAlloc( assert( iValue>=0 ); } } - pNew = sqlite3DbMallocZero(db, sizeof(Expr)+nExtra); + pNew = sqlite3DbMallocRawNN(db, sizeof(Expr)+nExtra); if( pNew ){ + memset(pNew, 0, sizeof(Expr)); pNew->op = (u8)op; pNew->iAgg = -1; if( pToken ){ @@ -85453,15 +89428,13 @@ SQLITE_PRIVATE Expr *sqlite3ExprAlloc( pNew->flags |= EP_IntValue; pNew->u.iValue = iValue; }else{ - int c; pNew->u.zToken = (char*)&pNew[1]; assert( pToken->z!=0 || pToken->n==0 ); if( pToken->n ) memcpy(pNew->u.zToken, pToken->z, pToken->n); pNew->u.zToken[pToken->n] = 0; - if( dequote && nExtra>=3 - && ((c = pToken->z[0])=='\'' || c=='"' || c=='[' || c=='`') ){ + if( dequote && sqlite3Isquote(pNew->u.zToken[0]) ){ + if( pNew->u.zToken[0]=='"' ) pNew->flags |= EP_DblQuoted; sqlite3Dequote(pNew->u.zToken); - if( c=='"' ) pNew->flags |= EP_DblQuoted; } } } @@ -85531,11 +89504,11 @@ SQLITE_PRIVATE Expr *sqlite3PExpr( const Token *pToken /* Argument token */ ){ Expr *p; - if( op==TK_AND && pLeft && pRight && pParse->nErr==0 ){ + if( op==TK_AND && pParse->nErr==0 ){ /* Take advantage of short-circuit false optimization for AND */ p = sqlite3ExprAnd(pParse->db, pLeft, pRight); }else{ - p = sqlite3ExprAlloc(pParse->db, op, pToken, 1); + p = sqlite3ExprAlloc(pParse->db, op & TKFLG_MASK, pToken, 1); sqlite3ExprAttachSubtrees(pParse->db, p, pLeft, pRight); } if( p ) { @@ -85544,6 +89517,22 @@ SQLITE_PRIVATE Expr *sqlite3PExpr( return p; } +/* +** Add pSelect to the Expr.x.pSelect field. Or, if pExpr is NULL (due +** do a memory allocation failure) then delete the pSelect object. +*/ +SQLITE_PRIVATE void sqlite3PExprAddSelect(Parse *pParse, Expr *pExpr, Select *pSelect){ + if( pExpr ){ + pExpr->x.pSelect = pSelect; + ExprSetProperty(pExpr, EP_xIsSelect|EP_Subquery); + sqlite3ExprSetHeightAndFlags(pParse, pExpr); + }else{ + assert( pParse->db->mallocFailed ); + sqlite3SelectDelete(pParse->db, pSelect); + } +} + + /* ** If the expression is always either TRUE or FALSE (respectively), ** then return 1. If one cannot determine the truth value of the @@ -85682,7 +89671,10 @@ SQLITE_PRIVATE void sqlite3ExprAssignVarNumber(Parse *pParse, Expr *pExpr){ if( x>pParse->nzVar ){ char **a; a = sqlite3DbRealloc(db, pParse->azVar, x*sizeof(a[0])); - if( a==0 ) return; /* Error reported through db->mallocFailed */ + if( a==0 ){ + assert( db->mallocFailed ); /* Error reported through mallocFailed */ + return; + } pParse->azVar = a; memset(&a[pParse->nzVar], 0, (x-pParse->nzVar)*sizeof(a[0])); pParse->nzVar = x; @@ -85701,8 +89693,8 @@ SQLITE_PRIVATE void sqlite3ExprAssignVarNumber(Parse *pParse, Expr *pExpr){ /* ** Recursively delete an expression tree. */ -SQLITE_PRIVATE void sqlite3ExprDelete(sqlite3 *db, Expr *p){ - if( p==0 ) return; +static SQLITE_NOINLINE void sqlite3ExprDeleteNN(sqlite3 *db, Expr *p){ + assert( p!=0 ); /* Sanity check: Assert that the IntValue is non-negative if it exists */ assert( !ExprHasProperty(p, EP_IntValue) || p->u.iValue>=0 ); if( !ExprHasProperty(p, EP_TokenOnly) ){ @@ -85721,6 +89713,9 @@ SQLITE_PRIVATE void sqlite3ExprDelete(sqlite3 *db, Expr *p){ sqlite3DbFree(db, p); } } +SQLITE_PRIVATE void sqlite3ExprDelete(sqlite3 *db, Expr *p){ + if( p ) sqlite3ExprDeleteNN(db, p); +} /* ** Return the number of bytes allocated for the expression structure @@ -85772,7 +89767,7 @@ static int dupedExprStructSize(Expr *p, int flags){ assert( flags==EXPRDUP_REDUCE || flags==0 ); /* Only one flag value allowed */ assert( EXPR_FULLSIZE<=0xfff ); assert( (0xfff & (EP_Reduced|EP_TokenOnly))==0 ); - if( 0==(flags&EXPRDUP_REDUCE) ){ + if( 0==flags ){ nSize = EXPR_FULLSIZE; }else{ assert( !ExprHasProperty(p, EP_TokenOnly|EP_Reduced) ); @@ -85834,84 +89829,88 @@ static int dupedExprSize(Expr *p, int flags){ ** if any. Before returning, *pzBuffer is set to the first byte past the ** portion of the buffer copied into by this function. */ -static Expr *exprDup(sqlite3 *db, Expr *p, int flags, u8 **pzBuffer){ - Expr *pNew = 0; /* Value to return */ - if( p ){ - const int isReduced = (flags&EXPRDUP_REDUCE); - u8 *zAlloc; - u32 staticFlag = 0; +static Expr *exprDup(sqlite3 *db, Expr *p, int dupFlags, u8 **pzBuffer){ + Expr *pNew; /* Value to return */ + u8 *zAlloc; /* Memory space from which to build Expr object */ + u32 staticFlag; /* EP_Static if space not obtained from malloc */ - assert( pzBuffer==0 || isReduced ); + assert( db!=0 ); + assert( p ); + assert( dupFlags==0 || dupFlags==EXPRDUP_REDUCE ); + assert( pzBuffer==0 || dupFlags==EXPRDUP_REDUCE ); - /* Figure out where to write the new Expr structure. */ - if( pzBuffer ){ - zAlloc = *pzBuffer; - staticFlag = EP_Static; + /* Figure out where to write the new Expr structure. */ + if( pzBuffer ){ + zAlloc = *pzBuffer; + staticFlag = EP_Static; + }else{ + zAlloc = sqlite3DbMallocRawNN(db, dupedExprSize(p, dupFlags)); + staticFlag = 0; + } + pNew = (Expr *)zAlloc; + + if( pNew ){ + /* Set nNewSize to the size allocated for the structure pointed to + ** by pNew. This is either EXPR_FULLSIZE, EXPR_REDUCEDSIZE or + ** EXPR_TOKENONLYSIZE. nToken is set to the number of bytes consumed + ** by the copy of the p->u.zToken string (if any). + */ + const unsigned nStructSize = dupedExprStructSize(p, dupFlags); + const int nNewSize = nStructSize & 0xfff; + int nToken; + if( !ExprHasProperty(p, EP_IntValue) && p->u.zToken ){ + nToken = sqlite3Strlen30(p->u.zToken) + 1; }else{ - zAlloc = sqlite3DbMallocRaw(db, dupedExprSize(p, flags)); + nToken = 0; } - pNew = (Expr *)zAlloc; - - if( pNew ){ - /* Set nNewSize to the size allocated for the structure pointed to - ** by pNew. This is either EXPR_FULLSIZE, EXPR_REDUCEDSIZE or - ** EXPR_TOKENONLYSIZE. nToken is set to the number of bytes consumed - ** by the copy of the p->u.zToken string (if any). - */ - const unsigned nStructSize = dupedExprStructSize(p, flags); - const int nNewSize = nStructSize & 0xfff; - int nToken; - if( !ExprHasProperty(p, EP_IntValue) && p->u.zToken ){ - nToken = sqlite3Strlen30(p->u.zToken) + 1; - }else{ - nToken = 0; - } - if( isReduced ){ - assert( ExprHasProperty(p, EP_Reduced)==0 ); - memcpy(zAlloc, p, nNewSize); - }else{ - int nSize = exprStructSize(p); - memcpy(zAlloc, p, nSize); + if( dupFlags ){ + assert( ExprHasProperty(p, EP_Reduced)==0 ); + memcpy(zAlloc, p, nNewSize); + }else{ + u32 nSize = (u32)exprStructSize(p); + memcpy(zAlloc, p, nSize); + if( nSizeflags &= ~(EP_Reduced|EP_TokenOnly|EP_Static|EP_MemToken); - pNew->flags |= nStructSize & (EP_Reduced|EP_TokenOnly); - pNew->flags |= staticFlag; + /* Set the EP_Reduced, EP_TokenOnly, and EP_Static flags appropriately. */ + pNew->flags &= ~(EP_Reduced|EP_TokenOnly|EP_Static|EP_MemToken); + pNew->flags |= nStructSize & (EP_Reduced|EP_TokenOnly); + pNew->flags |= staticFlag; - /* Copy the p->u.zToken string, if any. */ - if( nToken ){ - char *zToken = pNew->u.zToken = (char*)&zAlloc[nNewSize]; - memcpy(zToken, p->u.zToken, nToken); - } + /* Copy the p->u.zToken string, if any. */ + if( nToken ){ + char *zToken = pNew->u.zToken = (char*)&zAlloc[nNewSize]; + memcpy(zToken, p->u.zToken, nToken); + } - if( 0==((p->flags|pNew->flags) & EP_TokenOnly) ){ - /* Fill in the pNew->x.pSelect or pNew->x.pList member. */ - if( ExprHasProperty(p, EP_xIsSelect) ){ - pNew->x.pSelect = sqlite3SelectDup(db, p->x.pSelect, isReduced); - }else{ - pNew->x.pList = sqlite3ExprListDup(db, p->x.pList, isReduced); - } - } - - /* Fill in pNew->pLeft and pNew->pRight. */ - if( ExprHasProperty(pNew, EP_Reduced|EP_TokenOnly) ){ - zAlloc += dupedExprNodeSize(p, flags); - if( ExprHasProperty(pNew, EP_Reduced) ){ - pNew->pLeft = exprDup(db, p->pLeft, EXPRDUP_REDUCE, &zAlloc); - pNew->pRight = exprDup(db, p->pRight, EXPRDUP_REDUCE, &zAlloc); - } - if( pzBuffer ){ - *pzBuffer = zAlloc; - } + if( 0==((p->flags|pNew->flags) & EP_TokenOnly) ){ + /* Fill in the pNew->x.pSelect or pNew->x.pList member. */ + if( ExprHasProperty(p, EP_xIsSelect) ){ + pNew->x.pSelect = sqlite3SelectDup(db, p->x.pSelect, dupFlags); }else{ - if( !ExprHasProperty(p, EP_TokenOnly) ){ - pNew->pLeft = sqlite3ExprDup(db, p->pLeft, 0); - pNew->pRight = sqlite3ExprDup(db, p->pRight, 0); - } + pNew->x.pList = sqlite3ExprListDup(db, p->x.pList, dupFlags); } + } + /* Fill in pNew->pLeft and pNew->pRight. */ + if( ExprHasProperty(pNew, EP_Reduced|EP_TokenOnly) ){ + zAlloc += dupedExprNodeSize(p, dupFlags); + if( ExprHasProperty(pNew, EP_Reduced) ){ + pNew->pLeft = p->pLeft ? + exprDup(db, p->pLeft, EXPRDUP_REDUCE, &zAlloc) : 0; + pNew->pRight = p->pRight ? + exprDup(db, p->pRight, EXPRDUP_REDUCE, &zAlloc) : 0; + } + if( pzBuffer ){ + *pzBuffer = zAlloc; + } + }else{ + if( !ExprHasProperty(p, EP_TokenOnly) ){ + pNew->pLeft = sqlite3ExprDup(db, p->pLeft, 0); + pNew->pRight = sqlite3ExprDup(db, p->pRight, 0); + } } } return pNew; @@ -85962,18 +89961,20 @@ static With *withDup(sqlite3 *db, With *p){ ** part of the in-memory representation of the database schema. */ SQLITE_PRIVATE Expr *sqlite3ExprDup(sqlite3 *db, Expr *p, int flags){ - return exprDup(db, p, flags, 0); + assert( flags==0 || flags==EXPRDUP_REDUCE ); + return p ? exprDup(db, p, flags, 0) : 0; } SQLITE_PRIVATE ExprList *sqlite3ExprListDup(sqlite3 *db, ExprList *p, int flags){ ExprList *pNew; struct ExprList_item *pItem, *pOldItem; int i; + assert( db!=0 ); if( p==0 ) return 0; - pNew = sqlite3DbMallocRaw(db, sizeof(*pNew) ); + pNew = sqlite3DbMallocRawNN(db, sizeof(*pNew) ); if( pNew==0 ) return 0; pNew->nExpr = i = p->nExpr; if( (flags & EXPRDUP_REDUCE)==0 ) for(i=1; inExpr; i+=i){} - pNew->a = pItem = sqlite3DbMallocRaw(db, i*sizeof(p->a[0]) ); + pNew->a = pItem = sqlite3DbMallocRawNN(db, i*sizeof(p->a[0]) ); if( pItem==0 ){ sqlite3DbFree(db, pNew); return 0; @@ -86004,9 +90005,10 @@ SQLITE_PRIVATE SrcList *sqlite3SrcListDup(sqlite3 *db, SrcList *p, int flags){ SrcList *pNew; int i; int nByte; + assert( db!=0 ); if( p==0 ) return 0; nByte = sizeof(*p) + (p->nSrc>0 ? sizeof(p->a[0]) * (p->nSrc-1) : 0); - pNew = sqlite3DbMallocRaw(db, nByte ); + pNew = sqlite3DbMallocRawNN(db, nByte ); if( pNew==0 ) return 0; pNew->nSrc = pNew->nAlloc = p->nSrc; for(i=0; inSrc; i++){ @@ -86043,11 +90045,12 @@ SQLITE_PRIVATE SrcList *sqlite3SrcListDup(sqlite3 *db, SrcList *p, int flags){ SQLITE_PRIVATE IdList *sqlite3IdListDup(sqlite3 *db, IdList *p){ IdList *pNew; int i; + assert( db!=0 ); if( p==0 ) return 0; - pNew = sqlite3DbMallocRaw(db, sizeof(*pNew) ); + pNew = sqlite3DbMallocRawNN(db, sizeof(*pNew) ); if( pNew==0 ) return 0; pNew->nId = p->nId; - pNew->a = sqlite3DbMallocRaw(db, p->nId*sizeof(p->a[0]) ); + pNew->a = sqlite3DbMallocRawNN(db, p->nId*sizeof(p->a[0]) ); if( pNew->a==0 ){ sqlite3DbFree(db, pNew); return 0; @@ -86065,8 +90068,9 @@ SQLITE_PRIVATE IdList *sqlite3IdListDup(sqlite3 *db, IdList *p){ } SQLITE_PRIVATE Select *sqlite3SelectDup(sqlite3 *db, Select *p, int flags){ Select *pNew, *pPrior; + assert( db!=0 ); if( p==0 ) return 0; - pNew = sqlite3DbMallocRaw(db, sizeof(*p) ); + pNew = sqlite3DbMallocRawNN(db, sizeof(*p) ); if( pNew==0 ) return 0; pNew->pEList = sqlite3ExprListDup(db, p->pEList, flags); pNew->pSrc = sqlite3SrcListDup(db, p->pSrc, flags); @@ -86112,12 +90116,14 @@ SQLITE_PRIVATE ExprList *sqlite3ExprListAppend( Expr *pExpr /* Expression to be appended. Might be NULL */ ){ sqlite3 *db = pParse->db; + assert( db!=0 ); if( pList==0 ){ - pList = sqlite3DbMallocZero(db, sizeof(ExprList) ); + pList = sqlite3DbMallocRawNN(db, sizeof(ExprList) ); if( pList==0 ){ goto no_mem; } - pList->a = sqlite3DbMallocRaw(db, sizeof(pList->a[0])); + pList->nExpr = 0; + pList->a = sqlite3DbMallocRawNN(db, sizeof(pList->a[0])); if( pList->a==0 ) goto no_mem; }else if( (pList->nExpr & (pList->nExpr-1))==0 ){ struct ExprList_item *a; @@ -86178,7 +90184,7 @@ SQLITE_PRIVATE void sqlite3ExprListSetName( pItem = &pList->a[pList->nExpr-1]; assert( pItem->zName==0 ); pItem->zName = sqlite3DbStrNDup(pParse->db, pName->z, pName->n); - if( dequote && pItem->zName ) sqlite3Dequote(pItem->zName); + if( dequote ) sqlite3Dequote(pItem->zName); } } @@ -86227,10 +90233,9 @@ SQLITE_PRIVATE void sqlite3ExprListCheckLength( /* ** Delete an entire expression list. */ -SQLITE_PRIVATE void sqlite3ExprListDelete(sqlite3 *db, ExprList *pList){ +static SQLITE_NOINLINE void exprListDeleteNN(sqlite3 *db, ExprList *pList){ int i; struct ExprList_item *pItem; - if( pList==0 ) return; assert( pList->a!=0 || pList->nExpr==0 ); for(pItem=pList->a, i=0; inExpr; i++, pItem++){ sqlite3ExprDelete(db, pItem->pExpr); @@ -86240,6 +90245,9 @@ SQLITE_PRIVATE void sqlite3ExprListDelete(sqlite3 *db, ExprList *pList){ sqlite3DbFree(db, pList->a); sqlite3DbFree(db, pList); } +SQLITE_PRIVATE void sqlite3ExprListDelete(sqlite3 *db, ExprList *pList){ + if( pList ) exprListDeleteNN(db, pList); +} /* ** Return the bitwise-OR of all Expr.flags fields in the given @@ -86251,7 +90259,8 @@ SQLITE_PRIVATE u32 sqlite3ExprListFlags(const ExprList *pList){ if( pList ){ for(i=0; inExpr; i++){ Expr *pExpr = pList->a[i].pExpr; - if( ALWAYS(pExpr) ) m |= pExpr->flags; + assert( pExpr!=0 ); + m |= pExpr->flags; } } return m; @@ -86398,6 +90407,22 @@ SQLITE_PRIVATE int sqlite3ExprIsConstantOrFunction(Expr *p, u8 isInit){ return exprIsConst(p, 4+isInit, 0); } +#ifdef SQLITE_ENABLE_CURSOR_HINTS +/* +** Walk an expression tree. Return 1 if the expression contains a +** subquery of some kind. Return 0 if there are no subqueries. +*/ +SQLITE_PRIVATE int sqlite3ExprContainsSubquery(Expr *p){ + Walker w; + memset(&w, 0, sizeof(w)); + w.eCode = 1; + w.xExprCallback = sqlite3ExprWalkNoop; + w.xSelectCallback = selectNodeIsConstant; + sqlite3WalkExpr(&w, p); + return w.eCode==0; +} +#endif + /* ** If the expression p codes a constant integer that is small enough ** to fit in a 32-bit integer, return 1 and put the value of the integer @@ -86520,23 +90545,22 @@ SQLITE_PRIVATE int sqlite3IsRowid(const char *z){ } /* -** Return true if we are able to the IN operator optimization on a -** query of the form -** -** x IN (SELECT ...) -** -** Where the SELECT... clause is as specified by the parameter to this -** routine. -** -** The Select object passed in has already been preprocessed and no -** errors have been found. +** pX is the RHS of an IN operator. If pX is a SELECT statement +** that can be simplified to a direct table access, then return +** a pointer to the SELECT statement. If pX is not a SELECT statement, +** or if the SELECT statement needs to be manifested into a transient +** table, then return NULL. */ #ifndef SQLITE_OMIT_SUBQUERY -static int isCandidateForInOpt(Select *p){ +static Select *isCandidateForInOpt(Expr *pX){ + Select *p; SrcList *pSrc; ExprList *pEList; + Expr *pRes; Table *pTab; - if( p==0 ) return 0; /* right-hand side of IN is SELECT */ + if( !ExprHasProperty(pX, EP_xIsSelect) ) return 0; /* Not a subquery */ + if( ExprHasProperty(pX, EP_VarSelect) ) return 0; /* Correlated subq */ + p = pX->x.pSelect; if( p->pPrior ) return 0; /* Not a compound SELECT */ if( p->selFlags & (SF_Distinct|SF_Aggregate) ){ testcase( (p->selFlags & (SF_Distinct|SF_Aggregate))==SF_Distinct ); @@ -86552,13 +90576,15 @@ static int isCandidateForInOpt(Select *p){ if( pSrc->nSrc!=1 ) return 0; /* Single term in FROM clause */ if( pSrc->a[0].pSelect ) return 0; /* FROM is not a subquery or view */ pTab = pSrc->a[0].pTab; - if( NEVER(pTab==0) ) return 0; + assert( pTab!=0 ); assert( pTab->pSelect==0 ); /* FROM clause is not a view */ if( IsVirtual(pTab) ) return 0; /* FROM clause not a virtual table */ pEList = p->pEList; if( pEList->nExpr!=1 ) return 0; /* One column in the result set */ - if( pEList->a[0].pExpr->op!=TK_COLUMN ) return 0; /* Result is a column */ - return 1; + pRes = pEList->a[0].pExpr; + if( pRes->op!=TK_COLUMN ) return 0; /* Result is a column */ + assert( pRes->iTable==pSrc->a[0].iCursor ); /* Not a correlated subquery */ + return p; } #endif /* SQLITE_OMIT_SUBQUERY */ @@ -86690,15 +90716,13 @@ SQLITE_PRIVATE int sqlite3FindInIndex(Parse *pParse, Expr *pX, u32 inFlags, int ** satisfy the query. This is preferable to generating a new ** ephemeral table. */ - p = (ExprHasProperty(pX, EP_xIsSelect) ? pX->x.pSelect : 0); - if( pParse->nErr==0 && isCandidateForInOpt(p) ){ + if( pParse->nErr==0 && (p = isCandidateForInOpt(pX))!=0 ){ sqlite3 *db = pParse->db; /* Database connection */ Table *pTab; /* Table . */ Expr *pExpr; /* Expression */ i16 iCol; /* Index of column */ i16 iDb; /* Database idx for pTab */ - assert( p ); /* Because of isCandidateForInOpt(p) */ assert( p->pEList!=0 ); /* Because of isCandidateForInOpt(p) */ assert( p->pEList->a[0].pExpr!=0 ); /* Because of isCandidateForInOpt(p) */ assert( p->pSrc!=0 ); /* Because of isCandidateForInOpt(p) */ @@ -86856,9 +90880,10 @@ SQLITE_PRIVATE int sqlite3CodeSubselect( #ifndef SQLITE_OMIT_EXPLAIN if( pParse->explain==2 ){ - char *zMsg = sqlite3MPrintf( - pParse->db, "EXECUTE %s%s SUBQUERY %d", jmpIfDynamic>=0?"":"CORRELATED ", - pExpr->op==TK_IN?"LIST":"SCALAR", pParse->iNextSelectId + char *zMsg = sqlite3MPrintf(pParse->db, "EXECUTE %s%s SUBQUERY %d", + jmpIfDynamic>=0?"":"CORRELATED ", + pExpr->op==TK_IN?"LIST":"SCALAR", + pParse->iNextSelectId ); sqlite3VdbeAddOp4(v, OP_Explain, pParse->iSelectId, 0, 0, zMsg, P4_DYNAMIC); } @@ -87267,6 +91292,19 @@ static void codeInteger(Parse *pParse, Expr *pExpr, int negFlag, int iMem){ } } +#if defined(SQLITE_DEBUG) +/* +** Verify the consistency of the column cache +*/ +static int cacheIsValid(Parse *pParse){ + int i, n; + for(i=n=0; iaColCache[i].iReg>0 ) n++; + } + return n==pParse->nColCache; +} +#endif + /* ** Clear a cache entry. */ @@ -87277,6 +91315,9 @@ static void cacheEntryClear(Parse *pParse, struct yColCache *p){ } p->tempReg = 0; } + p->iReg = 0; + pParse->nColCache--; + assert( pParse->db->mallocFailed || cacheIsValid(pParse) ); } @@ -87320,6 +91361,8 @@ SQLITE_PRIVATE void sqlite3ExprCacheStore(Parse *pParse, int iTab, int iCol, int p->iReg = iReg; p->tempReg = 0; p->lru = pParse->iCacheCnt++; + pParse->nColCache++; + assert( pParse->db->mallocFailed || cacheIsValid(pParse) ); return; } } @@ -87341,6 +91384,7 @@ SQLITE_PRIVATE void sqlite3ExprCacheStore(Parse *pParse, int iTab, int iCol, int p->iReg = iReg; p->tempReg = 0; p->lru = pParse->iCacheCnt++; + assert( cacheIsValid(pParse) ); return; } } @@ -87350,15 +91394,13 @@ SQLITE_PRIVATE void sqlite3ExprCacheStore(Parse *pParse, int iTab, int iCol, int ** Purge the range of registers from the column cache. */ SQLITE_PRIVATE void sqlite3ExprCacheRemove(Parse *pParse, int iReg, int nReg){ - int i; - int iLast = iReg + nReg - 1; struct yColCache *p; - for(i=0, p=pParse->aColCache; iiReg; - if( r>=iReg && r<=iLast ){ - cacheEntryClear(pParse, p); - p->iReg = 0; - } + if( iReg<=0 || pParse->nColCache==0 ) return; + p = &pParse->aColCache[SQLITE_N_COLCACHE-1]; + while(1){ + if( p->iReg >= iReg && p->iReg < iReg+nReg ) cacheEntryClear(pParse, p); + if( p==pParse->aColCache ) break; + p--; } } @@ -87394,7 +91436,6 @@ SQLITE_PRIVATE void sqlite3ExprCachePop(Parse *pParse){ for(i=0, p=pParse->aColCache; iiReg && p->iLevel>pParse->iCacheLevel ){ cacheEntryClear(pParse, p); - p->iReg = 0; } } } @@ -87430,7 +91471,7 @@ SQLITE_PRIVATE void sqlite3ExprCodeLoadIndexColumn( assert( pIdx->aColExpr ); assert( pIdx->aColExpr->nExpr>iIdxCol ); pParse->iSelfTab = iTabCur; - sqlite3ExprCode(pParse, pIdx->aColExpr->a[iIdxCol].pExpr, regOut); + sqlite3ExprCodeCopy(pParse, pIdx->aColExpr->a[iIdxCol].pExpr, regOut); }else{ sqlite3ExprCodeGetColumnOfTable(pParse->pVdbe, pIdx->pTable, iTabCur, iTabCol, regOut); @@ -87464,9 +91505,12 @@ SQLITE_PRIVATE void sqlite3ExprCodeGetColumnOfTable( /* ** Generate code that will extract the iColumn-th column from -** table pTab and store the column value in a register. An effort -** is made to store the column value in register iReg, but this is -** not guaranteed. The location of the column value is returned. +** table pTab and store the column value in a register. +** +** An effort is made to store the column value in register iReg. This +** is not garanteeed for GetColumn() - the result can be stored in +** any register. But the result is guaranteed to land in register iReg +** for GetColumnToReg(). ** ** There must be an open cursor to pTab in iTable when this routine ** is called. If iColumn<0 then code is generated that extracts the rowid. @@ -87477,7 +91521,7 @@ SQLITE_PRIVATE int sqlite3ExprCodeGetColumn( int iColumn, /* Index of the table column */ int iTable, /* The cursor pointing to the table */ int iReg, /* Store results here */ - u8 p5 /* P5 value for OP_Column */ + u8 p5 /* P5 value for OP_Column + FLAGS */ ){ Vdbe *v = pParse->pVdbe; int i; @@ -87499,6 +91543,17 @@ SQLITE_PRIVATE int sqlite3ExprCodeGetColumn( } return iReg; } +SQLITE_PRIVATE void sqlite3ExprCodeGetColumnToReg( + Parse *pParse, /* Parsing and code generating context */ + Table *pTab, /* Description of the table we are reading from */ + int iColumn, /* Index of the table column */ + int iTable, /* The cursor pointing to the table */ + int iReg /* Store results here */ +){ + int r1 = sqlite3ExprCodeGetColumn(pParse, pTab, iColumn, iTable, iReg, 0); + if( r1!=iReg ) sqlite3VdbeAddOp2(pParse->pVdbe, OP_SCopy, r1, iReg); +} + /* ** Clear all column cache entries. @@ -87515,7 +91570,6 @@ SQLITE_PRIVATE void sqlite3ExprCacheClear(Parse *pParse){ for(i=0, p=pParse->aColCache; iiReg ){ cacheEntryClear(pParse, p); - p->iReg = 0; } } } @@ -87557,6 +91611,7 @@ static int usedAsColumnCache(Parse *pParse, int iFrom, int iTo){ } #endif /* SQLITE_DEBUG || SQLITE_COVERAGE_TEST */ + /* ** Convert an expression node to a TK_REGISTER */ @@ -87825,7 +91880,6 @@ SQLITE_PRIVATE int sqlite3ExprCodeTarget(Parse *pParse, Expr *pExpr, int target) ExprList *pFarg; /* List of function arguments */ int nFarg; /* Number of function arguments */ FuncDef *pDef; /* The function definition object */ - int nId; /* Length of the function name in bytes */ const char *zId; /* The function name */ u32 constMask = 0; /* Mask of function arguments that are constant */ int i; /* Loop counter */ @@ -87841,10 +91895,9 @@ SQLITE_PRIVATE int sqlite3ExprCodeTarget(Parse *pParse, Expr *pExpr, int target) nFarg = pFarg ? pFarg->nExpr : 0; assert( !ExprHasProperty(pExpr, EP_IntValue) ); zId = pExpr->u.zToken; - nId = sqlite3Strlen30(zId); - pDef = sqlite3FindFunction(db, zId, nId, nFarg, enc, 0); - if( pDef==0 || pDef->xFunc==0 ){ - sqlite3ErrorMsg(pParse, "unknown function: %.*s()", nId, zId); + pDef = sqlite3FindFunction(db, zId, nFarg, enc, 0); + if( pDef==0 || pDef->xFinalize!=0 ){ + sqlite3ErrorMsg(pParse, "unknown function: %s()", zId); break; } @@ -88009,6 +92062,7 @@ SQLITE_PRIVATE int sqlite3ExprCodeTarget(Parse *pParse, Expr *pExpr, int target) sqlite3ReleaseTempReg(pParse, r4); break; } + case TK_SPAN: case TK_COLLATE: case TK_UPLUS: { inReg = sqlite3ExprCodeTarget(pParse, pExpr->pLeft, target); @@ -88269,13 +92323,25 @@ SQLITE_PRIVATE void sqlite3ExprCode(Parse *pParse, Expr *pExpr, int target){ sqlite3VdbeAddOp2(pParse->pVdbe, OP_Copy, pExpr->iTable, target); }else{ inReg = sqlite3ExprCodeTarget(pParse, pExpr, target); - assert( pParse->pVdbe || pParse->db->mallocFailed ); + assert( pParse->pVdbe!=0 || pParse->db->mallocFailed ); if( inReg!=target && pParse->pVdbe ){ sqlite3VdbeAddOp2(pParse->pVdbe, OP_SCopy, inReg, target); } } } +/* +** Make a transient copy of expression pExpr and then code it using +** sqlite3ExprCode(). This routine works just like sqlite3ExprCode() +** except that the input expression is guaranteed to be unchanged. +*/ +SQLITE_PRIVATE void sqlite3ExprCodeCopy(Parse *pParse, Expr *pExpr, int target){ + sqlite3 *db = pParse->db; + pExpr = sqlite3ExprDup(db, pExpr, 0); + if( !db->mallocFailed ) sqlite3ExprCode(pParse, pExpr, target); + sqlite3ExprDelete(db, pExpr); +} + /* ** Generate code that will evaluate expression pExpr and store the ** results in register target. The results are guaranteed to appear @@ -88325,6 +92391,10 @@ SQLITE_PRIVATE void sqlite3ExprCodeAndCache(Parse *pParse, Expr *pExpr, int targ ** ** The SQLITE_ECEL_FACTOR argument allows constant arguments to be ** factored out into initialization code. +** +** The SQLITE_ECEL_REF flag means that expressions in the list with +** ExprList.a[].u.x.iOrderByCol>0 have already been evaluated and stored +** in registers at srcReg, and so the value can be copied from there. */ SQLITE_PRIVATE int sqlite3ExprCodeExprList( Parse *pParse, /* Parsing context */ @@ -88471,6 +92541,13 @@ SQLITE_PRIVATE void sqlite3ExprIfTrue(Parse *pParse, Expr *pExpr, int dest, int sqlite3ExprIfFalse(pParse, pExpr->pLeft, dest, jumpIfNull); break; } + case TK_IS: + case TK_ISNOT: + testcase( op==TK_IS ); + testcase( op==TK_ISNOT ); + op = (op==TK_IS) ? TK_EQ : TK_NE; + jumpIfNull = SQLITE_NULLEQ; + /* Fall thru */ case TK_LT: case TK_LE: case TK_GT: @@ -88486,23 +92563,12 @@ SQLITE_PRIVATE void sqlite3ExprIfTrue(Parse *pParse, Expr *pExpr, int dest, int assert(TK_LE==OP_Le); testcase(op==OP_Le); VdbeCoverageIf(v,op==OP_Le); assert(TK_GT==OP_Gt); testcase(op==OP_Gt); VdbeCoverageIf(v,op==OP_Gt); assert(TK_GE==OP_Ge); testcase(op==OP_Ge); VdbeCoverageIf(v,op==OP_Ge); - assert(TK_EQ==OP_Eq); testcase(op==OP_Eq); VdbeCoverageIf(v,op==OP_Eq); - assert(TK_NE==OP_Ne); testcase(op==OP_Ne); VdbeCoverageIf(v,op==OP_Ne); - testcase( regFree1==0 ); - testcase( regFree2==0 ); - break; - } - case TK_IS: - case TK_ISNOT: { - testcase( op==TK_IS ); - testcase( op==TK_ISNOT ); - r1 = sqlite3ExprCodeTemp(pParse, pExpr->pLeft, ®Free1); - r2 = sqlite3ExprCodeTemp(pParse, pExpr->pRight, ®Free2); - op = (op==TK_IS) ? TK_EQ : TK_NE; - codeCompare(pParse, pExpr->pLeft, pExpr->pRight, op, - r1, r2, dest, SQLITE_NULLEQ); - VdbeCoverageIf(v, op==TK_EQ); - VdbeCoverageIf(v, op==TK_NE); + assert(TK_EQ==OP_Eq); testcase(op==OP_Eq); + VdbeCoverageIf(v, op==OP_Eq && jumpIfNull==SQLITE_NULLEQ); + VdbeCoverageIf(v, op==OP_Eq && jumpIfNull!=SQLITE_NULLEQ); + assert(TK_NE==OP_Ne); testcase(op==OP_Ne); + VdbeCoverageIf(v, op==OP_Ne && jumpIfNull==SQLITE_NULLEQ); + VdbeCoverageIf(v, op==OP_Ne && jumpIfNull!=SQLITE_NULLEQ); testcase( regFree1==0 ); testcase( regFree2==0 ); break; @@ -88627,6 +92693,13 @@ SQLITE_PRIVATE void sqlite3ExprIfFalse(Parse *pParse, Expr *pExpr, int dest, int sqlite3ExprIfTrue(pParse, pExpr->pLeft, dest, jumpIfNull); break; } + case TK_IS: + case TK_ISNOT: + testcase( pExpr->op==TK_IS ); + testcase( pExpr->op==TK_ISNOT ); + op = (pExpr->op==TK_IS) ? TK_NE : TK_EQ; + jumpIfNull = SQLITE_NULLEQ; + /* Fall thru */ case TK_LT: case TK_LE: case TK_GT: @@ -88642,23 +92715,12 @@ SQLITE_PRIVATE void sqlite3ExprIfFalse(Parse *pParse, Expr *pExpr, int dest, int assert(TK_LE==OP_Le); testcase(op==OP_Le); VdbeCoverageIf(v,op==OP_Le); assert(TK_GT==OP_Gt); testcase(op==OP_Gt); VdbeCoverageIf(v,op==OP_Gt); assert(TK_GE==OP_Ge); testcase(op==OP_Ge); VdbeCoverageIf(v,op==OP_Ge); - assert(TK_EQ==OP_Eq); testcase(op==OP_Eq); VdbeCoverageIf(v,op==OP_Eq); - assert(TK_NE==OP_Ne); testcase(op==OP_Ne); VdbeCoverageIf(v,op==OP_Ne); - testcase( regFree1==0 ); - testcase( regFree2==0 ); - break; - } - case TK_IS: - case TK_ISNOT: { - testcase( pExpr->op==TK_IS ); - testcase( pExpr->op==TK_ISNOT ); - r1 = sqlite3ExprCodeTemp(pParse, pExpr->pLeft, ®Free1); - r2 = sqlite3ExprCodeTemp(pParse, pExpr->pRight, ®Free2); - op = (pExpr->op==TK_IS) ? TK_NE : TK_EQ; - codeCompare(pParse, pExpr->pLeft, pExpr->pRight, op, - r1, r2, dest, SQLITE_NULLEQ); - VdbeCoverageIf(v, op==TK_EQ); - VdbeCoverageIf(v, op==TK_NE); + assert(TK_EQ==OP_Eq); testcase(op==OP_Eq); + VdbeCoverageIf(v, op==OP_Eq && jumpIfNull!=SQLITE_NULLEQ); + VdbeCoverageIf(v, op==OP_Eq && jumpIfNull==SQLITE_NULLEQ); + assert(TK_NE==OP_Ne); testcase(op==OP_Ne); + VdbeCoverageIf(v, op==OP_Ne && jumpIfNull!=SQLITE_NULLEQ); + VdbeCoverageIf(v, op==OP_Ne && jumpIfNull==SQLITE_NULLEQ); testcase( regFree1==0 ); testcase( regFree2==0 ); break; @@ -88766,7 +92828,7 @@ SQLITE_PRIVATE int sqlite3ExprCompare(Expr *pA, Expr *pB, int iTab){ } return 2; } - if( pA->op!=TK_COLUMN && ALWAYS(pA->op!=TK_AGG_COLUMN) && pA->u.zToken ){ + if( pA->op!=TK_COLUMN && pA->op!=TK_AGG_COLUMN && pA->u.zToken ){ if( pA->op==TK_FUNCTION ){ if( sqlite3StrICmp(pA->u.zToken,pB->u.zToken)!=0 ) return 2; }else if( strcmp(pA->u.zToken,pB->u.zToken)!=0 ){ @@ -89053,7 +93115,7 @@ static int analyzeAggregate(Walker *pWalker, Expr *pExpr){ pItem->iMem = ++pParse->nMem; assert( !ExprHasProperty(pExpr, EP_IntValue) ); pItem->pFunc = sqlite3FindFunction(pParse->db, - pExpr->u.zToken, sqlite3Strlen30(pExpr->u.zToken), + pExpr->u.zToken, pExpr->x.pList ? pExpr->x.pList->nExpr : 0, enc, 0); if( pExpr->flags & EP_Distinct ){ pItem->iDistinct = pParse->nTab++; @@ -89182,6 +93244,29 @@ SQLITE_PRIVATE void sqlite3ClearTempRegCache(Parse *pParse){ pParse->nRangeReg = 0; } +/* +** Validate that no temporary register falls within the range of +** iFirst..iLast, inclusive. This routine is only call from within assert() +** statements. +*/ +#ifdef SQLITE_DEBUG +SQLITE_PRIVATE int sqlite3NoTempsInRange(Parse *pParse, int iFirst, int iLast){ + int i; + if( pParse->nRangeReg>0 + && pParse->iRangeReg+pParse->nRangeRegiRangeReg>=iFirst + ){ + return 0; + } + for(i=0; inTempReg; i++){ + if( pParse->aTempReg[i]>=iFirst && pParse->aTempReg[i]<=iLast ){ + return 0; + } + } + return 1; +} +#endif /* SQLITE_DEBUG */ + /************** End of expr.c ************************************************/ /************** Begin file alter.c *******************************************/ /* @@ -89415,7 +93500,7 @@ static void renameTriggerFunc( ** Register built-in functions used to help implement ALTER TABLE */ SQLITE_PRIVATE void sqlite3AlterFunctions(void){ - static SQLITE_WSD FuncDef aAlterTableFuncs[] = { + static FuncDef aAlterTableFuncs[] = { FUNCTION(sqlite_rename_table, 2, 0, 0, renameTableFunc), #ifndef SQLITE_OMIT_TRIGGER FUNCTION(sqlite_rename_trigger, 2, 0, 0, renameTriggerFunc), @@ -89424,13 +93509,7 @@ SQLITE_PRIVATE void sqlite3AlterFunctions(void){ FUNCTION(sqlite_rename_parent, 3, 0, 0, renameParentFunc), #endif }; - int i; - FuncDefHash *pHash = &GLOBAL(FuncDefHash, sqlite3GlobalFunctions); - FuncDef *aFunc = (FuncDef*)&GLOBAL(FuncDef, aAlterTableFuncs); - - for(i=0; iflags = savedDbFlags; } - -/* -** Generate code to make sure the file format number is at least minFormat. -** The generated code will increase the file format number if necessary. -*/ -SQLITE_PRIVATE void sqlite3MinimumFileFormat(Parse *pParse, int iDb, int minFormat){ - Vdbe *v; - v = sqlite3GetVdbe(pParse); - /* The VDBE should have been allocated before this routine is called. - ** If that allocation failed, we would have quit before reaching this - ** point */ - if( ALWAYS(v) ){ - int r1 = sqlite3GetTempReg(pParse); - int r2 = sqlite3GetTempReg(pParse); - int addr1; - sqlite3VdbeAddOp3(v, OP_ReadCookie, iDb, r1, BTREE_FILE_FORMAT); - sqlite3VdbeUsesBtree(v, iDb); - sqlite3VdbeAddOp2(v, OP_Integer, minFormat, r2); - addr1 = sqlite3VdbeAddOp3(v, OP_Ge, r2, 0, r1); - sqlite3VdbeChangeP5(v, SQLITE_NOTNULL); VdbeCoverage(v); - sqlite3VdbeAddOp3(v, OP_SetCookie, iDb, BTREE_FILE_FORMAT, r2); - sqlite3VdbeJumpHere(v, addr1); - sqlite3ReleaseTempReg(pParse, r1); - sqlite3ReleaseTempReg(pParse, r2); - } -} - /* ** This function is called after an "ALTER TABLE ... ADD" statement ** has been parsed. Argument pColDef contains the text of the new @@ -89819,9 +93871,11 @@ SQLITE_PRIVATE void sqlite3AlterFinishAddColumn(Parse *pParse, Token *pColDef){ Column *pCol; /* The new column */ Expr *pDflt; /* Default value for the new column */ sqlite3 *db; /* The database connection; */ + Vdbe *v = pParse->pVdbe; /* The prepared statement under construction */ db = pParse->db; if( pParse->nErr || db->mallocFailed ) return; + assert( v!=0 ); pNew = pParse->pNewTable; assert( pNew ); @@ -89845,7 +93899,8 @@ SQLITE_PRIVATE void sqlite3AlterFinishAddColumn(Parse *pParse, Token *pColDef){ ** literal NULL, then set pDflt to 0. This simplifies checking ** for an SQL NULL default below. */ - if( pDflt && pDflt->op==TK_NULL ){ + assert( pDflt==0 || pDflt->op==TK_SPAN ); + if( pDflt && pDflt->pLeft->op==TK_NULL ){ pDflt = 0; } @@ -89881,7 +93936,7 @@ SQLITE_PRIVATE void sqlite3AlterFinishAddColumn(Parse *pParse, Token *pColDef){ rc = sqlite3ValueFromExpr(db, pDflt, SQLITE_UTF8, SQLITE_AFF_BLOB, &pVal); assert( rc==SQLITE_OK || rc==SQLITE_NOMEM ); if( rc!=SQLITE_OK ){ - db->mallocFailed = 1; + assert( db->mallocFailed == 1 ); return; } if( !pVal ){ @@ -89911,11 +93966,16 @@ SQLITE_PRIVATE void sqlite3AlterFinishAddColumn(Parse *pParse, Token *pColDef){ db->flags = savedDbFlags; } - /* If the default value of the new column is NULL, then set the file + /* If the default value of the new column is NULL, then the file ** format to 2. If the default value of the new column is not NULL, - ** the file format becomes 3. + ** the file format be 3. Back when this feature was first added + ** in 2006, we went to the trouble to upgrade the file format to the + ** minimum support values. But 10-years on, we can assume that all + ** extent versions of SQLite support file-format 4, so we always and + ** unconditionally upgrade to 4. */ - sqlite3MinimumFileFormat(pParse, iDb, pDflt ? 3 : 2); + sqlite3VdbeAddOp3(v, OP_SetCookie, iDb, BTREE_FILE_FORMAT, + SQLITE_MAX_FILE_FORMAT); /* Reload the schema of the modified table. */ reloadTableSchema(pParse, pTab, pTab->zName); @@ -89989,7 +94049,7 @@ SQLITE_PRIVATE void sqlite3AlterBeginAddColumn(Parse *pParse, SrcList *pSrc){ pNew->aCol = (Column*)sqlite3DbMallocZero(db, sizeof(Column)*nAlloc); pNew->zName = sqlite3MPrintf(db, "sqlite_altertab_%s", pTab->zName); if( !pNew->aCol || !pNew->zName ){ - db->mallocFailed = 1; + assert( db->mallocFailed ); goto exit_begin_add_column; } memcpy(pNew->aCol, pTab->aCol, sizeof(Column)*pNew->nCol); @@ -89997,9 +94057,7 @@ SQLITE_PRIVATE void sqlite3AlterBeginAddColumn(Parse *pParse, SrcList *pSrc){ Column *pCol = &pNew->aCol[i]; pCol->zName = sqlite3DbStrDup(db, pCol->zName); pCol->zColl = 0; - pCol->zType = 0; pCol->pDflt = 0; - pCol->zDflt = 0; } pNew->pSchema = db->aDb[iDb].pSchema; pNew->addColOffset = pTab->addColOffset; @@ -90334,7 +94392,7 @@ static void sampleClear(sqlite3 *db, Stat4Sample *p){ static void sampleSetRowid(sqlite3 *db, Stat4Sample *p, int n, const u8 *pData){ assert( db!=0 ); if( p->nRowid ) sqlite3DbFree(db, p->u.aRowid); - p->u.aRowid = sqlite3DbMallocRaw(db, n); + p->u.aRowid = sqlite3DbMallocRawNN(db, n); if( p->u.aRowid ){ p->nRowid = n; memcpy(p->u.aRowid, pData, n); @@ -90499,12 +94557,10 @@ static const FuncDef statInitFuncdef = { SQLITE_UTF8, /* funcFlags */ 0, /* pUserData */ 0, /* pNext */ - statInit, /* xFunc */ - 0, /* xStep */ + statInit, /* xSFunc */ 0, /* xFinalize */ "stat_init", /* zName */ - 0, /* pHash */ - 0 /* pDestructor */ + {0} }; #ifdef SQLITE_ENABLE_STAT4 @@ -90800,12 +94856,10 @@ static const FuncDef statPushFuncdef = { SQLITE_UTF8, /* funcFlags */ 0, /* pUserData */ 0, /* pNext */ - statPush, /* xFunc */ - 0, /* xStep */ + statPush, /* xSFunc */ 0, /* xFinalize */ "stat_push", /* zName */ - 0, /* pHash */ - 0 /* pDestructor */ + {0} }; #define STAT_GET_STAT1 0 /* "stat" column of stat1 table */ @@ -90947,12 +95001,10 @@ static const FuncDef statGetFuncdef = { SQLITE_UTF8, /* funcFlags */ 0, /* pUserData */ 0, /* pNext */ - statGet, /* xFunc */ - 0, /* xStep */ + statGet, /* xSFunc */ 0, /* xFinalize */ "stat_get", /* zName */ - 0, /* pHash */ - 0 /* pDestructor */ + {0} }; static void callStatGet(Vdbe *v, int regStat4, int iParam, int regOut){ @@ -90964,8 +95016,8 @@ static void callStatGet(Vdbe *v, int regStat4, int iParam, int regOut){ #else UNUSED_PARAMETER( iParam ); #endif - sqlite3VdbeAddOp3(v, OP_Function0, 0, regStat4, regOut); - sqlite3VdbeChangeP4(v, -1, (char*)&statGetFuncdef, P4_FUNCDEF); + sqlite3VdbeAddOp4(v, OP_Function0, 0, regStat4, regOut, + (char*)&statGetFuncdef, P4_FUNCDEF); sqlite3VdbeChangeP5(v, 1 + IsStat34); } @@ -91011,7 +95063,7 @@ static void analyzeOneTable( /* Do not gather statistics on views or virtual tables */ return; } - if( sqlite3_strnicmp(pTab->zName, "sqlite_", 7)==0 ){ + if( sqlite3_strlike("sqlite_%", pTab->zName, 0)==0 ){ /* Do not gather statistics on system tables */ return; } @@ -91119,8 +95171,8 @@ static void analyzeOneTable( #endif sqlite3VdbeAddOp2(v, OP_Integer, nCol, regStat4+1); sqlite3VdbeAddOp2(v, OP_Integer, pIdx->nKeyCol, regStat4+2); - sqlite3VdbeAddOp3(v, OP_Function0, 0, regStat4+1, regStat4); - sqlite3VdbeChangeP4(v, -1, (char*)&statInitFuncdef, P4_FUNCDEF); + sqlite3VdbeAddOp4(v, OP_Function0, 0, regStat4+1, regStat4, + (char*)&statInitFuncdef, P4_FUNCDEF); sqlite3VdbeChangeP5(v, 2+IsStat34); /* Implementation of the following: @@ -91139,7 +95191,7 @@ static void analyzeOneTable( if( nColTest>0 ){ int endDistinctTest = sqlite3VdbeMakeLabel(v); int *aGotoChng; /* Array of jump instruction addresses */ - aGotoChng = sqlite3DbMallocRaw(db, sizeof(int)*nColTest); + aGotoChng = sqlite3DbMallocRawNN(db, sizeof(int)*nColTest); if( aGotoChng==0 ) continue; /* @@ -91216,8 +95268,8 @@ static void analyzeOneTable( } #endif assert( regChng==(regStat4+1) ); - sqlite3VdbeAddOp3(v, OP_Function0, 1, regStat4, regTemp); - sqlite3VdbeChangeP4(v, -1, (char*)&statPushFuncdef, P4_FUNCDEF); + sqlite3VdbeAddOp4(v, OP_Function0, 1, regStat4, regTemp, + (char*)&statPushFuncdef, P4_FUNCDEF); sqlite3VdbeChangeP5(v, 2+IsStat34); sqlite3VdbeAddOp2(v, OP_Next, iIdxCur, addrNextRow); VdbeCoverage(v); @@ -91547,7 +95599,7 @@ static int analysisLoader(void *pData, int argc, char **argv, char **NotUsed){ ** the old data with the new instead of allocating a new array. */ if( pIndex->aiRowEst==0 ){ pIndex->aiRowEst = (tRowcnt*)sqlite3MallocZero(sizeof(tRowcnt) * nCol); - if( pIndex->aiRowEst==0 ) pInfo->db->mallocFailed = 1; + if( pIndex->aiRowEst==0 ) sqlite3OomFault(pInfo->db); } aiRowEst = pIndex->aiRowEst; #endif @@ -91694,10 +95746,10 @@ static int loadStatTbl( Index *pPrevIdx = 0; /* Previous index in the loop */ IndexSample *pSample; /* A slot in pIdx->aSample[] */ - assert( db->lookaside.bEnabled==0 ); + assert( db->lookaside.bDisable ); zSql = sqlite3MPrintf(db, zSql1, zDb); if( !zSql ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } rc = sqlite3_prepare(db, zSql, -1, &pStmt, 0); sqlite3DbFree(db, zSql); @@ -91737,7 +95789,7 @@ static int loadStatTbl( pIdx->aSample = sqlite3DbMallocZero(db, nByte); if( pIdx->aSample==0 ){ sqlite3_finalize(pStmt); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pSpace = (tRowcnt*)&pIdx->aSample[nSample]; pIdx->aAvgEq = pSpace; pSpace += nIdxCol; @@ -91753,7 +95805,7 @@ static int loadStatTbl( zSql = sqlite3MPrintf(db, zSql2, zDb); if( !zSql ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } rc = sqlite3_prepare(db, zSql, -1, &pStmt, 0); sqlite3DbFree(db, zSql); @@ -91791,7 +95843,7 @@ static int loadStatTbl( pSample->p = sqlite3DbMallocZero(db, pSample->n + 2); if( pSample->p==0 ){ sqlite3_finalize(pStmt); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } memcpy(pSample->p, sqlite3_column_blob(pStmt, 4), pSample->n); pIdx->nSample++; @@ -91808,7 +95860,7 @@ static int loadStatTbl( static int loadStat4(sqlite3 *db, const char *zDb){ int rc = SQLITE_OK; /* Result codes from subroutines */ - assert( db->lookaside.bEnabled==0 ); + assert( db->lookaside.bDisable ); if( sqlite3FindTable(db, "sqlite_stat4", zDb) ){ rc = loadStatTbl(db, 0, "SELECT idx,count(*) FROM %Q.sqlite_stat4 GROUP BY idx", @@ -91853,7 +95905,7 @@ SQLITE_PRIVATE int sqlite3AnalysisLoad(sqlite3 *db, int iDb){ analysisInfo sInfo; HashElem *i; char *zSql; - int rc; + int rc = SQLITE_OK; assert( iDb>=0 && iDbnDb ); assert( db->aDb[iDb].pBt!=0 ); @@ -91862,38 +95914,40 @@ SQLITE_PRIVATE int sqlite3AnalysisLoad(sqlite3 *db, int iDb){ assert( sqlite3SchemaMutexHeld(db, iDb, 0) ); for(i=sqliteHashFirst(&db->aDb[iDb].pSchema->idxHash);i;i=sqliteHashNext(i)){ Index *pIdx = sqliteHashData(i); - sqlite3DefaultRowEst(pIdx); + pIdx->aiRowLogEst[0] = 0; #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 sqlite3DeleteIndexSamples(db, pIdx); pIdx->aSample = 0; #endif } - /* Check to make sure the sqlite_stat1 table exists */ + /* Load new statistics out of the sqlite_stat1 table */ sInfo.db = db; sInfo.zDatabase = db->aDb[iDb].zName; - if( sqlite3FindTable(db, "sqlite_stat1", sInfo.zDatabase)==0 ){ - return SQLITE_ERROR; + if( sqlite3FindTable(db, "sqlite_stat1", sInfo.zDatabase)!=0 ){ + zSql = sqlite3MPrintf(db, + "SELECT tbl,idx,stat FROM %Q.sqlite_stat1", sInfo.zDatabase); + if( zSql==0 ){ + rc = SQLITE_NOMEM_BKPT; + }else{ + rc = sqlite3_exec(db, zSql, analysisLoader, &sInfo, 0); + sqlite3DbFree(db, zSql); + } } - /* Load new statistics out of the sqlite_stat1 table */ - zSql = sqlite3MPrintf(db, - "SELECT tbl,idx,stat FROM %Q.sqlite_stat1", sInfo.zDatabase); - if( zSql==0 ){ - rc = SQLITE_NOMEM; - }else{ - rc = sqlite3_exec(db, zSql, analysisLoader, &sInfo, 0); - sqlite3DbFree(db, zSql); + /* Set appropriate defaults on all indexes not in the sqlite_stat1 table */ + assert( sqlite3SchemaMutexHeld(db, iDb, 0) ); + for(i=sqliteHashFirst(&db->aDb[iDb].pSchema->idxHash);i;i=sqliteHashNext(i)){ + Index *pIdx = sqliteHashData(i); + if( pIdx->aiRowLogEst[0]==0 ) sqlite3DefaultRowEst(pIdx); } - /* Load the statistics from the sqlite_stat4 table. */ #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 if( rc==SQLITE_OK && OptimizationEnabled(db, SQLITE_Stat34) ){ - int lookasideEnabled = db->lookaside.bEnabled; - db->lookaside.bEnabled = 0; + db->lookaside.bDisable++; rc = loadStat4(db, sInfo.zDatabase); - db->lookaside.bEnabled = lookasideEnabled; + db->lookaside.bDisable--; } for(i=sqliteHashFirst(&db->aDb[iDb].pSchema->idxHash);i;i=sqliteHashNext(i)){ Index *pIdx = sqliteHashData(i); @@ -91903,7 +95957,7 @@ SQLITE_PRIVATE int sqlite3AnalysisLoad(sqlite3 *db, int iDb){ #endif if( rc==SQLITE_NOMEM ){ - db->mallocFailed = 1; + sqlite3OomFault(db); } return rc; } @@ -92024,7 +96078,7 @@ static void attachFunc( ** hash tables. */ if( db->aDb==db->aDbStatic ){ - aNew = sqlite3DbMallocRaw(db, sizeof(db->aDb[0])*3 ); + aNew = sqlite3DbMallocRawNN(db, sizeof(db->aDb[0])*3 ); if( aNew==0 ) return; memcpy(aNew, db->aDb, sizeof(db->aDb[0])*2); }else{ @@ -92042,7 +96096,7 @@ static void attachFunc( flags = db->openFlags; rc = sqlite3ParseUri(db->pVfs->zName, zFile, &flags, &pVfs, &zPath, &zErr); if( rc!=SQLITE_OK ){ - if( rc==SQLITE_NOMEM ) db->mallocFailed = 1; + if( rc==SQLITE_NOMEM ) sqlite3OomFault(db); sqlite3_result_error(context, zErr, -1); sqlite3_free(zErr); return; @@ -92059,7 +96113,7 @@ static void attachFunc( Pager *pPager; aNew->pSchema = sqlite3SchemaGet(db, aNew->pBt); if( !aNew->pSchema ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else if( aNew->pSchema->file_format && aNew->pSchema->enc!=ENC(db) ){ zErrDyn = sqlite3MPrintf(db, "attached databases must use the same text encoding as main database"); @@ -92071,14 +96125,15 @@ static void attachFunc( sqlite3BtreeSecureDelete(aNew->pBt, sqlite3BtreeSecureDelete(db->aDb[0].pBt,-1) ); #ifndef SQLITE_OMIT_PAGER_PRAGMAS - sqlite3BtreeSetPagerFlags(aNew->pBt, 3 | (db->flags & PAGER_FLAGS_MASK)); + sqlite3BtreeSetPagerFlags(aNew->pBt, + PAGER_SYNCHRONOUS_FULL | (db->flags & PAGER_FLAGS_MASK)); #endif sqlite3BtreeLeave(aNew->pBt); } - aNew->safety_level = 3; + aNew->safety_level = SQLITE_DEFAULT_SYNCHRONOUS+1; aNew->zName = sqlite3DbStrDup(db, zName); if( rc==SQLITE_OK && aNew->zName==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } @@ -92144,7 +96199,7 @@ static void attachFunc( sqlite3ResetAllSchemasOfConnection(db); db->nDb = iDb; if( rc==SQLITE_NOMEM || rc==SQLITE_IOERR_NOMEM ){ - db->mallocFailed = 1; + sqlite3OomFault(db); sqlite3DbFree(db, zErrDyn); zErrDyn = sqlite3MPrintf(db, "out of memory"); }else if( zErrDyn==0 ){ @@ -92274,11 +96329,11 @@ static void codeAttach( assert( v || db->mallocFailed ); if( v ){ - sqlite3VdbeAddOp3(v, OP_Function0, 0, regArgs+3-pFunc->nArg, regArgs+3); + sqlite3VdbeAddOp4(v, OP_Function0, 0, regArgs+3-pFunc->nArg, regArgs+3, + (char *)pFunc, P4_FUNCDEF); assert( pFunc->nArg==-1 || (pFunc->nArg&0xff)==pFunc->nArg ); sqlite3VdbeChangeP5(v, (u8)(pFunc->nArg)); - sqlite3VdbeChangeP4(v, -1, (char *)pFunc, P4_FUNCDEF); - + /* Code an OP_Expire. For an ATTACH statement, set P1 to true (expire this ** statement only). For DETACH, set it to false (expire all existing ** statements). @@ -92303,12 +96358,10 @@ SQLITE_PRIVATE void sqlite3Detach(Parse *pParse, Expr *pDbname){ SQLITE_UTF8, /* funcFlags */ 0, /* pUserData */ 0, /* pNext */ - detachFunc, /* xFunc */ - 0, /* xStep */ + detachFunc, /* xSFunc */ 0, /* xFinalize */ "sqlite_detach", /* zName */ - 0, /* pHash */ - 0 /* pDestructor */ + {0} }; codeAttach(pParse, SQLITE_DETACH, &detach_func, pDbname, 0, 0, pDbname); } @@ -92324,12 +96377,10 @@ SQLITE_PRIVATE void sqlite3Attach(Parse *pParse, Expr *p, Expr *pDbname, Expr *p SQLITE_UTF8, /* funcFlags */ 0, /* pUserData */ 0, /* pNext */ - attachFunc, /* xFunc */ - 0, /* xStep */ + attachFunc, /* xSFunc */ 0, /* xFinalize */ "sqlite_attach", /* zName */ - 0, /* pHash */ - 0 /* pDestructor */ + {0} }; codeAttach(pParse, SQLITE_ATTACH, &attach_func, p, p, pDbname, pKey); } @@ -92789,15 +96840,6 @@ SQLITE_PRIVATE void sqlite3AuthContextPop(AuthContext *pContext){ */ /* #include "sqliteInt.h" */ -/* -** This routine is called when a new SQL statement is beginning to -** be parsed. Initialize the pParse structure as needed. -*/ -SQLITE_PRIVATE void sqlite3BeginParse(Parse *pParse, int explainFlag){ - pParse->explain = (u8)explainFlag; - pParse->nVar = 0; -} - #ifndef SQLITE_OMIT_SHARED_CACHE /* ** The TableLock structure is only used by the sqlite3TableLock() and @@ -92852,7 +96894,7 @@ SQLITE_PRIVATE void sqlite3TableLock( p->zName = zName; }else{ pToplevel->nTableLock = 0; - pToplevel->db->mallocFailed = 1; + sqlite3OomFault(pToplevel->db); } } @@ -93002,15 +97044,19 @@ SQLITE_PRIVATE void sqlite3FinishCoding(Parse *pParse){ if( pParse->pAinc!=0 && pParse->nTab==0 ) pParse->nTab = 1; sqlite3VdbeMakeReady(v, pParse); pParse->rc = SQLITE_DONE; - pParse->colNamesSet = 0; }else{ pParse->rc = SQLITE_ERROR; } + + /* We are done with this Parse object. There is no need to de-initialize it */ +#if 0 + pParse->colNamesSet = 0; pParse->nTab = 0; pParse->nMem = 0; pParse->nSet = 0; pParse->nVar = 0; DbMaskZero(pParse->cookieMask); +#endif } /* @@ -93141,12 +97187,7 @@ SQLITE_PRIVATE Table *sqlite3LocateTable( } pParse->checkSchema = 1; } -#if SQLITE_USER_AUTHENTICATION - else if( pParse->db->auth.authLevelpPartIdxWhere); sqlite3ExprListDelete(db, p->aColExpr); sqlite3DbFree(db, p->zColAff); - if( p->isResized ) sqlite3DbFree(db, p->azColl); + if( p->isResized ) sqlite3DbFree(db, (void *)p->azColl); #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 sqlite3_free(p->aiRowEst); #endif @@ -93274,7 +97315,6 @@ SQLITE_PRIVATE void sqlite3CollapseDatabaseArray(sqlite3 *db){ } j++; } - memset(&db->aDb[j], 0, (db->nDb-j)*sizeof(db->aDb[j])); db->nDb = j; if( db->nDb<=2 && db->aDb!=db->aDbStatic ){ memcpy(db->aDbStatic, db->aDb, 2*sizeof(db->aDb[0])); @@ -93347,8 +97387,6 @@ SQLITE_PRIVATE void sqlite3DeleteColumnNames(sqlite3 *db, Table *pTable){ for(i=0; inCol; i++, pCol++){ sqlite3DbFree(db, pCol->zName); sqlite3ExprDelete(db, pCol->pDflt); - sqlite3DbFree(db, pCol->zDflt); - sqlite3DbFree(db, pCol->zType); sqlite3DbFree(db, pCol->zColl); } sqlite3DbFree(db, pTable->aCol); @@ -93370,16 +97408,10 @@ SQLITE_PRIVATE void sqlite3DeleteColumnNames(sqlite3 *db, Table *pTable){ ** db parameter can be used with db->pnBytesFreed to measure the memory ** used by the Table object. */ -SQLITE_PRIVATE void sqlite3DeleteTable(sqlite3 *db, Table *pTable){ +static void SQLITE_NOINLINE deleteTable(sqlite3 *db, Table *pTable){ Index *pIndex, *pNext; TESTONLY( int nLookaside; ) /* Used to verify lookaside not used for schema */ - assert( !pTable || pTable->nRef>0 ); - - /* Do not delete the table until the reference count reaches zero. */ - if( !pTable ) return; - if( ((!db || db->pnBytesFreed==0) && (--pTable->nRef)>0) ) return; - /* Record the number of outstanding lookaside allocations in schema Tables ** prior to doing any free() operations. Since schema Tables do not use ** lookaside, this number should not change. */ @@ -93419,6 +97451,13 @@ SQLITE_PRIVATE void sqlite3DeleteTable(sqlite3 *db, Table *pTable){ /* Verify that no lookaside memory was used by schema tables */ assert( nLookaside==0 || nLookaside==db->lookaside.nOut ); } +SQLITE_PRIVATE void sqlite3DeleteTable(sqlite3 *db, Table *pTable){ + /* Do not delete the table until the reference count reaches zero. */ + if( !pTable ) return; + if( ((!db || db->pnBytesFreed==0) && (--pTable->nRef)>0) ) return; + deleteTable(db, pTable); +} + /* ** Unlink the given table from the hash tables and the delete the @@ -93486,12 +97525,8 @@ SQLITE_PRIVATE int sqlite3FindDbName(sqlite3 *db, const char *zName){ int i = -1; /* Database number */ if( zName ){ Db *pDb; - int n = sqlite3Strlen30(zName); for(i=(db->nDb-1), pDb=&db->aDb[i]; i>=0; i--, pDb--){ - if( (!OMIT_TEMPDB || i!=1 ) && n==sqlite3Strlen30(pDb->zName) && - 0==sqlite3StrICmp(pDb->zName, zName) ){ - break; - } + if( 0==sqlite3StrICmp(pDb->zName, zName) ) break; } } return i; @@ -93537,7 +97572,8 @@ SQLITE_PRIVATE int sqlite3TwoPartName( int iDb; /* Database holding the object */ sqlite3 *db = pParse->db; - if( ALWAYS(pName2!=0) && pName2->n>0 ){ + assert( pName2!=0 ); + if( pName2->n>0 ){ if( db->init.busy ) { sqlite3ErrorMsg(pParse, "corrupt database"); return -1; @@ -93626,62 +97662,46 @@ SQLITE_PRIVATE void sqlite3StartTable( int iDb; /* Database number to create the table in */ Token *pName; /* Unqualified name of the table to create */ - /* The table or view name to create is passed to this routine via tokens - ** pName1 and pName2. If the table name was fully qualified, for example: - ** - ** CREATE TABLE xxx.yyy (...); - ** - ** Then pName1 is set to "xxx" and pName2 "yyy". On the other hand if - ** the table name is not fully qualified, i.e.: - ** - ** CREATE TABLE yyy(...); - ** - ** Then pName1 is set to "yyy" and pName2 is "". - ** - ** The call below sets the pName pointer to point at the token (pName1 or - ** pName2) that stores the unqualified table name. The variable iDb is - ** set to the index of the database that the table or view is to be - ** created in. - */ - iDb = sqlite3TwoPartName(pParse, pName1, pName2, &pName); - if( iDb<0 ) return; - if( !OMIT_TEMPDB && isTemp && pName2->n>0 && iDb!=1 ){ - /* If creating a temp table, the name may not be qualified. Unless - ** the database name is "temp" anyway. */ - sqlite3ErrorMsg(pParse, "temporary table name must be unqualified"); - return; + if( db->init.busy && db->init.newTnum==1 ){ + /* Special case: Parsing the sqlite_master or sqlite_temp_master schema */ + iDb = db->init.iDb; + zName = sqlite3DbStrDup(db, SCHEMA_TABLE(iDb)); + pName = pName1; + }else{ + /* The common case */ + iDb = sqlite3TwoPartName(pParse, pName1, pName2, &pName); + if( iDb<0 ) return; + if( !OMIT_TEMPDB && isTemp && pName2->n>0 && iDb!=1 ){ + /* If creating a temp table, the name may not be qualified. Unless + ** the database name is "temp" anyway. */ + sqlite3ErrorMsg(pParse, "temporary table name must be unqualified"); + return; + } + if( !OMIT_TEMPDB && isTemp ) iDb = 1; + zName = sqlite3NameFromToken(db, pName); } - if( !OMIT_TEMPDB && isTemp ) iDb = 1; - pParse->sNameToken = *pName; - zName = sqlite3NameFromToken(db, pName); if( zName==0 ) return; if( SQLITE_OK!=sqlite3CheckObjectName(pParse, zName) ){ goto begin_table_error; } if( db->init.iDb==1 ) isTemp = 1; #ifndef SQLITE_OMIT_AUTHORIZATION - assert( (isTemp & 1)==isTemp ); + assert( isTemp==0 || isTemp==1 ); + assert( isView==0 || isView==1 ); { - int code; + static const u8 aCode[] = { + SQLITE_CREATE_TABLE, + SQLITE_CREATE_TEMP_TABLE, + SQLITE_CREATE_VIEW, + SQLITE_CREATE_TEMP_VIEW + }; char *zDb = db->aDb[iDb].zName; if( sqlite3AuthCheck(pParse, SQLITE_INSERT, SCHEMA_TABLE(isTemp), 0, zDb) ){ goto begin_table_error; } - if( isView ){ - if( !OMIT_TEMPDB && isTemp ){ - code = SQLITE_CREATE_TEMP_VIEW; - }else{ - code = SQLITE_CREATE_VIEW; - } - }else{ - if( !OMIT_TEMPDB && isTemp ){ - code = SQLITE_CREATE_TEMP_TABLE; - }else{ - code = SQLITE_CREATE_TABLE; - } - } - if( !isVirtual && sqlite3AuthCheck(pParse, code, zName, 0, zDb) ){ + if( !isVirtual && sqlite3AuthCheck(pParse, (int)aCode[isTemp+2*isView], + zName, 0, zDb) ){ goto begin_table_error; } } @@ -93717,8 +97737,8 @@ SQLITE_PRIVATE void sqlite3StartTable( pTable = sqlite3DbMallocZero(db, sizeof(Table)); if( pTable==0 ){ - db->mallocFailed = 1; - pParse->rc = SQLITE_NOMEM; + assert( db->mallocFailed ); + pParse->rc = SQLITE_NOMEM_BKPT; pParse->nErr++; goto begin_table_error; } @@ -93774,10 +97794,8 @@ SQLITE_PRIVATE void sqlite3StartTable( addr1 = sqlite3VdbeAddOp1(v, OP_If, reg3); VdbeCoverage(v); fileFormat = (db->flags & SQLITE_LegacyFileFmt)!=0 ? 1 : SQLITE_MAX_FILE_FORMAT; - sqlite3VdbeAddOp2(v, OP_Integer, fileFormat, reg3); - sqlite3VdbeAddOp3(v, OP_SetCookie, iDb, BTREE_FILE_FORMAT, reg3); - sqlite3VdbeAddOp2(v, OP_Integer, ENC(db), reg3); - sqlite3VdbeAddOp3(v, OP_SetCookie, iDb, BTREE_TEXT_ENCODING, reg3); + sqlite3VdbeAddOp3(v, OP_SetCookie, iDb, BTREE_FILE_FORMAT, fileFormat); + sqlite3VdbeAddOp3(v, OP_SetCookie, iDb, BTREE_TEXT_ENCODING, ENC(db)); sqlite3VdbeJumpHere(v, addr1); /* This just creates a place-holder record in the sqlite_master table. @@ -93814,18 +97832,19 @@ begin_table_error: return; } -/* -** This macro is used to compare two strings in a case-insensitive manner. -** It is slightly faster than calling sqlite3StrICmp() directly, but -** produces larger code. -** -** WARNING: This macro is not compatible with the strcmp() family. It -** returns true if the two strings are equal, otherwise false. +/* Set properties of a table column based on the (magical) +** name of the column. */ -#define STRICMP(x, y) (\ -sqlite3UpperToLower[*(unsigned char *)(x)]== \ -sqlite3UpperToLower[*(unsigned char *)(y)] \ -&& sqlite3StrICmp((x)+1,(y)+1)==0 ) +#if SQLITE_ENABLE_HIDDEN_COLUMNS +SQLITE_PRIVATE void sqlite3ColumnPropertiesFromName(Table *pTab, Column *pCol){ + if( sqlite3_strnicmp(pCol->zName, "__hidden__", 10)==0 ){ + pCol->colFlags |= COLFLAG_HIDDEN; + }else if( pTab && pCol!=pTab->aCol && (pCol[-1].colFlags & COLFLAG_HIDDEN) ){ + pTab->tabFlags |= TF_OOOHidden; + } +} +#endif + /* ** Add a new column to the table currently being constructed. @@ -93835,10 +97854,11 @@ sqlite3UpperToLower[*(unsigned char *)(y)] \ ** first to get things going. Then this routine is called for each ** column. */ -SQLITE_PRIVATE void sqlite3AddColumn(Parse *pParse, Token *pName){ +SQLITE_PRIVATE void sqlite3AddColumn(Parse *pParse, Token *pName, Token *pType){ Table *p; int i; char *z; + char *zType; Column *pCol; sqlite3 *db = pParse->db; if( (p = pParse->pNewTable)==0 ) return; @@ -93848,10 +97868,13 @@ SQLITE_PRIVATE void sqlite3AddColumn(Parse *pParse, Token *pName){ return; } #endif - z = sqlite3NameFromToken(db, pName); + z = sqlite3DbMallocRaw(db, pName->n + pType->n + 2); if( z==0 ) return; + memcpy(z, pName->z, pName->n); + z[pName->n] = 0; + sqlite3Dequote(z); for(i=0; inCol; i++){ - if( STRICMP(z, p->aCol[i].zName) ){ + if( sqlite3_stricmp(z, p->aCol[i].zName)==0 ){ sqlite3ErrorMsg(pParse, "duplicate column name: %s", z); sqlite3DbFree(db, z); return; @@ -93869,14 +97892,23 @@ SQLITE_PRIVATE void sqlite3AddColumn(Parse *pParse, Token *pName){ pCol = &p->aCol[p->nCol]; memset(pCol, 0, sizeof(p->aCol[0])); pCol->zName = z; + sqlite3ColumnPropertiesFromName(p, pCol); - /* If there is no type specified, columns have the default affinity - ** 'BLOB'. If there is a type specified, then sqlite3AddColumnType() will - ** be called next to set pCol->affinity correctly. - */ - pCol->affinity = SQLITE_AFF_BLOB; - pCol->szEst = 1; + if( pType->n==0 ){ + /* If there is no type specified, columns have the default affinity + ** 'BLOB'. */ + pCol->affinity = SQLITE_AFF_BLOB; + pCol->szEst = 1; + }else{ + zType = z + sqlite3Strlen30(z) + 1; + memcpy(zType, pType->z, pType->n); + zType[pType->n] = 0; + sqlite3Dequote(zType); + pCol->affinity = sqlite3AffinityType(zType, &pCol->szEst); + pCol->colFlags |= COLFLAG_HASTYPE; + } p->nCol++; + pParse->constraintName.n = 0; } /* @@ -93922,7 +97954,7 @@ SQLITE_PRIVATE char sqlite3AffinityType(const char *zIn, u8 *pszEst){ char aff = SQLITE_AFF_NUMERIC; const char *zChar = 0; - if( zIn==0 ) return aff; + assert( zIn!=0 ); while( zIn[0] ){ h = (h<<8) + sqlite3UpperToLower[(*zIn)&0xff]; zIn++; @@ -93979,28 +98011,6 @@ SQLITE_PRIVATE char sqlite3AffinityType(const char *zIn, u8 *pszEst){ return aff; } -/* -** This routine is called by the parser while in the middle of -** parsing a CREATE TABLE statement. The pFirst token is the first -** token in the sequence of tokens that describe the type of the -** column currently under construction. pLast is the last token -** in the sequence. Use this information to construct a string -** that contains the typename of the column and store that string -** in zType. -*/ -SQLITE_PRIVATE void sqlite3AddColumnType(Parse *pParse, Token *pType){ - Table *p; - Column *pCol; - - p = pParse->pNewTable; - if( p==0 || NEVER(p->nCol<1) ) return; - pCol = &p->aCol[p->nCol-1]; - assert( pCol->zType==0 || CORRUPT_DB ); - sqlite3DbFree(pParse->db, pCol->zType); - pCol->zType = sqlite3NameFromToken(pParse->db, pType); - pCol->affinity = sqlite3AffinityType(pCol->zType, &pCol->szEst); -} - /* ** The expression is the default value for the most recently added column ** of the table currently under construction. @@ -94026,11 +98036,16 @@ SQLITE_PRIVATE void sqlite3AddDefaultValue(Parse *pParse, ExprSpan *pSpan){ ** tokens that point to volatile memory. The 'span' of the expression ** is required by pragma table_info. */ + Expr x; sqlite3ExprDelete(db, pCol->pDflt); - pCol->pDflt = sqlite3ExprDup(db, pSpan->pExpr, EXPRDUP_REDUCE); - sqlite3DbFree(db, pCol->zDflt); - pCol->zDflt = sqlite3DbStrNDup(db, (char*)pSpan->zStart, - (int)(pSpan->zEnd - pSpan->zStart)); + memset(&x, 0, sizeof(x)); + x.op = TK_SPAN; + x.u.zToken = sqlite3DbStrNDup(db, (char*)pSpan->zStart, + (int)(pSpan->zEnd - pSpan->zStart)); + x.pLeft = pSpan->pExpr; + x.flags = EP_Skip; + pCol->pDflt = sqlite3ExprDup(db, &x, EXPRDUP_REDUCE); + sqlite3DbFree(db, x.u.zToken); } } sqlite3ExprDelete(db, pSpan->pExpr); @@ -94086,7 +98101,7 @@ SQLITE_PRIVATE void sqlite3AddPrimaryKey( int sortOrder /* SQLITE_SO_ASC or SQLITE_SO_DESC */ ){ Table *pTab = pParse->pNewTable; - char *zType = 0; + Column *pCol = 0; int iCol = -1, i; int nTerm; if( pTab==0 || IN_DECLARE_VTAB ) goto primary_key_exit; @@ -94098,8 +98113,8 @@ SQLITE_PRIVATE void sqlite3AddPrimaryKey( pTab->tabFlags |= TF_HasPrimaryKey; if( pList==0 ){ iCol = pTab->nCol - 1; - pTab->aCol[iCol].colFlags |= COLFLAG_PRIMKEY; - zType = pTab->aCol[iCol].zType; + pCol = &pTab->aCol[iCol]; + pCol->colFlags |= COLFLAG_PRIMKEY; nTerm = 1; }else{ nTerm = pList->nExpr; @@ -94111,8 +98126,8 @@ SQLITE_PRIVATE void sqlite3AddPrimaryKey( const char *zCName = pCExpr->u.zToken; for(iCol=0; iColnCol; iCol++){ if( sqlite3StrICmp(zCName, pTab->aCol[iCol].zName)==0 ){ - pTab->aCol[iCol].colFlags |= COLFLAG_PRIMKEY; - zType = pTab->aCol[iCol].zType; + pCol = &pTab->aCol[iCol]; + pCol->colFlags |= COLFLAG_PRIMKEY; break; } } @@ -94120,7 +98135,8 @@ SQLITE_PRIVATE void sqlite3AddPrimaryKey( } } if( nTerm==1 - && zType && sqlite3StrICmp(zType, "INTEGER")==0 + && pCol + && sqlite3StrICmp(sqlite3ColumnType(pCol,""), "INTEGER")==0 && sortOrder!=SQLITE_SO_DESC ){ pTab->iPKey = iCol; @@ -94260,13 +98276,11 @@ SQLITE_PRIVATE CollSeq *sqlite3LocateCollSeq(Parse *pParse, const char *zName){ ** 1 chance in 2^32. So we're safe enough. */ SQLITE_PRIVATE void sqlite3ChangeCookie(Parse *pParse, int iDb){ - int r1 = sqlite3GetTempReg(pParse); sqlite3 *db = pParse->db; Vdbe *v = pParse->pVdbe; assert( sqlite3SchemaMutexHeld(db, iDb, 0) ); - sqlite3VdbeAddOp2(v, OP_Integer, db->aDb[iDb].pSchema->schema_cookie+1, r1); - sqlite3VdbeAddOp3(v, OP_SetCookie, iDb, BTREE_SCHEMA_VERSION, r1); - sqlite3ReleaseTempReg(pParse, r1); + sqlite3VdbeAddOp3(v, OP_SetCookie, iDb, BTREE_SCHEMA_VERSION, + db->aDb[iDb].pSchema->schema_cookie+1); } /* @@ -94348,7 +98362,7 @@ static char *createTableStmt(sqlite3 *db, Table *p){ n += 35 + 6*p->nCol; zStmt = sqlite3DbMallocRaw(0, n); if( zStmt==0 ){ - db->mallocFailed = 1; + sqlite3OomFault(db); return 0; } sqlite3_snprintf(n, zStmt, "CREATE TABLE "); @@ -94401,9 +98415,9 @@ static int resizeIndexObject(sqlite3 *db, Index *pIdx, int N){ assert( pIdx->isResized==0 ); nByte = (sizeof(char*) + sizeof(i16) + 1)*N; zExtra = sqlite3DbMallocZero(db, nByte); - if( zExtra==0 ) return SQLITE_NOMEM; + if( zExtra==0 ) return SQLITE_NOMEM_BKPT; memcpy(zExtra, pIdx->azColl, sizeof(char*)*pIdx->nColumn); - pIdx->azColl = (char**)zExtra; + pIdx->azColl = (const char**)zExtra; zExtra += sizeof(char*)*N; memcpy(zExtra, pIdx->aiColumn, sizeof(i16)*pIdx->nColumn); pIdx->aiColumn = (i16*)zExtra; @@ -94497,8 +98511,7 @@ static void convertToWithoutRowidTable(Parse *pParse, Table *pTab){ if( pTab->iPKey>=0 ){ ExprList *pList; Token ipkToken; - ipkToken.z = pTab->aCol[pTab->iPKey].zName; - ipkToken.n = sqlite3Strlen30(ipkToken.z); + sqlite3TokenInit(&ipkToken, pTab->aCol[pTab->iPKey].zName); pList = sqlite3ExprListAppend(pParse, 0, sqlite3ExprAlloc(db, TK_ID, &ipkToken, 0)); if( pList==0 ) return; @@ -94542,7 +98555,7 @@ static void convertToWithoutRowidTable(Parse *pParse, Table *pTab){ ** do not enforce this for imposter tables.) */ if( !db->init.imposterTable ){ for(i=0; iaCol[pPk->aiColumn[i]].notNull = 1; + pTab->aCol[pPk->aiColumn[i]].notNull = OE_Abort; } pPk->uniqNotNull = 1; } @@ -94584,7 +98597,7 @@ static void convertToWithoutRowidTable(Parse *pParse, Table *pTab){ if( !hasColumn(pPk->aiColumn, j, i) ){ assert( jnColumn ); pPk->aiColumn[j] = i; - pPk->azColl[j] = "BINARY"; + pPk->azColl[j] = sqlite3StrBINARY; j++; } } @@ -94641,9 +98654,13 @@ SQLITE_PRIVATE void sqlite3EndTable( ** So do not write to the disk again. Extract the root page number ** for the table from the db->init.newTnum field. (The page number ** should have been put there by the sqliteOpenCb routine.) + ** + ** If the root page number is 1, that means this is the sqlite_master + ** table itself. So mark it read-only. */ if( db->init.busy ){ p->tnum = db->init.newTnum; + if( p->tnum==1 ) p->tabFlags |= TF_Readonly; } /* Special processing for WITHOUT ROWID Tables */ @@ -94744,7 +98761,7 @@ SQLITE_PRIVATE void sqlite3EndTable( sqlite3VdbeAddOp3(v, OP_InitCoroutine, regYield, 0, addrTop); sqlite3SelectDestInit(&dest, SRT_Coroutine, regYield); sqlite3Select(pParse, pSelect, &dest); - sqlite3VdbeAddOp1(v, OP_EndCoroutine, regYield); + sqlite3VdbeEndCoroutine(v, regYield); sqlite3VdbeJumpHere(v, addrTop - 1); if( pParse->nErr ) return; pSelTab = sqlite3ResultSetOfSelect(pParse, pSelect); @@ -94828,7 +98845,7 @@ SQLITE_PRIVATE void sqlite3EndTable( pOld = sqlite3HashInsert(&pSchema->tblHash, p->zName, p); if( pOld ){ assert( p==pOld ); /* Malloc must have failed inside HashInsert() */ - db->mallocFailed = 1; + sqlite3OomFault(db); return; } pParse->pNewTable = 0; @@ -94932,7 +98949,6 @@ SQLITE_PRIVATE int sqlite3ViewGetColumnNames(Parse *pParse, Table *pTable){ int n; /* Temporarily holds the number of cursors assigned */ sqlite3 *db = pParse->db; /* Database connection for malloc errors */ sqlite3_xauth xAuth; /* Saved xAuth pointer */ - u8 bEnabledLA; /* Saved db->lookaside.bEnabled state */ assert( pTable ); @@ -94978,45 +98994,56 @@ SQLITE_PRIVATE int sqlite3ViewGetColumnNames(Parse *pParse, Table *pTable){ ** statement that defines the view. */ assert( pTable->pSelect ); - bEnabledLA = db->lookaside.bEnabled; - if( pTable->pCheck ){ - db->lookaside.bEnabled = 0; - sqlite3ColumnsFromExprList(pParse, pTable->pCheck, - &pTable->nCol, &pTable->aCol); - }else{ - pSel = sqlite3SelectDup(db, pTable->pSelect, 0); - if( pSel ){ - n = pParse->nTab; - sqlite3SrcListAssignCursors(pParse, pSel->pSrc); - pTable->nCol = -1; - db->lookaside.bEnabled = 0; + pSel = sqlite3SelectDup(db, pTable->pSelect, 0); + if( pSel ){ + n = pParse->nTab; + sqlite3SrcListAssignCursors(pParse, pSel->pSrc); + pTable->nCol = -1; + db->lookaside.bDisable++; #ifndef SQLITE_OMIT_AUTHORIZATION - xAuth = db->xAuth; - db->xAuth = 0; - pSelTab = sqlite3ResultSetOfSelect(pParse, pSel); - db->xAuth = xAuth; + xAuth = db->xAuth; + db->xAuth = 0; + pSelTab = sqlite3ResultSetOfSelect(pParse, pSel); + db->xAuth = xAuth; #else - pSelTab = sqlite3ResultSetOfSelect(pParse, pSel); + pSelTab = sqlite3ResultSetOfSelect(pParse, pSel); #endif - pParse->nTab = n; - if( pSelTab ){ - assert( pTable->aCol==0 ); - pTable->nCol = pSelTab->nCol; - pTable->aCol = pSelTab->aCol; - pSelTab->nCol = 0; - pSelTab->aCol = 0; - sqlite3DeleteTable(db, pSelTab); - assert( sqlite3SchemaMutexHeld(db, 0, pTable->pSchema) ); - }else{ - pTable->nCol = 0; - nErr++; + pParse->nTab = n; + if( pTable->pCheck ){ + /* CREATE VIEW name(arglist) AS ... + ** The names of the columns in the table are taken from + ** arglist which is stored in pTable->pCheck. The pCheck field + ** normally holds CHECK constraints on an ordinary table, but for + ** a VIEW it holds the list of column names. + */ + sqlite3ColumnsFromExprList(pParse, pTable->pCheck, + &pTable->nCol, &pTable->aCol); + if( db->mallocFailed==0 + && pParse->nErr==0 + && pTable->nCol==pSel->pEList->nExpr + ){ + sqlite3SelectAddColumnTypeAndCollation(pParse, pTable, pSel); } - sqlite3SelectDelete(db, pSel); - } else { + }else if( pSelTab ){ + /* CREATE VIEW name AS... without an argument list. Construct + ** the column names from the SELECT statement that defines the view. + */ + assert( pTable->aCol==0 ); + pTable->nCol = pSelTab->nCol; + pTable->aCol = pSelTab->aCol; + pSelTab->nCol = 0; + pSelTab->aCol = 0; + assert( sqlite3SchemaMutexHeld(db, 0, pTable->pSchema) ); + }else{ + pTable->nCol = 0; nErr++; } + sqlite3DeleteTable(db, pSelTab); + sqlite3SelectDelete(db, pSel); + db->lookaside.bDisable--; + } else { + nErr++; } - db->lookaside.bEnabled = bEnabledLA; pTable->pSchema->schemaFlags |= DB_UnresetViews; #endif /* SQLITE_OMIT_VIEW */ return nErr; @@ -95096,6 +99123,7 @@ SQLITE_PRIVATE void sqlite3RootPageMoved(sqlite3 *db, int iDb, int iFrom, int iT static void destroyRootPage(Parse *pParse, int iTable, int iDb){ Vdbe *v = sqlite3GetVdbe(pParse); int r1 = sqlite3GetTempReg(pParse); + assert( iTable>1 ); sqlite3VdbeAddOp3(v, OP_Destroy, iTable, r1, iDb); sqlite3MayAbort(pParse); #ifndef SQLITE_OMIT_AUTOVACUUM @@ -95481,7 +99509,7 @@ SQLITE_PRIVATE void sqlite3CreateForeignKey( pFKey->zTo, (void *)pFKey ); if( pNextTo==pFKey ){ - db->mallocFailed = 1; + sqlite3OomFault(db); goto fk_end; } if( pNextTo ){ @@ -95563,6 +99591,7 @@ static void sqlite3RefillIndex(Parse *pParse, Index *pIndex, int memRootPage){ tnum = pIndex->tnum; } pKey = sqlite3KeyInfoOfIndex(pParse, pIndex); + assert( pKey!=0 || db->mallocFailed || pParse->nErr ); /* Open the sorter cursor if we are to use one. */ iSorter = pParse->nTab++; @@ -95586,8 +99615,7 @@ static void sqlite3RefillIndex(Parse *pParse, Index *pIndex, int memRootPage){ sqlite3VdbeChangeP5(v, OPFLAG_BULKCSR|((memRootPage>=0)?OPFLAG_P2ISREG:0)); addr1 = sqlite3VdbeAddOp2(v, OP_SorterSort, iSorter, 0); VdbeCoverage(v); - assert( pKey!=0 || db->mallocFailed || pParse->nErr ); - if( IsUniqueIndex(pIndex) && pKey!=0 ){ + if( IsUniqueIndex(pIndex) ){ int j2 = sqlite3VdbeCurrentAddr(v) + 3; sqlite3VdbeGoto(v, j2); addr2 = sqlite3VdbeCurrentAddr(v); @@ -95634,7 +99662,7 @@ SQLITE_PRIVATE Index *sqlite3AllocateIndexObject( p = sqlite3DbMallocZero(db, nByte + nExtra); if( p ){ char *pExtra = ((char*)p)+ROUND8(sizeof(Index)); - p->azColl = (char**)pExtra; pExtra += ROUND8(sizeof(char*)*nCol); + p->azColl = (const char**)pExtra; pExtra += ROUND8(sizeof(char*)*nCol); p->aiRowLogEst = (LogEst*)pExtra; pExtra += sizeof(LogEst)*(nCol+1); p->aiColumn = (i16*)pExtra; pExtra += sizeof(i16)*nCol; p->aSortOrder = (u8*)pExtra; @@ -95841,8 +99869,7 @@ SQLITE_PRIVATE Index *sqlite3CreateIndex( */ if( pList==0 ){ Token prevCol; - prevCol.z = pTab->aCol[pTab->nCol-1].zName; - prevCol.n = sqlite3Strlen30(prevCol.z); + sqlite3TokenInit(&prevCol, pTab->aCol[pTab->nCol-1].zName); pList = sqlite3ExprListAppend(pParse, 0, sqlite3ExprAlloc(db, TK_ID, &prevCol, 0)); if( pList==0 ) goto exit_create_index; @@ -95911,7 +99938,7 @@ SQLITE_PRIVATE Index *sqlite3CreateIndex( for(i=0, pListItem=pList->a; inExpr; i++, pListItem++){ Expr *pCExpr; /* The i-th index expression */ int requestedSortOrder; /* ASC or DESC on the i-th expression */ - char *zColl; /* Collation sequence name */ + const char *zColl; /* Collation sequence name */ sqlite3StringToId(pListItem->pExpr); sqlite3ResolveSelfReference(pParse, pTab, NC_IdxExpr, pListItem->pExpr, 0); @@ -95957,7 +99984,7 @@ SQLITE_PRIVATE Index *sqlite3CreateIndex( }else if( j>=0 ){ zColl = pTab->aCol[j].zColl; } - if( !zColl ) zColl = "BINARY"; + if( !zColl ) zColl = sqlite3StrBINARY; if( !db->init.busy && !sqlite3LocateCollSeq(pParse, zColl) ){ goto exit_create_index; } @@ -95986,11 +100013,25 @@ SQLITE_PRIVATE Index *sqlite3CreateIndex( assert( i==pIndex->nColumn ); }else{ pIndex->aiColumn[i] = XN_ROWID; - pIndex->azColl[i] = "BINARY"; + pIndex->azColl[i] = sqlite3StrBINARY; } sqlite3DefaultRowEst(pIndex); if( pParse->pNewTable==0 ) estimateIndexWidth(pIndex); + /* If this index contains every column of its table, then mark + ** it as a covering index */ + assert( HasRowid(pTab) + || pTab->iPKey<0 || sqlite3ColumnOfIndex(pIndex, pTab->iPKey)>=0 ); + if( pTblName!=0 && pIndex->nColumn>=pTab->nCol ){ + pIndex->isCovering = 1; + for(j=0; jnCol; j++){ + if( j==pTab->iPKey ) continue; + if( sqlite3ColumnOfIndex(pIndex,j)>=0 ) continue; + pIndex->isCovering = 0; + break; + } + } + if( pTab==pParse->pNewTable ){ /* This routine has been called to create an automatic index as a ** result of a PRIMARY KEY or UNIQUE clause on a column definition, or @@ -96028,7 +100069,7 @@ SQLITE_PRIVATE Index *sqlite3CreateIndex( if( pIdx->aiColumn[k]!=pIndex->aiColumn[k] ) break; z1 = pIdx->azColl[k]; z2 = pIndex->azColl[k]; - if( z1!=z2 && sqlite3StrICmp(z1, z2) ) break; + if( sqlite3StrICmp(z1, z2) ) break; } if( k==pIdx->nKeyCol ){ if( pIdx->onError!=pIndex->onError ){ @@ -96064,7 +100105,7 @@ SQLITE_PRIVATE Index *sqlite3CreateIndex( pIndex->zName, pIndex); if( p ){ assert( p==pIndex ); /* Malloc must have failed */ - db->mallocFailed = 1; + sqlite3OomFault(db); goto exit_create_index; } db->flags |= SQLITE_InternChanges; @@ -96493,10 +100534,12 @@ SQLITE_PRIVATE SrcList *sqlite3SrcListAppend( ){ struct SrcList_item *pItem; assert( pDatabase==0 || pTable!=0 ); /* Cannot have C without B */ + assert( db!=0 ); if( pList==0 ){ - pList = sqlite3DbMallocZero(db, sizeof(SrcList) ); + pList = sqlite3DbMallocRawNN(db, sizeof(SrcList) ); if( pList==0 ) return 0; pList->nAlloc = 1; + pList->nSrc = 0; } pList = sqlite3SrcListEnlarge(db, pList, 1, pList->nSrc); if( db->mallocFailed ){ @@ -96639,7 +100682,7 @@ SQLITE_PRIVATE void sqlite3SrcListIndexedBy(Parse *pParse, SrcList *p, Token *pI ** table-valued-function. */ SQLITE_PRIVATE void sqlite3SrcListFuncArgs(Parse *pParse, SrcList *p, ExprList *pList){ - if( p && pList ){ + if( p ){ struct SrcList_item *pItem = &p->a[p->nSrc-1]; assert( pItem->fg.notIndexed==0 ); assert( pItem->fg.isIndexedBy==0 ); @@ -96677,7 +100720,7 @@ SQLITE_PRIVATE void sqlite3SrcListShiftJoinType(SrcList *p){ } /* -** Begin a transaction +** Generate VDBE code for a BEGIN statement. */ SQLITE_PRIVATE void sqlite3BeginTransaction(Parse *pParse, int type){ sqlite3 *db; @@ -96687,7 +100730,6 @@ SQLITE_PRIVATE void sqlite3BeginTransaction(Parse *pParse, int type){ assert( pParse!=0 ); db = pParse->db; assert( db!=0 ); -/* if( db->aDb[0].pBt==0 ) return; */ if( sqlite3AuthCheck(pParse, SQLITE_TRANSACTION, "BEGIN", 0, 0) ){ return; } @@ -96699,11 +100741,11 @@ SQLITE_PRIVATE void sqlite3BeginTransaction(Parse *pParse, int type){ sqlite3VdbeUsesBtree(v, i); } } - sqlite3VdbeAddOp2(v, OP_AutoCommit, 0, 0); + sqlite3VdbeAddOp0(v, OP_AutoCommit); } /* -** Commit a transaction +** Generate VDBE code for a COMMIT statement. */ SQLITE_PRIVATE void sqlite3CommitTransaction(Parse *pParse){ Vdbe *v; @@ -96715,12 +100757,12 @@ SQLITE_PRIVATE void sqlite3CommitTransaction(Parse *pParse){ } v = sqlite3GetVdbe(pParse); if( v ){ - sqlite3VdbeAddOp2(v, OP_AutoCommit, 1, 0); + sqlite3VdbeAddOp1(v, OP_AutoCommit, 1); } } /* -** Rollback a transaction +** Generate VDBE code for a ROLLBACK statement. */ SQLITE_PRIVATE void sqlite3RollbackTransaction(Parse *pParse){ Vdbe *v; @@ -96782,7 +100824,7 @@ SQLITE_PRIVATE int sqlite3OpenTempDatabase(Parse *pParse){ db->aDb[1].pBt = pBt; assert( db->aDb[1].pSchema ); if( SQLITE_NOMEM==sqlite3BtreeSetPageSize(pBt, db->nextPagesize, -1, 0) ){ - db->mallocFailed = 1; + sqlite3OomFault(db); return 1; } } @@ -96899,7 +100941,7 @@ SQLITE_PRIVATE void sqlite3HaltConstraint( sqlite3MayAbort(pParse); } sqlite3VdbeAddOp4(v, OP_Halt, errCode, onError, 0, p4, p4type); - if( p5Errmsg ) sqlite3VdbeChangeP5(v, p5Errmsg); + sqlite3VdbeChangeP5(v, p5Errmsg); } /* @@ -96917,14 +100959,14 @@ SQLITE_PRIVATE void sqlite3UniqueConstraint( sqlite3StrAccumInit(&errMsg, pParse->db, 0, 0, 200); if( pIdx->aColExpr ){ - sqlite3XPrintf(&errMsg, 0, "index '%q'", pIdx->zName); + sqlite3XPrintf(&errMsg, "index '%q'", pIdx->zName); }else{ for(j=0; jnKeyCol; j++){ char *zCol; assert( pIdx->aiColumn[j]>=0 ); zCol = pTab->aCol[pIdx->aiColumn[j]].zName; if( j ) sqlite3StrAccumAppend(&errMsg, ", ", 2); - sqlite3XPrintf(&errMsg, 0, "%s.%s", pTab->zName, zCol); + sqlite3XPrintf(&errMsg, "%s.%s", pTab->zName, zCol); } } zErr = sqlite3StrAccumFinish(&errMsg); @@ -97110,9 +101152,8 @@ SQLITE_PRIVATE KeyInfo *sqlite3KeyInfoOfIndex(Parse *pParse, Index *pIdx){ if( pKey ){ assert( sqlite3KeyInfoIsWriteable(pKey) ); for(i=0; iazColl[i]; - assert( zColl!=0 ); - pKey->aColl[i] = strcmp(zColl,"BINARY")==0 ? 0 : + const char *zColl = pIdx->azColl[i]; + pKey->aColl[i] = zColl==sqlite3StrBINARY ? 0 : sqlite3LocateCollSeq(pParse, zColl); pKey->aSortOrder[i] = pIdx->aSortOrder[i]; } @@ -97158,10 +101199,9 @@ SQLITE_PRIVATE With *sqlite3WithAdd( }else{ pNew = sqlite3DbMallocZero(db, sizeof(*pWith)); } - assert( zName!=0 || pNew==0 ); - assert( db->mallocFailed==0 || pNew==0 ); + assert( (pNew!=0 && zName!=0) || db->mallocFailed ); - if( pNew==0 ){ + if( db->mallocFailed ){ sqlite3ExprListDelete(db, pArglist); sqlite3SelectDelete(db, pQuery); sqlite3DbFree(db, zName); @@ -97375,7 +101415,7 @@ static CollSeq *findCollSeqEntry( */ assert( pDel==0 || pDel==pColl ); if( pDel!=0 ){ - db->mallocFailed = 1; + sqlite3OomFault(db); sqlite3DbFree(db, pDel); pColl = 0; } @@ -97441,8 +101481,8 @@ SQLITE_PRIVATE CollSeq *sqlite3FindCollSeq( ** 5: UTF16 byte order conversion required - argument count matches exactly ** 6: Perfect match: encoding and argument count match exactly. ** -** If nArg==(-2) then any function with a non-null xStep or xFunc is -** a perfect match and any function with both xStep and xFunc NULL is +** If nArg==(-2) then any function with a non-null xSFunc is +** a perfect match and any function with xSFunc NULL is ** a non-match. */ #define FUNC_PERFECT_MATCH 6 /* The score for a perfect match */ @@ -97454,7 +101494,7 @@ static int matchQuality( int match; /* nArg of -2 is a special case */ - if( nArg==(-2) ) return (p->xFunc==0 && p->xStep==0) ? 0 : FUNC_PERFECT_MATCH; + if( nArg==(-2) ) return (p->xSFunc==0) ? 0 : FUNC_PERFECT_MATCH; /* Wrong number of arguments means "no match" */ if( p->nArg!=nArg && p->nArg>=0 ) return 0; @@ -97482,14 +101522,12 @@ static int matchQuality( ** a pointer to the matching FuncDef if found, or 0 if there is no match. */ static FuncDef *functionSearch( - FuncDefHash *pHash, /* Hash table to search */ int h, /* Hash of the name */ - const char *zFunc, /* Name of function */ - int nFunc /* Number of bytes in zFunc */ + const char *zFunc /* Name of function */ ){ FuncDef *p; - for(p=pHash->a[h]; p; p=p->pHash){ - if( sqlite3StrNICmp(p->zName, zFunc, nFunc)==0 && p->zName[nFunc]==0 ){ + for(p=sqlite3BuiltinFunctions.a[h]; p; p=p->u.pHash){ + if( sqlite3StrICmp(p->zName, zFunc)==0 ){ return p; } } @@ -97499,23 +101537,26 @@ static FuncDef *functionSearch( /* ** Insert a new FuncDef into a FuncDefHash hash table. */ -SQLITE_PRIVATE void sqlite3FuncDefInsert( - FuncDefHash *pHash, /* The hash table into which to insert */ - FuncDef *pDef /* The function definition to insert */ +SQLITE_PRIVATE void sqlite3InsertBuiltinFuncs( + FuncDef *aDef, /* List of global functions to be inserted */ + int nDef /* Length of the apDef[] list */ ){ - FuncDef *pOther; - int nName = sqlite3Strlen30(pDef->zName); - u8 c1 = (u8)pDef->zName[0]; - int h = (sqlite3UpperToLower[c1] + nName) % ArraySize(pHash->a); - pOther = functionSearch(pHash, h, pDef->zName, nName); - if( pOther ){ - assert( pOther!=pDef && pOther->pNext!=pDef ); - pDef->pNext = pOther->pNext; - pOther->pNext = pDef; - }else{ - pDef->pNext = 0; - pDef->pHash = pHash->a[h]; - pHash->a[h] = pDef; + int i; + for(i=0; ipNext!=&aDef[i] ); + aDef[i].pNext = pOther->pNext; + pOther->pNext = &aDef[i]; + }else{ + aDef[i].pNext = 0; + aDef[i].u.pHash = sqlite3BuiltinFunctions.a[h]; + sqlite3BuiltinFunctions.a[h] = &aDef[i]; + } } } @@ -97532,7 +101573,7 @@ SQLITE_PRIVATE void sqlite3FuncDefInsert( ** no matching function previously existed. ** ** If nArg is -2, then the first valid function found is returned. A -** function is valid if either xFunc or xStep is non-zero. The nArg==(-2) +** function is valid if xSFunc is non-zero. The nArg==(-2) ** case is used to see if zName is a valid function name for some number ** of arguments. If nArg is -2, then createFlag must be 0. ** @@ -97542,8 +101583,7 @@ SQLITE_PRIVATE void sqlite3FuncDefInsert( */ SQLITE_PRIVATE FuncDef *sqlite3FindFunction( sqlite3 *db, /* An open database */ - const char *zName, /* Name of the function. Not null-terminated */ - int nName, /* Number of characters in the name */ + const char *zName, /* Name of the function. zero-terminated */ int nArg, /* Number of arguments. -1 means any number */ u8 enc, /* Preferred text encoding */ u8 createFlag /* Create new entry if true and does not otherwise exist */ @@ -97552,14 +101592,15 @@ SQLITE_PRIVATE FuncDef *sqlite3FindFunction( FuncDef *pBest = 0; /* Best match found so far */ int bestScore = 0; /* Score of best match */ int h; /* Hash value */ + int nName; /* Length of the name */ assert( nArg>=(-2) ); assert( nArg>=(-1) || createFlag==0 ); - h = (sqlite3UpperToLower[(u8)zName[0]] + nName) % ArraySize(db->aFunc.a); + nName = sqlite3Strlen30(zName); /* First search for a match amongst the application-defined functions. */ - p = functionSearch(&db->aFunc, h, zName, nName); + p = (FuncDef*)sqlite3HashFind(&db->aFunc, zName); while( p ){ int score = matchQuality(p, nArg, enc); if( score>bestScore ){ @@ -97582,9 +101623,9 @@ SQLITE_PRIVATE FuncDef *sqlite3FindFunction( ** So we must not search for built-ins when creating a new function. */ if( !createFlag && (pBest==0 || (db->flags & SQLITE_PreferBuiltin)!=0) ){ - FuncDefHash *pHash = &GLOBAL(FuncDefHash, sqlite3GlobalFunctions); bestScore = 0; - p = functionSearch(pHash, h, zName, nName); + h = (sqlite3UpperToLower[(u8)zName[0]] + nName) % SQLITE_FUNC_HASH_SZ; + p = functionSearch(h, zName); while( p ){ int score = matchQuality(p, nArg, enc); if( score>bestScore ){ @@ -97601,15 +101642,22 @@ SQLITE_PRIVATE FuncDef *sqlite3FindFunction( */ if( createFlag && bestScorezName = (char *)&pBest[1]; + FuncDef *pOther; + pBest->zName = (const char*)&pBest[1]; pBest->nArg = (u16)nArg; pBest->funcFlags = enc; - memcpy(pBest->zName, zName, nName); - pBest->zName[nName] = 0; - sqlite3FuncDefInsert(&db->aFunc, pBest); + memcpy((char*)&pBest[1], zName, nName+1); + pOther = (FuncDef*)sqlite3HashInsert(&db->aFunc, pBest->zName, pBest); + if( pOther==pBest ){ + sqlite3DbFree(db, pBest); + sqlite3OomFault(db); + return 0; + }else{ + pBest->pNext = pOther; + } } - if( pBest && (pBest->xStep || pBest->xFunc || createFlag) ){ + if( pBest && (pBest->xSFunc || createFlag) ){ return pBest; } return 0; @@ -97663,7 +101711,7 @@ SQLITE_PRIVATE Schema *sqlite3SchemaGet(sqlite3 *db, Btree *pBt){ p = (Schema *)sqlite3DbMallocZero(0, sizeof(Schema)); } if( !p ){ - db->mallocFailed = 1; + sqlite3OomFault(db); }else if ( 0==p->file_format ){ sqlite3HashInit(&p->tblHash); sqlite3HashInit(&p->idxHash); @@ -97784,7 +101832,8 @@ SQLITE_PRIVATE void sqlite3MaterializeView( assert( pFrom->a[0].pOn==0 ); assert( pFrom->a[0].pUsing==0 ); } - pSel = sqlite3SelectNew(pParse, 0, pFrom, pWhere, 0, 0, 0, 0, 0, 0); + pSel = sqlite3SelectNew(pParse, 0, pFrom, pWhere, 0, 0, 0, + SF_IncludeHidden, 0, 0); sqlite3SelectDestInit(&dest, SRT_EphemTab, iCur); sqlite3Select(pParse, pSel, &dest); sqlite3SelectDelete(db, pSel); @@ -97820,7 +101869,7 @@ SQLITE_PRIVATE Expr *sqlite3LimitWhere( */ if( pOrderBy && (pLimit == 0) ) { sqlite3ErrorMsg(pParse, "ORDER BY without LIMIT on %s", zStmtType); - goto limit_where_cleanup_2; + goto limit_where_cleanup; } /* We only need to generate a select expression if there @@ -97842,16 +101891,16 @@ SQLITE_PRIVATE Expr *sqlite3LimitWhere( */ pSelectRowid = sqlite3PExpr(pParse, TK_ROW, 0, 0, 0); - if( pSelectRowid == 0 ) goto limit_where_cleanup_2; + if( pSelectRowid == 0 ) goto limit_where_cleanup; pEList = sqlite3ExprListAppend(pParse, 0, pSelectRowid); - if( pEList == 0 ) goto limit_where_cleanup_2; + if( pEList == 0 ) goto limit_where_cleanup; /* duplicate the FROM clause as it is needed by both the DELETE/UPDATE tree ** and the SELECT subtree. */ pSelectSrc = sqlite3SrcListDup(pParse->db, pSrc, 0); if( pSelectSrc == 0 ) { sqlite3ExprListDelete(pParse->db, pEList); - goto limit_where_cleanup_2; + goto limit_where_cleanup; } /* generate the SELECT expression tree. */ @@ -97861,21 +101910,11 @@ SQLITE_PRIVATE Expr *sqlite3LimitWhere( /* now generate the new WHERE rowid IN clause for the DELETE/UDPATE */ pWhereRowid = sqlite3PExpr(pParse, TK_ROW, 0, 0, 0); - if( pWhereRowid == 0 ) goto limit_where_cleanup_1; - pInClause = sqlite3PExpr(pParse, TK_IN, pWhereRowid, 0, 0); - if( pInClause == 0 ) goto limit_where_cleanup_1; - - pInClause->x.pSelect = pSelect; - pInClause->flags |= EP_xIsSelect; - sqlite3ExprSetHeightAndFlags(pParse, pInClause); + pInClause = pWhereRowid ? sqlite3PExpr(pParse, TK_IN, pWhereRowid, 0, 0) : 0; + sqlite3PExprAddSelect(pParse, pInClause, pSelect); return pInClause; - /* something went wrong. clean up anything allocated. */ -limit_where_cleanup_1: - sqlite3SelectDelete(pParse->db, pSelect); - return 0; - -limit_where_cleanup_2: +limit_where_cleanup: sqlite3ExprDelete(pParse->db, pWhere); sqlite3ExprListDelete(pParse->db, pOrderBy); sqlite3ExprDelete(pParse->db, pLimit); @@ -97926,11 +101965,12 @@ SQLITE_PRIVATE void sqlite3DeleteFrom( int addrBypass = 0; /* Address of jump over the delete logic */ int addrLoop = 0; /* Top of the delete loop */ int addrEphOpen = 0; /* Instruction to open the Ephemeral table */ + int bComplex; /* True if there are triggers or FKs or + ** subqueries in the WHERE clause */ #ifndef SQLITE_OMIT_TRIGGER int isView; /* True if attempting to delete from a view */ Trigger *pTrigger; /* List of table triggers, if required */ - int bComplex; /* True if there are either triggers or FKs */ #endif memset(&sContext, 0, sizeof(sContext)); @@ -97958,7 +101998,6 @@ SQLITE_PRIVATE void sqlite3DeleteFrom( #else # define pTrigger 0 # define isView 0 -# define bComplex 0 #endif #ifdef SQLITE_OMIT_VIEW # undef isView @@ -98043,6 +102082,9 @@ SQLITE_PRIVATE void sqlite3DeleteFrom( && pWhere==0 && !bComplex && !IsVirtual(pTab) +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + && db->xPreUpdateCallback==0 +#endif ){ assert( !isView ); sqlite3TableLock(pParse, iDb, pTab->tnum, 1, pTab->zName); @@ -98057,7 +102099,8 @@ SQLITE_PRIVATE void sqlite3DeleteFrom( }else #endif /* SQLITE_OMIT_TRUNCATE_OPTIMIZATION */ { - u16 wcf = WHERE_ONEPASS_DESIRED|WHERE_DUPLICATES_OK; + u16 wcf = WHERE_ONEPASS_DESIRED|WHERE_DUPLICATES_OK|WHERE_SEEK_TABLE; + if( sNC.ncFlags & NC_VarSelect ) bComplex = 1; wcf |= (bComplex ? 0 : WHERE_ONEPASS_MULTIROW); if( HasRowid(pTab) ){ /* For a rowid table, initialize the RowSet to an empty set */ @@ -98116,7 +102159,7 @@ SQLITE_PRIVATE void sqlite3DeleteFrom( ** one, so just keep it in its register(s) and fall through to the ** delete code. */ nKey = nPk; /* OP_Found will use an unpacked key */ - aToOpen = sqlite3DbMallocRaw(db, nIdx+2); + aToOpen = sqlite3DbMallocRawNN(db, nIdx+2); if( aToOpen==0 ){ sqlite3WhereEnd(pWInfo); goto delete_from_cleanup; @@ -98160,8 +102203,8 @@ SQLITE_PRIVATE void sqlite3DeleteFrom( iAddrOnce = sqlite3CodeOnce(pParse); VdbeCoverage(v); } testcase( IsVirtual(pTab) ); - sqlite3OpenTableAndIndices(pParse, pTab, OP_OpenWrite, iTabCur, aToOpen, - &iDataCur, &iIdxCur); + sqlite3OpenTableAndIndices(pParse, pTab, OP_OpenWrite, OPFLAG_FORDELETE, + iTabCur, aToOpen, &iDataCur, &iIdxCur); assert( pPk || IsVirtual(pTab) || iDataCur==iTabCur ); assert( pPk || IsVirtual(pTab) || iIdxCur==iDataCur+1 ); if( eOnePass==ONEPASS_MULTI ) sqlite3VdbeJumpHere(v, iAddrOnce); @@ -98392,17 +102435,27 @@ SQLITE_PRIVATE void sqlite3GenerateRowDelete( /* Delete the index and table entries. Skip this step if pTab is really ** a view (in which case the only effect of the DELETE statement is to - ** fire the INSTEAD OF triggers). */ + ** fire the INSTEAD OF triggers). + ** + ** If variable 'count' is non-zero, then this OP_Delete instruction should + ** invoke the update-hook. The pre-update-hook, on the other hand should + ** be invoked unless table pTab is a system table. The difference is that + ** the update-hook is not invoked for rows removed by REPLACE, but the + ** pre-update-hook is. + */ if( pTab->pSelect==0 ){ + u8 p5 = 0; sqlite3GenerateRowIndexDelete(pParse, pTab, iDataCur, iIdxCur,0,iIdxNoSeek); sqlite3VdbeAddOp2(v, OP_Delete, iDataCur, (count?OPFLAG_NCHANGE:0)); - if( count ){ - sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_TRANSIENT); + sqlite3VdbeChangeP4(v, -1, (char*)pTab, P4_TABLE); + if( eMode!=ONEPASS_OFF ){ + sqlite3VdbeChangeP5(v, OPFLAG_AUXDELETE); } if( iIdxNoSeek>=0 ){ sqlite3VdbeAddOp1(v, OP_Delete, iIdxNoSeek); } - sqlite3VdbeChangeP5(v, eMode==ONEPASS_MULTI); + if( eMode==ONEPASS_MULTI ) p5 |= OPFLAG_SAVEPOSITION; + sqlite3VdbeChangeP5(v, p5); } /* Do any ON CASCADE, SET NULL or SET DEFAULT operations required to @@ -98812,7 +102865,8 @@ static void printfFunc( x.nUsed = 0; x.apArg = argv+1; sqlite3StrAccumInit(&str, db, 0, 0, db->aLimit[SQLITE_LIMIT_LENGTH]); - sqlite3XPrintf(&str, SQLITE_PRINTF_SQLFUNC, zFormat, &x); + str.printfFlags = SQLITE_PRINTF_SQLFUNC; + sqlite3XPrintf(&str, zFormat, &x); n = str.nChar; sqlite3_result_text(context, sqlite3StrAccumFinish(&str), n, SQLITE_DYNAMIC); @@ -99140,10 +103194,10 @@ static void total_changes( ** A structure defining how to do GLOB-style comparisons. */ struct compareInfo { - u8 matchAll; - u8 matchOne; - u8 matchSet; - u8 noCase; + u8 matchAll; /* "*" or "%" */ + u8 matchOne; /* "?" or "_" */ + u8 matchSet; /* "[" or 0 */ + u8 noCase; /* true to ignore case differences */ }; /* @@ -99206,22 +103260,14 @@ static int patternCompare( const u8 *zPattern, /* The glob pattern */ const u8 *zString, /* The string to compare against the glob */ const struct compareInfo *pInfo, /* Information about how to do the compare */ - u32 esc /* The escape character */ + u32 matchOther /* The escape char (LIKE) or '[' (GLOB) */ ){ u32 c, c2; /* Next pattern and input string chars */ u32 matchOne = pInfo->matchOne; /* "?" or "_" */ u32 matchAll = pInfo->matchAll; /* "*" or "%" */ - u32 matchOther; /* "[" or the escape character */ u8 noCase = pInfo->noCase; /* True if uppercase==lowercase */ const u8 *zEscaped = 0; /* One past the last escaped input char */ - /* The GLOB operator does not have an ESCAPE clause. And LIKE does not - ** have the matchSet operator. So we either have to look for one or - ** the other, never both. Hence the single variable matchOther is used - ** to store the one we have to look for. - */ - matchOther = esc ? esc : pInfo->matchSet; - while( (c = Utf8Read(zPattern))!=0 ){ if( c==matchAll ){ /* Match "*" */ /* Skip over multiple "*" characters in the pattern. If there @@ -99235,7 +103281,7 @@ static int patternCompare( if( c==0 ){ return 1; /* "*" at the end of the pattern matches */ }else if( c==matchOther ){ - if( esc ){ + if( pInfo->matchSet==0 ){ c = sqlite3Utf8Read(&zPattern); if( c==0 ) return 0; }else{ @@ -99243,7 +103289,7 @@ static int patternCompare( ** recursive search in this case, but it is an unusual case. */ assert( matchOther<0x80 ); /* '[' is a single-byte character */ while( *zString - && patternCompare(&zPattern[-1],zString,pInfo,esc)==0 ){ + && patternCompare(&zPattern[-1],zString,pInfo,matchOther)==0 ){ SQLITE_SKIP_UTF8(zString); } return *zString!=0; @@ -99269,18 +103315,18 @@ static int patternCompare( } while( (c2 = *(zString++))!=0 ){ if( c2!=c && c2!=cx ) continue; - if( patternCompare(zPattern,zString,pInfo,esc) ) return 1; + if( patternCompare(zPattern,zString,pInfo,matchOther) ) return 1; } }else{ while( (c2 = Utf8Read(zString))!=0 ){ if( c2!=c ) continue; - if( patternCompare(zPattern,zString,pInfo,esc) ) return 1; + if( patternCompare(zPattern,zString,pInfo,matchOther) ) return 1; } } return 0; } if( c==matchOther ){ - if( esc ){ + if( pInfo->matchSet==0 ){ c = sqlite3Utf8Read(&zPattern); if( c==0 ) return 0; zEscaped = zPattern; @@ -99333,7 +103379,14 @@ static int patternCompare( ** The sqlite3_strglob() interface. */ SQLITE_API int SQLITE_STDCALL sqlite3_strglob(const char *zGlobPattern, const char *zString){ - return patternCompare((u8*)zGlobPattern, (u8*)zString, &globInfo, 0)==0; + return patternCompare((u8*)zGlobPattern, (u8*)zString, &globInfo, '[')==0; +} + +/* +** The sqlite3_strlike() interface. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_strlike(const char *zPattern, const char *zStr, unsigned int esc){ + return patternCompare((u8*)zPattern, (u8*)zStr, &likeInfoNorm, esc)==0; } /* @@ -99364,10 +103417,22 @@ static void likeFunc( sqlite3_value **argv ){ const unsigned char *zA, *zB; - u32 escape = 0; + u32 escape; int nPat; sqlite3 *db = sqlite3_context_db_handle(context); + struct compareInfo *pInfo = sqlite3_user_data(context); +#ifdef SQLITE_LIKE_DOESNT_MATCH_BLOBS + if( sqlite3_value_type(argv[0])==SQLITE_BLOB + || sqlite3_value_type(argv[1])==SQLITE_BLOB + ){ +#ifdef SQLITE_TEST + sqlite3_like_count++; +#endif + sqlite3_result_int(context, 0); + return; + } +#endif zB = sqlite3_value_text(argv[0]); zA = sqlite3_value_text(argv[1]); @@ -99395,13 +103460,13 @@ static void likeFunc( return; } escape = sqlite3Utf8Read(&zEsc); + }else{ + escape = pInfo->matchSet; } if( zA && zB ){ - struct compareInfo *pInfo = sqlite3_user_data(context); #ifdef SQLITE_TEST sqlite3_like_count++; #endif - sqlite3_result_int(context, patternCompare(zB, zA, pInfo, escape)); } } @@ -99947,6 +104012,14 @@ static void loadExt(sqlite3_context *context, int argc, sqlite3_value **argv){ sqlite3 *db = sqlite3_context_db_handle(context); char *zErrMsg = 0; + /* Disallow the load_extension() SQL function unless the SQLITE_LoadExtFunc + ** flag is set. See the sqlite3_enable_load_extension() API. + */ + if( (db->flags & SQLITE_LoadExtFunc)==0 ){ + sqlite3_result_error(context, "not authorized", -1); + return; + } + if( argc==2 ){ zProc = (const char *)sqlite3_value_text(argv[1]); }else{ @@ -100172,11 +104245,11 @@ static void groupConcatFinalize(sqlite3_context *context){ ** of the built-in functions above are part of the global function set. ** This routine only deals with those that are not global. */ -SQLITE_PRIVATE void sqlite3RegisterBuiltinFunctions(sqlite3 *db){ +SQLITE_PRIVATE void sqlite3RegisterPerConnectionBuiltinFunctions(sqlite3 *db){ int rc = sqlite3_overload_function(db, "MATCH", 2); assert( rc==SQLITE_NOMEM || rc==SQLITE_OK ); if( rc==SQLITE_NOMEM ){ - db->mallocFailed = 1; + sqlite3OomFault(db); } } @@ -100185,8 +104258,7 @@ SQLITE_PRIVATE void sqlite3RegisterBuiltinFunctions(sqlite3 *db){ */ static void setLikeOptFlag(sqlite3 *db, const char *zName, u8 flagVal){ FuncDef *pDef; - pDef = sqlite3FindFunction(db, zName, sqlite3Strlen30(zName), - 2, SQLITE_UTF8, 0); + pDef = sqlite3FindFunction(db, zName, 2, SQLITE_UTF8, 0); if( ALWAYS(pDef) ){ pDef->funcFlags |= flagVal; } @@ -100234,9 +104306,7 @@ SQLITE_PRIVATE int sqlite3IsLikeFunction(sqlite3 *db, Expr *pExpr, int *pIsNocas return 0; } assert( !ExprHasProperty(pExpr, EP_xIsSelect) ); - pDef = sqlite3FindFunction(db, pExpr->u.zToken, - sqlite3Strlen30(pExpr->u.zToken), - 2, SQLITE_UTF8, 0); + pDef = sqlite3FindFunction(db, pExpr->u.zToken, 2, SQLITE_UTF8, 0); if( NEVER(pDef==0) || (pDef->funcFlags & SQLITE_FUNC_LIKE)==0 ){ return 0; } @@ -100260,7 +104330,7 @@ SQLITE_PRIVATE int sqlite3IsLikeFunction(sqlite3 *db, Expr *pExpr, int *pIsNocas ** ** After this routine runs */ -SQLITE_PRIVATE void sqlite3RegisterGlobalFunctions(void){ +SQLITE_PRIVATE void sqlite3RegisterBuiltinFunctions(void){ /* ** The following array holds FuncDef structures for all of the functions ** defined in this file. @@ -100268,8 +104338,27 @@ SQLITE_PRIVATE void sqlite3RegisterGlobalFunctions(void){ ** The array cannot be constant since changes are made to the ** FuncDef.pHash elements at start-time. The elements of this array ** are read-only after initialization is complete. + ** + ** For peak efficiency, put the most frequently used function last. */ - static SQLITE_WSD FuncDef aBuiltinFunc[] = { + static FuncDef aBuiltinFunc[] = { +#ifdef SQLITE_SOUNDEX + FUNCTION(soundex, 1, 0, 0, soundexFunc ), +#endif +#ifndef SQLITE_OMIT_LOAD_EXTENSION + VFUNCTION(load_extension, 1, 0, 0, loadExt ), + VFUNCTION(load_extension, 2, 0, 0, loadExt ), +#endif +#if SQLITE_USER_AUTHENTICATION + FUNCTION(sqlite_crypt, 2, 0, 0, sqlite3CryptFunc ), +#endif +#ifndef SQLITE_OMIT_COMPILEOPTION_DIAGS + DFUNCTION(sqlite_compileoption_used,1, 0, 0, compileoptionusedFunc ), + DFUNCTION(sqlite_compileoption_get, 1, 0, 0, compileoptiongetFunc ), +#endif /* SQLITE_OMIT_COMPILEOPTION_DIAGS */ + FUNCTION2(unlikely, 1, 0, 0, noopFunc, SQLITE_FUNC_UNLIKELY), + FUNCTION2(likelihood, 2, 0, 0, noopFunc, SQLITE_FUNC_UNLIKELY), + FUNCTION2(likely, 1, 0, 0, noopFunc, SQLITE_FUNC_UNLIKELY), FUNCTION(ltrim, 1, 1, 0, trimFunc ), FUNCTION(ltrim, 2, 1, 0, trimFunc ), FUNCTION(rtrim, 1, 2, 0, trimFunc ), @@ -100287,8 +104376,6 @@ SQLITE_PRIVATE void sqlite3RegisterGlobalFunctions(void){ FUNCTION2(typeof, 1, 0, 0, typeofFunc, SQLITE_FUNC_TYPEOF), FUNCTION2(length, 1, 0, 0, lengthFunc, SQLITE_FUNC_LENGTH), FUNCTION(instr, 2, 0, 0, instrFunc ), - FUNCTION(substr, 2, 0, 0, substrFunc ), - FUNCTION(substr, 3, 0, 0, substrFunc ), FUNCTION(printf, -1, 0, 0, printfFunc ), FUNCTION(unicode, 1, 0, 0, unicodeFunc ), FUNCTION(char, -1, 0, 0, charFunc ), @@ -100299,40 +104386,22 @@ SQLITE_PRIVATE void sqlite3RegisterGlobalFunctions(void){ #endif FUNCTION(upper, 1, 0, 0, upperFunc ), FUNCTION(lower, 1, 0, 0, lowerFunc ), - FUNCTION(coalesce, 1, 0, 0, 0 ), - FUNCTION(coalesce, 0, 0, 0, 0 ), - FUNCTION2(coalesce, -1, 0, 0, noopFunc, SQLITE_FUNC_COALESCE), FUNCTION(hex, 1, 0, 0, hexFunc ), FUNCTION2(ifnull, 2, 0, 0, noopFunc, SQLITE_FUNC_COALESCE), - FUNCTION2(unlikely, 1, 0, 0, noopFunc, SQLITE_FUNC_UNLIKELY), - FUNCTION2(likelihood, 2, 0, 0, noopFunc, SQLITE_FUNC_UNLIKELY), - FUNCTION2(likely, 1, 0, 0, noopFunc, SQLITE_FUNC_UNLIKELY), VFUNCTION(random, 0, 0, 0, randomFunc ), VFUNCTION(randomblob, 1, 0, 0, randomBlob ), FUNCTION(nullif, 2, 0, 1, nullifFunc ), DFUNCTION(sqlite_version, 0, 0, 0, versionFunc ), DFUNCTION(sqlite_source_id, 0, 0, 0, sourceidFunc ), FUNCTION(sqlite_log, 2, 0, 0, errlogFunc ), -#if SQLITE_USER_AUTHENTICATION - FUNCTION(sqlite_crypt, 2, 0, 0, sqlite3CryptFunc ), -#endif -#ifndef SQLITE_OMIT_COMPILEOPTION_DIAGS - DFUNCTION(sqlite_compileoption_used,1, 0, 0, compileoptionusedFunc ), - DFUNCTION(sqlite_compileoption_get, 1, 0, 0, compileoptiongetFunc ), -#endif /* SQLITE_OMIT_COMPILEOPTION_DIAGS */ FUNCTION(quote, 1, 0, 0, quoteFunc ), VFUNCTION(last_insert_rowid, 0, 0, 0, last_insert_rowid), VFUNCTION(changes, 0, 0, 0, changes ), VFUNCTION(total_changes, 0, 0, 0, total_changes ), FUNCTION(replace, 3, 0, 0, replaceFunc ), FUNCTION(zeroblob, 1, 0, 0, zeroblobFunc ), - #ifdef SQLITE_SOUNDEX - FUNCTION(soundex, 1, 0, 0, soundexFunc ), - #endif - #ifndef SQLITE_OMIT_LOAD_EXTENSION - VFUNCTION(load_extension, 1, 0, 0, loadExt ), - VFUNCTION(load_extension, 2, 0, 0, loadExt ), - #endif + FUNCTION(substr, 2, 0, 0, substrFunc ), + FUNCTION(substr, 3, 0, 0, substrFunc ), AGGREGATE(sum, 1, 0, 0, sumStep, sumFinalize ), AGGREGATE(total, 1, 0, 0, sumStep, totalFinalize ), AGGREGATE(avg, 1, 0, 0, sumStep, avgFinalize ), @@ -100350,21 +104419,33 @@ SQLITE_PRIVATE void sqlite3RegisterGlobalFunctions(void){ LIKEFUNC(like, 2, &likeInfoNorm, SQLITE_FUNC_LIKE), LIKEFUNC(like, 3, &likeInfoNorm, SQLITE_FUNC_LIKE), #endif + FUNCTION(coalesce, 1, 0, 0, 0 ), + FUNCTION(coalesce, 0, 0, 0, 0 ), + FUNCTION2(coalesce, -1, 0, 0, noopFunc, SQLITE_FUNC_COALESCE), }; - - int i; - FuncDefHash *pHash = &GLOBAL(FuncDefHash, sqlite3GlobalFunctions); - FuncDef *aFunc = (FuncDef*)&GLOBAL(FuncDef, aBuiltinFunc); - - for(i=0; iu.pHash){ + int n = sqlite3Strlen30(p->zName); + int h = p->zName[0] + n; + printf(" %s(%d)", p->zName, h); + } + printf("\n"); + } + } #endif } @@ -100591,7 +104672,7 @@ SQLITE_PRIVATE int sqlite3FkLocateIndex( } }else if( paiCol ){ assert( nCol>1 ); - aiCol = (int *)sqlite3DbMallocRaw(pParse->db, nCol*sizeof(int)); + aiCol = (int *)sqlite3DbMallocRawNN(pParse->db, nCol*sizeof(int)); if( !aiCol ) return 1; *paiCol = aiCol; } @@ -100621,7 +104702,7 @@ SQLITE_PRIVATE int sqlite3FkLocateIndex( int i, j; for(i=0; iaiColumn[i]; /* Index of column in parent tbl */ - char *zDfltColl; /* Def. collation for column */ + const char *zDfltColl; /* Def. collation for column */ char *zIdxCol; /* Name of indexed column */ if( iCol<0 ) break; /* No foreign keys against expression indexes */ @@ -100630,9 +104711,7 @@ SQLITE_PRIVATE int sqlite3FkLocateIndex( ** the default collation sequence for the column, this index is ** unusable. Bail out early in this case. */ zDfltColl = pParent->aCol[iCol].zColl; - if( !zDfltColl ){ - zDfltColl = "BINARY"; - } + if( !zDfltColl ) zDfltColl = sqlite3StrBINARY; if( sqlite3StrICmp(pIdx->azColl[i], zDfltColl) ) break; zIdxCol = pParent->aCol[iCol].zName; @@ -101536,10 +105615,12 @@ static Trigger *fkActionTrigger( int iAction = (pChanges!=0); /* 1 for UPDATE, 0 for DELETE */ action = pFKey->aAction[iAction]; + if( action==OE_Restrict && (db->flags & SQLITE_DeferFKs) ){ + return 0; + } pTrigger = pFKey->apTrigger[iAction]; if( action!=OE_None && !pTrigger ){ - u8 enableLookaside; /* Copy of db->lookaside.bEnabled */ char const *zFrom; /* Name of child table */ int nFrom; /* Length in bytes of zFrom */ Index *pIdx = 0; /* Parent key index for this FK */ @@ -101566,11 +105647,9 @@ static Trigger *fkActionTrigger( assert( iFromCol>=0 ); assert( pIdx!=0 || (pTab->iPKey>=0 && pTab->iPKeynCol) ); assert( pIdx==0 || pIdx->aiColumn[i]>=0 ); - tToCol.z = pTab->aCol[pIdx ? pIdx->aiColumn[i] : pTab->iPKey].zName; - tFromCol.z = pFKey->pFrom->aCol[iFromCol].zName; - - tToCol.n = sqlite3Strlen30(tToCol.z); - tFromCol.n = sqlite3Strlen30(tFromCol.z); + sqlite3TokenInit(&tToCol, + pTab->aCol[pIdx ? pIdx->aiColumn[i] : pTab->iPKey].zName); + sqlite3TokenInit(&tFromCol, pFKey->pFrom->aCol[iFromCol].zName); /* Create the expression "OLD.zToCol = zFromCol". It is important ** that the "OLD.zToCol" term is on the LHS of the = operator, so @@ -101650,8 +105729,7 @@ static Trigger *fkActionTrigger( } /* Disable lookaside memory allocation */ - enableLookaside = db->lookaside.bEnabled; - db->lookaside.bEnabled = 0; + db->lookaside.bDisable++; pTrigger = (Trigger *)sqlite3DbMallocZero(db, sizeof(Trigger) + /* struct Trigger */ @@ -101673,7 +105751,7 @@ static Trigger *fkActionTrigger( } /* Re-enable the lookaside buffer, if it was disabled earlier. */ - db->lookaside.bEnabled = enableLookaside; + db->lookaside.bDisable--; sqlite3ExprDelete(db, pWhere); sqlite3ExprDelete(db, pWhen); @@ -101868,7 +105946,7 @@ SQLITE_PRIVATE const char *sqlite3IndexAffinityStr(sqlite3 *db, Index *pIdx){ Table *pTab = pIdx->pTable; pIdx->zColAff = (char *)sqlite3DbMallocRaw(0, pIdx->nColumn+1); if( !pIdx->zColAff ){ - db->mallocFailed = 1; + sqlite3OomFault(db); return 0; } for(n=0; nnColumn; n++){ @@ -101919,7 +105997,7 @@ SQLITE_PRIVATE void sqlite3TableAffinity(Vdbe *v, Table *pTab, int iReg){ sqlite3 *db = sqlite3VdbeDb(v); zColAff = (char *)sqlite3DbMallocRaw(0, pTab->nCol+1); if( !zColAff ){ - db->mallocFailed = 1; + sqlite3OomFault(db); return; } @@ -102015,7 +106093,7 @@ static int autoIncBegin( pInfo = pToplevel->pAinc; while( pInfo && pInfo->pTab!=pTab ){ pInfo = pInfo->pNext; } if( pInfo==0 ){ - pInfo = sqlite3DbMallocRaw(pParse->db, sizeof(*pInfo)); + pInfo = sqlite3DbMallocRawNN(pParse->db, sizeof(*pInfo)); if( pInfo==0 ) return 0; pInfo->pNext = pToplevel->pAinc; pToplevel->pAinc = pInfo; @@ -102039,7 +106117,6 @@ SQLITE_PRIVATE void sqlite3AutoincrementBegin(Parse *pParse){ sqlite3 *db = pParse->db; /* The database connection */ Db *pDb; /* Database only autoinc table */ int memId; /* Register holding max rowid */ - int addr; /* A VDBE address */ Vdbe *v = pParse->pVdbe; /* VDBE under construction */ /* This routine is never called during trigger-generation. It is @@ -102049,33 +106126,46 @@ SQLITE_PRIVATE void sqlite3AutoincrementBegin(Parse *pParse){ assert( v ); /* We failed long ago if this is not so */ for(p = pParse->pAinc; p; p = p->pNext){ + static const int iLn = VDBE_OFFSET_LINENO(2); + static const VdbeOpList autoInc[] = { + /* 0 */ {OP_Null, 0, 0, 0}, + /* 1 */ {OP_Rewind, 0, 9, 0}, + /* 2 */ {OP_Column, 0, 0, 0}, + /* 3 */ {OP_Ne, 0, 7, 0}, + /* 4 */ {OP_Rowid, 0, 0, 0}, + /* 5 */ {OP_Column, 0, 1, 0}, + /* 6 */ {OP_Goto, 0, 9, 0}, + /* 7 */ {OP_Next, 0, 2, 0}, + /* 8 */ {OP_Integer, 0, 0, 0}, + /* 9 */ {OP_Close, 0, 0, 0} + }; + VdbeOp *aOp; pDb = &db->aDb[p->iDb]; memId = p->regCtr; assert( sqlite3SchemaMutexHeld(db, 0, pDb->pSchema) ); sqlite3OpenTable(pParse, 0, p->iDb, pDb->pSchema->pSeqTab, OP_OpenRead); - sqlite3VdbeAddOp3(v, OP_Null, 0, memId, memId+1); - addr = sqlite3VdbeCurrentAddr(v); sqlite3VdbeLoadString(v, memId-1, p->pTab->zName); - sqlite3VdbeAddOp2(v, OP_Rewind, 0, addr+9); VdbeCoverage(v); - sqlite3VdbeAddOp3(v, OP_Column, 0, 0, memId); - sqlite3VdbeAddOp3(v, OP_Ne, memId-1, addr+7, memId); VdbeCoverage(v); - sqlite3VdbeChangeP5(v, SQLITE_JUMPIFNULL); - sqlite3VdbeAddOp2(v, OP_Rowid, 0, memId+1); - sqlite3VdbeAddOp3(v, OP_Column, 0, 1, memId); - sqlite3VdbeGoto(v, addr+9); - sqlite3VdbeAddOp2(v, OP_Next, 0, addr+2); VdbeCoverage(v); - sqlite3VdbeAddOp2(v, OP_Integer, 0, memId); - sqlite3VdbeAddOp0(v, OP_Close); + aOp = sqlite3VdbeAddOpList(v, ArraySize(autoInc), autoInc, iLn); + if( aOp==0 ) break; + aOp[0].p2 = memId; + aOp[0].p3 = memId+1; + aOp[2].p3 = memId; + aOp[3].p1 = memId-1; + aOp[3].p3 = memId; + aOp[3].p5 = SQLITE_JUMPIFNULL; + aOp[4].p2 = memId+1; + aOp[5].p3 = memId; + aOp[8].p2 = memId; } } /* ** Update the maximum rowid for an autoincrement calculation. ** -** This routine should be called when the top of the stack holds a +** This routine should be called when the regRowid register holds a ** new rowid that is about to be inserted. If that new rowid is ** larger than the maximum rowid in the memId memory cell, then the -** memory cell is updated. The stack is unchanged. +** memory cell is updated. */ static void autoIncStep(Parse *pParse, int memId, int regRowid){ if( memId>0 ){ @@ -102090,31 +106180,44 @@ static void autoIncStep(Parse *pParse, int memId, int regRowid){ ** table (either directly or through triggers) needs to call this ** routine just before the "exit" code. */ -SQLITE_PRIVATE void sqlite3AutoincrementEnd(Parse *pParse){ +static SQLITE_NOINLINE void autoIncrementEnd(Parse *pParse){ AutoincInfo *p; Vdbe *v = pParse->pVdbe; sqlite3 *db = pParse->db; assert( v ); for(p = pParse->pAinc; p; p = p->pNext){ + static const int iLn = VDBE_OFFSET_LINENO(2); + static const VdbeOpList autoIncEnd[] = { + /* 0 */ {OP_NotNull, 0, 2, 0}, + /* 1 */ {OP_NewRowid, 0, 0, 0}, + /* 2 */ {OP_MakeRecord, 0, 2, 0}, + /* 3 */ {OP_Insert, 0, 0, 0}, + /* 4 */ {OP_Close, 0, 0, 0} + }; + VdbeOp *aOp; Db *pDb = &db->aDb[p->iDb]; - int addr1; int iRec; int memId = p->regCtr; iRec = sqlite3GetTempReg(pParse); assert( sqlite3SchemaMutexHeld(db, 0, pDb->pSchema) ); sqlite3OpenTable(pParse, 0, p->iDb, pDb->pSchema->pSeqTab, OP_OpenWrite); - addr1 = sqlite3VdbeAddOp1(v, OP_NotNull, memId+1); VdbeCoverage(v); - sqlite3VdbeAddOp2(v, OP_NewRowid, 0, memId+1); - sqlite3VdbeJumpHere(v, addr1); - sqlite3VdbeAddOp3(v, OP_MakeRecord, memId-1, 2, iRec); - sqlite3VdbeAddOp3(v, OP_Insert, 0, iRec, memId+1); - sqlite3VdbeChangeP5(v, OPFLAG_APPEND); - sqlite3VdbeAddOp0(v, OP_Close); + aOp = sqlite3VdbeAddOpList(v, ArraySize(autoIncEnd), autoIncEnd, iLn); + if( aOp==0 ) break; + aOp[0].p1 = memId+1; + aOp[1].p2 = memId+1; + aOp[2].p1 = memId-1; + aOp[2].p3 = iRec; + aOp[3].p2 = iRec; + aOp[3].p3 = memId+1; + aOp[3].p5 = OPFLAG_APPEND; sqlite3ReleaseTempReg(pParse, iRec); } } +SQLITE_PRIVATE void sqlite3AutoincrementEnd(Parse *pParse){ + if( pParse->pAinc ) autoIncrementEnd(pParse); +} #else /* ** If SQLITE_OMIT_AUTOINCREMENT is defined, then the three routines @@ -102445,7 +106548,7 @@ SQLITE_PRIVATE void sqlite3Insert( rc = sqlite3Select(pParse, pSelect, &dest); regFromSelect = dest.iSdst; if( rc || db->mallocFailed || pParse->nErr ) goto insert_cleanup; - sqlite3VdbeAddOp1(v, OP_EndCoroutine, regYield); + sqlite3VdbeEndCoroutine(v, regYield); sqlite3VdbeJumpHere(v, addrTop - 1); /* label B: */ assert( pSelect->pEList ); nColumn = pSelect->pEList->nExpr; @@ -102521,10 +106624,8 @@ SQLITE_PRIVATE void sqlite3Insert( /* Make sure the number of columns in the source data matches the number ** of columns to be inserted into the table. */ - if( IsVirtual(pTab) ){ - for(i=0; inCol; i++){ - nHidden += (IsHiddenColumn(&pTab->aCol[i]) ? 1 : 0); - } + for(i=0; inCol; i++){ + nHidden += (IsHiddenColumn(&pTab->aCol[i]) ? 1 : 0); } if( pColumn==0 && nColumn && nColumn!=(pTab->nCol-nHidden) ){ sqlite3ErrorMsg(pParse, @@ -102547,9 +106648,9 @@ SQLITE_PRIVATE void sqlite3Insert( /* If this is not a view, open the table and and all indices */ if( !isView ){ int nIdx; - nIdx = sqlite3OpenTableAndIndices(pParse, pTab, OP_OpenWrite, -1, 0, + nIdx = sqlite3OpenTableAndIndices(pParse, pTab, OP_OpenWrite, 0, -1, 0, &iDataCur, &iIdxCur); - aRegIdx = sqlite3DbMallocRaw(db, sizeof(int)*(nIdx+1)); + aRegIdx = sqlite3DbMallocRawNN(db, sizeof(int)*(nIdx+1)); if( aRegIdx==0 ){ goto insert_cleanup; } @@ -102620,15 +106721,14 @@ SQLITE_PRIVATE void sqlite3Insert( /* Create the new column data */ - for(i=0; inCol; i++){ - if( pColumn==0 ){ - j = i; - }else{ + for(i=j=0; inCol; i++){ + if( pColumn ){ for(j=0; jnId; j++){ if( pColumn->a[j].idx==i ) break; } } - if( (!useTempTable && !pList) || (pColumn && j>=pColumn->nId) ){ + if( (!useTempTable && !pList) || (pColumn && j>=pColumn->nId) + || (pColumn==0 && IsOrdinaryHiddenColumn(&pTab->aCol[i])) ){ sqlite3ExprCode(pParse, pTab->aCol[i].pDflt, regCols+i+1); }else if( useTempTable ){ sqlite3VdbeAddOp3(v, OP_Column, srcTab, j, regCols+i+1); @@ -102636,6 +106736,7 @@ SQLITE_PRIVATE void sqlite3Insert( assert( pSelect==0 ); /* Otherwise useTempTable is true */ sqlite3ExprCodeAndCache(pParse, pList->a[j].pExpr, regCols+i+1); } + if( pColumn==0 && !IsOrdinaryHiddenColumn(&pTab->aCol[i]) ) j++; } /* If this is an INSERT on a view with an INSTEAD OF INSERT trigger, @@ -102719,7 +106820,6 @@ SQLITE_PRIVATE void sqlite3Insert( } if( pColumn==0 ){ if( IsHiddenColumn(&pTab->aCol[i]) ){ - assert( IsVirtual(pTab) ); j = -1; nHidden++; }else{ @@ -102758,7 +106858,7 @@ SQLITE_PRIVATE void sqlite3Insert( { int isReplace; /* Set to true if constraints may cause a replace */ sqlite3GenerateConstraintChecks(pParse, pTab, aRegIdx, iDataCur, iIdxCur, - regIns, 0, ipkColumn>=0, onError, endOfLoop, &isReplace + regIns, 0, ipkColumn>=0, onError, endOfLoop, &isReplace, 0 ); sqlite3FkCheck(pParse, pTab, 0, regIns, 0, 0); sqlite3CompleteInsertion(pParse, pTab, iDataCur, iIdxCur, @@ -102840,6 +106940,59 @@ insert_cleanup: #undef tmask #endif +/* +** Meanings of bits in of pWalker->eCode for checkConstraintUnchanged() +*/ +#define CKCNSTRNT_COLUMN 0x01 /* CHECK constraint uses a changing column */ +#define CKCNSTRNT_ROWID 0x02 /* CHECK constraint references the ROWID */ + +/* This is the Walker callback from checkConstraintUnchanged(). Set +** bit 0x01 of pWalker->eCode if +** pWalker->eCode to 0 if this expression node references any of the +** columns that are being modifed by an UPDATE statement. +*/ +static int checkConstraintExprNode(Walker *pWalker, Expr *pExpr){ + if( pExpr->op==TK_COLUMN ){ + assert( pExpr->iColumn>=0 || pExpr->iColumn==-1 ); + if( pExpr->iColumn>=0 ){ + if( pWalker->u.aiCol[pExpr->iColumn]>=0 ){ + pWalker->eCode |= CKCNSTRNT_COLUMN; + } + }else{ + pWalker->eCode |= CKCNSTRNT_ROWID; + } + } + return WRC_Continue; +} + +/* +** pExpr is a CHECK constraint on a row that is being UPDATE-ed. The +** only columns that are modified by the UPDATE are those for which +** aiChng[i]>=0, and also the ROWID is modified if chngRowid is true. +** +** Return true if CHECK constraint pExpr does not use any of the +** changing columns (or the rowid if it is changing). In other words, +** return true if this CHECK constraint can be skipped when validating +** the new row in the UPDATE statement. +*/ +static int checkConstraintUnchanged(Expr *pExpr, int *aiChng, int chngRowid){ + Walker w; + memset(&w, 0, sizeof(w)); + w.eCode = 0; + w.xExprCallback = checkConstraintExprNode; + w.u.aiCol = aiChng; + sqlite3WalkExpr(&w, pExpr); + if( !chngRowid ){ + testcase( (w.eCode & CKCNSTRNT_ROWID)!=0 ); + w.eCode &= ~CKCNSTRNT_ROWID; + } + testcase( w.eCode==0 ); + testcase( w.eCode==CKCNSTRNT_COLUMN ); + testcase( w.eCode==CKCNSTRNT_ROWID ); + testcase( w.eCode==(CKCNSTRNT_ROWID|CKCNSTRNT_COLUMN) ); + return !w.eCode; +} + /* ** Generate code to do constraint checks prior to an INSERT or an UPDATE ** on table pTab. @@ -102934,7 +107087,8 @@ SQLITE_PRIVATE void sqlite3GenerateConstraintChecks( u8 pkChng, /* Non-zero if the rowid or PRIMARY KEY changed */ u8 overrideError, /* Override onError to this if not OE_Default */ int ignoreDest, /* Jump to this label on an OE_Ignore resolution */ - int *pbMayReplace /* OUT: Set to true if constraint may cause a replace */ + int *pbMayReplace, /* OUT: Set to true if constraint may cause a replace */ + int *aiChng /* column i is unchanged if aiChng[i]<0 */ ){ Vdbe *v; /* VDBE under constrution */ Index *pIdx; /* Pointer to one of the indices */ @@ -102980,10 +107134,14 @@ SQLITE_PRIVATE void sqlite3GenerateConstraintChecks( */ for(i=0; iiPKey ){ + continue; /* ROWID is never NULL */ + } + if( aiChng && aiChng[i]<0 ){ + /* Don't bother checking for NOT NULL on columns that do not change */ continue; } onError = pTab->aCol[i].notNull; - if( onError==OE_None ) continue; + if( onError==OE_None ) continue; /* This column is allowed to be NULL */ if( overrideError!=OE_Default ){ onError = overrideError; }else if( onError==OE_Default ){ @@ -103032,8 +107190,11 @@ SQLITE_PRIVATE void sqlite3GenerateConstraintChecks( pParse->ckBase = regNewData+1; onError = overrideError!=OE_Default ? overrideError : OE_Abort; for(i=0; inExpr; i++){ - int allOk = sqlite3VdbeMakeLabel(v); - sqlite3ExprIfTrue(pParse, pCheck->a[i].pExpr, allOk, SQLITE_JUMPIFNULL); + int allOk; + Expr *pExpr = pCheck->a[i].pExpr; + if( aiChng && checkConstraintUnchanged(pExpr, aiChng, pkChng) ) continue; + allOk = sqlite3VdbeMakeLabel(v); + sqlite3ExprIfTrue(pParse, pExpr, allOk, SQLITE_JUMPIFNULL); if( onError==OE_Ignore ){ sqlite3VdbeGoto(v, ignoreDest); }else{ @@ -103133,9 +107294,18 @@ SQLITE_PRIVATE void sqlite3GenerateConstraintChecks( if( pTrigger || sqlite3FkRequired(pParse, pTab, 0, 0) ){ sqlite3MultiWrite(pParse); sqlite3GenerateRowDelete(pParse, pTab, pTrigger, iDataCur, iIdxCur, - regNewData, 1, 0, OE_Replace, - ONEPASS_SINGLE, -1); + regNewData, 1, 0, OE_Replace, 1, -1); }else{ +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + if( HasRowid(pTab) ){ + /* This OP_Delete opcode fires the pre-update-hook only. It does + ** not modify the b-tree. It is more efficient to let the coming + ** OP_Insert replace the existing entry than it is to delete the + ** existing entry and then insert a new one. */ + sqlite3VdbeAddOp2(v, OP_Delete, iDataCur, OPFLAG_ISNOOP); + sqlite3VdbeChangeP4(v, -1, (char *)pTab, P4_TABLE); + } +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ if( pTab->pIndex ){ sqlite3MultiWrite(pParse); sqlite3GenerateRowIndexDelete(pParse, pTab, iDataCur, iIdxCur,0,-1); @@ -103196,7 +107366,7 @@ SQLITE_PRIVATE void sqlite3GenerateConstraintChecks( int x; if( iField==XN_EXPR ){ pParse->ckBase = regNewData+1; - sqlite3ExprCode(pParse, pIdx->aColExpr->a[i].pExpr, regIdx+i); + sqlite3ExprCodeCopy(pParse, pIdx->aColExpr->a[i].pExpr, regIdx+i); pParse->ckBase = 0; VdbeComment((v, "%s column %d", pIdx->zName, i)); }else{ @@ -103207,7 +107377,7 @@ SQLITE_PRIVATE void sqlite3GenerateConstraintChecks( }else{ x = iField + regNewData + 1; } - sqlite3VdbeAddOp2(v, OP_SCopy, x, regIdx+i); + sqlite3VdbeAddOp2(v, iField<0 ? OP_IntCopy : OP_SCopy, x, regIdx+i); VdbeComment((v, "%s", iField<0 ? "rowid" : pTab->aCol[iField].zName)); } } @@ -103383,7 +107553,7 @@ SQLITE_PRIVATE void sqlite3CompleteInsertion( assert( pParse->nested==0 ); pik_flags |= OPFLAG_NCHANGE; } - if( pik_flags ) sqlite3VdbeChangeP5(v, pik_flags); + sqlite3VdbeChangeP5(v, pik_flags); } if( !HasRowid(pTab) ) return; regData = regNewData + 1; @@ -103405,7 +107575,7 @@ SQLITE_PRIVATE void sqlite3CompleteInsertion( } sqlite3VdbeAddOp3(v, OP_Insert, iDataCur, regRec, regNewData); if( !pParse->nested ){ - sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_TRANSIENT); + sqlite3VdbeChangeP4(v, -1, (char *)pTab, P4_TABLE); } sqlite3VdbeChangeP5(v, pik_flags); } @@ -103435,6 +107605,7 @@ SQLITE_PRIVATE int sqlite3OpenTableAndIndices( Parse *pParse, /* Parsing context */ Table *pTab, /* Table to be opened */ int op, /* OP_OpenRead or OP_OpenWrite */ + u8 p5, /* P5 value for OP_Open* opcodes (except on WITHOUT ROWID) */ int iBase, /* Use this for the table cursor, if there is one */ u8 *aToOpen, /* If not NULL: boolean for each table and index */ int *piDataCur, /* Write the database source cursor number here */ @@ -103447,6 +107618,7 @@ SQLITE_PRIVATE int sqlite3OpenTableAndIndices( Vdbe *v; assert( op==OP_OpenRead || op==OP_OpenWrite ); + assert( op==OP_OpenWrite || p5==0 ); if( IsVirtual(pTab) ){ /* This routine is a no-op for virtual tables. Leave the output ** variables *piDataCur and *piIdxCur uninitialized so that valgrind @@ -103468,14 +107640,16 @@ SQLITE_PRIVATE int sqlite3OpenTableAndIndices( for(i=0, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, i++){ int iIdxCur = iBase++; assert( pIdx->pSchema==pTab->pSchema ); - if( IsPrimaryKeyIndex(pIdx) && !HasRowid(pTab) && piDataCur ){ - *piDataCur = iIdxCur; - } if( aToOpen==0 || aToOpen[i+1] ){ sqlite3VdbeAddOp3(v, op, iIdxCur, pIdx->tnum, iDb); sqlite3VdbeSetP4KeyInfo(pParse, pIdx); VdbeComment((v, "%s", pIdx->zName)); } + if( IsPrimaryKeyIndex(pIdx) && !HasRowid(pTab) ){ + if( piDataCur ) *piDataCur = iIdxCur; + }else{ + sqlite3VdbeChangeP5(v, p5); + } } if( iBase>pParse->nTab ) pParse->nTab = iBase; return i; @@ -103494,20 +107668,6 @@ SQLITE_API int sqlite3_xferopt_count; #ifndef SQLITE_OMIT_XFER_OPT -/* -** Check to collation names to see if they are compatible. -*/ -static int xferCompatibleCollation(const char *z1, const char *z2){ - if( z1==0 ){ - return z2==0; - } - if( z2==0 ){ - return 0; - } - return sqlite3StrICmp(z1, z2)==0; -} - - /* ** Check to see if index pSrc is compatible as a source of data ** for index pDest in an insert transfer optimization. The rules @@ -103543,7 +107703,7 @@ static int xferCompatibleIndex(Index *pDest, Index *pSrc){ if( pSrc->aSortOrder[i]!=pDest->aSortOrder[i] ){ return 0; /* Different sort orders */ } - if( !xferCompatibleCollation(pSrc->azColl[i],pDest->azColl[i]) ){ + if( sqlite3_stricmp(pSrc->azColl[i],pDest->azColl[i])!=0 ){ return 0; /* Different collating sequences */ } } @@ -103658,7 +107818,7 @@ static int xferOptimization( return 0; /* The result set must have exactly one column */ } assert( pEList->a[0].pExpr ); - if( pEList->a[0].pExpr->op!=TK_ALL ){ + if( pEList->a[0].pExpr->op!=TK_ASTERISK ){ return 0; /* The result set must be the special operator "*" */ } @@ -103694,21 +107854,32 @@ static int xferOptimization( for(i=0; inCol; i++){ Column *pDestCol = &pDest->aCol[i]; Column *pSrcCol = &pSrc->aCol[i]; +#ifdef SQLITE_ENABLE_HIDDEN_COLUMNS + if( (db->flags & SQLITE_Vacuum)==0 + && (pDestCol->colFlags | pSrcCol->colFlags) & COLFLAG_HIDDEN + ){ + return 0; /* Neither table may have __hidden__ columns */ + } +#endif if( pDestCol->affinity!=pSrcCol->affinity ){ return 0; /* Affinity must be the same on all columns */ } - if( !xferCompatibleCollation(pDestCol->zColl, pSrcCol->zColl) ){ + if( sqlite3_stricmp(pDestCol->zColl, pSrcCol->zColl)!=0 ){ return 0; /* Collating sequence must be the same on all columns */ } if( pDestCol->notNull && !pSrcCol->notNull ){ return 0; /* tab2 must be NOT NULL if tab1 is */ } /* Default values for second and subsequent columns need to match. */ - if( i>0 - && ((pDestCol->zDflt==0)!=(pSrcCol->zDflt==0) - || (pDestCol->zDflt && strcmp(pDestCol->zDflt, pSrcCol->zDflt)!=0)) - ){ - return 0; /* Default values must be the same for all columns */ + if( i>0 ){ + assert( pDestCol->pDflt==0 || pDestCol->pDflt->op==TK_SPAN ); + assert( pSrcCol->pDflt==0 || pSrcCol->pDflt->op==TK_SPAN ); + if( (pDestCol->pDflt==0)!=(pSrcCol->pDflt==0) + || (pDestCol->pDflt && strcmp(pDestCol->pDflt->u.zToken, + pSrcCol->pDflt->u.zToken)!=0) + ){ + return 0; /* Default values must be the same for all columns */ + } } } for(pDestIdx=pDest->pIndex; pDestIdx; pDestIdx=pDestIdx->pNext){ @@ -103803,9 +107974,9 @@ static int xferOptimization( assert( (pDest->tabFlags & TF_Autoincrement)==0 ); } sqlite3VdbeAddOp2(v, OP_RowData, iSrc, regData); - sqlite3VdbeAddOp3(v, OP_Insert, iDest, regData, regRowid); + sqlite3VdbeAddOp4(v, OP_Insert, iDest, regData, regRowid, + (char*)pDest, P4_TABLE); sqlite3VdbeChangeP5(v, OPFLAG_NCHANGE|OPFLAG_LASTROWID|OPFLAG_APPEND); - sqlite3VdbeChangeP4(v, -1, pDest->zName, 0); sqlite3VdbeAddOp2(v, OP_Next, iSrc, addr1); VdbeCoverage(v); sqlite3VdbeAddOp2(v, OP_Close, iSrc, 0); sqlite3VdbeAddOp2(v, OP_Close, iDest, 0); @@ -103844,9 +108015,10 @@ static int xferOptimization( ** a VACUUM command. In that case keys may not be written in strictly ** sorted order. */ for(i=0; inColumn; i++){ - char *zColl = pSrcIdx->azColl[i]; - assert( zColl!=0 ); - if( sqlite3_stricmp("BINARY", zColl) ) break; + const char *zColl = pSrcIdx->azColl[i]; + assert( sqlite3_stricmp(sqlite3StrBINARY, zColl)!=0 + || sqlite3StrBINARY==zColl ); + if( sqlite3_stricmp(sqlite3StrBINARY, zColl) ) break; } if( i==pSrcIdx->nColumn ){ idxInsFlags = OPFLAG_USESEEKRESULT; @@ -103971,7 +108143,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_exec( for(i=0; imallocFailed = 1; + sqlite3OomFault(db); goto exec_out; } } @@ -104012,7 +108184,7 @@ exec_out: if( *pzErrMsg ){ memcpy(*pzErrMsg, sqlite3_errmsg(db), nErrMsg); }else{ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; sqlite3Error(db, SQLITE_NOMEM); } }else if( pzErrMsg ){ @@ -104323,6 +108495,12 @@ struct sqlite3_api_routines { /* Version 3.9.0 and later */ unsigned int (*value_subtype)(sqlite3_value*); void (*result_subtype)(sqlite3_context*,unsigned int); + /* Version 3.10.0 and later */ + int (*status64)(int,sqlite3_int64*,sqlite3_int64*,int); + int (*strlike)(const char*,const char*,unsigned int); + int (*db_cacheflush)(sqlite3*); + /* Version 3.12.0 and later */ + int (*system_errno)(sqlite3*); }; /* @@ -104562,6 +108740,12 @@ struct sqlite3_api_routines { /* Version 3.9.0 and later */ #define sqlite3_value_subtype sqlite3_api->value_subtype #define sqlite3_result_subtype sqlite3_api->result_subtype +/* Version 3.10.0 and later */ +#define sqlite3_status64 sqlite3_api->status64 +#define sqlite3_strlike sqlite3_api->strlike +#define sqlite3_db_cacheflush sqlite3_api->db_cacheflush +/* Version 3.12.0 and later */ +#define sqlite3_system_errno sqlite3_api->system_errno #endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */ #if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) @@ -104976,7 +109160,13 @@ static const sqlite3_api_routines sqlite3Apis = { sqlite3_bind_zeroblob64, /* Version 3.9.0 and later */ sqlite3_value_subtype, - sqlite3_result_subtype + sqlite3_result_subtype, + /* Version 3.10.0 and later */ + sqlite3_status64, + sqlite3_strlike, + sqlite3_db_cacheflush, + /* Version 3.12.0 and later */ + sqlite3_system_errno }; /* @@ -105024,8 +109214,9 @@ static int sqlite3LoadExtension( /* Ticket #1863. To avoid a creating security problems for older ** applications that relink against newer versions of SQLite, the ** ability to run load_extension is turned off by default. One - ** must call sqlite3_enable_load_extension() to turn on extension - ** loading. Otherwise you get the following error. + ** must call either sqlite3_enable_load_extension(db) or + ** sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, 0) + ** to turn on extension loading. */ if( (db->flags & SQLITE_LoadExtension)==0 ){ if( pzErrMsg ){ @@ -105040,7 +109231,7 @@ static int sqlite3LoadExtension( #if SQLITE_OS_UNIX || SQLITE_OS_WIN for(ii=0; ii=0 && zFile[iFile]!='/'; iFile--){} @@ -105119,7 +109310,7 @@ static int sqlite3LoadExtension( /* Append the new shared library handle to the db->aExtension array. */ aHandle = sqlite3DbMallocZero(db, sizeof(handle)*(db->nExtension+1)); if( aHandle==0 ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } if( db->nExtension>0 ){ memcpy(aHandle, db->aExtension, sizeof(handle)*db->nExtension); @@ -105164,9 +109355,9 @@ SQLITE_PRIVATE void sqlite3CloseExtensions(sqlite3 *db){ SQLITE_API int SQLITE_STDCALL sqlite3_enable_load_extension(sqlite3 *db, int onoff){ sqlite3_mutex_enter(db->mutex); if( onoff ){ - db->flags |= SQLITE_LoadExtension; + db->flags |= SQLITE_LoadExtension|SQLITE_LoadExtFunc; }else{ - db->flags &= ~SQLITE_LoadExtension; + db->flags &= ~(SQLITE_LoadExtension|SQLITE_LoadExtFunc); } sqlite3_mutex_leave(db->mutex); return SQLITE_OK; @@ -105181,7 +109372,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_enable_load_extension(sqlite3 *db, int ono ** dummy pointer. */ #ifdef SQLITE_OMIT_LOAD_EXTENSION -static const sqlite3_api_routines sqlite3Apis; +static const sqlite3_api_routines sqlite3Apis = { 0 }; #endif @@ -105241,7 +109432,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_auto_extension(void (*xInit)(void)){ void (**aNew)(void); aNew = sqlite3_realloc64(wsdAutoext.aExt, nByte); if( aNew==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else{ wsdAutoext.aExt = aNew; wsdAutoext.aExt[wsdAutoext.nExt] = xInit; @@ -105387,43 +109578,44 @@ SQLITE_PRIVATE void sqlite3AutoLoadExtensions(sqlite3 *db){ #define PragTyp_FLAG 2 #define PragTyp_BUSY_TIMEOUT 3 #define PragTyp_CACHE_SIZE 4 -#define PragTyp_CASE_SENSITIVE_LIKE 5 -#define PragTyp_COLLATION_LIST 6 -#define PragTyp_COMPILE_OPTIONS 7 -#define PragTyp_DATA_STORE_DIRECTORY 8 -#define PragTyp_DATABASE_LIST 9 -#define PragTyp_DEFAULT_CACHE_SIZE 10 -#define PragTyp_ENCODING 11 -#define PragTyp_FOREIGN_KEY_CHECK 12 -#define PragTyp_FOREIGN_KEY_LIST 13 -#define PragTyp_INCREMENTAL_VACUUM 14 -#define PragTyp_INDEX_INFO 15 -#define PragTyp_INDEX_LIST 16 -#define PragTyp_INTEGRITY_CHECK 17 -#define PragTyp_JOURNAL_MODE 18 -#define PragTyp_JOURNAL_SIZE_LIMIT 19 -#define PragTyp_LOCK_PROXY_FILE 20 -#define PragTyp_LOCKING_MODE 21 -#define PragTyp_PAGE_COUNT 22 -#define PragTyp_MMAP_SIZE 23 -#define PragTyp_PAGE_SIZE 24 -#define PragTyp_SECURE_DELETE 25 -#define PragTyp_SHRINK_MEMORY 26 -#define PragTyp_SOFT_HEAP_LIMIT 27 -#define PragTyp_STATS 28 -#define PragTyp_SYNCHRONOUS 29 -#define PragTyp_TABLE_INFO 30 -#define PragTyp_TEMP_STORE 31 -#define PragTyp_TEMP_STORE_DIRECTORY 32 -#define PragTyp_THREADS 33 -#define PragTyp_WAL_AUTOCHECKPOINT 34 -#define PragTyp_WAL_CHECKPOINT 35 -#define PragTyp_ACTIVATE_EXTENSIONS 36 -#define PragTyp_HEXKEY 37 -#define PragTyp_KEY 38 -#define PragTyp_REKEY 39 -#define PragTyp_LOCK_STATUS 40 -#define PragTyp_PARSER_TRACE 41 +#define PragTyp_CACHE_SPILL 5 +#define PragTyp_CASE_SENSITIVE_LIKE 6 +#define PragTyp_COLLATION_LIST 7 +#define PragTyp_COMPILE_OPTIONS 8 +#define PragTyp_DATA_STORE_DIRECTORY 9 +#define PragTyp_DATABASE_LIST 10 +#define PragTyp_DEFAULT_CACHE_SIZE 11 +#define PragTyp_ENCODING 12 +#define PragTyp_FOREIGN_KEY_CHECK 13 +#define PragTyp_FOREIGN_KEY_LIST 14 +#define PragTyp_INCREMENTAL_VACUUM 15 +#define PragTyp_INDEX_INFO 16 +#define PragTyp_INDEX_LIST 17 +#define PragTyp_INTEGRITY_CHECK 18 +#define PragTyp_JOURNAL_MODE 19 +#define PragTyp_JOURNAL_SIZE_LIMIT 20 +#define PragTyp_LOCK_PROXY_FILE 21 +#define PragTyp_LOCKING_MODE 22 +#define PragTyp_PAGE_COUNT 23 +#define PragTyp_MMAP_SIZE 24 +#define PragTyp_PAGE_SIZE 25 +#define PragTyp_SECURE_DELETE 26 +#define PragTyp_SHRINK_MEMORY 27 +#define PragTyp_SOFT_HEAP_LIMIT 28 +#define PragTyp_STATS 29 +#define PragTyp_SYNCHRONOUS 30 +#define PragTyp_TABLE_INFO 31 +#define PragTyp_TEMP_STORE 32 +#define PragTyp_TEMP_STORE_DIRECTORY 33 +#define PragTyp_THREADS 34 +#define PragTyp_WAL_AUTOCHECKPOINT 35 +#define PragTyp_WAL_CHECKPOINT 36 +#define PragTyp_ACTIVATE_EXTENSIONS 37 +#define PragTyp_HEXKEY 38 +#define PragTyp_KEY 39 +#define PragTyp_REKEY 40 +#define PragTyp_LOCK_STATUS 41 +#define PragTyp_PARSER_TRACE 42 #define PragFlag_NeedSchema 0x01 #define PragFlag_ReadOnly 0x02 static const struct sPragmaNames { @@ -105465,14 +109657,14 @@ static const struct sPragmaNames { #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) { /* zName: */ "cache_size", /* ePragTyp: */ PragTyp_CACHE_SIZE, - /* ePragFlag: */ 0, + /* ePragFlag: */ PragFlag_NeedSchema, /* iArg: */ 0 }, #endif #if !defined(SQLITE_OMIT_FLAG_PRAGMAS) { /* zName: */ "cache_spill", - /* ePragTyp: */ PragTyp_FLAG, + /* ePragTyp: */ PragTyp_CACHE_SPILL, /* ePragFlag: */ 0, - /* iArg: */ SQLITE_CacheSpill }, + /* iArg: */ 0 }, #endif { /* zName: */ "case_sensitive_like", /* ePragTyp: */ PragTyp_CASE_SENSITIVE_LIKE, @@ -105686,7 +109878,7 @@ static const struct sPragmaNames { /* ePragFlag: */ 0, /* iArg: */ 0 }, #endif -#if defined(SQLITE_DEBUG) +#if defined(SQLITE_DEBUG) && !defined(SQLITE_OMIT_PARSER_TRACE) { /* zName: */ "parser_trace", /* ePragTyp: */ PragTyp_PARSER_TRACE, /* ePragFlag: */ 0, @@ -105846,8 +110038,8 @@ static const struct sPragmaNames { /* ** Interpret the given string as a safety level. Return 0 for OFF, -** 1 for ON or NORMAL and 2 for FULL. Return 1 for an empty or -** unrecognized string argument. The FULL option is disallowed +** 1 for ON or NORMAL, 2 for FULL, and 3 for EXTRA. Return 1 for an empty or +** unrecognized string argument. The FULL and EXTRA option is disallowed ** if the omitFull parameter it 1. ** ** Note that the values returned are one less that the values that @@ -105856,18 +110048,21 @@ static const struct sPragmaNames { ** and older scripts may have used numbers 0 for OFF and 1 for ON. */ static u8 getSafetyLevel(const char *z, int omitFull, u8 dflt){ - /* 123456789 123456789 */ - static const char zText[] = "onoffalseyestruefull"; - static const u8 iOffset[] = {0, 1, 2, 4, 9, 12, 16}; - static const u8 iLength[] = {2, 2, 3, 5, 3, 4, 4}; - static const u8 iValue[] = {1, 0, 0, 0, 1, 1, 2}; + /* 123456789 123456789 123 */ + static const char zText[] = "onoffalseyestruextrafull"; + static const u8 iOffset[] = {0, 1, 2, 4, 9, 12, 15, 20}; + static const u8 iLength[] = {2, 2, 3, 5, 3, 4, 5, 4}; + static const u8 iValue[] = {1, 0, 0, 0, 1, 1, 3, 2}; + /* on no off false yes true extra full */ int i, n; if( sqlite3Isdigit(*z) ){ return (u8)sqlite3Atoi(z); } n = sqlite3Strlen30(z); - for(i=0; i, or NULL */ int minusFlag /* True if a '-' sign preceded */ ){ @@ -106127,7 +110322,7 @@ SQLITE_PRIVATE void sqlite3Pragma( sqlite3VdbeRunOnlyOnce(v); pParse->nMem = 2; - /* Interpret the [database.] part of the pragma statement. iDb is the + /* Interpret the [schema.] part of the pragma statement. iDb is the ** index of the database this pragma is being applied to in db.aDb[]. */ iDb = sqlite3TwoPartName(pParse, pId1, pId2, &pId); if( iDb<0 ) return; @@ -106216,8 +110411,8 @@ SQLITE_PRIVATE void sqlite3Pragma( #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) && !defined(SQLITE_OMIT_DEPRECATED) /* - ** PRAGMA [database.]default_cache_size - ** PRAGMA [database.]default_cache_size=N + ** PRAGMA [schema.]default_cache_size + ** PRAGMA [schema.]default_cache_size=N ** ** The first form reports the current persistent setting for the ** page cache size. The value returned is the maximum number of @@ -106244,20 +110439,21 @@ SQLITE_PRIVATE void sqlite3Pragma( { OP_Noop, 0, 0, 0}, { OP_ResultRow, 1, 1, 0}, }; - int addr; + VdbeOp *aOp; sqlite3VdbeUsesBtree(v, iDb); if( !zRight ){ setOneColumnName(v, "cache_size"); pParse->nMem += 2; - addr = sqlite3VdbeAddOpList(v, ArraySize(getCacheSize), getCacheSize,iLn); - sqlite3VdbeChangeP1(v, addr, iDb); - sqlite3VdbeChangeP1(v, addr+1, iDb); - sqlite3VdbeChangeP1(v, addr+6, SQLITE_DEFAULT_CACHE_SIZE); + sqlite3VdbeVerifyNoMallocRequired(v, ArraySize(getCacheSize)); + aOp = sqlite3VdbeAddOpList(v, ArraySize(getCacheSize), getCacheSize, iLn); + if( ONLY_IF_REALLOC_STRESS(aOp==0) ) break; + aOp[0].p1 = iDb; + aOp[1].p1 = iDb; + aOp[6].p1 = SQLITE_DEFAULT_CACHE_SIZE; }else{ int size = sqlite3AbsInt32(sqlite3Atoi(zRight)); sqlite3BeginWriteOperation(pParse, 0, iDb); - sqlite3VdbeAddOp2(v, OP_Integer, size, 1); - sqlite3VdbeAddOp3(v, OP_SetCookie, iDb, BTREE_DEFAULT_CACHE_SIZE, 1); + sqlite3VdbeAddOp3(v, OP_SetCookie, iDb, BTREE_DEFAULT_CACHE_SIZE, size); assert( sqlite3SchemaMutexHeld(db, iDb, 0) ); pDb->pSchema->cache_size = size; sqlite3BtreeSetCacheSize(pDb->pBt, pDb->pSchema->cache_size); @@ -106268,8 +110464,8 @@ SQLITE_PRIVATE void sqlite3Pragma( #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) /* - ** PRAGMA [database.]page_size - ** PRAGMA [database.]page_size=N + ** PRAGMA [schema.]page_size + ** PRAGMA [schema.]page_size=N ** ** The first form reports the current setting for the ** database page size in bytes. The second form sets the @@ -106288,15 +110484,15 @@ SQLITE_PRIVATE void sqlite3Pragma( */ db->nextPagesize = sqlite3Atoi(zRight); if( SQLITE_NOMEM==sqlite3BtreeSetPageSize(pBt, db->nextPagesize,-1,0) ){ - db->mallocFailed = 1; + sqlite3OomFault(db); } } break; } /* - ** PRAGMA [database.]secure_delete - ** PRAGMA [database.]secure_delete=ON/OFF + ** PRAGMA [schema.]secure_delete + ** PRAGMA [schema.]secure_delete=ON/OFF ** ** The first form reports the current setting for the ** secure_delete flag. The second form changes the secure_delete @@ -106321,8 +110517,8 @@ SQLITE_PRIVATE void sqlite3Pragma( } /* - ** PRAGMA [database.]max_page_count - ** PRAGMA [database.]max_page_count=N + ** PRAGMA [schema.]max_page_count + ** PRAGMA [schema.]max_page_count=N ** ** The first form reports the current setting for the ** maximum number of pages in the database file. The @@ -106333,7 +110529,7 @@ SQLITE_PRIVATE void sqlite3Pragma( ** change. The only purpose is to provide an easy way to test ** the sqlite3AbsInt32() function. ** - ** PRAGMA [database.]page_count + ** PRAGMA [schema.]page_count ** ** Return the number of pages in the specified database. */ @@ -106354,8 +110550,8 @@ SQLITE_PRIVATE void sqlite3Pragma( } /* - ** PRAGMA [database.]locking_mode - ** PRAGMA [database.]locking_mode = (normal|exclusive) + ** PRAGMA [schema.]locking_mode + ** PRAGMA [schema.]locking_mode = (normal|exclusive) */ case PragTyp_LOCKING_MODE: { const char *zRet = "normal"; @@ -106400,8 +110596,8 @@ SQLITE_PRIVATE void sqlite3Pragma( } /* - ** PRAGMA [database.]journal_mode - ** PRAGMA [database.]journal_mode = + ** PRAGMA [schema.]journal_mode + ** PRAGMA [schema.]journal_mode = ** (delete|persist|off|truncate|memory|wal|off) */ case PragTyp_JOURNAL_MODE: { @@ -106441,8 +110637,8 @@ SQLITE_PRIVATE void sqlite3Pragma( } /* - ** PRAGMA [database.]journal_size_limit - ** PRAGMA [database.]journal_size_limit=N + ** PRAGMA [schema.]journal_size_limit + ** PRAGMA [schema.]journal_size_limit=N ** ** Get or set the size limit on rollback journal files. */ @@ -106461,8 +110657,8 @@ SQLITE_PRIVATE void sqlite3Pragma( #endif /* SQLITE_OMIT_PAGER_PRAGMAS */ /* - ** PRAGMA [database.]auto_vacuum - ** PRAGMA [database.]auto_vacuum=N + ** PRAGMA [schema.]auto_vacuum + ** PRAGMA [schema.]auto_vacuum=N ** ** Get or set the value of the database 'auto-vacuum' parameter. ** The value is one of: 0 NONE 1 FULL 2 INCREMENTAL @@ -106495,16 +110691,18 @@ SQLITE_PRIVATE void sqlite3Pragma( { OP_ReadCookie, 0, 1, BTREE_LARGEST_ROOT_PAGE}, { OP_If, 1, 0, 0}, /* 2 */ { OP_Halt, SQLITE_OK, OE_Abort, 0}, /* 3 */ - { OP_Integer, 0, 1, 0}, /* 4 */ - { OP_SetCookie, 0, BTREE_INCR_VACUUM, 1}, /* 5 */ + { OP_SetCookie, 0, BTREE_INCR_VACUUM, 0}, /* 4 */ }; - int iAddr; - iAddr = sqlite3VdbeAddOpList(v, ArraySize(setMeta6), setMeta6, iLn); - sqlite3VdbeChangeP1(v, iAddr, iDb); - sqlite3VdbeChangeP1(v, iAddr+1, iDb); - sqlite3VdbeChangeP2(v, iAddr+2, iAddr+4); - sqlite3VdbeChangeP1(v, iAddr+4, eAuto-1); - sqlite3VdbeChangeP1(v, iAddr+5, iDb); + VdbeOp *aOp; + int iAddr = sqlite3VdbeCurrentAddr(v); + sqlite3VdbeVerifyNoMallocRequired(v, ArraySize(setMeta6)); + aOp = sqlite3VdbeAddOpList(v, ArraySize(setMeta6), setMeta6, iLn); + if( ONLY_IF_REALLOC_STRESS(aOp==0) ) break; + aOp[0].p1 = iDb; + aOp[1].p1 = iDb; + aOp[2].p2 = iAddr+4; + aOp[4].p1 = iDb; + aOp[4].p3 = eAuto - 1; sqlite3VdbeUsesBtree(v, iDb); } } @@ -106513,7 +110711,7 @@ SQLITE_PRIVATE void sqlite3Pragma( #endif /* - ** PRAGMA [database.]incremental_vacuum(N) + ** PRAGMA [schema.]incremental_vacuum(N) ** ** Do N steps of incremental vacuuming on a database. */ @@ -106536,8 +110734,8 @@ SQLITE_PRIVATE void sqlite3Pragma( #ifndef SQLITE_OMIT_PAGER_PRAGMAS /* - ** PRAGMA [database.]cache_size - ** PRAGMA [database.]cache_size=N + ** PRAGMA [schema.]cache_size + ** PRAGMA [schema.]cache_size=N ** ** The first form reports the current local setting for the ** page cache size. The second form sets the local @@ -106549,19 +110747,60 @@ SQLITE_PRIVATE void sqlite3Pragma( case PragTyp_CACHE_SIZE: { assert( sqlite3SchemaMutexHeld(db, iDb, 0) ); if( !zRight ){ - if( sqlite3ReadSchema(pParse) ) goto pragma_out; returnSingleInt(v, "cache_size", pDb->pSchema->cache_size); }else{ int size = sqlite3Atoi(zRight); pDb->pSchema->cache_size = size; sqlite3BtreeSetCacheSize(pDb->pBt, pDb->pSchema->cache_size); - if( sqlite3ReadSchema(pParse) ) goto pragma_out; } break; } /* - ** PRAGMA [database.]mmap_size(N) + ** PRAGMA [schema.]cache_spill + ** PRAGMA cache_spill=BOOLEAN + ** PRAGMA [schema.]cache_spill=N + ** + ** The first form reports the current local setting for the + ** page cache spill size. The second form turns cache spill on + ** or off. When turnning cache spill on, the size is set to the + ** current cache_size. The third form sets a spill size that + ** may be different form the cache size. + ** If N is positive then that is the + ** number of pages in the cache. If N is negative, then the + ** number of pages is adjusted so that the cache uses -N kibibytes + ** of memory. + ** + ** If the number of cache_spill pages is less then the number of + ** cache_size pages, no spilling occurs until the page count exceeds + ** the number of cache_size pages. + ** + ** The cache_spill=BOOLEAN setting applies to all attached schemas, + ** not just the schema specified. + */ + case PragTyp_CACHE_SPILL: { + assert( sqlite3SchemaMutexHeld(db, iDb, 0) ); + if( !zRight ){ + returnSingleInt(v, "cache_spill", + (db->flags & SQLITE_CacheSpill)==0 ? 0 : + sqlite3BtreeSetSpillSize(pDb->pBt,0)); + }else{ + int size = 1; + if( sqlite3GetInt32(zRight, &size) ){ + sqlite3BtreeSetSpillSize(pDb->pBt, size); + } + if( sqlite3GetBoolean(zRight, size!=0) ){ + db->flags |= SQLITE_CacheSpill; + }else{ + db->flags &= ~SQLITE_CacheSpill; + } + setAllPagerFlags(db); + } + break; + } + + /* + ** PRAGMA [schema.]mmap_size(N) ** ** Used to set mapping size limit. The mapping size limit is ** used to limit the aggregate size of all memory mapped regions of the @@ -106705,8 +110944,8 @@ SQLITE_PRIVATE void sqlite3Pragma( #if SQLITE_ENABLE_LOCKING_STYLE /* - ** PRAGMA [database.]lock_proxy_file - ** PRAGMA [database.]lock_proxy_file = ":auto:"|"lock_file_path" + ** PRAGMA [schema.]lock_proxy_file + ** PRAGMA [schema.]lock_proxy_file = ":auto:"|"lock_file_path" ** ** Return or set the value of the lock_proxy_file flag. Changing ** the value sets a specific file to be used for database access locks. @@ -106741,8 +110980,8 @@ SQLITE_PRIVATE void sqlite3Pragma( #endif /* SQLITE_ENABLE_LOCKING_STYLE */ /* - ** PRAGMA [database.]synchronous - ** PRAGMA [database.]synchronous=OFF|ON|NORMAL|FULL + ** PRAGMA [schema.]synchronous + ** PRAGMA [schema.]synchronous=OFF|ON|NORMAL|FULL|EXTRA ** ** Return or set the local value of the synchronous flag. Changing ** the local value does not make changes to the disk file and the @@ -106760,6 +110999,7 @@ SQLITE_PRIVATE void sqlite3Pragma( int iLevel = (getSafetyLevel(zRight,0,1)+1) & PAGER_SYNCHRONOUS_MASK; if( iLevel==0 ) iLevel = 1; pDb->safety_level = iLevel; + pDb->bSyncSet = 1; setAllPagerFlags(db); } } @@ -106843,12 +111083,13 @@ SQLITE_PRIVATE void sqlite3Pragma( }else{ for(k=1; k<=pTab->nCol && pPk->aiColumn[k-1]!=i; k++){} } + assert( pCol->pDflt==0 || pCol->pDflt->op==TK_SPAN ); sqlite3VdbeMultiLoad(v, 1, "issisi", i-nHidden, pCol->zName, - pCol->zType ? pCol->zType : "", + sqlite3ColumnType(pCol,""), pCol->notNull ? 1 : 0, - pCol->zDflt, + pCol->pDflt ? pCol->pDflt->u.zToken : 0, k); sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 6); } @@ -106869,14 +111110,14 @@ SQLITE_PRIVATE void sqlite3Pragma( sqlite3VdbeMultiLoad(v, 1, "ssii", pTab->zName, 0, - (int)sqlite3LogEstToInt(pTab->szTabRow), - (int)sqlite3LogEstToInt(pTab->nRowLogEst)); + pTab->szTabRow, + pTab->nRowLogEst); sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 4); for(pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext){ sqlite3VdbeMultiLoad(v, 2, "sii", pIdx->zName, - (int)sqlite3LogEstToInt(pIdx->szIdxRow), - (int)sqlite3LogEstToInt(pIdx->aiRowLogEst[0])); + pIdx->szIdxRow, + pIdx->aiRowLogEst[0]); sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 4); } } @@ -107138,7 +111379,7 @@ SQLITE_PRIVATE void sqlite3Pragma( case PragTyp_PARSER_TRACE: { if( zRight ){ if( sqlite3GetBoolean(zRight, 0) ){ - sqlite3ParserTrace(stderr, "parser: "); + sqlite3ParserTrace(stdout, "parser: "); }else{ sqlite3ParserTrace(0, 0); } @@ -107169,18 +111410,6 @@ SQLITE_PRIVATE void sqlite3Pragma( case PragTyp_INTEGRITY_CHECK: { int i, j, addr, mxErr; - /* Code that appears at the end of the integrity check. If no error - ** messages have been generated, output OK. Otherwise output the - ** error message - */ - static const int iLn = VDBE_OFFSET_LINENO(2); - static const VdbeOpList endCode[] = { - { OP_AddImm, 1, 0, 0}, /* 0 */ - { OP_If, 1, 0, 0}, /* 1 */ - { OP_String8, 0, 3, 0}, /* 2 */ - { OP_ResultRow, 3, 1, 0}, - }; - int isQuick = (sqlite3Tolower(zLeft[0])=='q'); /* If the PRAGMA command was of the form "PRAGMA .integrity_check", @@ -107214,7 +111443,10 @@ SQLITE_PRIVATE void sqlite3Pragma( for(i=0; inDb; i++){ HashElem *x; Hash *pTbls; + int *aRoot; int cnt = 0; + int mxIdx = 0; + int nIdx; if( OMIT_TEMPDB && i==1 ) continue; if( iDb>=0 && i!=iDb ) continue; @@ -107227,31 +111459,35 @@ SQLITE_PRIVATE void sqlite3Pragma( /* Do an integrity check of the B-Tree ** - ** Begin by filling registers 2, 3, ... with the root pages numbers + ** Begin by finding the root pages numbers ** for all tables and indices in the database. */ assert( sqlite3SchemaMutexHeld(db, i, 0) ); pTbls = &db->aDb[i].pSchema->tblHash; - for(x=sqliteHashFirst(pTbls); x; x=sqliteHashNext(x)){ + for(cnt=0, x=sqliteHashFirst(pTbls); x; x=sqliteHashNext(x)){ Table *pTab = sqliteHashData(x); Index *pIdx; - if( HasRowid(pTab) ){ - sqlite3VdbeAddOp2(v, OP_Integer, pTab->tnum, 2+cnt); - VdbeComment((v, "%s", pTab->zName)); - cnt++; - } + if( HasRowid(pTab) ) cnt++; + for(nIdx=0, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, nIdx++){ cnt++; } + if( nIdx>mxIdx ) mxIdx = nIdx; + } + aRoot = sqlite3DbMallocRawNN(db, sizeof(int)*(cnt+1)); + if( aRoot==0 ) break; + for(cnt=0, x=sqliteHashFirst(pTbls); x; x=sqliteHashNext(x)){ + Table *pTab = sqliteHashData(x); + Index *pIdx; + if( HasRowid(pTab) ) aRoot[cnt++] = pTab->tnum; for(pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext){ - sqlite3VdbeAddOp2(v, OP_Integer, pIdx->tnum, 2+cnt); - VdbeComment((v, "%s", pIdx->zName)); - cnt++; + aRoot[cnt++] = pIdx->tnum; } } + aRoot[cnt] = 0; /* Make sure sufficient number of registers have been allocated */ - pParse->nMem = MAX( pParse->nMem, cnt+8 ); + pParse->nMem = MAX( pParse->nMem, 8+mxIdx ); /* Do the b-tree integrity checks */ - sqlite3VdbeAddOp3(v, OP_IntegrityCk, 2, cnt, 1); + sqlite3VdbeAddOp4(v, OP_IntegrityCk, 2, cnt, 1, (char*)aRoot,P4_INTARRAY); sqlite3VdbeChangeP5(v, (u8)i); addr = sqlite3VdbeAddOp1(v, OP_IsNull, 2); VdbeCoverage(v); sqlite3VdbeAddOp4(v, OP_String8, 0, 3, 0, @@ -107279,13 +111515,14 @@ SQLITE_PRIVATE void sqlite3Pragma( sqlite3VdbeAddOp2(v, OP_Halt, 0, 0); sqlite3VdbeJumpHere(v, addr); sqlite3ExprCacheClear(pParse); - sqlite3OpenTableAndIndices(pParse, pTab, OP_OpenRead, + sqlite3OpenTableAndIndices(pParse, pTab, OP_OpenRead, 0, 1, 0, &iDataCur, &iIdxCur); sqlite3VdbeAddOp2(v, OP_Integer, 0, 7); for(j=0, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, j++){ sqlite3VdbeAddOp2(v, OP_Integer, 0, 8+j); /* index entries counter */ } - pParse->nMem = MAX(pParse->nMem, 8+j); + assert( pParse->nMem>=8+j ); + assert( sqlite3NoTempsInRange(pParse,1,7+j) ); sqlite3VdbeAddOp2(v, OP_Rewind, iDataCur, 0); VdbeCoverage(v); loopTop = sqlite3VdbeAddOp2(v, OP_AddImm, 7, 1); /* Verify that all NOT NULL columns really are NOT NULL */ @@ -107377,10 +111614,23 @@ SQLITE_PRIVATE void sqlite3Pragma( #endif /* SQLITE_OMIT_BTREECOUNT */ } } - addr = sqlite3VdbeAddOpList(v, ArraySize(endCode), endCode, iLn); - sqlite3VdbeChangeP2(v, addr, -mxErr); - sqlite3VdbeJumpHere(v, addr+1); - sqlite3VdbeChangeP4(v, addr+2, "ok", P4_STATIC); + { + static const int iLn = VDBE_OFFSET_LINENO(2); + static const VdbeOpList endCode[] = { + { OP_AddImm, 1, 0, 0}, /* 0 */ + { OP_If, 1, 4, 0}, /* 1 */ + { OP_String8, 0, 3, 0}, /* 2 */ + { OP_ResultRow, 3, 1, 0}, /* 3 */ + }; + VdbeOp *aOp; + + aOp = sqlite3VdbeAddOpList(v, ArraySize(endCode), endCode, iLn); + if( aOp ){ + aOp[0].p2 = -mxErr; + aOp[2].p4type = P4_STATIC; + aOp[2].p4.z = "ok"; + } + } } break; #endif /* SQLITE_OMIT_INTEGRITY_CHECK */ @@ -107458,16 +111708,18 @@ SQLITE_PRIVATE void sqlite3Pragma( #ifndef SQLITE_OMIT_SCHEMA_VERSION_PRAGMAS /* - ** PRAGMA [database.]schema_version - ** PRAGMA [database.]schema_version = + ** PRAGMA [schema.]schema_version + ** PRAGMA [schema.]schema_version = ** - ** PRAGMA [database.]user_version - ** PRAGMA [database.]user_version = + ** PRAGMA [schema.]user_version + ** PRAGMA [schema.]user_version = ** - ** PRAGMA [database.]freelist_count = + ** PRAGMA [schema.]freelist_count ** - ** PRAGMA [database.]application_id - ** PRAGMA [database.]application_id = + ** PRAGMA [schema.]data_version + ** + ** PRAGMA [schema.]application_id + ** PRAGMA [schema.]application_id = ** ** The pragma's schema_version and user_version are used to set or get ** the value of the schema-version and user-version, respectively. Both @@ -107494,14 +111746,16 @@ SQLITE_PRIVATE void sqlite3Pragma( /* Write the specified cookie value */ static const VdbeOpList setCookie[] = { { OP_Transaction, 0, 1, 0}, /* 0 */ - { OP_Integer, 0, 1, 0}, /* 1 */ - { OP_SetCookie, 0, 0, 1}, /* 2 */ + { OP_SetCookie, 0, 0, 0}, /* 1 */ }; - int addr = sqlite3VdbeAddOpList(v, ArraySize(setCookie), setCookie, 0); - sqlite3VdbeChangeP1(v, addr, iDb); - sqlite3VdbeChangeP1(v, addr+1, sqlite3Atoi(zRight)); - sqlite3VdbeChangeP1(v, addr+2, iDb); - sqlite3VdbeChangeP2(v, addr+2, iCookie); + VdbeOp *aOp; + sqlite3VdbeVerifyNoMallocRequired(v, ArraySize(setCookie)); + aOp = sqlite3VdbeAddOpList(v, ArraySize(setCookie), setCookie, 0); + if( ONLY_IF_REALLOC_STRESS(aOp==0) ) break; + aOp[0].p1 = iDb; + aOp[1].p1 = iDb; + aOp[1].p2 = iCookie; + aOp[1].p3 = sqlite3Atoi(zRight); }else{ /* Read the specified cookie value */ static const VdbeOpList readCookie[] = { @@ -107509,12 +111763,16 @@ SQLITE_PRIVATE void sqlite3Pragma( { OP_ReadCookie, 0, 1, 0}, /* 1 */ { OP_ResultRow, 1, 1, 0} }; - int addr = sqlite3VdbeAddOpList(v, ArraySize(readCookie), readCookie, 0); - sqlite3VdbeChangeP1(v, addr, iDb); - sqlite3VdbeChangeP1(v, addr+1, iDb); - sqlite3VdbeChangeP3(v, addr+1, iCookie); + VdbeOp *aOp; + sqlite3VdbeVerifyNoMallocRequired(v, ArraySize(readCookie)); + aOp = sqlite3VdbeAddOpList(v, ArraySize(readCookie),readCookie,0); + if( ONLY_IF_REALLOC_STRESS(aOp==0) ) break; + aOp[0].p1 = iDb; + aOp[1].p1 = iDb; + aOp[1].p3 = iCookie; sqlite3VdbeSetNumCols(v, 1); sqlite3VdbeSetColName(v, 0, COLNAME_NAME, zLeft, SQLITE_TRANSIENT); + sqlite3VdbeReusable(v); } } break; @@ -107536,13 +111794,14 @@ SQLITE_PRIVATE void sqlite3Pragma( sqlite3VdbeLoadString(v, 1, zOpt); sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 1); } + sqlite3VdbeReusable(v); } break; #endif /* SQLITE_OMIT_COMPILEOPTION_DIAGS */ #ifndef SQLITE_OMIT_WAL /* - ** PRAGMA [database.]wal_checkpoint = passive|full|restart|truncate + ** PRAGMA [schema.]wal_checkpoint = passive|full|restart|truncate ** ** Checkpoint the database. */ @@ -107770,13 +112029,12 @@ static void corruptSchema( if( !db->mallocFailed && (db->flags & SQLITE_RecoveryMode)==0 ){ char *z; if( zObj==0 ) zObj = "?"; - z = sqlite3_mprintf("malformed database schema (%s)", zObj); - if( z && zExtra ) z = sqlite3_mprintf("%z - %s", z, zExtra); + z = sqlite3MPrintf(db, "malformed database schema (%s)", zObj); + if( zExtra ) z = sqlite3MPrintf(db, "%z - %s", z, zExtra); sqlite3DbFree(db, *pData->pzErrMsg); *pData->pzErrMsg = z; - if( z==0 ) db->mallocFailed = 1; } - pData->rc = db->mallocFailed ? SQLITE_NOMEM : SQLITE_CORRUPT_BKPT; + pData->rc = db->mallocFailed ? SQLITE_NOMEM_BKPT : SQLITE_CORRUPT_BKPT; } /* @@ -107833,7 +112091,7 @@ SQLITE_PRIVATE int sqlite3InitCallback(void *pInit, int argc, char **argv, char }else{ pData->rc = rc; if( rc==SQLITE_NOMEM ){ - db->mallocFailed = 1; + sqlite3OomFault(db); }else if( rc!=SQLITE_INTERRUPT && (rc&0xFF)!=SQLITE_LOCKED ){ corruptSchema(pData, argv[0], sqlite3_errmsg(db)); } @@ -107879,61 +112137,27 @@ static int sqlite3InitOne(sqlite3 *db, int iDb, char **pzErrMsg){ #ifndef SQLITE_OMIT_DEPRECATED int size; #endif - Table *pTab; Db *pDb; char const *azArg[4]; int meta[5]; InitData initData; - char const *zMasterSchema; - char const *zMasterName; + const char *zMasterName; int openedTransaction = 0; - /* - ** The master database table has a structure like this - */ - static const char master_schema[] = - "CREATE TABLE sqlite_master(\n" - " type text,\n" - " name text,\n" - " tbl_name text,\n" - " rootpage integer,\n" - " sql text\n" - ")" - ; -#ifndef SQLITE_OMIT_TEMPDB - static const char temp_master_schema[] = - "CREATE TEMP TABLE sqlite_temp_master(\n" - " type text,\n" - " name text,\n" - " tbl_name text,\n" - " rootpage integer,\n" - " sql text\n" - ")" - ; -#else - #define temp_master_schema 0 -#endif - assert( iDb>=0 && iDbnDb ); assert( db->aDb[iDb].pSchema ); assert( sqlite3_mutex_held(db->mutex) ); assert( iDb==1 || sqlite3BtreeHoldsMutex(db->aDb[iDb].pBt) ); - /* zMasterSchema and zInitScript are set to point at the master schema - ** and initialisation script appropriate for the database being - ** initialized. zMasterName is the name of the master table. - */ - if( !OMIT_TEMPDB && iDb==1 ){ - zMasterSchema = temp_master_schema; - }else{ - zMasterSchema = master_schema; - } - zMasterName = SCHEMA_TABLE(iDb); - - /* Construct the schema tables. */ - azArg[0] = zMasterName; + /* Construct the in-memory representation schema tables (sqlite_master or + ** sqlite_temp_master) by invoking the parser directly. The appropriate + ** table name will be inserted automatically by the parser so we can just + ** use the abbreviation "x" here. The parser will also automatically tag + ** the schema table as read-only. */ + azArg[0] = zMasterName = SCHEMA_TABLE(iDb); azArg[1] = "1"; - azArg[2] = zMasterSchema; + azArg[2] = "CREATE TABLE x(type text,name text,tbl_name text," + "rootpage integer,sql text)"; azArg[3] = 0; initData.db = db; initData.iDb = iDb; @@ -107944,10 +112168,6 @@ static int sqlite3InitOne(sqlite3 *db, int iDb, char **pzErrMsg){ rc = initData.rc; goto error_out; } - pTab = sqlite3FindTable(db, zMasterName, db->aDb[iDb].zName); - if( ALWAYS(pTab) ){ - pTab->tabFlags |= TF_Readonly; - } /* Create a cursor to hold the database open */ @@ -108066,7 +112286,7 @@ static int sqlite3InitOne(sqlite3 *db, int iDb, char **pzErrMsg){ { char *zSql; zSql = sqlite3MPrintf(db, - "SELECT name, rootpage, sql FROM '%q'.%s ORDER BY rowid", + "SELECT name, rootpage, sql FROM \"%w\".%s ORDER BY rowid", db->aDb[iDb].zName, zMasterName); #ifndef SQLITE_OMIT_AUTHORIZATION { @@ -108088,7 +112308,7 @@ static int sqlite3InitOne(sqlite3 *db, int iDb, char **pzErrMsg){ #endif } if( db->mallocFailed ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; sqlite3ResetAllSchemasOfConnection(db); } if( rc==SQLITE_OK || (db->flags&SQLITE_RecoveryMode)){ @@ -108116,7 +112336,7 @@ initone_error_out: error_out: if( rc==SQLITE_NOMEM || rc==SQLITE_IOERR_NOMEM ){ - db->mallocFailed = 1; + sqlite3OomFault(db); } return rc; } @@ -108214,7 +112434,7 @@ static void schemaIsValid(Parse *pParse){ if( !sqlite3BtreeIsInReadTrans(pBt) ){ rc = sqlite3BtreeBeginTrans(pBt, 0); if( rc==SQLITE_NOMEM || rc==SQLITE_IOERR_NOMEM ){ - db->mallocFailed = 1; + sqlite3OomFault(db); } if( rc!=SQLITE_OK ) return; openedTransaction = 1; @@ -108277,6 +112497,11 @@ SQLITE_PRIVATE void sqlite3ParserReset(Parse *pParse){ sqlite3 *db = pParse->db; sqlite3DbFree(db, pParse->aLabel); sqlite3ExprListDelete(db, pParse->pConstExpr); + if( db ){ + assert( db->lookaside.bDisable >= pParse->disableLookaside ); + db->lookaside.bDisable -= pParse->disableLookaside; + } + pParse->disableLookaside = 0; } } @@ -108300,12 +112525,12 @@ static int sqlite3Prepare( /* Allocate the parsing context */ pParse = sqlite3StackAllocZero(db, sizeof(*pParse)); if( pParse==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto end_prepare; } pParse->pReprepare = pReprepare; assert( ppStmt && *ppStmt==0 ); - assert( !db->mallocFailed ); + /* assert( !db->mallocFailed ); // not true with SQLITE_USE_ALLOCA */ assert( sqlite3_mutex_held(db->mutex) ); /* Check to verify that it is possible to get a read lock on all @@ -108362,8 +112587,8 @@ static int sqlite3Prepare( zSqlCopy = sqlite3DbStrNDup(db, zSql, nBytes); if( zSqlCopy ){ sqlite3RunParser(pParse, zSqlCopy, &zErrMsg); - sqlite3DbFree(db, zSqlCopy); pParse->zTail = &zSql[pParse->zTail-zSqlCopy]; + sqlite3DbFree(db, zSqlCopy); }else{ pParse->zTail = &zSql[nBytes]; } @@ -108372,15 +112597,12 @@ static int sqlite3Prepare( } assert( 0==pParse->nQueryLoop ); - if( db->mallocFailed ){ - pParse->rc = SQLITE_NOMEM; - } if( pParse->rc==SQLITE_DONE ) pParse->rc = SQLITE_OK; if( pParse->checkSchema ){ schemaIsValid(pParse); } if( db->mallocFailed ){ - pParse->rc = SQLITE_NOMEM; + pParse->rc = SQLITE_NOMEM_BKPT; } if( pzTail ){ *pzTail = pParse->zTail; @@ -108496,7 +112718,7 @@ SQLITE_PRIVATE int sqlite3Reprepare(Vdbe *p){ rc = sqlite3LockAndPrepare(db, zSql, -1, 0, p, &pNew, 0); if( rc ){ if( rc==SQLITE_NOMEM ){ - db->mallocFailed = 1; + sqlite3OomFault(db); } assert( pNew==0 ); return rc; @@ -108692,6 +112914,7 @@ struct SortCtx { int regReturn; /* Register holding block-output return address */ int labelBkOut; /* Start label for the block-output subroutine */ int addrSortIndex; /* Address of the OP_SorterOpen or OP_OpenEphemeral */ + int labelDone; /* Jump here when done, ex: LIMIT reached */ u8 sortFlags; /* Zero or more SORTFLAG_* bits */ }; #define SORTFLAG_UseSorter 0x01 /* Use SorterOpen instead of OpenEphemeral */ @@ -108711,7 +112934,7 @@ static void clearSelect(sqlite3 *db, Select *p, int bFree){ sqlite3ExprListDelete(db, p->pOrderBy); sqlite3ExprDelete(db, p->pLimit); sqlite3ExprDelete(db, p->pOffset); - sqlite3WithDelete(db, p->pWith); + if( p->pWith ) sqlite3WithDelete(db, p->pWith); if( bFree ) sqlite3DbFree(db, p); p = pPrior; bFree = 1; @@ -108742,36 +112965,44 @@ SQLITE_PRIVATE Select *sqlite3SelectNew( ExprList *pGroupBy, /* the GROUP BY clause */ Expr *pHaving, /* the HAVING clause */ ExprList *pOrderBy, /* the ORDER BY clause */ - u16 selFlags, /* Flag parameters, such as SF_Distinct */ + u32 selFlags, /* Flag parameters, such as SF_Distinct */ Expr *pLimit, /* LIMIT value. NULL means not used */ Expr *pOffset /* OFFSET value. NULL means no offset */ ){ Select *pNew; Select standin; sqlite3 *db = pParse->db; - pNew = sqlite3DbMallocZero(db, sizeof(*pNew) ); + pNew = sqlite3DbMallocRawNN(db, sizeof(*pNew) ); if( pNew==0 ){ assert( db->mallocFailed ); pNew = &standin; - memset(pNew, 0, sizeof(*pNew)); } if( pEList==0 ){ - pEList = sqlite3ExprListAppend(pParse, 0, sqlite3Expr(db,TK_ALL,0)); + pEList = sqlite3ExprListAppend(pParse, 0, sqlite3Expr(db,TK_ASTERISK,0)); } pNew->pEList = pEList; + pNew->op = TK_SELECT; + pNew->selFlags = selFlags; + pNew->iLimit = 0; + pNew->iOffset = 0; +#if SELECTTRACE_ENABLED + pNew->zSelName[0] = 0; +#endif + pNew->addrOpenEphm[0] = -1; + pNew->addrOpenEphm[1] = -1; + pNew->nSelectRow = 0; if( pSrc==0 ) pSrc = sqlite3DbMallocZero(db, sizeof(*pSrc)); pNew->pSrc = pSrc; pNew->pWhere = pWhere; pNew->pGroupBy = pGroupBy; pNew->pHaving = pHaving; pNew->pOrderBy = pOrderBy; - pNew->selFlags = selFlags; - pNew->op = TK_SELECT; + pNew->pPrior = 0; + pNew->pNext = 0; pNew->pLimit = pLimit; pNew->pOffset = pOffset; + pNew->pWith = 0; assert( pOffset==0 || pLimit!=0 || pParse->nErr>0 || db->mallocFailed!=0 ); - pNew->addrOpenEphm[0] = -1; - pNew->addrOpenEphm[1] = -1; if( db->mallocFailed ) { clearSelect(db, pNew, pNew!=&standin); pNew = 0; @@ -108798,7 +113029,7 @@ SQLITE_PRIVATE void sqlite3SelectSetName(Select *p, const char *zName){ ** Delete the given Select structure and all of its substructures. */ SQLITE_PRIVATE void sqlite3SelectDelete(sqlite3 *db, Select *p){ - clearSelect(db, p, 1); + if( p ) clearSelect(db, p, 1); } /* @@ -109146,6 +113377,7 @@ static void pushOntoSorter( int regRecord = ++pParse->nMem; /* Assembled sorter record */ int nOBSat = pSort->nOBSat; /* ORDER BY terms to skip */ int op; /* Opcode to add sorter record to sorter */ + int iLimit; /* LIMIT counter */ assert( bSeq==0 || bSeq==1 ); assert( nData==1 || regData==regOrigData ); @@ -109156,6 +113388,9 @@ static void pushOntoSorter( regBase = pParse->nMem + 1; pParse->nMem += nBase; } + assert( pSelect->iOffset==0 || pSelect->iLimit!=0 ); + iLimit = pSelect->iOffset ? pSelect->iOffset+1 : pSelect->iLimit; + pSort->labelDone = sqlite3VdbeMakeLabel(v); sqlite3ExprCodeExprList(pParse, pSort->pOrderBy, regBase, regOrigData, SQLITE_ECEL_DUP|SQLITE_ECEL_REF); if( bSeq ){ @@ -109164,7 +113399,6 @@ static void pushOntoSorter( if( nPrefixReg==0 ){ sqlite3ExprCodeMove(pParse, regData, regBase+nExpr+bSeq, nData); } - sqlite3VdbeAddOp3(v, OP_MakeRecord, regBase+nOBSat, nBase-nOBSat, regRecord); if( nOBSat>0 ){ int regPrevKey; /* The first nOBSat columns of the previous row */ @@ -109199,6 +113433,10 @@ static void pushOntoSorter( pSort->regReturn = ++pParse->nMem; sqlite3VdbeAddOp2(v, OP_Gosub, pSort->regReturn, pSort->labelBkOut); sqlite3VdbeAddOp1(v, OP_ResetSorter, pSort->iECursor); + if( iLimit ){ + sqlite3VdbeAddOp2(v, OP_IfNot, iLimit, pSort->labelDone); + VdbeCoverage(v); + } sqlite3VdbeJumpHere(v, addrFirst); sqlite3ExprCodeMove(pParse, regBase, regPrevKey, pSort->nOBSat); sqlite3VdbeJumpHere(v, addrJmp); @@ -109209,14 +113447,8 @@ static void pushOntoSorter( op = OP_IdxInsert; } sqlite3VdbeAddOp2(v, op, pSort->iECursor, regRecord); - if( pSelect->iLimit ){ + if( iLimit ){ int addr; - int iLimit; - if( pSelect->iOffset ){ - iLimit = pSelect->iOffset+1; - }else{ - iLimit = pSelect->iLimit; - } addr = sqlite3VdbeAddOp3(v, OP_IfNotZero, iLimit, 0, 1); VdbeCoverage(v); sqlite3VdbeAddOp1(v, OP_Last, pSort->iECursor); sqlite3VdbeAddOp1(v, OP_Delete, pSort->iECursor); @@ -109633,8 +113865,8 @@ static void selectInnerLoop( ** X extra columns. */ SQLITE_PRIVATE KeyInfo *sqlite3KeyInfoAlloc(sqlite3 *db, int N, int X){ - KeyInfo *p = sqlite3DbMallocZero(0, - sizeof(KeyInfo) + (N+X)*(sizeof(CollSeq*)+1)); + int nExtra = (N+X)*(sizeof(CollSeq*)+1); + KeyInfo *p = sqlite3Malloc(sizeof(KeyInfo) + nExtra); if( p ){ p->aSortOrder = (u8*)&p->aColl[N+X]; p->nField = (u16)N; @@ -109642,8 +113874,9 @@ SQLITE_PRIVATE KeyInfo *sqlite3KeyInfoAlloc(sqlite3 *db, int N, int X){ p->enc = ENC(db); p->db = db; p->nRef = 1; + memset(&p[1], 0, nExtra); }else{ - db->mallocFailed = 1; + sqlite3OomFault(db); } return p; } @@ -109820,7 +114053,7 @@ static void generateSortTail( SelectDest *pDest /* Write the sorted results here */ ){ Vdbe *v = pParse->pVdbe; /* The prepared statement */ - int addrBreak = sqlite3VdbeMakeLabel(v); /* Jump here to exit loop */ + int addrBreak = pSort->labelDone; /* Jump here to exit loop */ int addrContinue = sqlite3VdbeMakeLabel(v); /* Jump here for next cycle */ int addr; int addrOnce = 0; @@ -109839,6 +114072,7 @@ static void generateSortTail( struct ExprList_item *aOutEx = p->pEList->a; #endif + assert( addrBreak<0 ); if( pSort->labelBkOut ){ sqlite3VdbeAddOp2(v, OP_Gosub, pSort->regReturn, pSort->labelBkOut); sqlite3VdbeGoto(v, addrBreak); @@ -109978,7 +114212,8 @@ static const char *columnTypeImpl( char const *zOrigCol = 0; #endif - if( NEVER(pExpr==0) || pNC->pSrcList==0 ) return 0; + assert( pExpr!=0 ); + assert( pNC->pSrcList!=0 ); switch( pExpr->op ){ case TK_AGG_COLUMN: case TK_COLUMN: { @@ -110054,8 +114289,8 @@ static const char *columnTypeImpl( zType = "INTEGER"; zOrigCol = "rowid"; }else{ - zType = pTab->aCol[iCol].zType; zOrigCol = pTab->aCol[iCol].zName; + zType = sqlite3ColumnType(&pTab->aCol[iCol],0); estWidth = pTab->aCol[iCol].szEst; } zOrigTab = pTab->zName; @@ -110067,7 +114302,7 @@ static const char *columnTypeImpl( if( iCol<0 ){ zType = "INTEGER"; }else{ - zType = pTab->aCol[iCol].zType; + zType = sqlite3ColumnType(&pTab->aCol[iCol],0); estWidth = pTab->aCol[iCol].szEst; } #endif @@ -110166,7 +114401,9 @@ static void generateColumnNames( } #endif - if( pParse->colNamesSet || NEVER(v==0) || db->mallocFailed ) return; + if( pParse->colNamesSet || db->mallocFailed ) return; + assert( v!=0 ); + assert( pTabList!=0 ); pParse->colNamesSet = 1; fullNames = (db->flags & SQLITE_FullColNames)!=0; shortNames = (db->flags & SQLITE_ShortColNames)!=0; @@ -110178,7 +114415,7 @@ static void generateColumnNames( if( pEList->a[i].zName ){ char *zName = pEList->a[i].zName; sqlite3VdbeSetColName(v, i, COLNAME_NAME, zName, SQLITE_TRANSIENT); - }else if( (p->op==TK_COLUMN || p->op==TK_AGG_COLUMN) && pTabList ){ + }else if( p->op==TK_COLUMN || p->op==TK_AGG_COLUMN ){ Table *pTab; char *zCol; int iCol = p->iColumn; @@ -110234,13 +114471,15 @@ SQLITE_PRIVATE int sqlite3ColumnsFromExprList( ){ sqlite3 *db = pParse->db; /* Database connection */ int i, j; /* Loop counters */ - int cnt; /* Index added to make the name unique */ + u32 cnt; /* Index added to make the name unique */ Column *aCol, *pCol; /* For looping over result columns */ int nCol; /* Number of columns in the result set */ Expr *p; /* Expression for a single result column */ char *zName; /* Column name */ int nName; /* Size of name in zName[] */ + Hash ht; /* Hash table of column names */ + sqlite3HashInit(&ht); if( pEList ){ nCol = pEList->nExpr; aCol = sqlite3DbMallocZero(db, sizeof(aCol[0])*nCol); @@ -110249,16 +114488,16 @@ SQLITE_PRIVATE int sqlite3ColumnsFromExprList( nCol = 0; aCol = 0; } + assert( nCol==(i16)nCol ); *pnCol = nCol; *paCol = aCol; - for(i=0, pCol=aCol; imallocFailed; i++, pCol++){ /* Get an appropriate name for the column */ p = sqlite3ExprSkipCollate(pEList->a[i].pExpr); if( (zName = pEList->a[i].zName)!=0 ){ /* If the column contains an "AS " phrase, use as the name */ - zName = sqlite3DbStrDup(db, zName); }else{ Expr *pColExpr = p; /* The expression that is the result column name */ Table *pTab; /* Table associated with this expression */ @@ -110271,41 +114510,37 @@ SQLITE_PRIVATE int sqlite3ColumnsFromExprList( int iCol = pColExpr->iColumn; pTab = pColExpr->pTab; if( iCol<0 ) iCol = pTab->iPKey; - zName = sqlite3MPrintf(db, "%s", - iCol>=0 ? pTab->aCol[iCol].zName : "rowid"); + zName = iCol>=0 ? pTab->aCol[iCol].zName : "rowid"; }else if( pColExpr->op==TK_ID ){ assert( !ExprHasProperty(pColExpr, EP_IntValue) ); - zName = sqlite3MPrintf(db, "%s", pColExpr->u.zToken); + zName = pColExpr->u.zToken; }else{ /* Use the original text of the column expression as its name */ - zName = sqlite3MPrintf(db, "%s", pEList->a[i].zSpan); + zName = pEList->a[i].zSpan; } } - if( db->mallocFailed ){ - sqlite3DbFree(db, zName); - break; - } + zName = sqlite3MPrintf(db, "%s", zName); /* Make sure the column name is unique. If the name is not unique, ** append an integer to the name so that it becomes unique. */ - nName = sqlite3Strlen30(zName); - for(j=cnt=0; j1 && sqlite3Isdigit(zName[k]); k--){} - if( k>=0 && zName[k]==':' ) nName = k; - zName[nName] = 0; - zNewName = sqlite3MPrintf(db, "%s:%d", zName, ++cnt); - sqlite3DbFree(db, zName); - zName = zNewName; - j = -1; - if( zName==0 ) break; + cnt = 0; + while( zName && sqlite3HashFind(&ht, zName)!=0 ){ + nName = sqlite3Strlen30(zName); + if( nName>0 ){ + for(j=nName-1; j>0 && sqlite3Isdigit(zName[j]); j--){} + if( zName[j]==':' ) nName = j; } + zName = sqlite3MPrintf(db, "%.*z:%u", nName, zName, ++cnt); + if( cnt>3 ) sqlite3_randomness(sizeof(cnt), &cnt); } pCol->zName = zName; + sqlite3ColumnPropertiesFromName(0, pCol); + if( zName && sqlite3HashInsert(&ht, zName, pCol)==pCol ){ + sqlite3OomFault(db); + } } + sqlite3HashClear(&ht); if( db->mallocFailed ){ for(j=0; jpSrc; a = pSelect->pEList->a; for(i=0, pCol=pTab->aCol; inCol; i++, pCol++){ + const char *zType; + int n, m; p = a[i].pExpr; - if( pCol->zType==0 ){ - pCol->zType = sqlite3DbStrDup(db, - columnType(&sNC, p,0,0,0, &pCol->szEst)); - } + zType = columnType(&sNC, p, 0, 0, 0, &pCol->szEst); szAll += pCol->szEst; pCol->affinity = sqlite3ExprAffinity(p); + if( zType && (m = sqlite3Strlen30(zType))>0 ){ + n = sqlite3Strlen30(pCol->zName); + pCol->zName = sqlite3DbReallocOrFree(db, pCol->zName, n+m+2); + if( pCol->zName ){ + memcpy(&pCol->zName[n+1], zType, m+1); + pCol->colFlags |= COLFLAG_HASTYPE; + } + } if( pCol->affinity==0 ) pCol->affinity = SQLITE_AFF_BLOB; pColl = sqlite3ExprCollSeq(pParse, p); if( pColl && pCol->zColl==0 ){ @@ -110389,12 +114631,12 @@ SQLITE_PRIVATE Table *sqlite3ResultSetOfSelect(Parse *pParse, Select *pSelect){ } /* The sqlite3ResultSetOfSelect() is only used n contexts where lookaside ** is disabled */ - assert( db->lookaside.bEnabled==0 ); + assert( db->lookaside.bDisable ); pTab->nRef = 1; pTab->zName = 0; pTab->nRowLogEst = 200; assert( 200==sqlite3LogEst(1048576) ); sqlite3ColumnsFromExprList(pParse, pSelect->pEList, &pTab->nCol, &pTab->aCol); - selectAddColumnTypeAndCollation(pParse, pTab, pSelect); + sqlite3SelectAddColumnTypeAndCollation(pParse, pTab, pSelect); pTab->iPKey = -1; if( db->mallocFailed ){ sqlite3DeleteTable(db, pTab); @@ -110407,20 +114649,20 @@ SQLITE_PRIVATE Table *sqlite3ResultSetOfSelect(Parse *pParse, Select *pSelect){ ** Get a VDBE for the given parser context. Create a new one if necessary. ** If an error occurs, return NULL and leave a message in pParse. */ -SQLITE_PRIVATE Vdbe *sqlite3GetVdbe(Parse *pParse){ - Vdbe *v = pParse->pVdbe; - if( v==0 ){ - v = pParse->pVdbe = sqlite3VdbeCreate(pParse); - if( v ) sqlite3VdbeAddOp0(v, OP_Init); - if( pParse->pToplevel==0 - && OptimizationEnabled(pParse->db,SQLITE_FactorOutConst) - ){ - pParse->okConstFactor = 1; - } - +static SQLITE_NOINLINE Vdbe *allocVdbe(Parse *pParse){ + Vdbe *v = pParse->pVdbe = sqlite3VdbeCreate(pParse); + if( v ) sqlite3VdbeAddOp0(v, OP_Init); + if( pParse->pToplevel==0 + && OptimizationEnabled(pParse->db,SQLITE_FactorOutConst) + ){ + pParse->okConstFactor = 1; } return v; } +SQLITE_PRIVATE Vdbe *sqlite3GetVdbe(Parse *pParse){ + Vdbe *v = pParse->pVdbe; + return v ? v : allocVdbe(pParse); +} /* @@ -110470,8 +114712,9 @@ static void computeLimitRegisters(Parse *pParse, Select *p, int iBreak){ VdbeComment((v, "LIMIT counter")); if( n==0 ){ sqlite3VdbeGoto(v, iBreak); - }else if( n>=0 && p->nSelectRow>(u64)n ){ - p->nSelectRow = n; + }else if( n>=0 && p->nSelectRow>sqlite3LogEst((u64)n) ){ + p->nSelectRow = sqlite3LogEst((u64)n); + p->selFlags |= SF_FixedLimit; } }else{ sqlite3ExprCode(pParse, p->pLimit, iLimit); @@ -110485,10 +114728,8 @@ static void computeLimitRegisters(Parse *pParse, Select *p, int iBreak){ sqlite3ExprCode(pParse, p->pOffset, iOffset); sqlite3VdbeAddOp1(v, OP_MustBeInt, iOffset); VdbeCoverage(v); VdbeComment((v, "OFFSET counter")); - sqlite3VdbeAddOp3(v, OP_SetIfNotPos, iOffset, iOffset, 0); - sqlite3VdbeAddOp3(v, OP_Add, iLimit, iOffset, iOffset+1); + sqlite3VdbeAddOp3(v, OP_OffsetLimit, iLimit, iOffset+1, iOffset); VdbeComment((v, "LIMIT+OFFSET")); - sqlite3VdbeAddOp3(v, OP_SetIfNotPos, iLimit, iOffset+1, -1); } } } @@ -110851,7 +115092,6 @@ static int multiSelect( if( dest.eDest==SRT_EphemTab ){ assert( p->pEList ); sqlite3VdbeAddOp2(v, OP_OpenEphemeral, dest.iSDParm, p->pEList->nExpr); - sqlite3VdbeChangeP5(v, BTREE_UNORDERED); dest.eDest = SRT_Table; } @@ -110905,9 +115145,8 @@ static int multiSelect( addr = sqlite3VdbeAddOp1(v, OP_IfNot, p->iLimit); VdbeCoverage(v); VdbeComment((v, "Jump ahead if LIMIT reached")); if( p->iOffset ){ - sqlite3VdbeAddOp3(v, OP_SetIfNotPos, p->iOffset, p->iOffset, 0); - sqlite3VdbeAddOp3(v, OP_Add, p->iLimit, p->iOffset, p->iOffset+1); - sqlite3VdbeAddOp3(v, OP_SetIfNotPos, p->iLimit, p->iOffset+1, -1); + sqlite3VdbeAddOp3(v, OP_OffsetLimit, + p->iLimit, p->iOffset+1, p->iOffset); } } explainSetInteger(iSub2, pParse->iNextSelectId); @@ -110915,12 +115154,12 @@ static int multiSelect( testcase( rc!=SQLITE_OK ); pDelete = p->pPrior; p->pPrior = pPrior; - p->nSelectRow += pPrior->nSelectRow; + p->nSelectRow = sqlite3LogEstAdd(p->nSelectRow, pPrior->nSelectRow); if( pPrior->pLimit && sqlite3ExprIsInteger(pPrior->pLimit, &nLimit) - && nLimit>0 && p->nSelectRow > (u64)nLimit + && nLimit>0 && p->nSelectRow > sqlite3LogEst((u64)nLimit) ){ - p->nSelectRow = nLimit; + p->nSelectRow = sqlite3LogEst((u64)nLimit); } if( addr ){ sqlite3VdbeJumpHere(v, addr); @@ -110992,7 +115231,9 @@ static int multiSelect( pDelete = p->pPrior; p->pPrior = pPrior; p->pOrderBy = 0; - if( p->op==TK_UNION ) p->nSelectRow += pPrior->nSelectRow; + if( p->op==TK_UNION ){ + p->nSelectRow = sqlite3LogEstAdd(p->nSelectRow, pPrior->nSelectRow); + } sqlite3ExprDelete(db, p->pLimit); p->pLimit = pLimit; p->pOffset = pOffset; @@ -111009,7 +115250,7 @@ static int multiSelect( if( dest.eDest==SRT_Output ){ Select *pFirst = p; while( pFirst->pPrior ) pFirst = pFirst->pPrior; - generateColumnNames(pParse, 0, pFirst->pEList); + generateColumnNames(pParse, pFirst->pSrc, pFirst->pEList); } iBreak = sqlite3VdbeMakeLabel(v); iCont = sqlite3VdbeMakeLabel(v); @@ -111084,7 +115325,7 @@ static int multiSelect( if( dest.eDest==SRT_Output ){ Select *pFirst = p; while( pFirst->pPrior ) pFirst = pFirst->pPrior; - generateColumnNames(pParse, 0, pFirst->pEList); + generateColumnNames(pParse, pFirst->pSrc, pFirst->pEList); } iBreak = sqlite3VdbeMakeLabel(v); iCont = sqlite3VdbeMakeLabel(v); @@ -111127,7 +115368,7 @@ static int multiSelect( nCol = p->pEList->nExpr; pKeyInfo = sqlite3KeyInfoAlloc(db, nCol, 1); if( !pKeyInfo ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto multi_select_end; } for(i=0, apColl=pKeyInfo->aColl; iflags |= EP_IntValue; pNew->u.iValue = i; pOrderBy = sqlite3ExprListAppend(pParse, pOrderBy, pNew); @@ -111498,10 +115739,11 @@ static int multiSelectOrderBy( ** to the right and the left are evaluated, they use the correct ** collation. */ - aPermute = sqlite3DbMallocRaw(db, sizeof(int)*nOrderBy); + aPermute = sqlite3DbMallocRawNN(db, sizeof(int)*(nOrderBy + 1)); if( aPermute ){ struct ExprList_item *pItem; - for(i=0, pItem=pOrderBy->a; ia; i<=nOrderBy; i++, pItem++){ assert( pItem->u.x.iOrderByCol>0 ); assert( pItem->u.x.iOrderByCol<=p->pEList->nExpr ); aPermute[i] = pItem->u.x.iOrderByCol - 1; @@ -111579,7 +115821,7 @@ static int multiSelectOrderBy( pPrior->iLimit = regLimitA; explainSetInteger(iSub1, pParse->iNextSelectId); sqlite3Select(pParse, pPrior, &destA); - sqlite3VdbeAddOp1(v, OP_EndCoroutine, regAddrA); + sqlite3VdbeEndCoroutine(v, regAddrA); sqlite3VdbeJumpHere(v, addr1); /* Generate a coroutine to evaluate the SELECT statement on @@ -111596,7 +115838,7 @@ static int multiSelectOrderBy( sqlite3Select(pParse, p, &destB); p->iLimit = savedLimit; p->iOffset = savedOffset; - sqlite3VdbeAddOp1(v, OP_EndCoroutine, regAddrB); + sqlite3VdbeEndCoroutine(v, regAddrB); /* Generate a subroutine that outputs the current row of the A ** select as the next output row of the compound select. @@ -111628,7 +115870,7 @@ static int multiSelectOrderBy( addrEofA_noB = sqlite3VdbeAddOp2(v, OP_Yield, regAddrB, labelEnd); VdbeCoverage(v); sqlite3VdbeGoto(v, addrEofA); - p->nSelectRow += pPrior->nSelectRow; + p->nSelectRow = sqlite3LogEstAdd(p->nSelectRow, pPrior->nSelectRow); } /* Generate a subroutine to run when the results from select B @@ -111699,7 +115941,7 @@ static int multiSelectOrderBy( if( pDest->eDest==SRT_Output ){ Select *pFirst = pPrior; while( pFirst->pPrior ) pFirst = pFirst->pPrior; - generateColumnNames(pParse, 0, pFirst->pEList); + generateColumnNames(pParse, pFirst->pSrc, pFirst->pEList); } /* Reassembly the compound query so that it will be freed correctly @@ -112264,6 +116506,7 @@ static int flattenSubquery( */ for(i=0; ia[i+iFrom].pUsing); + assert( pSrc->a[i+iFrom].fg.isTabFunc==0 ); pSrc->a[i+iFrom] = pSubSrc->a[i]; memset(&pSubSrc->a[i], 0, sizeof(pSubSrc->a[i])); } @@ -112402,12 +116645,18 @@ static int pushDownWhereTerms( ){ Expr *pNew; int nChng = 0; + Select *pX; /* For looping over compound SELECTs in pSubq */ if( pWhere==0 ) return 0; - if( (pSubq->selFlags & (SF_Aggregate|SF_Recursive))!=0 ){ - return 0; /* restrictions (1) and (2) */ + for(pX=pSubq; pX; pX=pX->pPrior){ + if( (pX->selFlags & (SF_Aggregate|SF_Recursive))!=0 ){ + testcase( pX->selFlags & SF_Aggregate ); + testcase( pX->selFlags & SF_Recursive ); + testcase( pX!=pSubq ); + return 0; /* restrictions (1) and (2) */ + } } if( pSubq->pLimit!=0 ){ - return 0; /* restriction (3) */ + return 0; /* restriction (3) */ } while( pWhere->op==TK_AND ){ nChng += pushDownWhereTerms(db, pSubq, pWhere->pRight, iCursor); @@ -112579,7 +116828,7 @@ static int convertCompoundSelectToSubquery(Walker *pWalker, Select *p){ if( pNewSrc==0 ) return WRC_Abort; *pNew = *p; p->pSrc = pNewSrc; - p->pEList = sqlite3ExprListAppend(pParse, 0, sqlite3Expr(db, TK_ALL, 0)); + p->pEList = sqlite3ExprListAppend(pParse, 0, sqlite3Expr(db, TK_ASTERISK, 0)); p->op = TK_SELECT; p->pWhere = 0; pNew->pGroupBy = 0; @@ -112598,6 +116847,19 @@ static int convertCompoundSelectToSubquery(Walker *pWalker, Select *p){ return WRC_Continue; } +/* +** Check to see if the FROM clause term pFrom has table-valued function +** arguments. If it does, leave an error message in pParse and return +** non-zero, since pFrom is not allowed to be a table-valued function. +*/ +static int cannotBeFunction(Parse *pParse, struct SrcList_item *pFrom){ + if( pFrom->fg.isTabFunc ){ + sqlite3ErrorMsg(pParse, "'%s' is not a function", pFrom->zName); + return 1; + } + return 0; +} + #ifndef SQLITE_OMIT_CTE /* ** Argument pWith (which may be NULL) points to a linked list of nested @@ -112610,7 +116872,7 @@ static int convertCompoundSelectToSubquery(Walker *pWalker, Select *p){ ** object that the returned CTE belongs to. */ static struct Cte *searchWith( - With *pWith, /* Current outermost WITH clause */ + With *pWith, /* Current innermost WITH clause */ struct SrcList_item *pItem, /* FROM clause element to resolve */ With **ppContext /* OUT: WITH clause return value belongs to */ ){ @@ -112641,11 +116903,12 @@ static struct Cte *searchWith( ** statement with which it is associated. */ SQLITE_PRIVATE void sqlite3WithPush(Parse *pParse, With *pWith, u8 bFree){ - assert( bFree==0 || pParse->pWith==0 ); + assert( bFree==0 || (pParse->pWith==0 && pParse->pWithToFree==0) ); if( pWith ){ + assert( pParse->pWith!=pWith ); pWith->pOuter = pParse->pWith; pParse->pWith = pWith; - pParse->bFreeWith = bFree; + if( bFree ) pParse->pWithToFree = pWith; } } @@ -112692,6 +116955,7 @@ static int withExpand( sqlite3ErrorMsg(pParse, pCte->zCteErr, pCte->zName); return SQLITE_ERROR; } + if( cannotBeFunction(pParse, pFrom) ) return SQLITE_ERROR; assert( pFrom->pTab==0 ); pFrom->pTab = pTab = sqlite3DbMallocZero(db, sizeof(Table)); @@ -112702,7 +116966,7 @@ static int withExpand( pTab->nRowLogEst = 200; assert( 200==sqlite3LogEst(1048576) ); pTab->tabFlags |= TF_Ephemeral | TF_NoVisibleRowid; pFrom->pSelect = sqlite3SelectDup(db, pCte->pSelect, 0); - if( db->mallocFailed ) return SQLITE_NOMEM; + if( db->mallocFailed ) return SQLITE_NOMEM_BKPT; assert( pFrom->pSelect ); /* Check if this is a recursive CTE. */ @@ -112738,6 +117002,7 @@ static int withExpand( pSavedWith = pParse->pWith; pParse->pWith = pWith; sqlite3WalkSelect(pWalker, bMayRecursive ? pSel->pPrior : pSel); + pParse->pWith = pWith; for(pLeft=pSel; pLeft->pPrior; pLeft=pLeft->pPrior); pEList = pLeft->pEList; @@ -112884,15 +117149,14 @@ static int selectExpander(Walker *pWalker, Select *p){ return WRC_Abort; } pTab->nRef++; + if( !IsVirtual(pTab) && cannotBeFunction(pParse, pFrom) ){ + return WRC_Abort; + } #if !defined(SQLITE_OMIT_VIEW) || !defined (SQLITE_OMIT_VIRTUALTABLE) - if( pTab->pSelect || IsVirtual(pTab) ){ + if( IsVirtual(pTab) || pTab->pSelect ){ i16 nCol; if( sqlite3ViewGetColumnNames(pParse, pTab) ) return WRC_Abort; assert( pFrom->pSelect==0 ); - if( pFrom->fg.isTabFunc && !IsVirtual(pTab) ){ - sqlite3ErrorMsg(pParse, "'%s' is not a function", pTab->zName); - return WRC_Abort; - } pFrom->pSelect = sqlite3SelectDup(db, pTab->pSelect, 0); sqlite3SelectSetName(pFrom->pSelect, pTab->zName); nCol = pTab->nCol; @@ -112918,19 +117182,20 @@ static int selectExpander(Walker *pWalker, Select *p){ /* For every "*" that occurs in the column list, insert the names of ** all columns in all tables. And for every TABLE.* insert the names ** of all columns in TABLE. The parser inserted a special expression - ** with the TK_ALL operator for each "*" that it found in the column list. - ** The following code just has to locate the TK_ALL expressions and expand - ** each one to the list of all columns in all tables. + ** with the TK_ASTERISK operator for each "*" that it found in the column + ** list. The following code just has to locate the TK_ASTERISK + ** expressions and expand each one to the list of all columns in + ** all tables. ** ** The first loop just checks to see if there are any "*" operators ** that need expanding. */ for(k=0; knExpr; k++){ pE = pEList->a[k].pExpr; - if( pE->op==TK_ALL ) break; + if( pE->op==TK_ASTERISK ) break; assert( pE->op!=TK_DOT || pE->pRight!=0 ); assert( pE->op!=TK_DOT || (pE->pLeft!=0 && pE->pLeft->op==TK_ID) ); - if( pE->op==TK_DOT && pE->pRight->op==TK_ALL ) break; + if( pE->op==TK_DOT && pE->pRight->op==TK_ASTERISK ) break; } if( knExpr ){ /* @@ -112948,7 +117213,9 @@ static int selectExpander(Walker *pWalker, Select *p){ pE = a[k].pExpr; pRight = pE->pRight; assert( pE->op!=TK_DOT || pRight!=0 ); - if( pE->op!=TK_ALL && (pE->op!=TK_DOT || pRight->op!=TK_ALL) ){ + if( pE->op!=TK_ASTERISK + && (pE->op!=TK_DOT || pRight->op!=TK_ASTERISK) + ){ /* This particular expression does not need to be expanded. */ pNew = sqlite3ExprListAppend(pParse, pNew, a[k].pExpr); @@ -113000,12 +117267,13 @@ static int selectExpander(Walker *pWalker, Select *p){ continue; } - /* If a column is marked as 'hidden' (currently only possible - ** for virtual tables), do not include it in the expanded - ** result-set list. + /* If a column is marked as 'hidden', omit it from the expanded + ** result-set list unless the SELECT has the SF_IncludeHidden + ** bit set. */ - if( IsHiddenColumn(&pTab->aCol[j]) ){ - assert(IsVirtual(pTab)); + if( (p->selFlags & SF_IncludeHidden)==0 + && IsHiddenColumn(&pTab->aCol[j]) + ){ continue; } tableSeen = 1; @@ -113043,8 +117311,7 @@ static int selectExpander(Walker *pWalker, Select *p){ pExpr = pRight; } pNew = sqlite3ExprListAppend(pParse, pNew, pExpr); - sColname.z = zColname; - sColname.n = sqlite3Strlen30(zColname); + sqlite3TokenInit(&sColname, zColname); sqlite3ExprListSetName(pParse, pNew, &sColname, 0); if( pNew && (p->selFlags & SF_NestedFrom)!=0 ){ struct ExprList_item *pX = &pNew->a[pNew->nExpr-1]; @@ -113076,6 +117343,7 @@ static int selectExpander(Walker *pWalker, Select *p){ #if SQLITE_MAX_COLUMN if( p->pEList && p->pEList->nExpr>db->aLimit[SQLITE_LIMIT_COLUMN] ){ sqlite3ErrorMsg(pParse, "too many columns in result set"); + return WRC_Abort; } #endif return WRC_Continue; @@ -113090,7 +117358,7 @@ static int selectExpander(Walker *pWalker, Select *p){ ** Walker.xSelectCallback is set to do something useful for every ** subquery in the parser tree. */ -static int exprWalkNoop(Walker *NotUsed, Expr *NotUsed2){ +SQLITE_PRIVATE int sqlite3ExprWalkNoop(Walker *NotUsed, Expr *NotUsed2){ UNUSED_PARAMETER2(NotUsed, NotUsed2); return WRC_Continue; } @@ -113111,7 +117379,7 @@ static int exprWalkNoop(Walker *NotUsed, Expr *NotUsed2){ static void sqlite3SelectExpand(Parse *pParse, Select *pSelect){ Walker w; memset(&w, 0, sizeof(w)); - w.xExprCallback = exprWalkNoop; + w.xExprCallback = sqlite3ExprWalkNoop; w.pParse = pParse; if( pParse->hasCompound ){ w.xSelectCallback = convertCompoundSelectToSubquery; @@ -113158,7 +117426,7 @@ static void selectAddSubqueryTypeInfo(Walker *pWalker, Select *p){ Select *pSel = pFrom->pSelect; if( pSel ){ while( pSel->pPrior ) pSel = pSel->pPrior; - selectAddColumnTypeAndCollation(pParse, pTab, pSel); + sqlite3SelectAddColumnTypeAndCollation(pParse, pTab, pSel); } } } @@ -113178,7 +117446,7 @@ static void sqlite3SelectAddTypeInfo(Parse *pParse, Select *pSelect){ Walker w; memset(&w, 0, sizeof(w)); w.xSelectCallback2 = selectAddSubqueryTypeInfo; - w.xExprCallback = exprWalkNoop; + w.xExprCallback = sqlite3ExprWalkNoop; w.pParse = pParse; sqlite3WalkSelect(&w, pSelect); #endif @@ -113578,10 +117846,24 @@ SQLITE_PRIVATE int sqlite3Select( } /* Generate code to implement the subquery + ** + ** The subquery is implemented as a co-routine if all of these are true: + ** (1) The subquery is guaranteed to be the outer loop (so that it + ** does not need to be computed more than once) + ** (2) The ALL keyword after SELECT is omitted. (Applications are + ** allowed to say "SELECT ALL" instead of just "SELECT" to disable + ** the use of co-routines.) + ** (3) Co-routines are not disabled using sqlite3_test_control() + ** with SQLITE_TESTCTRL_OPTIMIZATIONS. + ** + ** TODO: Are there other reasons beside (1) to use a co-routine + ** implementation? */ - if( pTabList->nSrc==1 - && (p->selFlags & SF_All)==0 - && OptimizationEnabled(db, SQLITE_SubqCoroutine) + if( i==0 + && (pTabList->nSrc==1 + || (pTabList->a[1].fg.jointype&(JT_LEFT|JT_CROSS))!=0) /* (1) */ + && (p->selFlags & SF_All)==0 /* (2) */ + && OptimizationEnabled(db, SQLITE_SubqCoroutine) /* (3) */ ){ /* Implement a co-routine that will return a single row of the result ** set on each invocation. @@ -113594,10 +117876,10 @@ SQLITE_PRIVATE int sqlite3Select( sqlite3SelectDestInit(&dest, SRT_Coroutine, pItem->regReturn); explainSetInteger(pItem->iSelectId, (u8)pParse->iNextSelectId); sqlite3Select(pParse, pSub, &dest); - pItem->pTab->nRowLogEst = sqlite3LogEst(pSub->nSelectRow); + pItem->pTab->nRowLogEst = pSub->nSelectRow; pItem->fg.viaCoroutine = 1; pItem->regResult = dest.iSdst; - sqlite3VdbeAddOp1(v, OP_EndCoroutine, pItem->regReturn); + sqlite3VdbeEndCoroutine(v, pItem->regReturn); sqlite3VdbeJumpHere(v, addrTop-1); sqlite3ClearTempRegCache(pParse); }else{ @@ -113625,7 +117907,7 @@ SQLITE_PRIVATE int sqlite3Select( sqlite3SelectDestInit(&dest, SRT_EphemTab, pItem->iCursor); explainSetInteger(pItem->iSelectId, (u8)pParse->iNextSelectId); sqlite3Select(pParse, pSub, &dest); - pItem->pTab->nRowLogEst = sqlite3LogEst(pSub->nSelectRow); + pItem->pTab->nRowLogEst = pSub->nSelectRow; if( onceAddr ) sqlite3VdbeJumpHere(v, onceAddr); retAddr = sqlite3VdbeAddOp1(v, OP_Return, pItem->regReturn); VdbeComment((v, "end %s", pItem->pTab->zName)); @@ -113676,6 +117958,13 @@ SQLITE_PRIVATE int sqlite3Select( ** the sDistinct.isTnct is still set. Hence, isTnct represents the ** original setting of the SF_Distinct flag, not the current setting */ assert( sDistinct.isTnct ); + +#if SELECTTRACE_ENABLED + if( sqlite3SelectTrace & 0x400 ){ + SELECTTRACE(0x400,pParse,p,("Transform DISTINCT into GROUP BY:\n")); + sqlite3TreeViewSelect(0, p, 0); + } +#endif } /* If there is an ORDER BY clause, then create an ephemeral index to @@ -113708,7 +117997,7 @@ SQLITE_PRIVATE int sqlite3Select( /* Set the limiter. */ iEnd = sqlite3VdbeMakeLabel(v); - p->nSelectRow = LARGEST_INT64; + p->nSelectRow = 320; /* 4 billion rows */ computeLimitRegisters(pParse, p, iEnd); if( p->iLimit==0 && sSort.addrSortIndex>=0 ){ sqlite3VdbeChangeOpcode(v, sSort.addrSortIndex, OP_SorterOpen); @@ -113732,10 +118021,12 @@ SQLITE_PRIVATE int sqlite3Select( if( !isAgg && pGroupBy==0 ){ /* No aggregate functions and no GROUP BY clause */ u16 wctrlFlags = (sDistinct.isTnct ? WHERE_WANT_DISTINCT : 0); + assert( WHERE_USE_LIMIT==SF_FixedLimit ); + wctrlFlags |= p->selFlags & SF_FixedLimit; /* Begin the database scan. */ pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, sSort.pOrderBy, - p->pEList, wctrlFlags, 0); + p->pEList, wctrlFlags, p->nSelectRow); if( pWInfo==0 ) goto select_end; if( sqlite3WhereOutputRowCount(pWInfo) < p->nSelectRow ){ p->nSelectRow = sqlite3WhereOutputRowCount(pWInfo); @@ -113795,9 +118086,11 @@ SQLITE_PRIVATE int sqlite3Select( for(k=pGroupBy->nExpr, pItem=pGroupBy->a; k>0; k--, pItem++){ pItem->u.x.iAlias = 0; } - if( p->nSelectRow>100 ) p->nSelectRow = 100; + assert( 66==sqlite3LogEst(100) ); + if( p->nSelectRow>66 ) p->nSelectRow = 66; }else{ - p->nSelectRow = 1; + assert( 0==sqlite3LogEst(1) ); + p->nSelectRow = 0; } /* If there is both a GROUP BY and an ORDER BY clause and they are @@ -113933,13 +118226,8 @@ SQLITE_PRIVATE int sqlite3Select( struct AggInfo_col *pCol = &sAggInfo.aCol[i]; if( pCol->iSorterColumn>=j ){ int r1 = j + regBase; - int r2; - - r2 = sqlite3ExprCodeGetColumn(pParse, - pCol->pTab, pCol->iColumn, pCol->iTable, r1, 0); - if( r1!=r2 ){ - sqlite3VdbeAddOp2(v, OP_SCopy, r2, r1); - } + sqlite3ExprCodeGetColumnToReg(pParse, + pCol->pTab, pCol->iColumn, pCol->iTable, r1); j++; } } @@ -114174,7 +118462,8 @@ SQLITE_PRIVATE int sqlite3Select( if( flag ){ pMinMax = sqlite3ExprListDup(db, pMinMax, 0); pDel = pMinMax; - if( pMinMax && !db->mallocFailed ){ + assert( db->mallocFailed || pMinMax!=0 ); + if( !db->mallocFailed ){ pMinMax->a[0].sortOrder = flag!=WHERE_ORDERBY_MIN ?1:0; pMinMax->a[0].pExpr->op = TK_COLUMN; } @@ -114358,7 +118647,7 @@ static int sqlite3_get_table_cb(void *pArg, int nCol, char **argv, char **colv){ return 0; malloc_failed: - p->rc = SQLITE_NOMEM; + p->rc = SQLITE_NOMEM_BKPT; return 1; } @@ -114399,7 +118688,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_get_table( res.azResult = sqlite3_malloc64(sizeof(char*)*res.nAlloc ); if( res.azResult==0 ){ db->errCode = SQLITE_NOMEM; - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } res.azResult[0] = 0; rc = sqlite3_exec(db, zSql, sqlite3_get_table_cb, &res, pzErrMsg); @@ -114428,7 +118717,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_get_table( if( azNew==0 ){ sqlite3_free_table(&res.azResult[1]); db->errCode = SQLITE_NOMEM; - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } res.azResult = azNew; } @@ -114747,8 +119036,7 @@ SQLITE_PRIVATE void sqlite3FinishTrigger( pStepList->pTrig = pTrig; pStepList = pStepList->pNext; } - nameToken.z = pTrig->zName; - nameToken.n = sqlite3Strlen30(nameToken.z); + sqlite3TokenInit(&nameToken, pTrig->zName); sqlite3FixInit(&sFix, pParse, iDb, "trigger", &nameToken); if( sqlite3FixTriggerStep(&sFix, pTrig->step_list) || sqlite3FixExpr(&sFix, pTrig->pWhen) @@ -114784,7 +119072,7 @@ SQLITE_PRIVATE void sqlite3FinishTrigger( assert( sqlite3SchemaMutexHeld(db, iDb, 0) ); pTrig = sqlite3HashInsert(pHash, zName, pTrig); if( pTrig ){ - db->mallocFailed = 1; + sqlite3OomFault(db); }else if( pLink->pSchema==pLink->pTabSchema ){ Table *pTab; pTab = sqlite3HashFind(&pLink->pTabSchema->tblHash, pLink->table); @@ -115019,31 +119307,12 @@ SQLITE_PRIVATE void sqlite3DropTriggerPtr(Parse *pParse, Trigger *pTrigger){ */ assert( pTable!=0 ); if( (v = sqlite3GetVdbe(pParse))!=0 ){ - int base; - static const int iLn = VDBE_OFFSET_LINENO(2); - static const VdbeOpList dropTrigger[] = { - { OP_Rewind, 0, ADDR(9), 0}, - { OP_String8, 0, 1, 0}, /* 1 */ - { OP_Column, 0, 1, 2}, - { OP_Ne, 2, ADDR(8), 1}, - { OP_String8, 0, 1, 0}, /* 4: "trigger" */ - { OP_Column, 0, 0, 2}, - { OP_Ne, 2, ADDR(8), 1}, - { OP_Delete, 0, 0, 0}, - { OP_Next, 0, ADDR(1), 0}, /* 8 */ - }; - - sqlite3BeginWriteOperation(pParse, 0, iDb); - sqlite3OpenMasterTable(pParse, iDb); - base = sqlite3VdbeAddOpList(v, ArraySize(dropTrigger), dropTrigger, iLn); - sqlite3VdbeChangeP4(v, base+1, pTrigger->zName, P4_TRANSIENT); - sqlite3VdbeChangeP4(v, base+4, "trigger", P4_STATIC); + sqlite3NestedParse(pParse, + "DELETE FROM %Q.%s WHERE name=%Q AND type='trigger'", + db->aDb[iDb].zName, SCHEMA_TABLE(iDb), pTrigger->zName + ); sqlite3ChangeCookie(pParse, iDb); - sqlite3VdbeAddOp2(v, OP_Close, 0, 0); sqlite3VdbeAddOp4(v, OP_DropTrigger, iDb, 0, 0, pTrigger->zName, 0); - if( pParse->nMem<3 ){ - pParse->nMem = 3; - } } } @@ -115431,8 +119700,8 @@ SQLITE_PRIVATE void sqlite3CodeRowTriggerDirect( if( pPrg ){ int bRecursive = (p->zName && 0==(pParse->db->flags&SQLITE_RecTriggers)); - sqlite3VdbeAddOp3(v, OP_Program, reg, ignoreJump, ++pParse->nMem); - sqlite3VdbeChangeP4(v, -1, (const char *)pPrg->pProgram, P4_SUBPROGRAM); + sqlite3VdbeAddOp4(v, OP_Program, reg, ignoreJump, ++pParse->nMem, + (const char *)pPrg->pProgram, P4_SUBPROGRAM); VdbeComment( (v, "Call: %s.%s", (p->zName?p->zName:"fkey"), onErrorText(orconf))); @@ -115779,7 +120048,7 @@ SQLITE_PRIVATE void sqlite3Update( /* Allocate space for aXRef[], aRegIdx[], and aToOpen[]. ** Initialize aXRef[] and aToOpen[] to their default values. */ - aXRef = sqlite3DbMallocRaw(db, sizeof(int) * (pTab->nCol+nIdx) + nIdx+2 ); + aXRef = sqlite3DbMallocRawNN(db, sizeof(int) * (pTab->nCol+nIdx) + nIdx+2 ); if( aXRef==0 ) goto update_cleanup; aRegIdx = aXRef+pTab->nCol; aToOpen = (u8*)(aRegIdx+nIdx); @@ -115845,10 +120114,12 @@ SQLITE_PRIVATE void sqlite3Update( assert( chngPk==0 || chngPk==1 ); chngKey = chngRowid + chngPk; - /* The SET expressions are not actually used inside the WHERE loop. - ** So reset the colUsed mask + /* The SET expressions are not actually used inside the WHERE loop. + ** So reset the colUsed mask. Unless this is a virtual table. In that + ** case, set all bits of the colUsed mask (to ensure that the virtual + ** table implementation makes all columns available). */ - pTabList->a[0].colUsed = 0; + pTabList->a[0].colUsed = IsVirtual(pTab) ? ALLBITS : 0; hasFK = sqlite3FkRequired(pParse, pTab, aXRef, chngKey); @@ -115932,7 +120203,8 @@ SQLITE_PRIVATE void sqlite3Update( if( HasRowid(pTab) ){ sqlite3VdbeAddOp3(v, OP_Null, 0, regRowSet, regOldRowid); pWInfo = sqlite3WhereBegin( - pParse, pTabList, pWhere, 0, 0, WHERE_ONEPASS_DESIRED, iIdxCur + pParse, pTabList, pWhere, 0, 0, + WHERE_ONEPASS_DESIRED | WHERE_SEEK_TABLE, iIdxCur ); if( pWInfo==0 ) goto update_cleanup; okOnePass = sqlite3WhereOkOnePass(pWInfo, aiCurOnePass); @@ -116011,7 +120283,7 @@ SQLITE_PRIVATE void sqlite3Update( if( aiCurOnePass[0]>=0 ) aToOpen[aiCurOnePass[0]-iBaseCur] = 0; if( aiCurOnePass[1]>=0 ) aToOpen[aiCurOnePass[1]-iBaseCur] = 0; } - sqlite3OpenTableAndIndices(pParse, pTab, OP_OpenWrite, iBaseCur, aToOpen, + sqlite3OpenTableAndIndices(pParse, pTab, OP_OpenWrite, 0, iBaseCur, aToOpen, 0, 0); } @@ -116104,7 +120376,7 @@ SQLITE_PRIVATE void sqlite3Update( */ testcase( i==31 ); testcase( i==32 ); - sqlite3ExprCodeGetColumnOfTable(v, pTab, iDataCur, i, regNew+i); + sqlite3ExprCodeGetColumnToReg(pParse, pTab, i, iDataCur, regNew+i); }else{ sqlite3VdbeAddOp2(v, OP_Null, 0, regNew+i); } @@ -116152,7 +120424,8 @@ SQLITE_PRIVATE void sqlite3Update( /* Do constraint checks. */ assert( regOldRowid>0 ); sqlite3GenerateConstraintChecks(pParse, pTab, aRegIdx, iDataCur, iIdxCur, - regNewRowid, regOldRowid, chngKey, onError, labelContinue, &bReplace); + regNewRowid, regOldRowid, chngKey, onError, labelContinue, &bReplace, + aXRef); /* Do FK constraint checks. */ if( hasFK ){ @@ -116169,11 +120442,30 @@ SQLITE_PRIVATE void sqlite3Update( VdbeCoverageNeverTaken(v); } sqlite3GenerateRowIndexDelete(pParse, pTab, iDataCur, iIdxCur, aRegIdx, -1); - - /* If changing the record number, delete the old record. */ + + /* If changing the rowid value, or if there are foreign key constraints + ** to process, delete the old record. Otherwise, add a noop OP_Delete + ** to invoke the pre-update hook. + ** + ** That (regNew==regnewRowid+1) is true is also important for the + ** pre-update hook. If the caller invokes preupdate_new(), the returned + ** value is copied from memory cell (regNewRowid+1+iCol), where iCol + ** is the column index supplied by the user. + */ + assert( regNew==regNewRowid+1 ); +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + sqlite3VdbeAddOp3(v, OP_Delete, iDataCur, + OPFLAG_ISUPDATE | ((hasFK || chngKey || pPk!=0) ? 0 : OPFLAG_ISNOOP), + regNewRowid + ); + if( !pParse->nested ){ + sqlite3VdbeChangeP4(v, -1, (char*)pTab, P4_TABLE); + } +#else if( hasFK || chngKey || pPk!=0 ){ sqlite3VdbeAddOp2(v, OP_Delete, iDataCur, 0); } +#endif if( bReplace || chngKey ){ sqlite3VdbeJumpHere(v, addr1); } @@ -116430,7 +120722,7 @@ static int execSql(sqlite3 *db, char **pzErrMsg, const char *zSql){ sqlite3_stmt *pStmt; VVA_ONLY( int rc; ) if( !zSql ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } if( SQLITE_OK!=sqlite3_prepare(db, zSql, -1, &pStmt, 0) ){ sqlite3SetString(pzErrMsg, db, sqlite3_errmsg(db)); @@ -116611,7 +120903,7 @@ SQLITE_PRIVATE int sqlite3RunVacuum(char **pzErrMsg, sqlite3 *db){ || (!isMemDb && sqlite3BtreeSetPageSize(pTemp, db->nextPagesize, nRes, 0)) || NEVER(db->mallocFailed) ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; goto end_of_vacuum; } @@ -116817,7 +121109,7 @@ static int createModule( rc = SQLITE_MISUSE_BKPT; }else{ Module *pMod; - pMod = (Module *)sqlite3DbMallocRaw(db, sizeof(Module) + nName + 1); + pMod = (Module *)sqlite3DbMallocRawNN(db, sizeof(Module) + nName + 1); if( pMod ){ Module *pDel; char *zCopy = (char *)(&pMod[1]); @@ -116830,7 +121122,7 @@ static int createModule( pDel = (Module *)sqlite3HashInsert(&db->aModule,zCopy,(void*)pMod); assert( pDel==0 || pDel==pMod ); if( pDel ){ - db->mallocFailed = 1; + sqlite3OomFault(db); sqlite3DbFree(db, pDel); } } @@ -117207,7 +121499,7 @@ SQLITE_PRIVATE void sqlite3VtabFinishParse(Parse *pParse, Token *pEnd){ assert( sqlite3SchemaMutexHeld(db, 0, pSchema) ); pOld = sqlite3HashInsert(&pSchema->tblHash, zName, pTab); if( pOld ){ - db->mallocFailed = 1; + sqlite3OomFault(db); assert( pTab==pOld ); /* Malloc must have failed inside HashInsert() */ return; } @@ -117274,13 +121566,13 @@ static int vtabCallConstructor( zModuleName = sqlite3MPrintf(db, "%s", pTab->zName); if( !zModuleName ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pVTable = sqlite3DbMallocZero(db, sizeof(VTable)); if( !pVTable ){ sqlite3DbFree(db, zModuleName); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } pVTable->db = db; pVTable->pMod = pMod; @@ -117298,7 +121590,7 @@ static int vtabCallConstructor( db->pVtabCtx = &sCtx; rc = xConstruct(db, pMod->pAux, nArg, azArg, &pVTable->pVtab, &zErr); db->pVtabCtx = sCtx.pPrior; - if( rc==SQLITE_NOMEM ) db->mallocFailed = 1; + if( rc==SQLITE_NOMEM ) sqlite3OomFault(db); assert( sCtx.pTab==pTab ); if( SQLITE_OK!=rc ){ @@ -117332,22 +121624,16 @@ static int vtabCallConstructor( pTab->pVTable = pVTable; for(iCol=0; iColnCol; iCol++){ - char *zType = pTab->aCol[iCol].zType; + char *zType = sqlite3ColumnType(&pTab->aCol[iCol], ""); int nType; int i = 0; - if( !zType ){ - pTab->tabFlags |= oooHidden; - continue; - } nType = sqlite3Strlen30(zType); - if( sqlite3StrNICmp("hidden", zType, 6)||(zType[6] && zType[6]!=' ') ){ - for(i=0; inVTrans + ARRAY_INCR); aVTrans = sqlite3DbRealloc(db, (void *)db->aVTrans, nBytes); if( !aVTrans ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } memset(&aVTrans[db->nVTrans], 0, sizeof(sqlite3_vtab *)*ARRAY_INCR); db->aVTrans = aVTrans; @@ -117515,7 +121801,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_declare_vtab(sqlite3 *db, const char *zCre pParse = sqlite3StackAllocZero(db, sizeof(*pParse)); if( pParse==0 ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; }else{ pParse->declareVtab = 1; pParse->db = db; @@ -117784,7 +122070,7 @@ SQLITE_PRIVATE FuncDef *sqlite3VtabOverloadFunction( Table *pTab; sqlite3_vtab *pVtab; sqlite3_module *pMod; - void (*xFunc)(sqlite3_context*,int,sqlite3_value**) = 0; + void (*xSFunc)(sqlite3_context*,int,sqlite3_value**) = 0; void *pArg = 0; FuncDef *pNew; int rc = 0; @@ -117812,7 +122098,7 @@ SQLITE_PRIVATE FuncDef *sqlite3VtabOverloadFunction( for(z=(unsigned char*)zLowerName; *z; z++){ *z = sqlite3UpperToLower[*z]; } - rc = pMod->xFindFunction(pVtab, nArg, zLowerName, &xFunc, &pArg); + rc = pMod->xFindFunction(pVtab, nArg, zLowerName, &xSFunc, &pArg); sqlite3DbFree(db, zLowerName); } if( rc==0 ){ @@ -117827,9 +122113,9 @@ SQLITE_PRIVATE FuncDef *sqlite3VtabOverloadFunction( return pDef; } *pNew = *pDef; - pNew->zName = (char *)&pNew[1]; - memcpy(pNew->zName, pDef->zName, sqlite3Strlen30(pDef->zName)+1); - pNew->xFunc = xFunc; + pNew->zName = (const char*)&pNew[1]; + memcpy((char*)&pNew[1], pDef->zName, sqlite3Strlen30(pDef->zName)+1); + pNew->xSFunc = xSFunc; pNew->pUserData = pArg; pNew->funcFlags |= SQLITE_FUNC_EPHEM; return pNew; @@ -117856,7 +122142,7 @@ SQLITE_PRIVATE void sqlite3VtabMakeWritable(Parse *pParse, Table *pTab){ pToplevel->apVtabLock = apVtabLock; pToplevel->apVtabLock[pToplevel->nVtabLock++] = pTab; }else{ - pToplevel->db->mallocFailed = 1; + sqlite3OomFault(pToplevel->db); } } @@ -118074,8 +122360,10 @@ struct WhereLevel { int addrCont; /* Jump here to continue with the next loop cycle */ int addrFirst; /* First instruction of interior of the loop */ int addrBody; /* Beginning of the body of this loop */ - int iLikeRepCntr; /* LIKE range processing counter register */ +#ifndef SQLITE_LIKE_DOESNT_MATCH_BLOBS + u32 iLikeRepCntr; /* LIKE range processing counter register (times 2) */ int addrLikeRep; /* LIKE range processing address */ +#endif u8 iFrom; /* Which entry in the FROM clause */ u8 op, p3, p5; /* Opcode, P3 & P5 of the opcode that ends the loop */ int p1, p2; /* Operands of the opcode used to ends the loop */ @@ -118258,6 +122546,7 @@ struct WhereTerm { u16 eOperator; /* A WO_xx value describing */ u16 wtFlags; /* TERM_xxx bit flags. See below */ u8 nChild; /* Number of children that must disable us */ + u8 eMatchOp; /* Op for vtab MATCH/LIKE/GLOB/REGEXP terms */ WhereClause *pWC; /* The clause this term is part of */ Bitmask prereqRight; /* Bitmask of tables used by pExpr->pRight */ Bitmask prereqAll; /* Bitmask of tables referenced by pExpr */ @@ -118290,7 +122579,7 @@ struct WhereTerm { struct WhereScan { WhereClause *pOrigWC; /* Original, innermost WhereClause */ WhereClause *pWC; /* WhereClause currently being scanned */ - char *zCollName; /* Required collating sequence, if not NULL */ + const char *zCollName; /* Required collating sequence, if not NULL */ Expr *pIdxExpr; /* Search for this index expression */ char idxaff; /* Must match this affinity, if zCollName!=NULL */ unsigned char nEquiv; /* Number of entries in aEquiv[] */ @@ -118410,10 +122699,11 @@ struct WhereInfo { Parse *pParse; /* Parsing and code generating context */ SrcList *pTabList; /* List of tables in the join */ ExprList *pOrderBy; /* The ORDER BY clause or NULL */ - ExprList *pResultSet; /* Result set. DISTINCT operates on these */ + ExprList *pDistinctSet; /* DISTINCT over all these values */ WhereLoop *pLoops; /* List of all WhereLoop objects */ Bitmask revMask; /* Mask of ORDER BY terms that need reversing */ LogEst nRowOut; /* Estimated number of output rows */ + LogEst iLimit; /* LIMIT if wctrlFlags has WHERE_USE_LIMIT */ u16 wctrlFlags; /* Flags originally passed to sqlite3WhereBegin() */ i8 nOBSat; /* Number of ORDER BY terms satisfied by indices */ u8 sorted; /* True if really sorted (not just grouped) */ @@ -118493,6 +122783,14 @@ SQLITE_PRIVATE void sqlite3WhereTabFuncArgs(Parse*, struct SrcList_item*, WhereC ** operators that are of interest to the query planner. An ** OR-ed combination of these values can be used when searching for ** particular WhereTerms within a WhereClause. +** +** Value constraints: +** WO_EQ == SQLITE_INDEX_CONSTRAINT_EQ +** WO_LT == SQLITE_INDEX_CONSTRAINT_LT +** WO_LE == SQLITE_INDEX_CONSTRAINT_LE +** WO_GT == SQLITE_INDEX_CONSTRAINT_GT +** WO_GE == SQLITE_INDEX_CONSTRAINT_GE +** WO_MATCH == SQLITE_INDEX_CONSTRAINT_MATCH */ #define WO_IN 0x0001 #define WO_EQ 0x0002 @@ -118595,7 +122893,7 @@ static void explainIndexRange(StrAccum *pStr, WhereLoop *pLoop){ for(i=0; i=nSkip ? "%s=?" : "ANY(%s)", z); + sqlite3XPrintf(pStr, i>=nSkip ? "%s=?" : "ANY(%s)", z); } j = i; @@ -118654,13 +122952,13 @@ SQLITE_PRIVATE int sqlite3WhereExplainOneScan( sqlite3StrAccumInit(&str, db, zBuf, sizeof(zBuf), SQLITE_MAX_LENGTH); sqlite3StrAccumAppendAll(&str, isSearch ? "SEARCH" : "SCAN"); if( pItem->pSelect ){ - sqlite3XPrintf(&str, 0, " SUBQUERY %d", pItem->iSelectId); + sqlite3XPrintf(&str, " SUBQUERY %d", pItem->iSelectId); }else{ - sqlite3XPrintf(&str, 0, " TABLE %s", pItem->zName); + sqlite3XPrintf(&str, " TABLE %s", pItem->zName); } if( pItem->zAlias ){ - sqlite3XPrintf(&str, 0, " AS %s", pItem->zAlias); + sqlite3XPrintf(&str, " AS %s", pItem->zAlias); } if( (flags & (WHERE_IPK|WHERE_VIRTUALTABLE))==0 ){ const char *zFmt = 0; @@ -118684,7 +122982,7 @@ SQLITE_PRIVATE int sqlite3WhereExplainOneScan( } if( zFmt ){ sqlite3StrAccumAppend(&str, " USING ", 7); - sqlite3XPrintf(&str, 0, zFmt, pIdx->zName); + sqlite3XPrintf(&str, zFmt, pIdx->zName); explainIndexRange(&str, pLoop); } }else if( (flags & WHERE_IPK)!=0 && (flags & WHERE_CONSTRAINT)!=0 ){ @@ -118699,17 +122997,17 @@ SQLITE_PRIVATE int sqlite3WhereExplainOneScan( assert( flags&WHERE_TOP_LIMIT); zRangeOp = "<"; } - sqlite3XPrintf(&str, 0, " USING INTEGER PRIMARY KEY (rowid%s?)",zRangeOp); + sqlite3XPrintf(&str, " USING INTEGER PRIMARY KEY (rowid%s?)",zRangeOp); } #ifndef SQLITE_OMIT_VIRTUALTABLE else if( (flags & WHERE_VIRTUALTABLE)!=0 ){ - sqlite3XPrintf(&str, 0, " VIRTUAL TABLE INDEX %d:%s", + sqlite3XPrintf(&str, " VIRTUAL TABLE INDEX %d:%s", pLoop->u.vtab.idxNum, pLoop->u.vtab.idxStr); } #endif #ifdef SQLITE_EXPLAIN_ESTIMATED_ROWS if( pLoop->nOut>=10 ){ - sqlite3XPrintf(&str, 0, " (~%llu rows)", sqlite3LogEstToInt(pLoop->nOut)); + sqlite3XPrintf(&str, " (~%llu rows)", sqlite3LogEstToInt(pLoop->nOut)); }else{ sqlite3StrAccumAppend(&str, " (~1 row)", 9); } @@ -118846,8 +123144,7 @@ static void codeApplyAffinity(Parse *pParse, int base, int n, char *zAff){ /* Code the OP_Affinity opcode if there is anything left to do. */ if( n>0 ){ - sqlite3VdbeAddOp2(v, OP_Affinity, base, n); - sqlite3VdbeChangeP4(v, -1, zAff, n); + sqlite3VdbeAddOp4(v, OP_Affinity, base, n, 0, zAff, n); sqlite3ExprCacheAffinityChange(pParse, base, n); } } @@ -119015,9 +123312,7 @@ static int codeAllEqualityTerms( pParse->nMem += nReg; zAff = sqlite3DbStrDup(pParse->db,sqlite3IndexAffinityStr(pParse->db,pIdx)); - if( !zAff ){ - pParse->db->mallocFailed = 1; - } + assert( zAff!=0 || pParse->db->mallocFailed ); if( nSkip ){ int iIdxCur = pLevel->iIdxCur; @@ -119080,10 +123375,12 @@ static int codeAllEqualityTerms( return regBase; } +#ifndef SQLITE_LIKE_DOESNT_MATCH_BLOBS /* -** If the most recently coded instruction is a constant range contraint -** that originated from the LIKE optimization, then change the P3 to be -** pLoop->iLikeRepCntr and set P5. +** If the most recently coded instruction is a constant range constraint +** (a string literal) that originated from the LIKE optimization, then +** set P3 and P5 on the OP_String opcode so that the string will be cast +** to a BLOB at appropriate times. ** ** The LIKE optimization trys to evaluate "x LIKE 'abc%'" as a range ** expression: "x>='ABC' AND x<'abd'". But this requires that the range @@ -119091,6 +123388,10 @@ static int codeAllEqualityTerms( ** The OP_String opcodes on the second pass convert the upper and lower ** bound string contants to blobs. This routine makes the necessary changes ** to the OP_String opcodes for that to happen. +** +** Except, of course, if SQLITE_LIKE_DOESNT_MATCH_BLOBS is defined, then +** only the one pass through the string space is required, so this routine +** becomes a no-op. */ static void whereLikeOptimizationStringFixup( Vdbe *v, /* prepared statement under construction */ @@ -119104,11 +123405,210 @@ static void whereLikeOptimizationStringFixup( assert( pOp!=0 ); assert( pOp->opcode==OP_String8 || pTerm->pWC->pWInfo->pParse->db->mallocFailed ); - pOp->p3 = pLevel->iLikeRepCntr; - pOp->p5 = 1; + pOp->p3 = (int)(pLevel->iLikeRepCntr>>1); /* Register holding counter */ + pOp->p5 = (u8)(pLevel->iLikeRepCntr&1); /* ASC or DESC */ } } +#else +# define whereLikeOptimizationStringFixup(A,B,C) +#endif +#ifdef SQLITE_ENABLE_CURSOR_HINTS +/* +** Information is passed from codeCursorHint() down to individual nodes of +** the expression tree (by sqlite3WalkExpr()) using an instance of this +** structure. +*/ +struct CCurHint { + int iTabCur; /* Cursor for the main table */ + int iIdxCur; /* Cursor for the index, if pIdx!=0. Unused otherwise */ + Index *pIdx; /* The index used to access the table */ +}; + +/* +** This function is called for every node of an expression that is a candidate +** for a cursor hint on an index cursor. For TK_COLUMN nodes that reference +** the table CCurHint.iTabCur, verify that the same column can be +** accessed through the index. If it cannot, then set pWalker->eCode to 1. +*/ +static int codeCursorHintCheckExpr(Walker *pWalker, Expr *pExpr){ + struct CCurHint *pHint = pWalker->u.pCCurHint; + assert( pHint->pIdx!=0 ); + if( pExpr->op==TK_COLUMN + && pExpr->iTable==pHint->iTabCur + && sqlite3ColumnOfIndex(pHint->pIdx, pExpr->iColumn)<0 + ){ + pWalker->eCode = 1; + } + return WRC_Continue; +} + + +/* +** This function is called on every node of an expression tree used as an +** argument to the OP_CursorHint instruction. If the node is a TK_COLUMN +** that accesses any table other than the one identified by +** CCurHint.iTabCur, then do the following: +** +** 1) allocate a register and code an OP_Column instruction to read +** the specified column into the new register, and +** +** 2) transform the expression node to a TK_REGISTER node that reads +** from the newly populated register. +** +** Also, if the node is a TK_COLUMN that does access the table idenified +** by pCCurHint.iTabCur, and an index is being used (which we will +** know because CCurHint.pIdx!=0) then transform the TK_COLUMN into +** an access of the index rather than the original table. +*/ +static int codeCursorHintFixExpr(Walker *pWalker, Expr *pExpr){ + int rc = WRC_Continue; + struct CCurHint *pHint = pWalker->u.pCCurHint; + if( pExpr->op==TK_COLUMN ){ + if( pExpr->iTable!=pHint->iTabCur ){ + Vdbe *v = pWalker->pParse->pVdbe; + int reg = ++pWalker->pParse->nMem; /* Register for column value */ + sqlite3ExprCodeGetColumnOfTable( + v, pExpr->pTab, pExpr->iTable, pExpr->iColumn, reg + ); + pExpr->op = TK_REGISTER; + pExpr->iTable = reg; + }else if( pHint->pIdx!=0 ){ + pExpr->iTable = pHint->iIdxCur; + pExpr->iColumn = sqlite3ColumnOfIndex(pHint->pIdx, pExpr->iColumn); + assert( pExpr->iColumn>=0 ); + } + }else if( pExpr->op==TK_AGG_FUNCTION ){ + /* An aggregate function in the WHERE clause of a query means this must + ** be a correlated sub-query, and expression pExpr is an aggregate from + ** the parent context. Do not walk the function arguments in this case. + ** + ** todo: It should be possible to replace this node with a TK_REGISTER + ** expression, as the result of the expression must be stored in a + ** register at this point. The same holds for TK_AGG_COLUMN nodes. */ + rc = WRC_Prune; + } + return rc; +} + +/* +** Insert an OP_CursorHint instruction if it is appropriate to do so. +*/ +static void codeCursorHint( + WhereInfo *pWInfo, /* The where clause */ + WhereLevel *pLevel, /* Which loop to provide hints for */ + WhereTerm *pEndRange /* Hint this end-of-scan boundary term if not NULL */ +){ + Parse *pParse = pWInfo->pParse; + sqlite3 *db = pParse->db; + Vdbe *v = pParse->pVdbe; + Expr *pExpr = 0; + WhereLoop *pLoop = pLevel->pWLoop; + int iCur; + WhereClause *pWC; + WhereTerm *pTerm; + int i, j; + struct CCurHint sHint; + Walker sWalker; + + if( OptimizationDisabled(db, SQLITE_CursorHints) ) return; + iCur = pLevel->iTabCur; + assert( iCur==pWInfo->pTabList->a[pLevel->iFrom].iCursor ); + sHint.iTabCur = iCur; + sHint.iIdxCur = pLevel->iIdxCur; + sHint.pIdx = pLoop->u.btree.pIndex; + memset(&sWalker, 0, sizeof(sWalker)); + sWalker.pParse = pParse; + sWalker.u.pCCurHint = &sHint; + pWC = &pWInfo->sWC; + for(i=0; inTerm; i++){ + pTerm = &pWC->a[i]; + if( pTerm->wtFlags & (TERM_VIRTUAL|TERM_CODED) ) continue; + if( pTerm->prereqAll & pLevel->notReady ) continue; + if( ExprHasProperty(pTerm->pExpr, EP_FromJoin) ) continue; + + /* All terms in pWLoop->aLTerm[] except pEndRange are used to initialize + ** the cursor. These terms are not needed as hints for a pure range + ** scan (that has no == terms) so omit them. */ + if( pLoop->u.btree.nEq==0 && pTerm!=pEndRange ){ + for(j=0; jnLTerm && pLoop->aLTerm[j]!=pTerm; j++){} + if( jnLTerm ) continue; + } + + /* No subqueries or non-deterministic functions allowed */ + if( sqlite3ExprContainsSubquery(pTerm->pExpr) ) continue; + + /* For an index scan, make sure referenced columns are actually in + ** the index. */ + if( sHint.pIdx!=0 ){ + sWalker.eCode = 0; + sWalker.xExprCallback = codeCursorHintCheckExpr; + sqlite3WalkExpr(&sWalker, pTerm->pExpr); + if( sWalker.eCode ) continue; + } + + /* If we survive all prior tests, that means this term is worth hinting */ + pExpr = sqlite3ExprAnd(db, pExpr, sqlite3ExprDup(db, pTerm->pExpr, 0)); + } + if( pExpr!=0 ){ + sWalker.xExprCallback = codeCursorHintFixExpr; + sqlite3WalkExpr(&sWalker, pExpr); + sqlite3VdbeAddOp4(v, OP_CursorHint, + (sHint.pIdx ? sHint.iIdxCur : sHint.iTabCur), 0, 0, + (const char*)pExpr, P4_EXPR); + } +} +#else +# define codeCursorHint(A,B,C) /* No-op */ +#endif /* SQLITE_ENABLE_CURSOR_HINTS */ + +/* +** Cursor iCur is open on an intkey b-tree (a table). Register iRowid contains +** a rowid value just read from cursor iIdxCur, open on index pIdx. This +** function generates code to do a deferred seek of cursor iCur to the +** rowid stored in register iRowid. +** +** Normally, this is just: +** +** OP_Seek $iCur $iRowid +** +** However, if the scan currently being coded is a branch of an OR-loop and +** the statement currently being coded is a SELECT, then P3 of the OP_Seek +** is set to iIdxCur and P4 is set to point to an array of integers +** containing one entry for each column of the table cursor iCur is open +** on. For each table column, if the column is the i'th column of the +** index, then the corresponding array entry is set to (i+1). If the column +** does not appear in the index at all, the array entry is set to 0. +*/ +static void codeDeferredSeek( + WhereInfo *pWInfo, /* Where clause context */ + Index *pIdx, /* Index scan is using */ + int iCur, /* Cursor for IPK b-tree */ + int iIdxCur /* Index cursor */ +){ + Parse *pParse = pWInfo->pParse; /* Parse context */ + Vdbe *v = pParse->pVdbe; /* Vdbe to generate code within */ + + assert( iIdxCur>0 ); + assert( pIdx->aiColumn[pIdx->nColumn-1]==-1 ); + + sqlite3VdbeAddOp3(v, OP_Seek, iIdxCur, 0, iCur); + if( (pWInfo->wctrlFlags & WHERE_FORCE_TABLE) + && DbMaskAllZero(sqlite3ParseToplevel(pParse)->writeMask) + ){ + int i; + Table *pTab = pIdx->pTable; + int *ai = (int*)sqlite3DbMallocZero(pParse->db, sizeof(int)*(pTab->nCol+1)); + if( ai ){ + ai[0] = pTab->nCol; + for(i=0; inColumn-1; i++){ + assert( pIdx->aiColumn[i]nCol ); + if( pIdx->aiColumn[i]>=0 ) ai[pIdx->aiColumn[i]+1] = i+1; + } + sqlite3VdbeChangeP4(v, -1, (char*)ai, P4_INTARRAY); + } + } +} /* ** Generate code for the start of the iLevel-th loop in the WHERE clause @@ -119192,6 +123692,7 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( int iReg; /* P3 Value for OP_VFilter */ int addrNotFound; int nConstraint = pLoop->nLTerm; + int iIn; /* Counter for IN constraints */ sqlite3ExprCachePush(pParse); iReg = sqlite3GetTempRange(pParse, nConstraint+2); @@ -119199,7 +123700,7 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( for(j=0; jaLTerm[j]; - if( pTerm==0 ) continue; + if( NEVER(pTerm==0) ) continue; if( pTerm->eOperator & WO_IN ){ codeEqualityTerm(pParse, pTerm, pLevel, j, bRev, iTarget); addrNotFound = pLevel->addrNxt; @@ -119214,15 +123715,57 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( pLoop->u.vtab.needFree ? P4_MPRINTF : P4_STATIC); VdbeCoverage(v); pLoop->u.vtab.needFree = 0; - for(j=0; ju.vtab.omitMask>>j)&1 ){ - disableTerm(pLevel, pLoop->aLTerm[j]); - } - } pLevel->p1 = iCur; pLevel->op = pWInfo->eOnePass ? OP_Noop : OP_VNext; pLevel->p2 = sqlite3VdbeCurrentAddr(v); - sqlite3ReleaseTempRange(pParse, iReg, nConstraint+2); + iIn = pLevel->u.in.nIn; + for(j=nConstraint-1; j>=0; j--){ + pTerm = pLoop->aLTerm[j]; + if( j<16 && (pLoop->u.vtab.omitMask>>j)&1 ){ + disableTerm(pLevel, pTerm); + }else if( (pTerm->eOperator & WO_IN)!=0 ){ + Expr *pCompare; /* The comparison operator */ + Expr *pRight; /* RHS of the comparison */ + VdbeOp *pOp; /* Opcode to access the value of the IN constraint */ + + /* Reload the constraint value into reg[iReg+j+2]. The same value + ** was loaded into the same register prior to the OP_VFilter, but + ** the xFilter implementation might have changed the datatype or + ** encoding of the value in the register, so it *must* be reloaded. */ + assert( pLevel->u.in.aInLoop!=0 || db->mallocFailed ); + if( !db->mallocFailed ){ + assert( iIn>0 ); + pOp = sqlite3VdbeGetOp(v, pLevel->u.in.aInLoop[--iIn].addrInTop); + assert( pOp->opcode==OP_Column || pOp->opcode==OP_Rowid ); + assert( pOp->opcode!=OP_Column || pOp->p3==iReg+j+2 ); + assert( pOp->opcode!=OP_Rowid || pOp->p2==iReg+j+2 ); + testcase( pOp->opcode==OP_Rowid ); + sqlite3VdbeAddOp3(v, pOp->opcode, pOp->p1, pOp->p2, pOp->p3); + } + + /* Generate code that will continue to the next row if + ** the IN constraint is not satisfied */ + pCompare = sqlite3PExpr(pParse, TK_EQ, 0, 0, 0); + assert( pCompare!=0 || db->mallocFailed ); + if( pCompare ){ + pCompare->pLeft = pTerm->pExpr->pLeft; + pCompare->pRight = pRight = sqlite3Expr(db, TK_REGISTER, 0); + if( pRight ){ + pRight->iTable = iReg+j+2; + sqlite3ExprIfFalse(pParse, pCompare, pLevel->addrCont, 0); + } + pCompare->pLeft = 0; + sqlite3ExprDelete(db, pCompare); + } + } + } + /* These registers need to be preserved in case there is an IN operator + ** loop. So we could deallocate the registers here (and potentially + ** reuse them later) if (pLoop->wsFlags & WHERE_IN_ABLE)==0. But it seems + ** simpler and safer to simply not reuse the registers. + ** + ** sqlite3ReleaseTempRange(pParse, iReg, nConstraint+2); + */ sqlite3ExprCachePop(pParse); }else #endif /* SQLITE_OMIT_VIRTUALTABLE */ @@ -119273,6 +123816,7 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( pStart = pEnd; pEnd = pTerm; } + codeCursorHint(pWInfo, pLevel, pEnd); if( pStart ){ Expr *pX; /* The expression that defines the start bound */ int r1, rTemp; /* Registers for holding the start boundary */ @@ -119445,18 +123989,23 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( if( pLoop->wsFlags & WHERE_TOP_LIMIT ){ pRangeEnd = pLoop->aLTerm[j++]; nExtraReg = 1; +#ifndef SQLITE_LIKE_DOESNT_MATCH_BLOBS if( (pRangeEnd->wtFlags & TERM_LIKEOPT)!=0 ){ assert( pRangeStart!=0 ); /* LIKE opt constraints */ assert( pRangeStart->wtFlags & TERM_LIKEOPT ); /* occur in pairs */ - pLevel->iLikeRepCntr = ++pParse->nMem; - testcase( bRev ); - testcase( pIdx->aSortOrder[nEq]==SQLITE_SO_DESC ); - sqlite3VdbeAddOp2(v, OP_Integer, - bRev ^ (pIdx->aSortOrder[nEq]==SQLITE_SO_DESC), - pLevel->iLikeRepCntr); + pLevel->iLikeRepCntr = (u32)++pParse->nMem; + sqlite3VdbeAddOp2(v, OP_Integer, 1, (int)pLevel->iLikeRepCntr); VdbeComment((v, "LIKE loop counter")); pLevel->addrLikeRep = sqlite3VdbeCurrentAddr(v); + /* iLikeRepCntr actually stores 2x the counter register number. The + ** bottom bit indicates whether the search order is ASC or DESC. */ + testcase( bRev ); + testcase( pIdx->aSortOrder[nEq]==SQLITE_SO_DESC ); + assert( (bRev & ~1)==0 ); + pLevel->iLikeRepCntr <<=1; + pLevel->iLikeRepCntr |= bRev ^ (pIdx->aSortOrder[nEq]==SQLITE_SO_DESC); } +#endif if( pRangeStart==0 && (j = pIdx->aiColumn[nEq])>=0 && pIdx->pTable->aCol[j].notNull==0 @@ -119466,15 +124015,6 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( } assert( pRangeEnd==0 || (pRangeEnd->wtFlags & TERM_VNULL)==0 ); - /* Generate code to evaluate all constraint terms using == or IN - ** and store the values of those terms in an array of registers - ** starting at regBase. - */ - regBase = codeAllEqualityTerms(pParse,pLevel,bRev,nExtraReg,&zStartAff); - assert( zStartAff==0 || sqlite3Strlen30(zStartAff)>=nEq ); - if( zStartAff ) cEndAff = zStartAff[nEq]; - addrNxt = pLevel->addrNxt; - /* If we are doing a reverse order scan on an ascending index, or ** a forward order scan on a descending index, interchange the ** start and end terms (pRangeStart and pRangeEnd). @@ -119486,6 +124026,16 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( SWAP(u8, bSeekPastNull, bStopAtNull); } + /* Generate code to evaluate all constraint terms using == or IN + ** and store the values of those terms in an array of registers + ** starting at regBase. + */ + codeCursorHint(pWInfo, pLevel, pRangeEnd); + regBase = codeAllEqualityTerms(pParse,pLevel,bRev,nExtraReg,&zStartAff); + assert( zStartAff==0 || sqlite3Strlen30(zStartAff)>=nEq ); + if( zStartAff ) cEndAff = zStartAff[nEq]; + addrNxt = pLevel->addrNxt; + testcase( pRangeStart && (pRangeStart->eOperator & WO_LE)!=0 ); testcase( pRangeStart && (pRangeStart->eOperator & WO_GE)!=0 ); testcase( pRangeEnd && (pRangeEnd->eOperator & WO_LE)!=0 ); @@ -119526,16 +124076,22 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( start_constraints = 1; } codeApplyAffinity(pParse, regBase, nConstraint - bSeekPastNull, zStartAff); - op = aStartOp[(start_constraints<<2) + (startEq<<1) + bRev]; - assert( op!=0 ); - sqlite3VdbeAddOp4Int(v, op, iIdxCur, addrNxt, regBase, nConstraint); - VdbeCoverage(v); - VdbeCoverageIf(v, op==OP_Rewind); testcase( op==OP_Rewind ); - VdbeCoverageIf(v, op==OP_Last); testcase( op==OP_Last ); - VdbeCoverageIf(v, op==OP_SeekGT); testcase( op==OP_SeekGT ); - VdbeCoverageIf(v, op==OP_SeekGE); testcase( op==OP_SeekGE ); - VdbeCoverageIf(v, op==OP_SeekLE); testcase( op==OP_SeekLE ); - VdbeCoverageIf(v, op==OP_SeekLT); testcase( op==OP_SeekLT ); + if( pLoop->nSkip>0 && nConstraint==pLoop->nSkip ){ + /* The skip-scan logic inside the call to codeAllEqualityConstraints() + ** above has already left the cursor sitting on the correct row, + ** so no further seeking is needed */ + }else{ + op = aStartOp[(start_constraints<<2) + (startEq<<1) + bRev]; + assert( op!=0 ); + sqlite3VdbeAddOp4Int(v, op, iIdxCur, addrNxt, regBase, nConstraint); + VdbeCoverage(v); + VdbeCoverageIf(v, op==OP_Rewind); testcase( op==OP_Rewind ); + VdbeCoverageIf(v, op==OP_Last); testcase( op==OP_Last ); + VdbeCoverageIf(v, op==OP_SeekGT); testcase( op==OP_SeekGT ); + VdbeCoverageIf(v, op==OP_SeekGE); testcase( op==OP_SeekGE ); + VdbeCoverageIf(v, op==OP_SeekLE); testcase( op==OP_SeekLE ); + VdbeCoverageIf(v, op==OP_SeekLT); testcase( op==OP_SeekLT ); + } /* Load the value for the inequality constraint at the end of the ** range (if any). @@ -119585,14 +124141,14 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( if( omitTable ){ /* pIdx is a covering index. No need to access the main table. */ }else if( HasRowid(pIdx->pTable) ){ - iRowidReg = ++pParse->nMem; - sqlite3VdbeAddOp2(v, OP_IdxRowid, iIdxCur, iRowidReg); - sqlite3ExprCacheStore(pParse, iCur, -1, iRowidReg); - if( pWInfo->eOnePass!=ONEPASS_OFF ){ + if( (pWInfo->wctrlFlags & WHERE_SEEK_TABLE)!=0 ){ + iRowidReg = ++pParse->nMem; + sqlite3VdbeAddOp2(v, OP_IdxRowid, iIdxCur, iRowidReg); + sqlite3ExprCacheStore(pParse, iCur, -1, iRowidReg); sqlite3VdbeAddOp3(v, OP_NotExists, iCur, 0, iRowidReg); VdbeCoverage(v); }else{ - sqlite3VdbeAddOp2(v, OP_Seek, iCur, iRowidReg); /* Deferred seek */ + codeDeferredSeek(pWInfo, pIdx, iCur, iIdxCur); } }else if( iCur!=iIdxCur ){ Index *pPk = sqlite3PrimaryKeyIndex(pIdx->pTable); @@ -119761,14 +124317,16 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( Expr *pExpr = pWC->a[iTerm].pExpr; if( &pWC->a[iTerm] == pTerm ) continue; if( ExprHasProperty(pExpr, EP_FromJoin) ) continue; - if( (pWC->a[iTerm].wtFlags & TERM_VIRTUAL)!=0 ) continue; + testcase( pWC->a[iTerm].wtFlags & TERM_VIRTUAL ); + testcase( pWC->a[iTerm].wtFlags & TERM_CODED ); + if( (pWC->a[iTerm].wtFlags & (TERM_VIRTUAL|TERM_CODED))!=0 ) continue; if( (pWC->a[iTerm].eOperator & WO_ALL)==0 ) continue; testcase( pWC->a[iTerm].wtFlags & TERM_ORINFO ); pExpr = sqlite3ExprDup(db, pExpr, 0); pAndExpr = sqlite3ExprAnd(db, pAndExpr, pExpr); } if( pAndExpr ){ - pAndExpr = sqlite3PExpr(pParse, TK_AND, 0, pAndExpr, 0); + pAndExpr = sqlite3PExpr(pParse, TK_AND|TKFLG_DONTFOLD, 0, pAndExpr, 0); } } @@ -119779,7 +124337,8 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( wctrlFlags = WHERE_OMIT_OPEN_CLOSE | WHERE_FORCE_TABLE | WHERE_ONETABLE_ONLY - | WHERE_NO_AUTOINDEX; + | WHERE_NO_AUTOINDEX + | (pWInfo->wctrlFlags & WHERE_SEEK_TABLE); for(ii=0; iinTerm; ii++){ WhereTerm *pOrTerm = &pOrWc->a[ii]; if( pOrTerm->leftCursor==iCur || (pOrTerm->eOperator & WO_AND)!=0 ){ @@ -119824,11 +124383,7 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( r = sqlite3GetTempRange(pParse, nPk); for(iPk=0; iPkaiColumn[iPk]; - int rx; - rx = sqlite3ExprCodeGetColumn(pParse, pTab, iCol, iCur,r+iPk,0); - if( rx!=r+iPk ){ - sqlite3VdbeAddOp2(v, OP_SCopy, rx, r+iPk); - } + sqlite3ExprCodeGetColumnToReg(pParse, pTab, iCol, iCur, r+iPk); } /* Check if the temp table already contains this key. If so, @@ -119928,6 +124483,7 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( ** a pseudo-cursor. No need to Rewind or Next such cursors. */ pLevel->op = OP_Noop; }else{ + codeCursorHint(pWInfo, pLevel, 0); pLevel->op = aStep[bRev]; pLevel->p1 = iCur; pLevel->p2 = 1 + sqlite3VdbeAddOp2(v, aStart[bRev], iCur, addrBrk); @@ -119962,9 +124518,19 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( continue; } if( pTerm->wtFlags & TERM_LIKECOND ){ - assert( pLevel->iLikeRepCntr>0 ); - skipLikeAddr = sqlite3VdbeAddOp1(v, OP_IfNot, pLevel->iLikeRepCntr); + /* If the TERM_LIKECOND flag is set, that means that the range search + ** is sufficient to guarantee that the LIKE operator is true, so we + ** can skip the call to the like(A,B) function. But this only works + ** for strings. So do not skip the call to the function on the pass + ** that compares BLOBs. */ +#ifdef SQLITE_LIKE_DOESNT_MATCH_BLOBS + continue; +#else + u32 x = pLevel->iLikeRepCntr; + assert( x>0 ); + skipLikeAddr = sqlite3VdbeAddOp1(v, (x&1)? OP_IfNot : OP_If, (int)(x>>1)); VdbeCoverage(v); +#endif } sqlite3ExprIfFalse(pParse, pE, addrCont, SQLITE_JUMPIFNULL); if( skipLikeAddr ) sqlite3VdbeJumpHere(v, skipLikeAddr); @@ -120100,7 +124666,7 @@ static int whereClauseInsert(WhereClause *pWC, Expr *p, u16 wtFlags){ if( pWC->nTerm>=pWC->nSlot ){ WhereTerm *pOld = pWC->a; sqlite3 *db = pWC->pWInfo->pParse->db; - pWC->a = sqlite3DbMallocRaw(db, sizeof(pWC->a[0])*pWC->nSlot*2 ); + pWC->a = sqlite3DbMallocRawNN(db, sizeof(pWC->a[0])*pWC->nSlot*2 ); if( pWC->a==0 ){ if( wtFlags & TERM_DYNAMIC ){ sqlite3ExprDelete(db, p); @@ -120238,6 +124804,7 @@ static int isLikeOrGlob( sqlite3 *db = pParse->db; /* Database connection */ sqlite3_value *pVal = 0; int op; /* Opcode of pRight */ + int rc; /* Result code to return */ if( !sqlite3IsLikeFunction(db, pExpr, pnoCase, wc) ){ return 0; @@ -120303,8 +124870,9 @@ static int isLikeOrGlob( } } + rc = (z!=0); sqlite3ValueFree(pVal); - return (z!=0); + return rc; } #endif /* SQLITE_OMIT_LIKE_OPTIMIZATION */ @@ -120313,29 +124881,48 @@ static int isLikeOrGlob( /* ** Check to see if the given expression is of the form ** -** column MATCH expr +** column OP expr +** +** where OP is one of MATCH, GLOB, LIKE or REGEXP and "column" is a +** column of a virtual table. ** ** If it is then return TRUE. If not, return FALSE. */ static int isMatchOfColumn( - Expr *pExpr /* Test this expression */ + Expr *pExpr, /* Test this expression */ + unsigned char *peOp2 /* OUT: 0 for MATCH, or else an op2 value */ ){ + struct Op2 { + const char *zOp; + unsigned char eOp2; + } aOp[] = { + { "match", SQLITE_INDEX_CONSTRAINT_MATCH }, + { "glob", SQLITE_INDEX_CONSTRAINT_GLOB }, + { "like", SQLITE_INDEX_CONSTRAINT_LIKE }, + { "regexp", SQLITE_INDEX_CONSTRAINT_REGEXP } + }; ExprList *pList; + Expr *pCol; /* Column reference */ + int i; if( pExpr->op!=TK_FUNCTION ){ return 0; } - if( sqlite3StrICmp(pExpr->u.zToken,"match")!=0 ){ - return 0; - } pList = pExpr->x.pList; - if( pList->nExpr!=2 ){ + if( pList==0 || pList->nExpr!=2 ){ return 0; } - if( pList->a[1].pExpr->op != TK_COLUMN ){ + pCol = pList->a[1].pExpr; + if( pCol->op!=TK_COLUMN || !IsVirtual(pCol->pTab) ){ return 0; } - return 1; + for(i=0; iu.zToken, aOp[i].zOp)==0 ){ + *peOp2 = aOp[i].eOp2; + return 1; + } + } + return 0; } #endif /* SQLITE_OMIT_VIRTUALTABLE */ @@ -120548,6 +125135,7 @@ static void exprAnalyzeOrTerm( if( pOrInfo==0 ) return; pTerm->wtFlags |= TERM_ORINFO; pOrWc = &pOrInfo->wc; + memset(pOrWc->aStatic, 0, sizeof(pOrWc->aStatic)); sqlite3WhereClauseInit(pOrWc, pWInfo); sqlite3WhereSplit(pOrWc, pExpr, TK_OR); sqlite3WhereExprAnalyze(pSrc, pOrWc); @@ -120564,7 +125152,7 @@ static void exprAnalyzeOrTerm( WhereAndInfo *pAndInfo; assert( (pOrTerm->wtFlags & (TERM_ANDINFO|TERM_ORINFO))==0 ); chngToIN = 0; - pAndInfo = sqlite3DbMallocRaw(db, sizeof(*pAndInfo)); + pAndInfo = sqlite3DbMallocRawNN(db, sizeof(*pAndInfo)); if( pAndInfo ){ WhereClause *pAndWC; WhereTerm *pAndTerm; @@ -120574,11 +125162,11 @@ static void exprAnalyzeOrTerm( pOrTerm->wtFlags |= TERM_ANDINFO; pOrTerm->eOperator = WO_AND; pAndWC = &pAndInfo->wc; + memset(pAndWC->aStatic, 0, sizeof(pAndWC->aStatic)); sqlite3WhereClauseInit(pAndWC, pWC->pWInfo); sqlite3WhereSplit(pAndWC, pOrTerm->pExpr, TK_AND); sqlite3WhereExprAnalyze(pSrc, pAndWC); pAndWC->pOuter = pWC; - testcase( db->mallocFailed ); if( !db->mallocFailed ){ for(j=0, pAndTerm=pAndWC->a; jnTerm; j++, pAndTerm++){ assert( pAndTerm->pExpr ); @@ -120912,6 +125500,7 @@ static void exprAnalyze( int op; /* Top-level operator. pExpr->op */ Parse *pParse = pWInfo->pParse; /* Parsing context */ sqlite3 *db = pParse->db; /* Database connection */ + unsigned char eOp2; /* op2 value for LIKE/REGEXP/GLOB */ if( db->mallocFailed ){ return; @@ -121135,7 +125724,7 @@ static void exprAnalyze( ** virtual tables. The native query optimizer does not attempt ** to do anything with MATCH functions. */ - if( isMatchOfColumn(pExpr) ){ + if( isMatchOfColumn(pExpr, &eOp2) ){ int idxNew; Expr *pRight, *pLeft; WhereTerm *pNewTerm; @@ -121156,6 +125745,7 @@ static void exprAnalyze( pNewTerm->leftCursor = pLeft->iTable; pNewTerm->u.leftColumn = pLeft->iColumn; pNewTerm->eOperator = WO_MATCH; + pNewTerm->eMatchOp = eOp2; markTermAsChild(pWC, idxNew, idxTerm); pTerm = &pWC->a[idxTerm]; pTerm->wtFlags |= TERM_COPIED; @@ -121258,7 +125848,8 @@ SQLITE_PRIVATE void sqlite3WhereClauseInit( /* ** Deallocate a WhereClause structure. The WhereClause structure -** itself is not freed. This routine is the inverse of sqlite3WhereClauseInit(). +** itself is not freed. This routine is the inverse of +** sqlite3WhereClauseInit(). */ SQLITE_PRIVATE void sqlite3WhereClauseClear(WhereClause *pWC){ int i; @@ -121293,10 +125884,10 @@ SQLITE_PRIVATE Bitmask sqlite3WhereExprUsage(WhereMaskSet *pMaskSet, Expr *p){ return mask; } mask = sqlite3WhereExprUsage(pMaskSet, p->pRight); - mask |= sqlite3WhereExprUsage(pMaskSet, p->pLeft); + if( p->pLeft ) mask |= sqlite3WhereExprUsage(pMaskSet, p->pLeft); if( ExprHasProperty(p, EP_xIsSelect) ){ mask |= exprSelectUsage(pMaskSet, p->x.pSelect); - }else{ + }else if( p->x.pList ){ mask |= sqlite3WhereExprListUsage(pMaskSet, p->x.pList); } return mask; @@ -121352,9 +125943,9 @@ SQLITE_PRIVATE void sqlite3WhereTabFuncArgs( pTab = pItem->pTab; assert( pTab!=0 ); pArgs = pItem->u1.pFuncArg; - assert( pArgs!=0 ); + if( pArgs==0 ) return; for(j=k=0; jnExpr; j++){ - while( knCol && (pTab->aCol[k].colFlags & COLFLAG_HIDDEN)==0 ){ k++; } + while( knCol && (pTab->aCol[k].colFlags & COLFLAG_HIDDEN)==0 ){k++;} if( k>=pTab->nCol ){ sqlite3ErrorMsg(pParse, "too many arguments on %s() - max %d", pTab->zName, j); @@ -121406,8 +125997,8 @@ static int whereLoopResize(sqlite3*, WhereLoop*, int); /* ** Return the estimated number of output rows from a WHERE clause */ -SQLITE_PRIVATE u64 sqlite3WhereOutputRowCount(WhereInfo *pWInfo){ - return sqlite3LogEstToInt(pWInfo->nRowOut); +SQLITE_PRIVATE LogEst sqlite3WhereOutputRowCount(WhereInfo *pWInfo){ + return pWInfo->nRowOut; } /* @@ -121636,7 +126227,10 @@ static WhereTerm *whereScanNext(WhereScan *pScan){ ** ** The scanner will be searching the WHERE clause pWC. It will look ** for terms of the form "X " where X is column iColumn of table -** iCur. The must be one of the operators described by opMask. +** iCur. Or if pIdx!=0 then X is column iColumn of index pIdx. pIdx +** must be one of the indexes of table iCur. +** +** The must be one of the operators described by opMask. ** ** If the search is for X and the WHERE clause contains terms of the ** form X=Y then this routine might also return terms of the form @@ -121664,6 +126258,7 @@ static WhereTerm *whereScanInit( j = iColumn; iColumn = pIdx->aiColumn[j]; if( iColumn==XN_EXPR ) pScan->pIdxExpr = pIdx->aColExpr->a[j].pExpr; + if( iColumn==pIdx->pTable->iPKey ) iColumn = XN_ROWID; } if( pIdx && iColumn>=0 ){ pScan->idxaff = pIdx->pTable->aCol[iColumn].affinity; @@ -121683,11 +126278,12 @@ static WhereTerm *whereScanInit( /* ** Search for a term in the WHERE clause that is of the form "X " -** where X is a reference to the iColumn of table iCur and is one of -** the WO_xx operator codes specified by the op parameter. -** Return a pointer to the term. Return 0 if not found. +** where X is a reference to the iColumn of table iCur or of index pIdx +** if pIdx!=0 and is one of the WO_xx operator codes specified by +** the op parameter. Return a pointer to the term. Return 0 if not found. ** -** If pIdx!=0 then search for terms matching the iColumn-th column of pIdx +** If pIdx!=0 then it must be one of the indexes of table iCur. +** Search for terms matching the iColumn-th column of pIdx ** rather than the iColumn-th column of table iCur. ** ** The term returned might by Y= if there is another constraint in @@ -122002,7 +126598,7 @@ static void constructAutomaticIndex( Expr *pPartial = 0; /* Partial Index Expression */ int iContinue = 0; /* Jump here to skip excluded rows */ struct SrcList_item *pTabItem; /* FROM clause term being indexed */ - int addrCounter; /* Address where integer counter is initialized */ + int addrCounter = 0; /* Address where integer counter is initialized */ int regBase; /* Array of registers where record is assembled */ /* Generate code to skip over the creation and initialization of the @@ -122093,7 +126689,7 @@ static void constructAutomaticIndex( idxCols |= cMask; pIdx->aiColumn[n] = pTerm->u.leftColumn; pColl = sqlite3BinaryCompareCollSeq(pParse, pX->pLeft, pX->pRight); - pIdx->azColl[n] = pColl ? pColl->zName : "BINARY"; + pIdx->azColl[n] = pColl ? pColl->zName : sqlite3StrBINARY; n++; } } @@ -122105,20 +126701,20 @@ static void constructAutomaticIndex( for(i=0; iaiColumn[n] = i; - pIdx->azColl[n] = "BINARY"; + pIdx->azColl[n] = sqlite3StrBINARY; n++; } } if( pSrc->colUsed & MASKBIT(BMS-1) ){ for(i=BMS-1; inCol; i++){ pIdx->aiColumn[n] = i; - pIdx->azColl[n] = "BINARY"; + pIdx->azColl[n] = sqlite3StrBINARY; n++; } } assert( n==nKeyCol ); pIdx->aiColumn[n] = XN_ROWID; - pIdx->azColl[n] = "BINARY"; + pIdx->azColl[n] = sqlite3StrBINARY; /* Create the automatic index */ assert( pLevel->iIdxCur>=0 ); @@ -122268,6 +126864,9 @@ static sqlite3_index_info *allocateIndexInfo( pIdxCons[j].iTermOffset = i; op = (u8)pTerm->eOperator & WO_ALL; if( op==WO_IN ) op = WO_EQ; + if( op==WO_MATCH ){ + op = pTerm->eMatchOp; + } pIdxCons[j].op = op; /* The direct assignment in the previous line is possible only because ** the WO_ and SQLITE_INDEX_CONSTRAINT_ codes are identical. The @@ -122306,7 +126905,6 @@ static sqlite3_index_info *allocateIndexInfo( */ static int vtabBestIndex(Parse *pParse, Table *pTab, sqlite3_index_info *p){ sqlite3_vtab *pVtab = sqlite3GetVTable(pParse->db, pTab)->pVtab; - int i; int rc; TRACE_IDX_INPUTS(p); @@ -122315,7 +126913,7 @@ static int vtabBestIndex(Parse *pParse, Table *pTab, sqlite3_index_info *p){ if( rc!=SQLITE_OK ){ if( rc==SQLITE_NOMEM ){ - pParse->db->mallocFailed = 1; + sqlite3OomFault(pParse->db); }else if( !pVtab->zErrMsg ){ sqlite3ErrorMsg(pParse, "%s", sqlite3ErrStr(rc)); }else{ @@ -122325,12 +126923,16 @@ static int vtabBestIndex(Parse *pParse, Table *pTab, sqlite3_index_info *p){ sqlite3_free(pVtab->zErrMsg); pVtab->zErrMsg = 0; +#if 0 + /* This error is now caught by the caller. + ** Search for "xBestIndex malfunction" below */ for(i=0; inConstraint; i++){ if( !p->aConstraint[i].usable && p->aConstraintUsage[i].argvIndex>0 ){ sqlite3ErrorMsg(pParse, "table %s: xBestIndex returned an invalid plan", pTab->zName); } } +#endif return pParse->nErr; } @@ -122933,7 +127535,8 @@ static int whereEqualScanEst( pBuilder->nRecValid = nEq; whereKeyStats(pParse, p, pRec, 0, a); - WHERETRACE(0x10,("equality scan regions: %d\n", (int)a[1])); + WHERETRACE(0x10,("equality scan regions %s(%d): %d\n", + p->zName, nEq-1, (int)a[1])); *pnRow = a[1]; return rc; @@ -123018,11 +127621,12 @@ static void whereTermPrint(WhereTerm *pTerm, int iTerm){ */ static void whereLoopPrint(WhereLoop *p, WhereClause *pWC){ WhereInfo *pWInfo = pWC->pWInfo; - int nb = 1+(pWInfo->pTabList->nSrc+7)/8; + int nb = 1+(pWInfo->pTabList->nSrc+3)/4; struct SrcList_item *pItem = pWInfo->pTabList->a + p->iTab; Table *pTab = pItem->pTab; + Bitmask mAll = (((Bitmask)1)<<(nb*4)) - 1; sqlite3DebugPrintf("%c%2d.%0*llx.%0*llx", p->cId, - p->iTab, nb, p->maskSelf, nb, p->prereq); + p->iTab, nb, p->maskSelf, nb, p->prereq & mAll); sqlite3DebugPrintf(" %12s", pItem->zAlias ? pItem->zAlias : pTab->zName); if( (p->wsFlags & WHERE_VIRTUALTABLE)==0 ){ @@ -123107,8 +127711,8 @@ static int whereLoopResize(sqlite3 *db, WhereLoop *p, int n){ WhereTerm **paNew; if( p->nLSlot>=n ) return SQLITE_OK; n = (n+7)&~7; - paNew = sqlite3DbMallocRaw(db, sizeof(p->aLTerm[0])*n); - if( paNew==0 ) return SQLITE_NOMEM; + paNew = sqlite3DbMallocRawNN(db, sizeof(p->aLTerm[0])*n); + if( paNew==0 ) return SQLITE_NOMEM_BKPT; memcpy(paNew, p->aLTerm, sizeof(p->aLTerm[0])*p->nLSlot); if( p->aLTerm!=p->aLTermSpace ) sqlite3DbFree(db, p->aLTerm); p->aLTerm = paNew; @@ -123123,7 +127727,7 @@ static int whereLoopXfer(sqlite3 *db, WhereLoop *pTo, WhereLoop *pFrom){ whereLoopClearUnion(db, pTo); if( whereLoopResize(db, pTo, pFrom->nLTerm) ){ memset(&pTo->u, 0, sizeof(pTo->u)); - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } memcpy(pTo, pFrom, WHERE_LOOP_XFER_SZ); memcpy(pTo->aLTerm, pFrom->aLTerm, pTo->nLTerm*sizeof(pTo->aLTerm[0])); @@ -123347,6 +127951,7 @@ static int whereLoopInsert(WhereLoopBuilder *pBuilder, WhereLoop *pTemplate){ WhereLoop **ppPrev, *p; WhereInfo *pWInfo = pBuilder->pWInfo; sqlite3 *db = pWInfo->pParse->db; + int rc; /* If pBuilder->pOrSet is defined, then only keep track of the costs ** and prereqs. @@ -123404,8 +128009,8 @@ static int whereLoopInsert(WhereLoopBuilder *pBuilder, WhereLoop *pTemplate){ #endif if( p==0 ){ /* Allocate a new WhereLoop to add to the end of the list */ - *ppPrev = p = sqlite3DbMallocRaw(db, sizeof(WhereLoop)); - if( p==0 ) return SQLITE_NOMEM; + *ppPrev = p = sqlite3DbMallocRawNN(db, sizeof(WhereLoop)); + if( p==0 ) return SQLITE_NOMEM_BKPT; whereLoopInit(p); p->pNextLoop = 0; }else{ @@ -123429,14 +128034,14 @@ static int whereLoopInsert(WhereLoopBuilder *pBuilder, WhereLoop *pTemplate){ whereLoopDelete(db, pToDel); } } - whereLoopXfer(db, p, pTemplate); + rc = whereLoopXfer(db, p, pTemplate); if( (p->wsFlags & WHERE_VIRTUALTABLE)==0 ){ Index *pIndex = p->u.btree.pIndex; if( pIndex && pIndex->tnum==0 ){ p->u.btree.pIndex = 0; } } - return SQLITE_OK; + return rc; } /* @@ -123561,14 +128166,12 @@ static int whereLoopAddBtreeIndex( WhereTerm *pTop = 0, *pBtm = 0; /* Top and bottom range constraints */ pNew = pBuilder->pNew; - if( db->mallocFailed ) return SQLITE_NOMEM; + if( db->mallocFailed ) return SQLITE_NOMEM_BKPT; assert( (pNew->wsFlags & WHERE_VIRTUALTABLE)==0 ); assert( (pNew->wsFlags & WHERE_TOP_LIMIT)==0 ); if( pNew->wsFlags & WHERE_BTM_LIMIT ){ opMask = WO_LT|WO_LE; - }else if( /*pProbe->tnum<=0 ||*/ (pSrc->fg.jointype & JT_LEFT)!=0 ){ - opMask = WO_EQ|WO_IN|WO_GT|WO_GE|WO_LT|WO_LE; }else{ opMask = WO_EQ|WO_IN|WO_GT|WO_GE|WO_LT|WO_LE|WO_ISNULL|WO_IS; } @@ -123606,6 +128209,18 @@ static int whereLoopAddBtreeIndex( ** to mix with a lower range bound from some other source */ if( pTerm->wtFlags & TERM_LIKEOPT && pTerm->eOperator==WO_LT ) continue; + /* Do not allow IS constraints from the WHERE clause to be used by the + ** right table of a LEFT JOIN. Only constraints in the ON clause are + ** allowed */ + if( (pSrc->fg.jointype & JT_LEFT)!=0 + && !ExprHasProperty(pTerm->pExpr, EP_FromJoin) + && (eOp & (WO_IS|WO_ISNULL))!=0 + ){ + testcase( eOp & WO_IS ); + testcase( eOp & WO_ISNULL ); + continue; + } + pNew->wsFlags = saved_wsFlags; pNew->u.btree.nEq = saved_nEq; pNew->nLTerm = saved_nLTerm; @@ -123925,7 +128540,7 @@ static int whereUsablePartialIndex(int iTab, WhereClause *pWC, Expr *pWhere){ */ static int whereLoopAddBtree( WhereLoopBuilder *pBuilder, /* WHERE clause information */ - Bitmask mExtra /* Extra prerequesites for using this table */ + Bitmask mPrereq /* Extra prerequesites for using this table */ ){ WhereInfo *pWInfo; /* WHERE analysis context */ Index *pProbe; /* An index we are evaluating */ @@ -124025,7 +128640,7 @@ static int whereLoopAddBtree( pNew->nOut = 43; assert( 43==sqlite3LogEst(20) ); pNew->rRun = sqlite3LogEstAdd(rLogSize,pNew->nOut); pNew->wsFlags = WHERE_AUTO_INDEX; - pNew->prereq = mExtra | pTerm->prereqRight; + pNew->prereq = mPrereq | pTerm->prereqRight; rc = whereLoopInsert(pBuilder, pNew); } } @@ -124046,7 +128661,7 @@ static int whereLoopAddBtree( pNew->nLTerm = 0; pNew->iSortIdx = 0; pNew->rSetup = 0; - pNew->prereq = mExtra; + pNew->prereq = mPrereq; pNew->nOut = rSize; pNew->u.btree.pIndex = pProbe; b = indexMightHelpWithOrderBy(pBuilder, pProbe, pSrc->iCursor); @@ -124119,12 +128734,160 @@ static int whereLoopAddBtree( } #ifndef SQLITE_OMIT_VIRTUALTABLE + +/* +** Argument pIdxInfo is already populated with all constraints that may +** be used by the virtual table identified by pBuilder->pNew->iTab. This +** function marks a subset of those constraints usable, invokes the +** xBestIndex method and adds the returned plan to pBuilder. +** +** A constraint is marked usable if: +** +** * Argument mUsable indicates that its prerequisites are available, and +** +** * It is not one of the operators specified in the mExclude mask passed +** as the fourth argument (which in practice is either WO_IN or 0). +** +** Argument mPrereq is a mask of tables that must be scanned before the +** virtual table in question. These are added to the plans prerequisites +** before it is added to pBuilder. +** +** Output parameter *pbIn is set to true if the plan added to pBuilder +** uses one or more WO_IN terms, or false otherwise. +*/ +static int whereLoopAddVirtualOne( + WhereLoopBuilder *pBuilder, + Bitmask mPrereq, /* Mask of tables that must be used. */ + Bitmask mUsable, /* Mask of usable tables */ + u16 mExclude, /* Exclude terms using these operators */ + sqlite3_index_info *pIdxInfo, /* Populated object for xBestIndex */ + int *pbIn /* OUT: True if plan uses an IN(...) op */ +){ + WhereClause *pWC = pBuilder->pWC; + struct sqlite3_index_constraint *pIdxCons; + struct sqlite3_index_constraint_usage *pUsage = pIdxInfo->aConstraintUsage; + int i; + int mxTerm; + int rc = SQLITE_OK; + WhereLoop *pNew = pBuilder->pNew; + Parse *pParse = pBuilder->pWInfo->pParse; + struct SrcList_item *pSrc = &pBuilder->pWInfo->pTabList->a[pNew->iTab]; + int nConstraint = pIdxInfo->nConstraint; + + assert( (mUsable & mPrereq)==mPrereq ); + *pbIn = 0; + pNew->prereq = mPrereq; + + /* Set the usable flag on the subset of constraints identified by + ** arguments mUsable and mExclude. */ + pIdxCons = *(struct sqlite3_index_constraint**)&pIdxInfo->aConstraint; + for(i=0; ia[pIdxCons->iTermOffset]; + pIdxCons->usable = 0; + if( (pTerm->prereqRight & mUsable)==pTerm->prereqRight + && (pTerm->eOperator & mExclude)==0 + ){ + pIdxCons->usable = 1; + } + } + + /* Initialize the output fields of the sqlite3_index_info structure */ + memset(pUsage, 0, sizeof(pUsage[0])*nConstraint); + assert( pIdxInfo->needToFreeIdxStr==0 ); + pIdxInfo->idxStr = 0; + pIdxInfo->idxNum = 0; + pIdxInfo->orderByConsumed = 0; + pIdxInfo->estimatedCost = SQLITE_BIG_DBL / (double)2; + pIdxInfo->estimatedRows = 25; + pIdxInfo->idxFlags = 0; + pIdxInfo->colUsed = (sqlite3_int64)pSrc->colUsed; + + /* Invoke the virtual table xBestIndex() method */ + rc = vtabBestIndex(pParse, pSrc->pTab, pIdxInfo); + if( rc ) return rc; + + mxTerm = -1; + assert( pNew->nLSlot>=nConstraint ); + for(i=0; iaLTerm[i] = 0; + pNew->u.vtab.omitMask = 0; + pIdxCons = *(struct sqlite3_index_constraint**)&pIdxInfo->aConstraint; + for(i=0; i=0 ){ + WhereTerm *pTerm; + int j = pIdxCons->iTermOffset; + if( iTerm>=nConstraint + || j<0 + || j>=pWC->nTerm + || pNew->aLTerm[iTerm]!=0 + || pIdxCons->usable==0 + ){ + rc = SQLITE_ERROR; + sqlite3ErrorMsg(pParse,"%s.xBestIndex malfunction",pSrc->pTab->zName); + return rc; + } + testcase( iTerm==nConstraint-1 ); + testcase( j==0 ); + testcase( j==pWC->nTerm-1 ); + pTerm = &pWC->a[j]; + pNew->prereq |= pTerm->prereqRight; + assert( iTermnLSlot ); + pNew->aLTerm[iTerm] = pTerm; + if( iTerm>mxTerm ) mxTerm = iTerm; + testcase( iTerm==15 ); + testcase( iTerm==16 ); + if( iTerm<16 && pUsage[i].omit ) pNew->u.vtab.omitMask |= 1<eOperator & WO_IN)!=0 ){ + /* A virtual table that is constrained by an IN clause may not + ** consume the ORDER BY clause because (1) the order of IN terms + ** is not necessarily related to the order of output terms and + ** (2) Multiple outputs from a single IN value will not merge + ** together. */ + pIdxInfo->orderByConsumed = 0; + pIdxInfo->idxFlags &= ~SQLITE_INDEX_SCAN_UNIQUE; + *pbIn = 1; assert( (mExclude & WO_IN)==0 ); + } + } + } + + pNew->nLTerm = mxTerm+1; + assert( pNew->nLTerm<=pNew->nLSlot ); + pNew->u.vtab.idxNum = pIdxInfo->idxNum; + pNew->u.vtab.needFree = pIdxInfo->needToFreeIdxStr; + pIdxInfo->needToFreeIdxStr = 0; + pNew->u.vtab.idxStr = pIdxInfo->idxStr; + pNew->u.vtab.isOrdered = (i8)(pIdxInfo->orderByConsumed ? + pIdxInfo->nOrderBy : 0); + pNew->rSetup = 0; + pNew->rRun = sqlite3LogEstFromDouble(pIdxInfo->estimatedCost); + pNew->nOut = sqlite3LogEst(pIdxInfo->estimatedRows); + + /* Set the WHERE_ONEROW flag if the xBestIndex() method indicated + ** that the scan will visit at most one row. Clear it otherwise. */ + if( pIdxInfo->idxFlags & SQLITE_INDEX_SCAN_UNIQUE ){ + pNew->wsFlags |= WHERE_ONEROW; + }else{ + pNew->wsFlags &= ~WHERE_ONEROW; + } + rc = whereLoopInsert(pBuilder, pNew); + if( pNew->u.vtab.needFree ){ + sqlite3_free(pNew->u.vtab.idxStr); + pNew->u.vtab.needFree = 0; + } + WHERETRACE(0xffff, (" bIn=%d prereqIn=%04llx prereqOut=%04llx\n", + *pbIn, (sqlite3_uint64)mPrereq, + (sqlite3_uint64)(pNew->prereq & ~mPrereq))); + + return rc; +} + + /* ** Add all WhereLoop objects for a table of the join identified by ** pBuilder->pNew->iTab. That table is guaranteed to be a virtual table. ** -** If there are no LEFT or CROSS JOIN joins in the query, both mExtra and -** mUnusable are set to 0. Otherwise, mExtra is a mask of all FROM clause +** If there are no LEFT or CROSS JOIN joins in the query, both mPrereq and +** mUnusable are set to 0. Otherwise, mPrereq is a mask of all FROM clause ** entries that occur before the virtual table in the FROM clause and are ** separated from it by at least one LEFT or CROSS JOIN. Similarly, the ** mUnusable mask contains all FROM clause entries that occur after the @@ -124135,187 +128898,122 @@ static int whereLoopAddBtree( ** ** ... FROM t1, t2 LEFT JOIN t3, t4, vt CROSS JOIN t5, t6; ** -** then mExtra corresponds to (t1, t2) and mUnusable to (t5, t6). +** then mPrereq corresponds to (t1, t2) and mUnusable to (t5, t6). ** -** All the tables in mExtra must be scanned before the current virtual +** All the tables in mPrereq must be scanned before the current virtual ** table. So any terms for which all prerequisites are satisfied by -** mExtra may be specified as "usable" in all calls to xBestIndex. +** mPrereq may be specified as "usable" in all calls to xBestIndex. ** Conversely, all tables in mUnusable must be scanned after the current ** virtual table, so any terms for which the prerequisites overlap with ** mUnusable should always be configured as "not-usable" for xBestIndex. */ static int whereLoopAddVirtual( WhereLoopBuilder *pBuilder, /* WHERE clause information */ - Bitmask mExtra, /* Tables that must be scanned before this one */ + Bitmask mPrereq, /* Tables that must be scanned before this one */ Bitmask mUnusable /* Tables that must be scanned after this one */ ){ + int rc = SQLITE_OK; /* Return code */ WhereInfo *pWInfo; /* WHERE analysis context */ Parse *pParse; /* The parsing context */ WhereClause *pWC; /* The WHERE clause */ struct SrcList_item *pSrc; /* The FROM clause term to search */ - Table *pTab; - sqlite3 *db; - sqlite3_index_info *pIdxInfo; - struct sqlite3_index_constraint *pIdxCons; - struct sqlite3_index_constraint_usage *pUsage; - WhereTerm *pTerm; - int i, j; - int iTerm, mxTerm; - int nConstraint; - int seenIn = 0; /* True if an IN operator is seen */ - int seenVar = 0; /* True if a non-constant constraint is seen */ - int iPhase; /* 0: const w/o IN, 1: const, 2: no IN, 2: IN */ + sqlite3_index_info *p; /* Object to pass to xBestIndex() */ + int nConstraint; /* Number of constraints in p */ + int bIn; /* True if plan uses IN(...) operator */ WhereLoop *pNew; - int rc = SQLITE_OK; + Bitmask mBest; /* Tables used by best possible plan */ - assert( (mExtra & mUnusable)==0 ); + assert( (mPrereq & mUnusable)==0 ); pWInfo = pBuilder->pWInfo; pParse = pWInfo->pParse; - db = pParse->db; pWC = pBuilder->pWC; pNew = pBuilder->pNew; pSrc = &pWInfo->pTabList->a[pNew->iTab]; - pTab = pSrc->pTab; - assert( IsVirtual(pTab) ); - pIdxInfo = allocateIndexInfo(pParse, pWC, mUnusable, pSrc,pBuilder->pOrderBy); - if( pIdxInfo==0 ) return SQLITE_NOMEM; - pNew->prereq = 0; + assert( IsVirtual(pSrc->pTab) ); + p = allocateIndexInfo(pParse, pWC, mUnusable, pSrc, pBuilder->pOrderBy); + if( p==0 ) return SQLITE_NOMEM_BKPT; pNew->rSetup = 0; pNew->wsFlags = WHERE_VIRTUALTABLE; pNew->nLTerm = 0; pNew->u.vtab.needFree = 0; - pUsage = pIdxInfo->aConstraintUsage; - nConstraint = pIdxInfo->nConstraint; - if( whereLoopResize(db, pNew, nConstraint) ){ - sqlite3DbFree(db, pIdxInfo); - return SQLITE_NOMEM; + nConstraint = p->nConstraint; + if( whereLoopResize(pParse->db, pNew, nConstraint) ){ + sqlite3DbFree(pParse->db, p); + return SQLITE_NOMEM_BKPT; } - for(iPhase=0; iPhase<=3; iPhase++){ - if( !seenIn && (iPhase&1)!=0 ){ - iPhase++; - if( iPhase>3 ) break; - } - if( !seenVar && iPhase>1 ) break; - pIdxCons = *(struct sqlite3_index_constraint**)&pIdxInfo->aConstraint; - for(i=0; inConstraint; i++, pIdxCons++){ - j = pIdxCons->iTermOffset; - pTerm = &pWC->a[j]; - switch( iPhase ){ - case 0: /* Constants without IN operator */ - pIdxCons->usable = 0; - if( (pTerm->eOperator & WO_IN)!=0 ){ - seenIn = 1; - } - if( (pTerm->prereqRight & ~mExtra)!=0 ){ - seenVar = 1; - }else if( (pTerm->eOperator & WO_IN)==0 ){ - pIdxCons->usable = 1; - } - break; - case 1: /* Constants with IN operators */ - assert( seenIn ); - pIdxCons->usable = (pTerm->prereqRight & ~mExtra)==0; - break; - case 2: /* Variables without IN */ - assert( seenVar ); - pIdxCons->usable = (pTerm->eOperator & WO_IN)==0; - break; - default: /* Variables with IN */ - assert( seenVar && seenIn ); - pIdxCons->usable = 1; - break; - } - } - memset(pUsage, 0, sizeof(pUsage[0])*pIdxInfo->nConstraint); - if( pIdxInfo->needToFreeIdxStr ) sqlite3_free(pIdxInfo->idxStr); - pIdxInfo->idxStr = 0; - pIdxInfo->idxNum = 0; - pIdxInfo->needToFreeIdxStr = 0; - pIdxInfo->orderByConsumed = 0; - pIdxInfo->estimatedCost = SQLITE_BIG_DBL / (double)2; - pIdxInfo->estimatedRows = 25; - pIdxInfo->idxFlags = 0; - rc = vtabBestIndex(pParse, pTab, pIdxInfo); - if( rc ) goto whereLoopAddVtab_exit; - pIdxCons = *(struct sqlite3_index_constraint**)&pIdxInfo->aConstraint; - pNew->prereq = mExtra; - mxTerm = -1; - assert( pNew->nLSlot>=nConstraint ); - for(i=0; iaLTerm[i] = 0; - pNew->u.vtab.omitMask = 0; - for(i=0; i=0 ){ - j = pIdxCons->iTermOffset; - if( iTerm>=nConstraint - || j<0 - || j>=pWC->nTerm - || pNew->aLTerm[iTerm]!=0 - ){ - rc = SQLITE_ERROR; - sqlite3ErrorMsg(pParse, "%s.xBestIndex() malfunction", pTab->zName); - goto whereLoopAddVtab_exit; - } - testcase( iTerm==nConstraint-1 ); - testcase( j==0 ); - testcase( j==pWC->nTerm-1 ); - pTerm = &pWC->a[j]; - pNew->prereq |= pTerm->prereqRight; - assert( iTermnLSlot ); - pNew->aLTerm[iTerm] = pTerm; - if( iTerm>mxTerm ) mxTerm = iTerm; - testcase( iTerm==15 ); - testcase( iTerm==16 ); - if( iTerm<16 && pUsage[i].omit ) pNew->u.vtab.omitMask |= 1<eOperator & WO_IN)!=0 ){ - if( pUsage[i].omit==0 ){ - /* Do not attempt to use an IN constraint if the virtual table - ** says that the equivalent EQ constraint cannot be safely omitted. - ** If we do attempt to use such a constraint, some rows might be - ** repeated in the output. */ - break; - } - /* A virtual table that is constrained by an IN clause may not - ** consume the ORDER BY clause because (1) the order of IN terms - ** is not necessarily related to the order of output terms and - ** (2) Multiple outputs from a single IN value will not merge - ** together. */ - pIdxInfo->orderByConsumed = 0; - pIdxInfo->idxFlags &= ~SQLITE_INDEX_SCAN_UNIQUE; - } - } - } - if( i>=nConstraint ){ - pNew->nLTerm = mxTerm+1; - assert( pNew->nLTerm<=pNew->nLSlot ); - pNew->u.vtab.idxNum = pIdxInfo->idxNum; - pNew->u.vtab.needFree = pIdxInfo->needToFreeIdxStr; - pIdxInfo->needToFreeIdxStr = 0; - pNew->u.vtab.idxStr = pIdxInfo->idxStr; - pNew->u.vtab.isOrdered = (i8)(pIdxInfo->orderByConsumed ? - pIdxInfo->nOrderBy : 0); - pNew->rSetup = 0; - pNew->rRun = sqlite3LogEstFromDouble(pIdxInfo->estimatedCost); - pNew->nOut = sqlite3LogEst(pIdxInfo->estimatedRows); + /* First call xBestIndex() with all constraints usable. */ + WHERETRACE(0x40, (" VirtualOne: all usable\n")); + rc = whereLoopAddVirtualOne(pBuilder, mPrereq, ALLBITS, 0, p, &bIn); - /* Set the WHERE_ONEROW flag if the xBestIndex() method indicated - ** that the scan will visit at most one row. Clear it otherwise. */ - if( pIdxInfo->idxFlags & SQLITE_INDEX_SCAN_UNIQUE ){ - pNew->wsFlags |= WHERE_ONEROW; - }else{ - pNew->wsFlags &= ~WHERE_ONEROW; - } - whereLoopInsert(pBuilder, pNew); - if( pNew->u.vtab.needFree ){ - sqlite3_free(pNew->u.vtab.idxStr); - pNew->u.vtab.needFree = 0; + /* If the call to xBestIndex() with all terms enabled produced a plan + ** that does not require any source tables (IOW: a plan with mBest==0), + ** then there is no point in making any further calls to xBestIndex() + ** since they will all return the same result (if the xBestIndex() + ** implementation is sane). */ + if( rc==SQLITE_OK && (mBest = (pNew->prereq & ~mPrereq))!=0 ){ + int seenZero = 0; /* True if a plan with no prereqs seen */ + int seenZeroNoIN = 0; /* Plan with no prereqs and no IN(...) seen */ + Bitmask mPrev = 0; + Bitmask mBestNoIn = 0; + + /* If the plan produced by the earlier call uses an IN(...) term, call + ** xBestIndex again, this time with IN(...) terms disabled. */ + if( bIn ){ + WHERETRACE(0x40, (" VirtualOne: all usable w/o IN\n")); + rc = whereLoopAddVirtualOne(pBuilder, mPrereq, ALLBITS, WO_IN, p, &bIn); + assert( bIn==0 ); + mBestNoIn = pNew->prereq & ~mPrereq; + if( mBestNoIn==0 ){ + seenZero = 1; + seenZeroNoIN = 1; } } - } -whereLoopAddVtab_exit: - if( pIdxInfo->needToFreeIdxStr ) sqlite3_free(pIdxInfo->idxStr); - sqlite3DbFree(db, pIdxInfo); + /* Call xBestIndex once for each distinct value of (prereqRight & ~mPrereq) + ** in the set of terms that apply to the current virtual table. */ + while( rc==SQLITE_OK ){ + int i; + Bitmask mNext = ALLBITS; + assert( mNext>0 ); + for(i=0; ia[p->aConstraint[i].iTermOffset].prereqRight & ~mPrereq + ); + if( mThis>mPrev && mThisprereq==mPrereq ){ + seenZero = 1; + if( bIn==0 ) seenZeroNoIN = 1; + } + } + + /* If the calls to xBestIndex() in the above loop did not find a plan + ** that requires no source tables at all (i.e. one guaranteed to be + ** usable), make a call here with all source tables disabled */ + if( rc==SQLITE_OK && seenZero==0 ){ + WHERETRACE(0x40, (" VirtualOne: all disabled\n")); + rc = whereLoopAddVirtualOne(pBuilder, mPrereq, mPrereq, 0, p, &bIn); + if( bIn==0 ) seenZeroNoIN = 1; + } + + /* If the calls to xBestIndex() have so far failed to find a plan + ** that requires no source tables at all and does not use an IN(...) + ** operator, make a final call to obtain one here. */ + if( rc==SQLITE_OK && seenZeroNoIN==0 ){ + WHERETRACE(0x40, (" VirtualOne: all disabled and w/o IN\n")); + rc = whereLoopAddVirtualOne(pBuilder, mPrereq, mPrereq, WO_IN, p, &bIn); + } + } + + if( p->needToFreeIdxStr ) sqlite3_free(p->idxStr); + sqlite3DbFree(pParse->db, p); return rc; } #endif /* SQLITE_OMIT_VIRTUALTABLE */ @@ -124326,7 +129024,7 @@ whereLoopAddVtab_exit: */ static int whereLoopAddOr( WhereLoopBuilder *pBuilder, - Bitmask mExtra, + Bitmask mPrereq, Bitmask mUnusable ){ WhereInfo *pWInfo = pBuilder->pWInfo; @@ -124387,14 +129085,14 @@ static int whereLoopAddOr( #endif #ifndef SQLITE_OMIT_VIRTUALTABLE if( IsVirtual(pItem->pTab) ){ - rc = whereLoopAddVirtual(&sSubBuild, mExtra, mUnusable); + rc = whereLoopAddVirtual(&sSubBuild, mPrereq, mUnusable); }else #endif { - rc = whereLoopAddBtree(&sSubBuild, mExtra); + rc = whereLoopAddBtree(&sSubBuild, mPrereq); } if( rc==SQLITE_OK ){ - rc = whereLoopAddOr(&sSubBuild, mExtra, mUnusable); + rc = whereLoopAddOr(&sSubBuild, mPrereq, mUnusable); } assert( rc==SQLITE_OK || sCur.n==0 ); if( sCur.n==0 ){ @@ -124451,7 +129149,7 @@ static int whereLoopAddOr( */ static int whereLoopAddAll(WhereLoopBuilder *pBuilder){ WhereInfo *pWInfo = pBuilder->pWInfo; - Bitmask mExtra = 0; + Bitmask mPrereq = 0; Bitmask mPrior = 0; int iTab; SrcList *pTabList = pWInfo->pTabList; @@ -124472,7 +129170,7 @@ static int whereLoopAddAll(WhereLoopBuilder *pBuilder){ if( ((pItem->fg.jointype|priorJointype) & (JT_LEFT|JT_CROSS))!=0 ){ /* This condition is true when pItem is the FROM clause term on the ** right-hand-side of a LEFT or CROSS JOIN. */ - mExtra = mPrior; + mPrereq = mPrior; } priorJointype = pItem->fg.jointype; if( IsVirtual(pItem->pTab) ){ @@ -124482,12 +129180,12 @@ static int whereLoopAddAll(WhereLoopBuilder *pBuilder){ mUnusable |= sqlite3WhereGetMask(&pWInfo->sMaskSet, p->iCursor); } } - rc = whereLoopAddVirtual(pBuilder, mExtra, mUnusable); + rc = whereLoopAddVirtual(pBuilder, mPrereq, mUnusable); }else{ - rc = whereLoopAddBtree(pBuilder, mExtra); + rc = whereLoopAddBtree(pBuilder, mPrereq); } if( rc==SQLITE_OK ){ - rc = whereLoopAddOr(pBuilder, mExtra, mUnusable); + rc = whereLoopAddOr(pBuilder, mPrereq, mUnusable); } mPrior |= pNew->maskSelf; if( rc || db->mallocFailed ) break; @@ -124830,15 +129528,14 @@ static LogEst whereSortingCost( LogEst rScale, rSortCost; assert( nOrderBy>0 && 66==sqlite3LogEst(100) ); rScale = sqlite3LogEst((nOrderBy-nSorted)*100/nOrderBy) - 66; - rSortCost = nRow + estLog(nRow) + rScale + 16; + rSortCost = nRow + rScale + 16; - /* TUNING: The cost of implementing DISTINCT using a B-TREE is - ** similar but with a larger constant of proportionality. - ** Multiply by an additional factor of 3.0. */ - if( pWInfo->wctrlFlags & WHERE_WANT_DISTINCT ){ - rSortCost += 16; + /* Multiple by log(M) where M is the number of output rows. + ** Use the LIMIT for M if it is smaller */ + if( (pWInfo->wctrlFlags & WHERE_USE_LIMIT)!=0 && pWInfo->iLimitiLimit; } - + rSortCost += estLog(nRow); return rSortCost; } @@ -124900,8 +129597,8 @@ static int wherePathSolver(WhereInfo *pWInfo, LogEst nRowEst){ /* Allocate and initialize space for aTo, aFrom and aSortCost[] */ nSpace = (sizeof(WherePath)+sizeof(WhereLoop*)*nLoop)*mxChoice*2; nSpace += sizeof(LogEst) * nOrderBy; - pSpace = sqlite3DbMallocRaw(db, nSpace); - if( pSpace==0 ) return SQLITE_NOMEM; + pSpace = sqlite3DbMallocRawNN(db, nSpace); + if( pSpace==0 ) return SQLITE_NOMEM_BKPT; aTo = (WherePath*)pSpace; aFrom = aTo+mxChoice; memset(aFrom, 0, sizeof(aFrom[0])); @@ -124956,6 +129653,12 @@ static int wherePathSolver(WhereInfo *pWInfo, LogEst nRowEst){ if( (pWLoop->prereq & ~pFrom->maskLoop)!=0 ) continue; if( (pWLoop->maskSelf & pFrom->maskLoop)!=0 ) continue; + if( (pWLoop->wsFlags & WHERE_AUTO_INDEX)!=0 && pFrom->nRow<10 ){ + /* Do not use an automatic index if the this loop is expected + ** to run less than 2 times. */ + assert( 10==sqlite3LogEst(2) ); + continue; + } /* At this point, pWLoop is a candidate to be the next loop. ** Compute its cost */ rUnsorted = sqlite3LogEstAdd(pWLoop->rSetup,pWLoop->rRun + pFrom->nRow); @@ -125148,9 +129851,9 @@ static int wherePathSolver(WhereInfo *pWInfo, LogEst nRowEst){ && nRowEst ){ Bitmask notUsed; - int rc = wherePathSatisfiesOrderBy(pWInfo, pWInfo->pResultSet, pFrom, + int rc = wherePathSatisfiesOrderBy(pWInfo, pWInfo->pDistinctSet, pFrom, WHERE_DISTINCTBY, nLoop-1, pFrom->aLoop[nLoop-1], ¬Used); - if( rc==pWInfo->pResultSet->nExpr ){ + if( rc==pWInfo->pDistinctSet->nExpr ){ pWInfo->eDistinct = WHERE_DISTINCT_ORDERED; } } @@ -125208,7 +129911,7 @@ static int whereShortCut(WhereLoopBuilder *pBuilder){ int j; Table *pTab; Index *pIdx; - + pWInfo = pBuilder->pWInfo; if( pWInfo->wctrlFlags & WHERE_FORCE_TABLE ) return 0; assert( pWInfo->pTabList->nSrc>=1 ); @@ -125365,13 +130068,14 @@ static int whereShortCut(WhereLoopBuilder *pBuilder){ ** used. */ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( - Parse *pParse, /* The parser context */ - SrcList *pTabList, /* FROM clause: A list of all tables to be scanned */ - Expr *pWhere, /* The WHERE clause */ - ExprList *pOrderBy, /* An ORDER BY (or GROUP BY) clause, or NULL */ - ExprList *pResultSet, /* Result set of the query */ - u16 wctrlFlags, /* One of the WHERE_* flags defined in sqliteInt.h */ - int iIdxCur /* If WHERE_ONETABLE_ONLY is set, index cursor number */ + Parse *pParse, /* The parser context */ + SrcList *pTabList, /* FROM clause: A list of all tables to be scanned */ + Expr *pWhere, /* The WHERE clause */ + ExprList *pOrderBy, /* An ORDER BY (or GROUP BY) clause, or NULL */ + ExprList *pDistinctSet, /* Try not to output two rows that duplicate these */ + u16 wctrlFlags, /* The WHERE_* flags defined in sqliteInt.h */ + int iAuxArg /* If WHERE_ONETABLE_ONLY is set, index cursor number + ** If WHERE_USE_LIMIT, then the limit amount */ ){ int nByteWInfo; /* Num. bytes allocated for WhereInfo struct */ int nTabList; /* Number of elements in pTabList */ @@ -125385,12 +130089,17 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( int ii; /* Loop counter */ sqlite3 *db; /* Database connection */ int rc; /* Return code */ + u8 bFordelete = 0; /* OPFLAG_FORDELETE or zero, as appropriate */ assert( (wctrlFlags & WHERE_ONEPASS_MULTIROW)==0 || ( (wctrlFlags & WHERE_ONEPASS_DESIRED)!=0 && (wctrlFlags & WHERE_OMIT_OPEN_CLOSE)==0 )); + /* Only one of WHERE_ONETABLE_ONLY or WHERE_USE_LIMIT */ + assert( (wctrlFlags & WHERE_ONETABLE_ONLY)==0 + || (wctrlFlags & WHERE_USE_LIMIT)==0 ); + /* Variable initialization */ db = pParse->db; memset(&sWLB, 0, sizeof(sWLB)); @@ -125441,9 +130150,10 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( pWInfo->pParse = pParse; pWInfo->pTabList = pTabList; pWInfo->pOrderBy = pOrderBy; - pWInfo->pResultSet = pResultSet; + pWInfo->pDistinctSet = pDistinctSet; pWInfo->iBreak = pWInfo->iContinue = sqlite3VdbeMakeLabel(v); pWInfo->wctrlFlags = wctrlFlags; + pWInfo->iLimit = iAuxArg; pWInfo->savedNQueryLoop = pParse->nQueryLoop; assert( pWInfo->eOnePass==ONEPASS_OFF ); /* ONEPASS defaults to OFF */ pMaskSet = &pWInfo->sMaskSet; @@ -125513,20 +130223,25 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( if( db->mallocFailed ) goto whereBeginError; if( wctrlFlags & WHERE_WANT_DISTINCT ){ - if( isDistinctRedundant(pParse, pTabList, &pWInfo->sWC, pResultSet) ){ + if( isDistinctRedundant(pParse, pTabList, &pWInfo->sWC, pDistinctSet) ){ /* The DISTINCT marking is pointless. Ignore it. */ pWInfo->eDistinct = WHERE_DISTINCT_UNIQUE; }else if( pOrderBy==0 ){ /* Try to ORDER BY the result set to make distinct processing easier */ pWInfo->wctrlFlags |= WHERE_DISTINCTBY; - pWInfo->pOrderBy = pResultSet; + pWInfo->pOrderBy = pDistinctSet; } } /* Construct the WhereLoop objects */ - WHERETRACE(0xffff,("*** Optimizer Start *** (wctrlFlags: 0x%x)\n", - wctrlFlags)); #if defined(WHERETRACE_ENABLED) + if( sqlite3WhereTrace & 0xffff ){ + sqlite3DebugPrintf("*** Optimizer Start *** (wctrlFlags: 0x%x",wctrlFlags); + if( wctrlFlags & WHERE_USE_LIMIT ){ + sqlite3DebugPrintf(", limit: %d", iAuxArg); + } + sqlite3DebugPrintf(")\n"); + } if( sqlite3WhereTrace & 0x100 ){ /* Display all terms of the WHERE clause */ int i; for(i=0; inTerm; i++){ @@ -125560,7 +130275,7 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( } } if( pWInfo->pOrderBy==0 && (db->flags & SQLITE_ReverseOrder)!=0 ){ - pWInfo->revMask = (Bitmask)(-1); + pWInfo->revMask = ALLBITS; } if( pParse->nErr || NEVER(db->mallocFailed) ){ goto whereBeginError; @@ -125593,10 +130308,10 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( #endif /* Attempt to omit tables from the join that do not effect the result */ if( pWInfo->nLevel>=2 - && pResultSet!=0 + && pDistinctSet!=0 && OptimizationEnabled(db, SQLITE_OmitNoopJoin) ){ - Bitmask tabUsed = sqlite3WhereExprListUsage(pMaskSet, pResultSet); + Bitmask tabUsed = sqlite3WhereExprListUsage(pMaskSet, pDistinctSet); if( sWLB.pOrderBy ){ tabUsed |= sqlite3WhereExprListUsage(pMaskSet, sWLB.pOrderBy); } @@ -125629,19 +130344,21 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( /* If the caller is an UPDATE or DELETE statement that is requesting ** to use a one-pass algorithm, determine if this is appropriate. - ** The one-pass algorithm only works if the WHERE clause constrains - ** the statement to update or delete a single row. */ assert( (wctrlFlags & WHERE_ONEPASS_DESIRED)==0 || pWInfo->nLevel==1 ); if( (wctrlFlags & WHERE_ONEPASS_DESIRED)!=0 ){ int wsFlags = pWInfo->a[0].pWLoop->wsFlags; int bOnerow = (wsFlags & WHERE_ONEROW)!=0; - if( bOnerow || ( (wctrlFlags & WHERE_ONEPASS_MULTIROW) - && 0==(wsFlags & WHERE_VIRTUALTABLE) - )){ + if( bOnerow + || ((wctrlFlags & WHERE_ONEPASS_MULTIROW)!=0 + && 0==(wsFlags & WHERE_VIRTUALTABLE)) + ){ pWInfo->eOnePass = bOnerow ? ONEPASS_SINGLE : ONEPASS_MULTI; - if( HasRowid(pTabList->a[0].pTab) ){ - pWInfo->a[0].pWLoop->wsFlags &= ~WHERE_IDX_ONLY; + if( HasRowid(pTabList->a[0].pTab) && (wsFlags & WHERE_IDX_ONLY) ){ + if( wctrlFlags & WHERE_ONEPASS_MULTIROW ){ + bFordelete = OPFLAG_FORDELETE; + } + pWInfo->a[0].pWLoop->wsFlags = (wsFlags & ~WHERE_IDX_ONLY); } } } @@ -125685,10 +130402,17 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( Bitmask b = pTabItem->colUsed; int n = 0; for(; b; b=b>>1, n++){} - sqlite3VdbeChangeP4(v, sqlite3VdbeCurrentAddr(v)-1, - SQLITE_INT_TO_PTR(n), P4_INT32); + sqlite3VdbeChangeP4(v, -1, SQLITE_INT_TO_PTR(n), P4_INT32); assert( n<=pTab->nCol ); } +#ifdef SQLITE_ENABLE_CURSOR_HINTS + if( pLoop->u.btree.pIndex!=0 ){ + sqlite3VdbeChangeP5(v, OPFLAG_SEEKEQ|bFordelete); + }else +#endif + { + sqlite3VdbeChangeP5(v, bFordelete); + } #ifdef SQLITE_ENABLE_COLUMN_USED_MASK sqlite3VdbeAddOp4Dup8(v, OP_ColumnsUsed, pTabItem->iCursor, 0, 0, (const u8*)&pTabItem->colUsed, P4_INT64); @@ -125700,8 +130424,8 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( Index *pIx = pLoop->u.btree.pIndex; int iIndexCur; int op = OP_OpenRead; - /* iIdxCur is always set if to a positive value if ONEPASS is possible */ - assert( iIdxCur!=0 || (pWInfo->wctrlFlags & WHERE_ONEPASS_DESIRED)==0 ); + /* iAuxArg is always set if to a positive value if ONEPASS is possible */ + assert( iAuxArg!=0 || (pWInfo->wctrlFlags & WHERE_ONEPASS_DESIRED)==0 ); if( !HasRowid(pTab) && IsPrimaryKeyIndex(pIx) && (wctrlFlags & WHERE_ONETABLE_ONLY)!=0 ){ @@ -125711,7 +130435,7 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( op = 0; }else if( pWInfo->eOnePass!=ONEPASS_OFF ){ Index *pJ = pTabItem->pTab->pIndex; - iIndexCur = iIdxCur; + iIndexCur = iAuxArg; assert( wctrlFlags & WHERE_ONEPASS_DESIRED ); while( ALWAYS(pJ) && pJ!=pIx ){ iIndexCur++; @@ -125719,8 +130443,8 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( } op = OP_OpenWrite; pWInfo->aiCurOnePass[1] = iIndexCur; - }else if( iIdxCur && (wctrlFlags & WHERE_ONETABLE_ONLY)!=0 ){ - iIndexCur = iIdxCur; + }else if( iAuxArg && (wctrlFlags & WHERE_ONETABLE_ONLY)!=0 ){ + iIndexCur = iAuxArg; if( wctrlFlags & WHERE_REOPEN_IDX ) op = OP_ReopenIdx; }else{ iIndexCur = pParse->nTab++; @@ -125851,16 +130575,13 @@ SQLITE_PRIVATE void sqlite3WhereEnd(WhereInfo *pWInfo){ sqlite3VdbeJumpHere(v, pLevel->addrSkip); sqlite3VdbeJumpHere(v, pLevel->addrSkip-2); } +#ifndef SQLITE_LIKE_DOESNT_MATCH_BLOBS if( pLevel->addrLikeRep ){ - int op; - if( sqlite3VdbeGetOp(v, pLevel->addrLikeRep-1)->p1 ){ - op = OP_DecrJumpZero; - }else{ - op = OP_JumpZeroIncr; - } - sqlite3VdbeAddOp2(v, op, pLevel->iLikeRepCntr, pLevel->addrLikeRep); + sqlite3VdbeAddOp2(v, OP_DecrJumpZero, (int)(pLevel->iLikeRepCntr>>1), + pLevel->addrLikeRep); VdbeCoverage(v); } +#endif if( pLevel->iLeftJoin ){ addr = sqlite3VdbeAddOp1(v, OP_IfPos, pLevel->iLeftJoin); VdbeCoverage(v); assert( (pLoop->wsFlags & WHERE_IDX_ONLY)==0 @@ -125984,18 +130705,32 @@ SQLITE_PRIVATE void sqlite3WhereEnd(WhereInfo *pWInfo){ /************** End of where.c ***********************************************/ /************** Begin file parse.c *******************************************/ -/* Driver template for the LEMON parser generator. -** The author disclaims copyright to this source code. +/* +** 2000-05-29 ** -** This version of "lempar.c" is modified, slightly, for use by SQLite. -** The only modifications are the addition of a couple of NEVER() -** macros to disable tests that are needed in the case of a general -** LALR(1) grammar but which are always false in the -** specific grammar used by SQLite. +** The author disclaims copyright to this source code. In place of +** a legal notice, here is a blessing: +** +** May you do good and not evil. +** May you find forgiveness for yourself and forgive others. +** May you share freely, never taking more than you give. +** +************************************************************************* +** Driver template for the LEMON parser generator. +** +** The "lemon" program processes an LALR(1) input grammar file, then uses +** this template to construct a parser. The "lemon" program inserts text +** at each "%%" line. Also, any "P-a-r-s-e" identifer prefix (without the +** interstitial "-" characters) contained in this template is changed into +** the value of the %name directive from the grammar. Otherwise, the content +** of this template is copied straight through into the generate parser +** source file. +** +** The following is the concatenation of all %include directives from the +** input grammar file: */ -/* First off, code is included that follows the "include" declaration -** in the input grammar file. */ /* #include */ +/************ Begin %include sections from the grammar ************************/ /* #include "sqliteInt.h" */ @@ -126010,6 +130745,18 @@ SQLITE_PRIVATE void sqlite3WhereEnd(WhereInfo *pWInfo){ */ #define yytestcase(X) testcase(X) +/* +** Indicate that sqlite3ParserFree() will never be called with a null +** pointer. +*/ +#define YYPARSEFREENEVERNULL 1 + +/* +** Alternative datatype for the argument to the malloc() routine passed +** into sqlite3ParserAlloc(). The default is size_t. +*/ +#define YYMALLOCARGTYPE u64 + /* ** An instance of this structure holds information about the ** LIMIT clause of a SELECT statement. @@ -126044,6 +130791,15 @@ struct TrigEvent { int a; IdList * b; }; */ struct AttachKey { int type; Token key; }; +/* +** Disable lookaside memory allocation for objects that might be +** shared across database connections. +*/ +static void disableLookaside(Parse *pParse){ + pParse->disableLookaside++; + pParse->db->lookaside.bDisable++; +} + /* ** For a compound SELECT statement, make sure p->pPrior->pNext==p for @@ -126080,46 +130836,51 @@ struct AttachKey { int type; Token key; }; ** new Expr to populate pOut. Set the span of pOut to be the identifier ** that created the expression. */ - static void spanExpr(ExprSpan *pOut, Parse *pParse, int op, Token *pValue){ - pOut->pExpr = sqlite3PExpr(pParse, op, 0, 0, pValue); - pOut->zStart = pValue->z; - pOut->zEnd = &pValue->z[pValue->n]; + static void spanExpr(ExprSpan *pOut, Parse *pParse, int op, Token t){ + pOut->pExpr = sqlite3PExpr(pParse, op, 0, 0, &t); + pOut->zStart = t.z; + pOut->zEnd = &t.z[t.n]; } /* This routine constructs a binary expression node out of two ExprSpan ** objects and uses the result to populate a new ExprSpan object. */ static void spanBinaryExpr( - ExprSpan *pOut, /* Write the result here */ Parse *pParse, /* The parsing context. Errors accumulate here */ int op, /* The binary operation */ - ExprSpan *pLeft, /* The left operand */ + ExprSpan *pLeft, /* The left operand, and output */ ExprSpan *pRight /* The right operand */ ){ - pOut->pExpr = sqlite3PExpr(pParse, op, pLeft->pExpr, pRight->pExpr, 0); - pOut->zStart = pLeft->zStart; - pOut->zEnd = pRight->zEnd; + pLeft->pExpr = sqlite3PExpr(pParse, op, pLeft->pExpr, pRight->pExpr, 0); + pLeft->zEnd = pRight->zEnd; + } + + /* If doNot is true, then add a TK_NOT Expr-node wrapper around the + ** outside of *ppExpr. + */ + static void exprNot(Parse *pParse, int doNot, ExprSpan *pSpan){ + if( doNot ){ + pSpan->pExpr = sqlite3PExpr(pParse, TK_NOT, pSpan->pExpr, 0, 0); + } } /* Construct an expression node for a unary postfix operator */ static void spanUnaryPostfix( - ExprSpan *pOut, /* Write the new expression node here */ Parse *pParse, /* Parsing context to record errors */ int op, /* The operator */ - ExprSpan *pOperand, /* The operand */ + ExprSpan *pOperand, /* The operand, and output */ Token *pPostOp /* The operand token for setting the span */ ){ - pOut->pExpr = sqlite3PExpr(pParse, op, pOperand->pExpr, 0, 0); - pOut->zStart = pOperand->zStart; - pOut->zEnd = &pPostOp->z[pPostOp->n]; + pOperand->pExpr = sqlite3PExpr(pParse, op, pOperand->pExpr, 0, 0); + pOperand->zEnd = &pPostOp->z[pPostOp->n]; } /* A routine to convert a binary TK_IS or TK_ISNOT expression into a ** unary TK_ISNULL or TK_NOTNULL expression. */ static void binaryToUnaryIfNull(Parse *pParse, Expr *pY, Expr *pA, int op){ sqlite3 *db = pParse->db; - if( pY && pA && pY->op==TK_NULL ){ + if( pA && pY && pY->op==TK_NULL ){ pA->op = (u8)op; sqlite3ExprDelete(db, pA->pRight); pA->pRight = 0; @@ -126135,8 +130896,8 @@ struct AttachKey { int type; Token key; }; ExprSpan *pOperand, /* The operand */ Token *pPreOp /* The operand token for setting the span */ ){ - pOut->pExpr = sqlite3PExpr(pParse, op, pOperand->pExpr, 0, 0); pOut->zStart = pPreOp->z; + pOut->pExpr = sqlite3PExpr(pParse, op, pOperand->pExpr, 0, 0); pOut->zEnd = pOperand->zEnd; } @@ -126162,44 +130923,42 @@ struct AttachKey { int type; Token key; }; sqlite3ExprListSetName(pParse, p, pIdToken, 1); return p; } -/* Next is all token values, in a form suitable for use by makeheaders. -** This section will be null unless lemon is run with the -m switch. -*/ -/* -** These constants (all generated automatically by the parser generator) -** specify the various kinds of tokens (terminals) that the parser -** understands. -** -** Each symbol here is a terminal symbol in the grammar. -*/ -/* Make sure the INTERFACE macro is defined. -*/ -#ifndef INTERFACE -# define INTERFACE 1 -#endif -/* The next thing included is series of defines which control +/**************** End of %include directives **********************************/ +/* These constants specify the various numeric values for terminal symbols +** in a format understandable to "makeheaders". This section is blank unless +** "lemon" is run with the "-m" command-line option. +***************** Begin makeheaders token definitions *************************/ +/**************** End makeheaders token definitions ***************************/ + +/* The next sections is a series of control #defines. ** various aspects of the generated parser. -** YYCODETYPE is the data type used for storing terminal -** and nonterminal numbers. "unsigned char" is -** used if there are fewer than 250 terminals -** and nonterminals. "int" is used otherwise. -** YYNOCODE is a number of type YYCODETYPE which corresponds -** to no legal terminal or nonterminal number. This -** number is used to fill in empty slots of the hash -** table. +** YYCODETYPE is the data type used to store the integer codes +** that represent terminal and non-terminal symbols. +** "unsigned char" is used if there are fewer than +** 256 symbols. Larger types otherwise. +** YYNOCODE is a number of type YYCODETYPE that is not used for +** any terminal or nonterminal symbol. ** YYFALLBACK If defined, this indicates that one or more tokens -** have fall-back values which should be used if the -** original value of the token will not parse. -** YYACTIONTYPE is the data type used for storing terminal -** and nonterminal numbers. "unsigned char" is -** used if there are fewer than 250 rules and -** states combined. "int" is used otherwise. -** sqlite3ParserTOKENTYPE is the data type used for minor tokens given -** directly to the parser from the tokenizer. -** YYMINORTYPE is the data type used for all minor tokens. +** (also known as: "terminal symbols") have fall-back +** values which should be used if the original symbol +** would not parse. This permits keywords to sometimes +** be used as identifiers, for example. +** YYACTIONTYPE is the data type used for "action codes" - numbers +** that indicate what to do in response to the next +** token. +** sqlite3ParserTOKENTYPE is the data type used for minor type for terminal +** symbols. Background: A "minor type" is a semantic +** value associated with a terminal or non-terminal +** symbols. For example, for an "ID" terminal symbol, +** the minor type might be the name of the identifier. +** Each non-terminal can have a different minor type. +** Terminal symbols all have the same minor type, though. +** This macros defines the minor type for terminal +** symbols. +** YYMINORTYPE is the data type used for all minor types. ** This is typically a union of many types, one of ** which is sqlite3ParserTOKENTYPE. The entry in the union -** for base tokens is called "yy0". +** for terminal symbols is called "yy0". ** YYSTACKDEPTH is the maximum depth of the parser's stack. If ** zero the stack is dynamically sized using realloc() ** sqlite3ParserARG_SDECL A static variable declaration for the %extra_argument @@ -126218,29 +130977,31 @@ struct AttachKey { int type; Token key; }; ** YY_ACCEPT_ACTION The yy_action[] code for accept ** YY_NO_ACTION The yy_action[] code for no-op */ +#ifndef INTERFACE +# define INTERFACE 1 +#endif +/************* Begin control #defines *****************************************/ #define YYCODETYPE unsigned char -#define YYNOCODE 254 +#define YYNOCODE 251 #define YYACTIONTYPE unsigned short int -#define YYWILDCARD 70 +#define YYWILDCARD 96 #define sqlite3ParserTOKENTYPE Token typedef union { int yyinit; sqlite3ParserTOKENTYPE yy0; - Select* yy3; - ExprList* yy14; - With* yy59; - SrcList* yy65; - struct LikeOp yy96; - Expr* yy132; - u8 yy186; - int yy328; - ExprSpan yy346; - struct TrigEvent yy378; - u16 yy381; - IdList* yy408; - struct {int value; int mask;} yy429; - TriggerStep* yy473; - struct LimitVal yy476; + struct LimitVal yy64; + Expr* yy122; + Select* yy159; + IdList* yy180; + struct {int value; int mask;} yy207; + struct LikeOp yy318; + TriggerStep* yy327; + With* yy331; + ExprSpan yy342; + SrcList* yy347; + int yy392; + struct TrigEvent yy410; + ExprList* yy442; } YYMINORTYPE; #ifndef YYSTACKDEPTH #define YYSTACKDEPTH 100 @@ -126250,20 +131011,17 @@ typedef union { #define sqlite3ParserARG_FETCH Parse *pParse = yypParser->pParse #define sqlite3ParserARG_STORE yypParser->pParse = pParse #define YYFALLBACK 1 -#define YYNSTATE 436 -#define YYNRULE 328 -#define YY_MAX_SHIFT 435 +#define YYNSTATE 440 +#define YYNRULE 326 +#define YY_MAX_SHIFT 439 #define YY_MIN_SHIFTREDUCE 649 -#define YY_MAX_SHIFTREDUCE 976 -#define YY_MIN_REDUCE 977 -#define YY_MAX_REDUCE 1304 -#define YY_ERROR_ACTION 1305 -#define YY_ACCEPT_ACTION 1306 -#define YY_NO_ACTION 1307 - -/* The yyzerominor constant is used to initialize instances of -** YYMINORTYPE objects to zero. */ -static const YYMINORTYPE yyzerominor = { 0 }; +#define YY_MAX_SHIFTREDUCE 974 +#define YY_MIN_REDUCE 975 +#define YY_MAX_REDUCE 1300 +#define YY_ERROR_ACTION 1301 +#define YY_ACCEPT_ACTION 1302 +#define YY_NO_ACTION 1303 +/************* End control #defines *******************************************/ /* Define the yytestcase() macro to be a no-op if is not already defined ** otherwise. @@ -126328,451 +131086,453 @@ static const YYMINORTYPE yyzerominor = { 0 }; ** yy_reduce_ofst[] For each state, the offset into yy_action for ** shifting non-terminals after a reduce. ** yy_default[] Default action for each state. -*/ +** +*********** Begin parsing tables **********************************************/ #define YY_ACTTAB_COUNT (1501) static const YYACTIONTYPE yy_action[] = { - /* 0 */ 311, 1306, 145, 651, 2, 192, 652, 338, 780, 92, - /* 10 */ 92, 92, 92, 85, 90, 90, 90, 90, 89, 89, - /* 20 */ 88, 88, 88, 87, 335, 88, 88, 88, 87, 335, - /* 30 */ 327, 856, 856, 92, 92, 92, 92, 776, 90, 90, - /* 40 */ 90, 90, 89, 89, 88, 88, 88, 87, 335, 86, - /* 50 */ 83, 166, 93, 94, 84, 868, 871, 860, 860, 91, - /* 60 */ 91, 92, 92, 92, 92, 335, 90, 90, 90, 90, - /* 70 */ 89, 89, 88, 88, 88, 87, 335, 311, 780, 90, - /* 80 */ 90, 90, 90, 89, 89, 88, 88, 88, 87, 335, - /* 90 */ 123, 808, 689, 689, 689, 689, 112, 230, 430, 257, - /* 100 */ 809, 698, 430, 86, 83, 166, 324, 55, 856, 856, - /* 110 */ 201, 158, 276, 387, 271, 386, 188, 689, 689, 828, - /* 120 */ 833, 49, 944, 269, 833, 49, 123, 87, 335, 93, - /* 130 */ 94, 84, 868, 871, 860, 860, 91, 91, 92, 92, - /* 140 */ 92, 92, 342, 90, 90, 90, 90, 89, 89, 88, - /* 150 */ 88, 88, 87, 335, 311, 328, 333, 332, 701, 408, - /* 160 */ 394, 69, 690, 691, 690, 691, 715, 910, 251, 354, - /* 170 */ 250, 698, 704, 430, 908, 430, 909, 89, 89, 88, - /* 180 */ 88, 88, 87, 335, 391, 856, 856, 690, 691, 183, - /* 190 */ 95, 340, 384, 381, 380, 833, 31, 833, 49, 912, - /* 200 */ 912, 333, 332, 379, 123, 311, 93, 94, 84, 868, - /* 210 */ 871, 860, 860, 91, 91, 92, 92, 92, 92, 114, - /* 220 */ 90, 90, 90, 90, 89, 89, 88, 88, 88, 87, - /* 230 */ 335, 430, 408, 399, 435, 657, 856, 856, 346, 57, - /* 240 */ 232, 828, 109, 20, 912, 912, 231, 393, 937, 760, - /* 250 */ 97, 751, 752, 833, 49, 708, 708, 93, 94, 84, - /* 260 */ 868, 871, 860, 860, 91, 91, 92, 92, 92, 92, - /* 270 */ 707, 90, 90, 90, 90, 89, 89, 88, 88, 88, - /* 280 */ 87, 335, 311, 114, 22, 706, 688, 58, 408, 390, - /* 290 */ 251, 349, 240, 749, 752, 689, 689, 847, 685, 115, - /* 300 */ 21, 231, 393, 689, 689, 697, 183, 355, 430, 384, - /* 310 */ 381, 380, 192, 856, 856, 780, 123, 160, 159, 223, - /* 320 */ 379, 738, 25, 315, 362, 841, 143, 689, 689, 835, - /* 330 */ 833, 48, 339, 937, 93, 94, 84, 868, 871, 860, - /* 340 */ 860, 91, 91, 92, 92, 92, 92, 914, 90, 90, - /* 350 */ 90, 90, 89, 89, 88, 88, 88, 87, 335, 311, - /* 360 */ 840, 840, 840, 266, 430, 690, 691, 778, 114, 1300, - /* 370 */ 1300, 430, 1, 690, 691, 697, 688, 689, 689, 689, - /* 380 */ 689, 689, 689, 287, 298, 780, 833, 10, 686, 115, - /* 390 */ 856, 856, 355, 833, 10, 828, 366, 690, 691, 363, - /* 400 */ 321, 76, 123, 74, 23, 737, 807, 323, 356, 353, - /* 410 */ 847, 93, 94, 84, 868, 871, 860, 860, 91, 91, - /* 420 */ 92, 92, 92, 92, 940, 90, 90, 90, 90, 89, - /* 430 */ 89, 88, 88, 88, 87, 335, 311, 806, 841, 429, - /* 440 */ 713, 941, 835, 430, 251, 354, 250, 690, 691, 690, - /* 450 */ 691, 690, 691, 86, 83, 166, 24, 942, 151, 753, - /* 460 */ 285, 907, 403, 907, 164, 833, 10, 856, 856, 965, - /* 470 */ 306, 754, 679, 840, 840, 840, 795, 216, 794, 222, - /* 480 */ 906, 344, 906, 904, 86, 83, 166, 286, 93, 94, - /* 490 */ 84, 868, 871, 860, 860, 91, 91, 92, 92, 92, - /* 500 */ 92, 430, 90, 90, 90, 90, 89, 89, 88, 88, - /* 510 */ 88, 87, 335, 311, 430, 724, 352, 705, 427, 699, - /* 520 */ 700, 376, 210, 833, 49, 793, 397, 857, 857, 940, - /* 530 */ 213, 762, 727, 334, 699, 700, 833, 10, 86, 83, - /* 540 */ 166, 345, 396, 902, 856, 856, 941, 385, 833, 9, - /* 550 */ 406, 869, 872, 187, 890, 728, 347, 398, 404, 977, - /* 560 */ 652, 338, 942, 954, 413, 93, 94, 84, 868, 871, - /* 570 */ 860, 860, 91, 91, 92, 92, 92, 92, 861, 90, - /* 580 */ 90, 90, 90, 89, 89, 88, 88, 88, 87, 335, - /* 590 */ 311, 1219, 114, 430, 834, 430, 5, 165, 192, 688, - /* 600 */ 832, 780, 430, 723, 430, 234, 325, 189, 163, 316, - /* 610 */ 356, 955, 115, 235, 269, 833, 35, 833, 36, 747, - /* 620 */ 720, 856, 856, 793, 833, 12, 833, 27, 745, 174, - /* 630 */ 968, 1290, 968, 1291, 1290, 310, 1291, 693, 317, 245, - /* 640 */ 264, 311, 93, 94, 84, 868, 871, 860, 860, 91, - /* 650 */ 91, 92, 92, 92, 92, 832, 90, 90, 90, 90, - /* 660 */ 89, 89, 88, 88, 88, 87, 335, 430, 320, 213, - /* 670 */ 762, 780, 856, 856, 920, 920, 369, 257, 966, 220, - /* 680 */ 966, 396, 663, 664, 665, 242, 259, 244, 262, 833, - /* 690 */ 37, 650, 2, 93, 94, 84, 868, 871, 860, 860, - /* 700 */ 91, 91, 92, 92, 92, 92, 430, 90, 90, 90, - /* 710 */ 90, 89, 89, 88, 88, 88, 87, 335, 311, 430, - /* 720 */ 239, 430, 917, 368, 430, 238, 916, 793, 833, 38, - /* 730 */ 430, 825, 430, 66, 430, 392, 430, 766, 766, 430, - /* 740 */ 367, 833, 39, 833, 28, 430, 833, 29, 68, 856, - /* 750 */ 856, 900, 833, 40, 833, 41, 833, 42, 833, 11, - /* 760 */ 72, 833, 43, 243, 305, 970, 114, 833, 99, 961, - /* 770 */ 93, 94, 84, 868, 871, 860, 860, 91, 91, 92, - /* 780 */ 92, 92, 92, 430, 90, 90, 90, 90, 89, 89, - /* 790 */ 88, 88, 88, 87, 335, 311, 430, 361, 430, 165, - /* 800 */ 147, 430, 186, 185, 184, 833, 44, 430, 289, 430, - /* 810 */ 246, 430, 971, 430, 212, 163, 430, 357, 833, 45, - /* 820 */ 833, 32, 932, 833, 46, 793, 856, 856, 718, 833, - /* 830 */ 47, 833, 33, 833, 117, 833, 118, 75, 833, 119, - /* 840 */ 288, 305, 967, 214, 935, 322, 311, 93, 94, 84, - /* 850 */ 868, 871, 860, 860, 91, 91, 92, 92, 92, 92, - /* 860 */ 430, 90, 90, 90, 90, 89, 89, 88, 88, 88, - /* 870 */ 87, 335, 430, 832, 426, 317, 288, 856, 856, 114, - /* 880 */ 763, 257, 833, 53, 930, 219, 364, 257, 257, 971, - /* 890 */ 361, 396, 257, 257, 833, 34, 257, 311, 93, 94, - /* 900 */ 84, 868, 871, 860, 860, 91, 91, 92, 92, 92, - /* 910 */ 92, 430, 90, 90, 90, 90, 89, 89, 88, 88, - /* 920 */ 88, 87, 335, 430, 217, 318, 124, 253, 856, 856, - /* 930 */ 218, 943, 257, 833, 100, 898, 759, 774, 361, 755, - /* 940 */ 423, 329, 758, 1017, 289, 833, 50, 682, 311, 93, - /* 950 */ 82, 84, 868, 871, 860, 860, 91, 91, 92, 92, - /* 960 */ 92, 92, 430, 90, 90, 90, 90, 89, 89, 88, - /* 970 */ 88, 88, 87, 335, 430, 256, 419, 114, 249, 856, - /* 980 */ 856, 331, 114, 400, 833, 101, 359, 187, 1064, 726, - /* 990 */ 725, 739, 401, 416, 420, 360, 833, 102, 424, 311, - /* 1000 */ 258, 94, 84, 868, 871, 860, 860, 91, 91, 92, - /* 1010 */ 92, 92, 92, 430, 90, 90, 90, 90, 89, 89, - /* 1020 */ 88, 88, 88, 87, 335, 430, 221, 261, 114, 114, - /* 1030 */ 856, 856, 808, 114, 156, 833, 98, 772, 733, 734, - /* 1040 */ 275, 809, 771, 316, 263, 265, 960, 833, 116, 307, - /* 1050 */ 741, 274, 722, 84, 868, 871, 860, 860, 91, 91, - /* 1060 */ 92, 92, 92, 92, 430, 90, 90, 90, 90, 89, - /* 1070 */ 89, 88, 88, 88, 87, 335, 80, 425, 830, 3, - /* 1080 */ 1214, 191, 430, 721, 336, 336, 833, 113, 252, 80, - /* 1090 */ 425, 68, 3, 913, 913, 428, 270, 336, 336, 430, - /* 1100 */ 377, 784, 430, 197, 833, 106, 430, 716, 428, 430, - /* 1110 */ 267, 430, 897, 68, 414, 430, 769, 409, 430, 71, - /* 1120 */ 430, 833, 105, 123, 833, 103, 847, 414, 833, 49, - /* 1130 */ 843, 833, 104, 833, 52, 800, 123, 833, 54, 847, - /* 1140 */ 833, 51, 833, 26, 831, 802, 77, 78, 191, 389, - /* 1150 */ 430, 372, 114, 79, 432, 431, 911, 911, 835, 77, - /* 1160 */ 78, 779, 893, 408, 410, 197, 79, 432, 431, 791, - /* 1170 */ 226, 835, 833, 30, 772, 80, 425, 716, 3, 771, - /* 1180 */ 411, 412, 897, 336, 336, 290, 291, 839, 703, 840, - /* 1190 */ 840, 840, 842, 19, 428, 695, 684, 672, 111, 671, - /* 1200 */ 843, 673, 840, 840, 840, 842, 19, 207, 661, 278, - /* 1210 */ 148, 304, 280, 414, 282, 6, 822, 348, 248, 241, - /* 1220 */ 358, 934, 720, 80, 425, 847, 3, 161, 382, 273, - /* 1230 */ 284, 336, 336, 415, 296, 958, 895, 894, 157, 674, - /* 1240 */ 107, 194, 428, 948, 135, 77, 78, 777, 953, 951, - /* 1250 */ 56, 319, 79, 432, 431, 121, 66, 835, 59, 128, - /* 1260 */ 146, 414, 350, 130, 351, 819, 131, 132, 133, 375, - /* 1270 */ 173, 149, 138, 847, 936, 365, 178, 70, 425, 827, - /* 1280 */ 3, 889, 62, 371, 915, 336, 336, 792, 840, 840, - /* 1290 */ 840, 842, 19, 77, 78, 208, 428, 144, 179, 373, - /* 1300 */ 79, 432, 431, 255, 180, 835, 260, 675, 181, 308, - /* 1310 */ 388, 744, 326, 743, 742, 414, 731, 718, 712, 402, - /* 1320 */ 309, 711, 788, 65, 277, 272, 789, 847, 730, 710, - /* 1330 */ 709, 279, 193, 787, 281, 876, 840, 840, 840, 842, - /* 1340 */ 19, 786, 283, 73, 418, 330, 422, 77, 78, 227, - /* 1350 */ 96, 407, 67, 405, 79, 432, 431, 292, 228, 835, - /* 1360 */ 215, 202, 229, 293, 767, 303, 302, 301, 204, 299, - /* 1370 */ 294, 295, 676, 7, 681, 433, 669, 206, 110, 224, - /* 1380 */ 203, 205, 434, 667, 666, 658, 120, 168, 656, 237, - /* 1390 */ 840, 840, 840, 842, 19, 337, 155, 233, 236, 341, - /* 1400 */ 167, 905, 108, 313, 903, 826, 314, 125, 126, 127, - /* 1410 */ 129, 170, 247, 756, 172, 928, 134, 136, 171, 60, - /* 1420 */ 61, 123, 169, 137, 175, 933, 176, 927, 8, 13, - /* 1430 */ 177, 254, 191, 918, 139, 370, 924, 140, 678, 150, - /* 1440 */ 374, 274, 182, 378, 141, 122, 63, 14, 383, 729, - /* 1450 */ 268, 15, 64, 225, 846, 845, 874, 16, 765, 770, - /* 1460 */ 4, 162, 209, 395, 211, 142, 878, 796, 801, 312, - /* 1470 */ 190, 71, 68, 875, 873, 939, 199, 938, 17, 195, - /* 1480 */ 18, 196, 417, 975, 152, 653, 976, 198, 153, 421, - /* 1490 */ 877, 154, 200, 844, 696, 81, 343, 297, 1019, 1018, - /* 1500 */ 300, + /* 0 */ 315, 810, 339, 804, 5, 194, 194, 798, 92, 93, + /* 10 */ 83, 819, 819, 831, 834, 823, 823, 90, 90, 91, + /* 20 */ 91, 91, 91, 290, 89, 89, 89, 89, 88, 88, + /* 30 */ 87, 87, 87, 86, 339, 315, 952, 952, 803, 803, + /* 40 */ 803, 922, 342, 92, 93, 83, 819, 819, 831, 834, + /* 50 */ 823, 823, 90, 90, 91, 91, 91, 91, 123, 89, + /* 60 */ 89, 89, 89, 88, 88, 87, 87, 87, 86, 339, + /* 70 */ 88, 88, 87, 87, 87, 86, 339, 772, 952, 952, + /* 80 */ 315, 87, 87, 87, 86, 339, 773, 68, 92, 93, + /* 90 */ 83, 819, 819, 831, 834, 823, 823, 90, 90, 91, + /* 100 */ 91, 91, 91, 434, 89, 89, 89, 89, 88, 88, + /* 110 */ 87, 87, 87, 86, 339, 1302, 146, 921, 2, 315, + /* 120 */ 427, 24, 679, 953, 48, 86, 339, 92, 93, 83, + /* 130 */ 819, 819, 831, 834, 823, 823, 90, 90, 91, 91, + /* 140 */ 91, 91, 94, 89, 89, 89, 89, 88, 88, 87, + /* 150 */ 87, 87, 86, 339, 933, 933, 315, 259, 412, 398, + /* 160 */ 396, 57, 733, 733, 92, 93, 83, 819, 819, 831, + /* 170 */ 834, 823, 823, 90, 90, 91, 91, 91, 91, 56, + /* 180 */ 89, 89, 89, 89, 88, 88, 87, 87, 87, 86, + /* 190 */ 339, 315, 1245, 922, 342, 268, 934, 935, 241, 92, + /* 200 */ 93, 83, 819, 819, 831, 834, 823, 823, 90, 90, + /* 210 */ 91, 91, 91, 91, 291, 89, 89, 89, 89, 88, + /* 220 */ 88, 87, 87, 87, 86, 339, 315, 913, 1295, 682, + /* 230 */ 687, 1295, 233, 397, 92, 93, 83, 819, 819, 831, + /* 240 */ 834, 823, 823, 90, 90, 91, 91, 91, 91, 326, + /* 250 */ 89, 89, 89, 89, 88, 88, 87, 87, 87, 86, + /* 260 */ 339, 315, 85, 82, 168, 680, 431, 938, 939, 92, + /* 270 */ 93, 83, 819, 819, 831, 834, 823, 823, 90, 90, + /* 280 */ 91, 91, 91, 91, 291, 89, 89, 89, 89, 88, + /* 290 */ 88, 87, 87, 87, 86, 339, 315, 319, 913, 1296, + /* 300 */ 797, 911, 1296, 681, 92, 93, 83, 819, 819, 831, + /* 310 */ 834, 823, 823, 90, 90, 91, 91, 91, 91, 335, + /* 320 */ 89, 89, 89, 89, 88, 88, 87, 87, 87, 86, + /* 330 */ 339, 315, 876, 876, 373, 85, 82, 168, 944, 92, + /* 340 */ 93, 83, 819, 819, 831, 834, 823, 823, 90, 90, + /* 350 */ 91, 91, 91, 91, 896, 89, 89, 89, 89, 88, + /* 360 */ 88, 87, 87, 87, 86, 339, 315, 370, 307, 973, + /* 370 */ 367, 1, 911, 433, 92, 93, 83, 819, 819, 831, + /* 380 */ 834, 823, 823, 90, 90, 91, 91, 91, 91, 189, + /* 390 */ 89, 89, 89, 89, 88, 88, 87, 87, 87, 86, + /* 400 */ 339, 315, 720, 948, 933, 933, 149, 718, 948, 92, + /* 410 */ 93, 83, 819, 819, 831, 834, 823, 823, 90, 90, + /* 420 */ 91, 91, 91, 91, 434, 89, 89, 89, 89, 88, + /* 430 */ 88, 87, 87, 87, 86, 339, 338, 938, 939, 947, + /* 440 */ 694, 940, 974, 315, 953, 48, 934, 935, 715, 689, + /* 450 */ 71, 92, 93, 83, 819, 819, 831, 834, 823, 823, + /* 460 */ 90, 90, 91, 91, 91, 91, 320, 89, 89, 89, + /* 470 */ 89, 88, 88, 87, 87, 87, 86, 339, 315, 412, + /* 480 */ 403, 820, 820, 832, 835, 74, 92, 81, 83, 819, + /* 490 */ 819, 831, 834, 823, 823, 90, 90, 91, 91, 91, + /* 500 */ 91, 698, 89, 89, 89, 89, 88, 88, 87, 87, + /* 510 */ 87, 86, 339, 315, 259, 654, 655, 656, 393, 111, + /* 520 */ 331, 153, 93, 83, 819, 819, 831, 834, 823, 823, + /* 530 */ 90, 90, 91, 91, 91, 91, 434, 89, 89, 89, + /* 540 */ 89, 88, 88, 87, 87, 87, 86, 339, 315, 188, + /* 550 */ 187, 186, 824, 937, 328, 219, 953, 48, 83, 819, + /* 560 */ 819, 831, 834, 823, 823, 90, 90, 91, 91, 91, + /* 570 */ 91, 956, 89, 89, 89, 89, 88, 88, 87, 87, + /* 580 */ 87, 86, 339, 79, 429, 738, 3, 1174, 955, 348, + /* 590 */ 737, 332, 792, 933, 933, 937, 79, 429, 730, 3, + /* 600 */ 203, 160, 278, 391, 273, 390, 190, 892, 434, 400, + /* 610 */ 741, 76, 77, 271, 287, 253, 353, 242, 78, 340, + /* 620 */ 340, 85, 82, 168, 76, 77, 233, 397, 953, 48, + /* 630 */ 432, 78, 340, 340, 277, 934, 935, 185, 439, 651, + /* 640 */ 388, 385, 384, 432, 234, 276, 107, 418, 349, 337, + /* 650 */ 336, 383, 893, 728, 215, 949, 123, 971, 308, 810, + /* 660 */ 418, 436, 435, 412, 394, 798, 400, 873, 894, 123, + /* 670 */ 721, 872, 810, 889, 436, 435, 215, 949, 798, 351, + /* 680 */ 722, 697, 380, 434, 771, 371, 22, 434, 400, 79, + /* 690 */ 429, 232, 3, 189, 413, 870, 803, 803, 803, 805, + /* 700 */ 18, 54, 148, 953, 48, 956, 113, 953, 9, 803, + /* 710 */ 803, 803, 805, 18, 310, 123, 748, 76, 77, 742, + /* 720 */ 123, 325, 955, 866, 78, 340, 340, 113, 350, 359, + /* 730 */ 85, 82, 168, 343, 960, 960, 432, 770, 412, 414, + /* 740 */ 407, 23, 1240, 1240, 79, 429, 357, 3, 166, 91, + /* 750 */ 91, 91, 91, 418, 89, 89, 89, 89, 88, 88, + /* 760 */ 87, 87, 87, 86, 339, 810, 434, 436, 435, 792, + /* 770 */ 320, 798, 76, 77, 789, 271, 123, 434, 360, 78, + /* 780 */ 340, 340, 864, 85, 82, 168, 953, 9, 395, 743, + /* 790 */ 360, 432, 253, 358, 252, 933, 933, 953, 30, 889, + /* 800 */ 327, 216, 803, 803, 803, 805, 18, 113, 418, 89, + /* 810 */ 89, 89, 89, 88, 88, 87, 87, 87, 86, 339, + /* 820 */ 810, 113, 436, 435, 792, 185, 798, 288, 388, 385, + /* 830 */ 384, 123, 113, 920, 2, 796, 696, 934, 935, 383, + /* 840 */ 69, 429, 434, 3, 218, 110, 738, 253, 358, 252, + /* 850 */ 434, 737, 933, 933, 892, 359, 222, 803, 803, 803, + /* 860 */ 805, 18, 953, 47, 933, 933, 933, 933, 76, 77, + /* 870 */ 953, 9, 366, 904, 217, 78, 340, 340, 677, 305, + /* 880 */ 304, 303, 206, 301, 224, 259, 664, 432, 337, 336, + /* 890 */ 434, 228, 247, 144, 934, 935, 933, 933, 667, 893, + /* 900 */ 324, 1259, 96, 434, 418, 796, 934, 935, 934, 935, + /* 910 */ 953, 48, 401, 148, 289, 894, 810, 417, 436, 435, + /* 920 */ 677, 759, 798, 953, 9, 314, 220, 162, 161, 170, + /* 930 */ 402, 239, 953, 8, 194, 683, 683, 410, 934, 935, + /* 940 */ 238, 959, 933, 933, 225, 408, 945, 365, 957, 212, + /* 950 */ 958, 172, 757, 803, 803, 803, 805, 18, 173, 365, + /* 960 */ 176, 123, 171, 113, 244, 952, 246, 434, 356, 796, + /* 970 */ 372, 365, 236, 960, 960, 810, 290, 804, 191, 165, + /* 980 */ 852, 798, 259, 316, 934, 935, 237, 953, 34, 404, + /* 990 */ 91, 91, 91, 91, 84, 89, 89, 89, 89, 88, + /* 1000 */ 88, 87, 87, 87, 86, 339, 701, 952, 434, 240, + /* 1010 */ 347, 758, 803, 803, 803, 434, 245, 1179, 434, 389, + /* 1020 */ 434, 376, 434, 895, 167, 434, 405, 702, 953, 35, + /* 1030 */ 673, 321, 221, 434, 333, 953, 11, 434, 953, 26, + /* 1040 */ 953, 36, 953, 37, 251, 953, 38, 434, 259, 434, + /* 1050 */ 757, 434, 329, 953, 27, 434, 223, 953, 28, 434, + /* 1060 */ 690, 434, 67, 434, 65, 434, 862, 953, 39, 953, + /* 1070 */ 40, 953, 41, 423, 434, 953, 10, 434, 772, 953, + /* 1080 */ 42, 953, 98, 953, 43, 953, 44, 773, 434, 346, + /* 1090 */ 434, 75, 434, 73, 953, 31, 434, 953, 45, 434, + /* 1100 */ 259, 434, 690, 434, 757, 434, 887, 434, 953, 46, + /* 1110 */ 953, 32, 953, 115, 434, 266, 953, 116, 951, 953, + /* 1120 */ 117, 953, 52, 953, 33, 953, 99, 953, 49, 726, + /* 1130 */ 434, 909, 434, 19, 953, 100, 434, 344, 434, 113, + /* 1140 */ 434, 258, 692, 434, 259, 434, 670, 434, 20, 434, + /* 1150 */ 953, 101, 953, 97, 434, 259, 953, 114, 953, 112, + /* 1160 */ 953, 105, 113, 953, 104, 953, 102, 953, 103, 953, + /* 1170 */ 51, 434, 148, 434, 953, 53, 167, 434, 259, 113, + /* 1180 */ 300, 307, 912, 363, 311, 860, 248, 261, 209, 264, + /* 1190 */ 416, 953, 50, 953, 25, 420, 727, 953, 29, 430, + /* 1200 */ 321, 424, 757, 428, 322, 124, 1269, 214, 165, 710, + /* 1210 */ 859, 908, 806, 794, 309, 158, 193, 361, 254, 723, + /* 1220 */ 364, 67, 381, 269, 735, 199, 67, 70, 113, 700, + /* 1230 */ 699, 707, 708, 884, 113, 766, 113, 855, 193, 883, + /* 1240 */ 199, 869, 869, 675, 868, 868, 109, 368, 255, 260, + /* 1250 */ 263, 280, 859, 265, 806, 974, 267, 711, 695, 272, + /* 1260 */ 764, 282, 795, 284, 150, 744, 755, 415, 292, 293, + /* 1270 */ 802, 678, 672, 661, 660, 662, 927, 6, 306, 386, + /* 1280 */ 352, 786, 243, 250, 886, 362, 163, 286, 419, 298, + /* 1290 */ 930, 159, 968, 196, 126, 903, 901, 965, 55, 58, + /* 1300 */ 323, 275, 857, 136, 147, 694, 856, 121, 65, 354, + /* 1310 */ 355, 379, 175, 61, 151, 369, 180, 871, 375, 129, + /* 1320 */ 257, 756, 210, 181, 145, 131, 132, 377, 262, 663, + /* 1330 */ 133, 134, 139, 783, 791, 182, 392, 183, 312, 330, + /* 1340 */ 714, 888, 713, 851, 692, 195, 712, 406, 686, 705, + /* 1350 */ 313, 685, 64, 839, 274, 72, 684, 334, 942, 95, + /* 1360 */ 752, 279, 281, 704, 753, 751, 422, 283, 411, 750, + /* 1370 */ 426, 66, 204, 409, 21, 285, 928, 669, 437, 205, + /* 1380 */ 207, 208, 438, 658, 657, 652, 118, 108, 119, 226, + /* 1390 */ 650, 341, 157, 235, 169, 345, 106, 734, 790, 296, + /* 1400 */ 294, 295, 120, 297, 867, 865, 127, 128, 130, 724, + /* 1410 */ 229, 174, 249, 882, 137, 230, 138, 135, 885, 231, + /* 1420 */ 59, 60, 177, 881, 7, 178, 12, 179, 256, 874, + /* 1430 */ 140, 193, 962, 374, 141, 152, 666, 378, 276, 184, + /* 1440 */ 270, 122, 142, 382, 387, 62, 13, 14, 703, 63, + /* 1450 */ 125, 317, 318, 227, 809, 808, 837, 732, 15, 164, + /* 1460 */ 736, 4, 765, 211, 399, 213, 192, 143, 760, 70, + /* 1470 */ 67, 16, 17, 838, 836, 891, 841, 890, 198, 197, + /* 1480 */ 917, 154, 421, 923, 918, 155, 200, 977, 425, 840, + /* 1490 */ 156, 201, 807, 676, 80, 302, 299, 977, 202, 1261, + /* 1500 */ 1260, }; static const YYCODETYPE yy_lookahead[] = { - /* 0 */ 19, 144, 145, 146, 147, 24, 1, 2, 27, 80, - /* 10 */ 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, - /* 20 */ 91, 92, 93, 94, 95, 91, 92, 93, 94, 95, - /* 30 */ 19, 50, 51, 80, 81, 82, 83, 212, 85, 86, - /* 40 */ 87, 88, 89, 90, 91, 92, 93, 94, 95, 224, - /* 50 */ 225, 226, 71, 72, 73, 74, 75, 76, 77, 78, - /* 60 */ 79, 80, 81, 82, 83, 95, 85, 86, 87, 88, - /* 70 */ 89, 90, 91, 92, 93, 94, 95, 19, 97, 85, - /* 80 */ 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, - /* 90 */ 66, 33, 27, 28, 27, 28, 22, 201, 152, 152, - /* 100 */ 42, 27, 152, 224, 225, 226, 95, 211, 50, 51, - /* 110 */ 99, 100, 101, 102, 103, 104, 105, 27, 28, 59, - /* 120 */ 174, 175, 243, 112, 174, 175, 66, 94, 95, 71, - /* 130 */ 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, - /* 140 */ 82, 83, 195, 85, 86, 87, 88, 89, 90, 91, - /* 150 */ 92, 93, 94, 95, 19, 209, 89, 90, 173, 209, - /* 160 */ 210, 26, 97, 98, 97, 98, 181, 100, 108, 109, - /* 170 */ 110, 97, 174, 152, 107, 152, 109, 89, 90, 91, - /* 180 */ 92, 93, 94, 95, 163, 50, 51, 97, 98, 99, - /* 190 */ 55, 244, 102, 103, 104, 174, 175, 174, 175, 132, - /* 200 */ 133, 89, 90, 113, 66, 19, 71, 72, 73, 74, - /* 210 */ 75, 76, 77, 78, 79, 80, 81, 82, 83, 198, - /* 220 */ 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, - /* 230 */ 95, 152, 209, 210, 148, 149, 50, 51, 100, 53, - /* 240 */ 154, 59, 156, 22, 132, 133, 119, 120, 163, 163, - /* 250 */ 22, 192, 193, 174, 175, 27, 28, 71, 72, 73, - /* 260 */ 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, - /* 270 */ 174, 85, 86, 87, 88, 89, 90, 91, 92, 93, - /* 280 */ 94, 95, 19, 198, 198, 174, 152, 24, 209, 210, - /* 290 */ 108, 109, 110, 192, 193, 27, 28, 69, 164, 165, - /* 300 */ 79, 119, 120, 27, 28, 27, 99, 222, 152, 102, - /* 310 */ 103, 104, 24, 50, 51, 27, 66, 89, 90, 185, - /* 320 */ 113, 187, 22, 157, 239, 97, 58, 27, 28, 101, - /* 330 */ 174, 175, 246, 163, 71, 72, 73, 74, 75, 76, - /* 340 */ 77, 78, 79, 80, 81, 82, 83, 11, 85, 86, - /* 350 */ 87, 88, 89, 90, 91, 92, 93, 94, 95, 19, - /* 360 */ 132, 133, 134, 23, 152, 97, 98, 91, 198, 119, - /* 370 */ 120, 152, 22, 97, 98, 97, 152, 27, 28, 27, - /* 380 */ 28, 27, 28, 227, 160, 97, 174, 175, 164, 165, - /* 390 */ 50, 51, 222, 174, 175, 59, 230, 97, 98, 233, - /* 400 */ 188, 137, 66, 139, 234, 187, 177, 188, 152, 239, - /* 410 */ 69, 71, 72, 73, 74, 75, 76, 77, 78, 79, - /* 420 */ 80, 81, 82, 83, 12, 85, 86, 87, 88, 89, - /* 430 */ 90, 91, 92, 93, 94, 95, 19, 177, 97, 152, - /* 440 */ 23, 29, 101, 152, 108, 109, 110, 97, 98, 97, - /* 450 */ 98, 97, 98, 224, 225, 226, 22, 45, 24, 47, - /* 460 */ 152, 152, 152, 152, 152, 174, 175, 50, 51, 249, - /* 470 */ 250, 59, 21, 132, 133, 134, 124, 221, 124, 188, - /* 480 */ 171, 172, 171, 172, 224, 225, 226, 152, 71, 72, - /* 490 */ 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, - /* 500 */ 83, 152, 85, 86, 87, 88, 89, 90, 91, 92, - /* 510 */ 93, 94, 95, 19, 152, 183, 65, 23, 170, 171, - /* 520 */ 172, 19, 23, 174, 175, 26, 152, 50, 51, 12, - /* 530 */ 196, 197, 37, 170, 171, 172, 174, 175, 224, 225, - /* 540 */ 226, 232, 208, 232, 50, 51, 29, 52, 174, 175, - /* 550 */ 188, 74, 75, 51, 103, 60, 222, 163, 209, 0, - /* 560 */ 1, 2, 45, 152, 47, 71, 72, 73, 74, 75, - /* 570 */ 76, 77, 78, 79, 80, 81, 82, 83, 101, 85, - /* 580 */ 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, - /* 590 */ 19, 140, 198, 152, 23, 152, 22, 98, 24, 152, - /* 600 */ 152, 27, 152, 183, 152, 152, 111, 213, 214, 107, - /* 610 */ 152, 164, 165, 152, 112, 174, 175, 174, 175, 181, - /* 620 */ 182, 50, 51, 124, 174, 175, 174, 175, 190, 26, - /* 630 */ 22, 23, 22, 23, 26, 166, 26, 168, 169, 16, - /* 640 */ 16, 19, 71, 72, 73, 74, 75, 76, 77, 78, - /* 650 */ 79, 80, 81, 82, 83, 152, 85, 86, 87, 88, - /* 660 */ 89, 90, 91, 92, 93, 94, 95, 152, 220, 196, - /* 670 */ 197, 97, 50, 51, 108, 109, 110, 152, 70, 221, - /* 680 */ 70, 208, 7, 8, 9, 62, 62, 64, 64, 174, - /* 690 */ 175, 146, 147, 71, 72, 73, 74, 75, 76, 77, - /* 700 */ 78, 79, 80, 81, 82, 83, 152, 85, 86, 87, - /* 710 */ 88, 89, 90, 91, 92, 93, 94, 95, 19, 152, - /* 720 */ 195, 152, 31, 220, 152, 152, 35, 26, 174, 175, - /* 730 */ 152, 163, 152, 130, 152, 115, 152, 117, 118, 152, - /* 740 */ 49, 174, 175, 174, 175, 152, 174, 175, 26, 50, - /* 750 */ 51, 152, 174, 175, 174, 175, 174, 175, 174, 175, - /* 760 */ 138, 174, 175, 140, 22, 23, 198, 174, 175, 152, - /* 770 */ 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, - /* 780 */ 81, 82, 83, 152, 85, 86, 87, 88, 89, 90, - /* 790 */ 91, 92, 93, 94, 95, 19, 152, 152, 152, 98, - /* 800 */ 24, 152, 108, 109, 110, 174, 175, 152, 152, 152, - /* 810 */ 152, 152, 70, 152, 213, 214, 152, 152, 174, 175, - /* 820 */ 174, 175, 152, 174, 175, 124, 50, 51, 106, 174, - /* 830 */ 175, 174, 175, 174, 175, 174, 175, 138, 174, 175, - /* 840 */ 152, 22, 23, 22, 163, 189, 19, 71, 72, 73, - /* 850 */ 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, - /* 860 */ 152, 85, 86, 87, 88, 89, 90, 91, 92, 93, - /* 870 */ 94, 95, 152, 152, 168, 169, 152, 50, 51, 198, - /* 880 */ 197, 152, 174, 175, 152, 240, 152, 152, 152, 70, - /* 890 */ 152, 208, 152, 152, 174, 175, 152, 19, 71, 72, - /* 900 */ 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, - /* 910 */ 83, 152, 85, 86, 87, 88, 89, 90, 91, 92, - /* 920 */ 93, 94, 95, 152, 195, 247, 248, 152, 50, 51, - /* 930 */ 195, 195, 152, 174, 175, 195, 195, 26, 152, 195, - /* 940 */ 252, 220, 163, 122, 152, 174, 175, 163, 19, 71, - /* 950 */ 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, - /* 960 */ 82, 83, 152, 85, 86, 87, 88, 89, 90, 91, - /* 970 */ 92, 93, 94, 95, 152, 195, 252, 198, 240, 50, - /* 980 */ 51, 189, 198, 19, 174, 175, 19, 51, 23, 100, - /* 990 */ 101, 26, 28, 163, 163, 28, 174, 175, 163, 19, - /* 1000 */ 152, 72, 73, 74, 75, 76, 77, 78, 79, 80, - /* 1010 */ 81, 82, 83, 152, 85, 86, 87, 88, 89, 90, - /* 1020 */ 91, 92, 93, 94, 95, 152, 240, 152, 198, 198, - /* 1030 */ 50, 51, 33, 198, 123, 174, 175, 116, 7, 8, - /* 1040 */ 101, 42, 121, 107, 152, 152, 23, 174, 175, 26, - /* 1050 */ 152, 112, 183, 73, 74, 75, 76, 77, 78, 79, - /* 1060 */ 80, 81, 82, 83, 152, 85, 86, 87, 88, 89, - /* 1070 */ 90, 91, 92, 93, 94, 95, 19, 20, 23, 22, - /* 1080 */ 23, 26, 152, 152, 27, 28, 174, 175, 23, 19, - /* 1090 */ 20, 26, 22, 132, 133, 38, 152, 27, 28, 152, - /* 1100 */ 23, 215, 152, 26, 174, 175, 152, 27, 38, 152, - /* 1110 */ 23, 152, 27, 26, 57, 152, 23, 163, 152, 26, - /* 1120 */ 152, 174, 175, 66, 174, 175, 69, 57, 174, 175, - /* 1130 */ 27, 174, 175, 174, 175, 152, 66, 174, 175, 69, - /* 1140 */ 174, 175, 174, 175, 152, 23, 89, 90, 26, 91, - /* 1150 */ 152, 236, 198, 96, 97, 98, 132, 133, 101, 89, - /* 1160 */ 90, 152, 23, 209, 210, 26, 96, 97, 98, 152, - /* 1170 */ 212, 101, 174, 175, 116, 19, 20, 97, 22, 121, - /* 1180 */ 152, 193, 97, 27, 28, 152, 152, 152, 152, 132, - /* 1190 */ 133, 134, 135, 136, 38, 23, 152, 152, 26, 152, - /* 1200 */ 97, 152, 132, 133, 134, 135, 136, 235, 152, 212, - /* 1210 */ 199, 150, 212, 57, 212, 200, 203, 216, 241, 216, - /* 1220 */ 241, 203, 182, 19, 20, 69, 22, 186, 178, 177, - /* 1230 */ 216, 27, 28, 229, 202, 39, 177, 177, 200, 155, - /* 1240 */ 245, 122, 38, 41, 22, 89, 90, 91, 159, 159, - /* 1250 */ 242, 159, 96, 97, 98, 71, 130, 101, 242, 191, - /* 1260 */ 223, 57, 18, 194, 159, 203, 194, 194, 194, 18, - /* 1270 */ 158, 223, 191, 69, 203, 159, 158, 19, 20, 191, - /* 1280 */ 22, 203, 137, 46, 238, 27, 28, 159, 132, 133, - /* 1290 */ 134, 135, 136, 89, 90, 159, 38, 22, 158, 179, - /* 1300 */ 96, 97, 98, 237, 158, 101, 159, 159, 158, 179, - /* 1310 */ 107, 176, 48, 176, 176, 57, 184, 106, 176, 125, - /* 1320 */ 179, 178, 218, 107, 217, 176, 218, 69, 184, 176, - /* 1330 */ 176, 217, 159, 218, 217, 159, 132, 133, 134, 135, - /* 1340 */ 136, 218, 217, 137, 179, 95, 179, 89, 90, 228, - /* 1350 */ 129, 126, 128, 127, 96, 97, 98, 206, 231, 101, - /* 1360 */ 5, 25, 231, 205, 207, 10, 11, 12, 13, 14, - /* 1370 */ 204, 203, 17, 26, 162, 161, 13, 6, 180, 180, - /* 1380 */ 153, 153, 151, 151, 151, 151, 167, 32, 4, 34, - /* 1390 */ 132, 133, 134, 135, 136, 3, 22, 142, 43, 68, - /* 1400 */ 15, 23, 16, 251, 23, 120, 251, 248, 131, 111, - /* 1410 */ 123, 56, 16, 20, 125, 1, 123, 131, 63, 79, - /* 1420 */ 79, 66, 67, 111, 36, 28, 122, 1, 5, 22, - /* 1430 */ 107, 140, 26, 54, 54, 44, 61, 107, 20, 24, - /* 1440 */ 19, 112, 105, 53, 22, 40, 22, 22, 53, 30, - /* 1450 */ 23, 22, 22, 53, 23, 23, 23, 22, 116, 23, - /* 1460 */ 22, 122, 23, 26, 23, 22, 11, 124, 28, 114, - /* 1470 */ 36, 26, 26, 23, 23, 23, 122, 23, 36, 26, - /* 1480 */ 36, 22, 24, 23, 22, 1, 23, 26, 22, 24, - /* 1490 */ 23, 22, 122, 23, 23, 22, 141, 23, 122, 122, - /* 1500 */ 15, + /* 0 */ 19, 95, 53, 97, 22, 24, 24, 101, 27, 28, + /* 10 */ 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + /* 20 */ 39, 40, 41, 152, 43, 44, 45, 46, 47, 48, + /* 30 */ 49, 50, 51, 52, 53, 19, 55, 55, 132, 133, + /* 40 */ 134, 1, 2, 27, 28, 29, 30, 31, 32, 33, + /* 50 */ 34, 35, 36, 37, 38, 39, 40, 41, 92, 43, + /* 60 */ 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + /* 70 */ 47, 48, 49, 50, 51, 52, 53, 61, 97, 97, + /* 80 */ 19, 49, 50, 51, 52, 53, 70, 26, 27, 28, + /* 90 */ 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + /* 100 */ 39, 40, 41, 152, 43, 44, 45, 46, 47, 48, + /* 110 */ 49, 50, 51, 52, 53, 144, 145, 146, 147, 19, + /* 120 */ 249, 22, 172, 172, 173, 52, 53, 27, 28, 29, + /* 130 */ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + /* 140 */ 40, 41, 81, 43, 44, 45, 46, 47, 48, 49, + /* 150 */ 50, 51, 52, 53, 55, 56, 19, 152, 207, 208, + /* 160 */ 115, 24, 117, 118, 27, 28, 29, 30, 31, 32, + /* 170 */ 33, 34, 35, 36, 37, 38, 39, 40, 41, 79, + /* 180 */ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, + /* 190 */ 53, 19, 0, 1, 2, 23, 97, 98, 193, 27, + /* 200 */ 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + /* 210 */ 38, 39, 40, 41, 152, 43, 44, 45, 46, 47, + /* 220 */ 48, 49, 50, 51, 52, 53, 19, 22, 23, 172, + /* 230 */ 23, 26, 119, 120, 27, 28, 29, 30, 31, 32, + /* 240 */ 33, 34, 35, 36, 37, 38, 39, 40, 41, 187, + /* 250 */ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, + /* 260 */ 53, 19, 221, 222, 223, 23, 168, 169, 170, 27, + /* 270 */ 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + /* 280 */ 38, 39, 40, 41, 152, 43, 44, 45, 46, 47, + /* 290 */ 48, 49, 50, 51, 52, 53, 19, 157, 22, 23, + /* 300 */ 23, 96, 26, 172, 27, 28, 29, 30, 31, 32, + /* 310 */ 33, 34, 35, 36, 37, 38, 39, 40, 41, 187, + /* 320 */ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, + /* 330 */ 53, 19, 108, 109, 110, 221, 222, 223, 185, 27, + /* 340 */ 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + /* 350 */ 38, 39, 40, 41, 240, 43, 44, 45, 46, 47, + /* 360 */ 48, 49, 50, 51, 52, 53, 19, 227, 22, 23, + /* 370 */ 230, 22, 96, 152, 27, 28, 29, 30, 31, 32, + /* 380 */ 33, 34, 35, 36, 37, 38, 39, 40, 41, 30, + /* 390 */ 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, + /* 400 */ 53, 19, 190, 191, 55, 56, 24, 190, 191, 27, + /* 410 */ 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + /* 420 */ 38, 39, 40, 41, 152, 43, 44, 45, 46, 47, + /* 430 */ 48, 49, 50, 51, 52, 53, 168, 169, 170, 179, + /* 440 */ 180, 171, 96, 19, 172, 173, 97, 98, 188, 179, + /* 450 */ 138, 27, 28, 29, 30, 31, 32, 33, 34, 35, + /* 460 */ 36, 37, 38, 39, 40, 41, 107, 43, 44, 45, + /* 470 */ 46, 47, 48, 49, 50, 51, 52, 53, 19, 207, + /* 480 */ 208, 30, 31, 32, 33, 138, 27, 28, 29, 30, + /* 490 */ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + /* 500 */ 41, 181, 43, 44, 45, 46, 47, 48, 49, 50, + /* 510 */ 51, 52, 53, 19, 152, 7, 8, 9, 49, 22, + /* 520 */ 19, 24, 28, 29, 30, 31, 32, 33, 34, 35, + /* 530 */ 36, 37, 38, 39, 40, 41, 152, 43, 44, 45, + /* 540 */ 46, 47, 48, 49, 50, 51, 52, 53, 19, 108, + /* 550 */ 109, 110, 101, 55, 53, 193, 172, 173, 29, 30, + /* 560 */ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + /* 570 */ 41, 152, 43, 44, 45, 46, 47, 48, 49, 50, + /* 580 */ 51, 52, 53, 19, 20, 116, 22, 23, 169, 170, + /* 590 */ 121, 207, 85, 55, 56, 97, 19, 20, 195, 22, + /* 600 */ 99, 100, 101, 102, 103, 104, 105, 12, 152, 206, + /* 610 */ 210, 47, 48, 112, 152, 108, 109, 110, 54, 55, + /* 620 */ 56, 221, 222, 223, 47, 48, 119, 120, 172, 173, + /* 630 */ 66, 54, 55, 56, 101, 97, 98, 99, 148, 149, + /* 640 */ 102, 103, 104, 66, 154, 112, 156, 83, 229, 47, + /* 650 */ 48, 113, 57, 163, 194, 195, 92, 246, 247, 95, + /* 660 */ 83, 97, 98, 207, 208, 101, 206, 59, 73, 92, + /* 670 */ 75, 63, 95, 163, 97, 98, 194, 195, 101, 219, + /* 680 */ 85, 181, 19, 152, 175, 77, 196, 152, 206, 19, + /* 690 */ 20, 199, 22, 30, 163, 11, 132, 133, 134, 135, + /* 700 */ 136, 209, 152, 172, 173, 152, 196, 172, 173, 132, + /* 710 */ 133, 134, 135, 136, 164, 92, 213, 47, 48, 49, + /* 720 */ 92, 186, 169, 170, 54, 55, 56, 196, 100, 219, + /* 730 */ 221, 222, 223, 243, 132, 133, 66, 175, 207, 208, + /* 740 */ 152, 231, 119, 120, 19, 20, 236, 22, 152, 38, + /* 750 */ 39, 40, 41, 83, 43, 44, 45, 46, 47, 48, + /* 760 */ 49, 50, 51, 52, 53, 95, 152, 97, 98, 85, + /* 770 */ 107, 101, 47, 48, 163, 112, 92, 152, 152, 54, + /* 780 */ 55, 56, 229, 221, 222, 223, 172, 173, 163, 49, + /* 790 */ 152, 66, 108, 109, 110, 55, 56, 172, 173, 163, + /* 800 */ 186, 22, 132, 133, 134, 135, 136, 196, 83, 43, + /* 810 */ 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + /* 820 */ 95, 196, 97, 98, 85, 99, 101, 152, 102, 103, + /* 830 */ 104, 92, 196, 146, 147, 152, 181, 97, 98, 113, + /* 840 */ 19, 20, 152, 22, 218, 22, 116, 108, 109, 110, + /* 850 */ 152, 121, 55, 56, 12, 219, 218, 132, 133, 134, + /* 860 */ 135, 136, 172, 173, 55, 56, 55, 56, 47, 48, + /* 870 */ 172, 173, 236, 152, 5, 54, 55, 56, 55, 10, + /* 880 */ 11, 12, 13, 14, 186, 152, 17, 66, 47, 48, + /* 890 */ 152, 210, 16, 84, 97, 98, 55, 56, 21, 57, + /* 900 */ 217, 122, 22, 152, 83, 152, 97, 98, 97, 98, + /* 910 */ 172, 173, 152, 152, 224, 73, 95, 75, 97, 98, + /* 920 */ 97, 124, 101, 172, 173, 164, 193, 47, 48, 60, + /* 930 */ 163, 62, 172, 173, 24, 55, 56, 186, 97, 98, + /* 940 */ 71, 100, 55, 56, 183, 207, 185, 152, 107, 23, + /* 950 */ 109, 82, 26, 132, 133, 134, 135, 136, 89, 152, + /* 960 */ 26, 92, 93, 196, 88, 55, 90, 152, 91, 152, + /* 970 */ 217, 152, 152, 132, 133, 95, 152, 97, 211, 212, + /* 980 */ 103, 101, 152, 114, 97, 98, 152, 172, 173, 19, + /* 990 */ 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + /* 1000 */ 48, 49, 50, 51, 52, 53, 65, 97, 152, 152, + /* 1010 */ 141, 124, 132, 133, 134, 152, 140, 140, 152, 78, + /* 1020 */ 152, 233, 152, 193, 98, 152, 56, 86, 172, 173, + /* 1030 */ 166, 167, 237, 152, 217, 172, 173, 152, 172, 173, + /* 1040 */ 172, 173, 172, 173, 237, 172, 173, 152, 152, 152, + /* 1050 */ 124, 152, 111, 172, 173, 152, 237, 172, 173, 152, + /* 1060 */ 55, 152, 26, 152, 130, 152, 152, 172, 173, 172, + /* 1070 */ 173, 172, 173, 249, 152, 172, 173, 152, 61, 172, + /* 1080 */ 173, 172, 173, 172, 173, 172, 173, 70, 152, 193, + /* 1090 */ 152, 137, 152, 139, 172, 173, 152, 172, 173, 152, + /* 1100 */ 152, 152, 97, 152, 26, 152, 163, 152, 172, 173, + /* 1110 */ 172, 173, 172, 173, 152, 16, 172, 173, 26, 172, + /* 1120 */ 173, 172, 173, 172, 173, 172, 173, 172, 173, 163, + /* 1130 */ 152, 152, 152, 22, 172, 173, 152, 241, 152, 196, + /* 1140 */ 152, 193, 106, 152, 152, 152, 163, 152, 37, 152, + /* 1150 */ 172, 173, 172, 173, 152, 152, 172, 173, 172, 173, + /* 1160 */ 172, 173, 196, 172, 173, 172, 173, 172, 173, 172, + /* 1170 */ 173, 152, 152, 152, 172, 173, 98, 152, 152, 196, + /* 1180 */ 160, 22, 23, 19, 164, 193, 152, 88, 232, 90, + /* 1190 */ 191, 172, 173, 172, 173, 163, 193, 172, 173, 166, + /* 1200 */ 167, 163, 124, 163, 244, 245, 23, 211, 212, 26, + /* 1210 */ 55, 23, 55, 23, 26, 123, 26, 152, 23, 193, + /* 1220 */ 56, 26, 23, 23, 23, 26, 26, 26, 196, 100, + /* 1230 */ 101, 7, 8, 152, 196, 23, 196, 23, 26, 152, + /* 1240 */ 26, 132, 133, 23, 132, 133, 26, 152, 152, 152, + /* 1250 */ 152, 210, 97, 152, 97, 96, 152, 152, 152, 152, + /* 1260 */ 152, 210, 152, 210, 197, 152, 152, 152, 152, 152, + /* 1270 */ 152, 152, 152, 152, 152, 152, 152, 198, 150, 176, + /* 1280 */ 214, 201, 214, 238, 201, 238, 184, 214, 226, 200, + /* 1290 */ 155, 198, 67, 122, 242, 159, 159, 69, 239, 239, + /* 1300 */ 159, 175, 175, 22, 220, 180, 175, 27, 130, 18, + /* 1310 */ 159, 18, 158, 137, 220, 159, 158, 235, 74, 189, + /* 1320 */ 234, 159, 159, 158, 22, 192, 192, 177, 159, 159, + /* 1330 */ 192, 192, 189, 201, 189, 158, 107, 158, 177, 76, + /* 1340 */ 174, 201, 174, 201, 106, 159, 174, 125, 174, 182, + /* 1350 */ 177, 176, 107, 159, 174, 137, 174, 53, 174, 129, + /* 1360 */ 216, 215, 215, 182, 216, 216, 177, 215, 126, 216, + /* 1370 */ 177, 128, 25, 127, 26, 215, 13, 162, 161, 153, + /* 1380 */ 153, 6, 151, 151, 151, 151, 165, 178, 165, 178, + /* 1390 */ 4, 3, 22, 142, 15, 94, 16, 205, 120, 202, + /* 1400 */ 204, 203, 165, 201, 23, 23, 131, 111, 123, 20, + /* 1410 */ 225, 125, 16, 1, 131, 228, 111, 123, 56, 228, + /* 1420 */ 37, 37, 64, 1, 5, 122, 22, 107, 140, 80, + /* 1430 */ 80, 26, 87, 72, 107, 24, 20, 19, 112, 105, + /* 1440 */ 23, 68, 22, 79, 79, 22, 22, 22, 58, 22, + /* 1450 */ 245, 248, 248, 79, 23, 23, 23, 116, 22, 122, + /* 1460 */ 23, 22, 56, 23, 26, 23, 64, 22, 124, 26, + /* 1470 */ 26, 64, 64, 23, 23, 23, 11, 23, 22, 26, + /* 1480 */ 23, 22, 24, 1, 23, 22, 26, 250, 24, 23, + /* 1490 */ 22, 122, 23, 23, 22, 15, 23, 250, 122, 122, + /* 1500 */ 122, }; -#define YY_SHIFT_USE_DFLT (-72) -#define YY_SHIFT_COUNT (435) -#define YY_SHIFT_MIN (-71) -#define YY_SHIFT_MAX (1485) +#define YY_SHIFT_USE_DFLT (-95) +#define YY_SHIFT_COUNT (439) +#define YY_SHIFT_MIN (-94) +#define YY_SHIFT_MAX (1482) static const short yy_shift_ofst[] = { - /* 0 */ 5, 1057, 1355, 1070, 1204, 1204, 1204, 90, 60, -19, - /* 10 */ 58, 58, 186, 1204, 1204, 1204, 1204, 1204, 1204, 1204, - /* 20 */ 67, 67, 182, 336, 65, 250, 135, 263, 340, 417, - /* 30 */ 494, 571, 622, 699, 776, 827, 827, 827, 827, 827, - /* 40 */ 827, 827, 827, 827, 827, 827, 827, 827, 827, 827, - /* 50 */ 878, 827, 929, 980, 980, 1156, 1204, 1204, 1204, 1204, - /* 60 */ 1204, 1204, 1204, 1204, 1204, 1204, 1204, 1204, 1204, 1204, - /* 70 */ 1204, 1204, 1204, 1204, 1204, 1204, 1204, 1204, 1204, 1204, - /* 80 */ 1204, 1204, 1204, 1204, 1258, 1204, 1204, 1204, 1204, 1204, - /* 90 */ 1204, 1204, 1204, 1204, 1204, 1204, 1204, 1204, -71, -47, - /* 100 */ -47, -47, -47, -47, -6, 88, -66, 65, 65, 451, - /* 110 */ 502, 112, 112, 33, 127, 278, -30, -72, -72, -72, - /* 120 */ 11, 412, 412, 268, 608, 610, 65, 65, 65, 65, - /* 130 */ 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, - /* 140 */ 65, 65, 65, 65, 65, 559, 138, 278, 127, 24, - /* 150 */ 24, 24, 24, 24, 24, -72, -72, -72, 228, 341, - /* 160 */ 341, 207, 276, 300, 352, 354, 350, 65, 65, 65, - /* 170 */ 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, - /* 180 */ 65, 65, 65, 65, 495, 495, 495, 65, 65, 499, - /* 190 */ 65, 65, 65, 574, 65, 65, 517, 65, 65, 65, - /* 200 */ 65, 65, 65, 65, 65, 65, 65, 566, 691, 288, - /* 210 */ 288, 288, 701, 620, 1058, 675, 603, 964, 964, 967, - /* 220 */ 603, 967, 722, 965, 936, 999, 964, 264, 999, 999, - /* 230 */ 911, 921, 434, 1196, 1119, 1119, 1202, 1202, 1119, 1222, - /* 240 */ 1184, 1126, 1244, 1244, 1244, 1244, 1119, 1251, 1126, 1222, - /* 250 */ 1184, 1184, 1126, 1119, 1251, 1145, 1237, 1119, 1119, 1251, - /* 260 */ 1275, 1119, 1251, 1119, 1251, 1275, 1203, 1203, 1203, 1264, - /* 270 */ 1275, 1203, 1211, 1203, 1264, 1203, 1203, 1194, 1216, 1194, - /* 280 */ 1216, 1194, 1216, 1194, 1216, 1119, 1119, 1206, 1275, 1250, - /* 290 */ 1250, 1275, 1221, 1225, 1224, 1226, 1126, 1336, 1347, 1363, - /* 300 */ 1363, 1371, 1371, 1371, 1371, -72, -72, -72, -72, -72, - /* 310 */ -72, 477, 623, 742, 819, 624, 694, 74, 1023, 221, - /* 320 */ 1055, 1065, 1077, 1087, 1080, 889, 1031, 939, 1093, 1122, - /* 330 */ 1085, 1139, 961, 1024, 1172, 1103, 821, 1384, 1392, 1374, - /* 340 */ 1255, 1385, 1331, 1386, 1378, 1381, 1285, 1277, 1298, 1287, - /* 350 */ 1393, 1289, 1396, 1414, 1293, 1286, 1340, 1341, 1312, 1397, - /* 360 */ 1388, 1304, 1426, 1423, 1407, 1323, 1291, 1379, 1406, 1380, - /* 370 */ 1375, 1391, 1330, 1415, 1418, 1421, 1329, 1337, 1422, 1390, - /* 380 */ 1424, 1425, 1427, 1429, 1395, 1419, 1430, 1400, 1405, 1431, - /* 390 */ 1432, 1433, 1342, 1435, 1436, 1438, 1437, 1339, 1439, 1441, - /* 400 */ 1440, 1434, 1443, 1343, 1445, 1442, 1446, 1444, 1445, 1450, - /* 410 */ 1451, 1452, 1453, 1454, 1459, 1455, 1460, 1462, 1458, 1461, - /* 420 */ 1463, 1466, 1465, 1461, 1467, 1469, 1470, 1471, 1473, 1354, - /* 430 */ 1370, 1376, 1377, 1474, 1485, 1484, + /* 0 */ 40, 564, 869, 577, 725, 725, 725, 739, -19, 16, + /* 10 */ 16, 100, 725, 725, 725, 725, 725, 725, 725, 841, + /* 20 */ 841, 538, 507, 684, 623, 61, 137, 172, 207, 242, + /* 30 */ 277, 312, 347, 382, 424, 424, 424, 424, 424, 424, + /* 40 */ 424, 424, 424, 424, 424, 424, 424, 424, 424, 459, + /* 50 */ 424, 494, 529, 529, 670, 725, 725, 725, 725, 725, + /* 60 */ 725, 725, 725, 725, 725, 725, 725, 725, 725, 725, + /* 70 */ 725, 725, 725, 725, 725, 725, 725, 725, 725, 725, + /* 80 */ 725, 725, 725, 821, 725, 725, 725, 725, 725, 725, + /* 90 */ 725, 725, 725, 725, 725, 725, 725, 952, 711, 711, + /* 100 */ 711, 711, 711, 766, 23, 32, 811, 877, 663, 602, + /* 110 */ 602, 811, 73, 113, -51, -95, -95, -95, 501, 501, + /* 120 */ 501, 595, 595, 809, 205, 276, 811, 811, 811, 811, + /* 130 */ 811, 811, 811, 811, 811, 811, 811, 811, 811, 811, + /* 140 */ 811, 811, 811, 811, 811, 811, 192, 628, 498, 498, + /* 150 */ 113, -34, -34, -34, -34, -34, -34, -95, -95, -95, + /* 160 */ 880, -94, -94, 726, 740, 99, 797, 887, 349, 811, + /* 170 */ 811, 811, 811, 811, 811, 811, 811, 811, 811, 811, + /* 180 */ 811, 811, 811, 811, 811, 811, 941, 941, 941, 811, + /* 190 */ 811, 926, 811, 811, 811, -18, 811, 811, 842, 811, + /* 200 */ 811, 811, 811, 811, 811, 811, 811, 811, 811, 224, + /* 210 */ 608, 910, 910, 910, 1078, 45, 469, 508, 934, 970, + /* 220 */ 970, 1164, 934, 1164, 1036, 1183, 359, 1017, 970, 954, + /* 230 */ 1017, 1017, 1092, 730, 497, 1225, 1171, 1171, 1228, 1228, + /* 240 */ 1171, 1281, 1280, 1178, 1291, 1291, 1291, 1291, 1171, 1293, + /* 250 */ 1178, 1281, 1280, 1280, 1178, 1171, 1293, 1176, 1244, 1171, + /* 260 */ 1171, 1293, 1302, 1171, 1293, 1171, 1293, 1302, 1229, 1229, + /* 270 */ 1229, 1263, 1302, 1229, 1238, 1229, 1263, 1229, 1229, 1222, + /* 280 */ 1245, 1222, 1245, 1222, 1245, 1222, 1245, 1171, 1171, 1218, + /* 290 */ 1302, 1304, 1304, 1302, 1230, 1242, 1243, 1246, 1178, 1347, + /* 300 */ 1348, 1363, 1363, 1375, 1375, 1375, 1375, -95, -95, -95, + /* 310 */ -95, -95, -95, -95, -95, 451, 876, 346, 1159, 1099, + /* 320 */ 441, 823, 1188, 1111, 1190, 1195, 1199, 1200, 1005, 1129, + /* 330 */ 1224, 533, 1201, 1212, 1155, 1214, 1109, 1112, 1220, 1157, + /* 340 */ 779, 1386, 1388, 1370, 1251, 1379, 1301, 1380, 1381, 1382, + /* 350 */ 1278, 1275, 1296, 1285, 1389, 1286, 1396, 1412, 1294, 1283, + /* 360 */ 1383, 1384, 1305, 1362, 1358, 1303, 1422, 1419, 1404, 1320, + /* 370 */ 1288, 1349, 1405, 1350, 1345, 1361, 1327, 1411, 1416, 1418, + /* 380 */ 1326, 1334, 1420, 1364, 1423, 1424, 1417, 1425, 1365, 1390, + /* 390 */ 1427, 1374, 1373, 1431, 1432, 1433, 1341, 1436, 1437, 1439, + /* 400 */ 1438, 1337, 1440, 1442, 1406, 1402, 1445, 1344, 1443, 1407, + /* 410 */ 1444, 1408, 1443, 1450, 1451, 1452, 1453, 1454, 1456, 1465, + /* 420 */ 1457, 1459, 1458, 1460, 1461, 1463, 1464, 1460, 1466, 1468, + /* 430 */ 1469, 1470, 1472, 1369, 1376, 1377, 1378, 1473, 1480, 1482, }; -#define YY_REDUCE_USE_DFLT (-176) -#define YY_REDUCE_COUNT (310) -#define YY_REDUCE_MIN (-175) -#define YY_REDUCE_MAX (1234) +#define YY_REDUCE_USE_DFLT (-130) +#define YY_REDUCE_COUNT (314) +#define YY_REDUCE_MIN (-129) +#define YY_REDUCE_MAX (1237) static const short yy_reduce_ofst[] = { - /* 0 */ -143, 954, 86, 21, -50, 23, 79, 134, 170, -175, - /* 10 */ 229, 260, -121, 212, 219, 291, -54, 349, 362, 156, - /* 20 */ 309, 311, 334, 85, 224, 394, 314, 314, 314, 314, - /* 30 */ 314, 314, 314, 314, 314, 314, 314, 314, 314, 314, - /* 40 */ 314, 314, 314, 314, 314, 314, 314, 314, 314, 314, - /* 50 */ 314, 314, 314, 314, 314, 374, 441, 443, 450, 452, - /* 60 */ 515, 554, 567, 569, 572, 578, 580, 582, 584, 587, - /* 70 */ 593, 631, 644, 646, 649, 655, 657, 659, 661, 664, - /* 80 */ 708, 720, 759, 771, 810, 822, 861, 873, 912, 930, - /* 90 */ 947, 950, 957, 959, 963, 966, 968, 998, 314, 314, - /* 100 */ 314, 314, 314, 314, 314, 314, 314, 447, -53, 166, - /* 110 */ 438, 348, 363, 314, 473, 469, 314, 314, 314, 314, - /* 120 */ -15, 59, 101, 688, 220, 220, 525, 256, 729, 735, - /* 130 */ 736, 740, 741, 744, 645, 448, 738, 458, 786, 503, - /* 140 */ 780, 656, 721, 724, 792, 545, 568, 706, 683, 681, - /* 150 */ 779, 784, 830, 831, 835, 678, 601, -104, -2, 96, - /* 160 */ 111, 218, 287, 308, 310, 312, 335, 411, 453, 461, - /* 170 */ 573, 599, 617, 658, 665, 670, 732, 734, 775, 848, - /* 180 */ 875, 892, 893, 898, 332, 420, 869, 931, 944, 886, - /* 190 */ 983, 992, 1009, 958, 1017, 1028, 988, 1033, 1034, 1035, - /* 200 */ 287, 1036, 1044, 1045, 1047, 1049, 1056, 915, 972, 997, - /* 210 */ 1000, 1002, 886, 1011, 1015, 1061, 1013, 1001, 1003, 977, - /* 220 */ 1018, 979, 1050, 1041, 1040, 1052, 1014, 1004, 1059, 1060, - /* 230 */ 1032, 1038, 1084, 995, 1089, 1090, 1008, 1016, 1092, 1037, - /* 240 */ 1068, 1062, 1069, 1072, 1073, 1074, 1105, 1112, 1071, 1048, - /* 250 */ 1081, 1088, 1078, 1116, 1118, 1046, 1066, 1128, 1136, 1140, - /* 260 */ 1120, 1147, 1146, 1148, 1150, 1130, 1135, 1137, 1138, 1132, - /* 270 */ 1141, 1142, 1143, 1149, 1144, 1153, 1154, 1104, 1107, 1108, - /* 280 */ 1114, 1115, 1117, 1123, 1125, 1173, 1176, 1121, 1165, 1127, - /* 290 */ 1131, 1167, 1157, 1151, 1158, 1166, 1168, 1212, 1214, 1227, - /* 300 */ 1228, 1231, 1232, 1233, 1234, 1152, 1155, 1159, 1198, 1199, - /* 310 */ 1219, + /* 0 */ -29, 531, 490, 625, -49, 272, 456, 510, 400, 509, + /* 10 */ 562, 114, 535, 614, 698, 384, 738, 751, 690, 419, + /* 20 */ 553, 761, 460, 636, 767, 41, 41, 41, 41, 41, + /* 30 */ 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, + /* 40 */ 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, + /* 50 */ 41, 41, 41, 41, 760, 815, 856, 863, 866, 868, + /* 60 */ 870, 873, 881, 885, 895, 897, 899, 903, 907, 909, + /* 70 */ 911, 913, 922, 925, 936, 938, 940, 944, 947, 949, + /* 80 */ 951, 953, 955, 962, 978, 980, 984, 986, 988, 991, + /* 90 */ 993, 995, 997, 1002, 1019, 1021, 1025, 41, 41, 41, + /* 100 */ 41, 41, 41, 41, 41, 41, 896, 140, 260, 98, + /* 110 */ 268, 1020, 41, 482, 41, 41, 41, 41, 270, 270, + /* 120 */ 270, 212, 217, -129, 411, 411, 550, 5, 626, 362, + /* 130 */ 733, 830, 992, 1003, 1026, 795, 683, 807, 638, 819, + /* 140 */ 753, 948, 62, 817, 824, 132, 687, 611, 864, 1033, + /* 150 */ 403, 943, 966, 983, 1032, 1038, 1040, 960, 996, 492, + /* 160 */ -50, 57, 131, 153, 221, 462, 588, 596, 675, 721, + /* 170 */ 820, 834, 857, 914, 979, 1034, 1065, 1081, 1087, 1095, + /* 180 */ 1096, 1097, 1098, 1101, 1104, 1105, 320, 500, 655, 1106, + /* 190 */ 1107, 503, 1108, 1110, 1113, 681, 1114, 1115, 999, 1116, + /* 200 */ 1117, 1118, 221, 1119, 1120, 1121, 1122, 1123, 1124, 788, + /* 210 */ 956, 1041, 1051, 1053, 503, 1067, 1079, 1128, 1080, 1066, + /* 220 */ 1068, 1045, 1083, 1047, 1103, 1102, 1125, 1126, 1073, 1062, + /* 230 */ 1127, 1131, 1089, 1093, 1135, 1052, 1136, 1137, 1059, 1060, + /* 240 */ 1141, 1084, 1130, 1132, 1133, 1134, 1138, 1139, 1151, 1154, + /* 250 */ 1140, 1094, 1143, 1145, 1142, 1156, 1158, 1082, 1086, 1162, + /* 260 */ 1163, 1165, 1150, 1169, 1177, 1170, 1179, 1161, 1166, 1168, + /* 270 */ 1172, 1167, 1173, 1174, 1175, 1180, 1181, 1182, 1184, 1144, + /* 280 */ 1146, 1148, 1147, 1149, 1152, 1153, 1160, 1186, 1194, 1185, + /* 290 */ 1189, 1187, 1191, 1193, 1192, 1196, 1198, 1197, 1202, 1215, + /* 300 */ 1217, 1226, 1227, 1231, 1232, 1233, 1234, 1203, 1204, 1205, + /* 310 */ 1221, 1223, 1209, 1211, 1237, }; static const YYACTIONTYPE yy_default[] = { - /* 0 */ 982, 1300, 1300, 1300, 1214, 1214, 1214, 1305, 1300, 1109, - /* 10 */ 1138, 1138, 1274, 1305, 1305, 1305, 1305, 1305, 1305, 1212, - /* 20 */ 1305, 1305, 1305, 1300, 1305, 1113, 1144, 1305, 1305, 1305, - /* 30 */ 1305, 1305, 1305, 1305, 1305, 1273, 1275, 1152, 1151, 1254, - /* 40 */ 1125, 1149, 1142, 1146, 1215, 1208, 1209, 1207, 1211, 1216, - /* 50 */ 1305, 1145, 1177, 1192, 1176, 1305, 1305, 1305, 1305, 1305, - /* 60 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 70 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 80 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 90 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1186, 1191, - /* 100 */ 1198, 1190, 1187, 1179, 1178, 1180, 1181, 1305, 1305, 1008, - /* 110 */ 1074, 1305, 1305, 1182, 1305, 1020, 1183, 1195, 1194, 1193, - /* 120 */ 1015, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 130 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 140 */ 1305, 1305, 1305, 1305, 1305, 982, 1300, 1305, 1305, 1300, - /* 150 */ 1300, 1300, 1300, 1300, 1300, 1292, 1113, 1103, 1305, 1305, - /* 160 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1280, 1278, - /* 170 */ 1305, 1227, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 180 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 190 */ 1305, 1305, 1305, 1109, 1305, 1305, 1305, 1305, 1305, 1305, - /* 200 */ 1305, 1305, 1305, 1305, 1305, 1305, 988, 1305, 1247, 1109, - /* 210 */ 1109, 1109, 1111, 1089, 1101, 990, 1148, 1127, 1127, 1259, - /* 220 */ 1148, 1259, 1045, 1068, 1042, 1138, 1127, 1210, 1138, 1138, - /* 230 */ 1110, 1101, 1305, 1285, 1118, 1118, 1277, 1277, 1118, 1157, - /* 240 */ 1078, 1148, 1085, 1085, 1085, 1085, 1118, 1005, 1148, 1157, - /* 250 */ 1078, 1078, 1148, 1118, 1005, 1253, 1251, 1118, 1118, 1005, - /* 260 */ 1220, 1118, 1005, 1118, 1005, 1220, 1076, 1076, 1076, 1060, - /* 270 */ 1220, 1076, 1045, 1076, 1060, 1076, 1076, 1131, 1126, 1131, - /* 280 */ 1126, 1131, 1126, 1131, 1126, 1118, 1118, 1305, 1220, 1224, - /* 290 */ 1224, 1220, 1143, 1132, 1141, 1139, 1148, 1011, 1063, 998, - /* 300 */ 998, 987, 987, 987, 987, 1297, 1297, 1292, 1047, 1047, - /* 310 */ 1030, 1305, 1305, 1305, 1305, 1305, 1305, 1022, 1305, 1229, - /* 320 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 330 */ 1305, 1305, 1305, 1305, 1305, 1305, 1164, 1305, 983, 1287, - /* 340 */ 1305, 1305, 1284, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 350 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 360 */ 1305, 1257, 1305, 1305, 1305, 1305, 1305, 1305, 1250, 1249, - /* 370 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 380 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, - /* 390 */ 1305, 1305, 1092, 1305, 1305, 1305, 1096, 1305, 1305, 1305, - /* 400 */ 1305, 1305, 1305, 1305, 1140, 1305, 1133, 1305, 1213, 1305, - /* 410 */ 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1305, 1302, - /* 420 */ 1305, 1305, 1305, 1301, 1305, 1305, 1305, 1305, 1305, 1166, - /* 430 */ 1305, 1165, 1169, 1305, 996, 1305, + /* 0 */ 1250, 1240, 1240, 1240, 1174, 1174, 1174, 1240, 1071, 1100, + /* 10 */ 1100, 1224, 1301, 1301, 1301, 1301, 1301, 1301, 1173, 1301, + /* 20 */ 1301, 1301, 1301, 1240, 1075, 1106, 1301, 1301, 1301, 1301, + /* 30 */ 1301, 1301, 1301, 1301, 1223, 1225, 1114, 1113, 1206, 1087, + /* 40 */ 1111, 1104, 1108, 1175, 1169, 1170, 1168, 1172, 1176, 1301, + /* 50 */ 1107, 1138, 1153, 1137, 1301, 1301, 1301, 1301, 1301, 1301, + /* 60 */ 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, + /* 70 */ 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, + /* 80 */ 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, + /* 90 */ 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1147, 1152, 1159, + /* 100 */ 1151, 1148, 1140, 1139, 1141, 1142, 1301, 994, 1042, 1301, + /* 110 */ 1301, 1301, 1143, 1301, 1144, 1156, 1155, 1154, 1231, 1258, + /* 120 */ 1257, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, + /* 130 */ 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, + /* 140 */ 1301, 1301, 1301, 1301, 1301, 1301, 1250, 1240, 1000, 1000, + /* 150 */ 1301, 1240, 1240, 1240, 1240, 1240, 1240, 1236, 1075, 1066, + /* 160 */ 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, + /* 170 */ 1228, 1226, 1301, 1187, 1301, 1301, 1301, 1301, 1301, 1301, + /* 180 */ 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, + /* 190 */ 1301, 1301, 1301, 1301, 1301, 1071, 1301, 1301, 1301, 1301, + /* 200 */ 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1252, 1301, + /* 210 */ 1201, 1071, 1071, 1071, 1073, 1055, 1065, 979, 1110, 1089, + /* 220 */ 1089, 1290, 1110, 1290, 1017, 1272, 1014, 1100, 1089, 1171, + /* 230 */ 1100, 1100, 1072, 1065, 1301, 1293, 1080, 1080, 1292, 1292, + /* 240 */ 1080, 1119, 1045, 1110, 1051, 1051, 1051, 1051, 1080, 991, + /* 250 */ 1110, 1119, 1045, 1045, 1110, 1080, 991, 1205, 1287, 1080, + /* 260 */ 1080, 991, 1180, 1080, 991, 1080, 991, 1180, 1043, 1043, + /* 270 */ 1043, 1032, 1180, 1043, 1017, 1043, 1032, 1043, 1043, 1093, + /* 280 */ 1088, 1093, 1088, 1093, 1088, 1093, 1088, 1080, 1080, 1301, + /* 290 */ 1180, 1184, 1184, 1180, 1105, 1094, 1103, 1101, 1110, 997, + /* 300 */ 1035, 1255, 1255, 1251, 1251, 1251, 1251, 1298, 1298, 1236, + /* 310 */ 1267, 1267, 1019, 1019, 1267, 1301, 1301, 1301, 1301, 1301, + /* 320 */ 1301, 1262, 1301, 1189, 1301, 1301, 1301, 1301, 1301, 1301, + /* 330 */ 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, + /* 340 */ 1125, 1301, 975, 1233, 1301, 1301, 1232, 1301, 1301, 1301, + /* 350 */ 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, + /* 360 */ 1301, 1301, 1301, 1301, 1301, 1289, 1301, 1301, 1301, 1301, + /* 370 */ 1301, 1301, 1204, 1203, 1301, 1301, 1301, 1301, 1301, 1301, + /* 380 */ 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1301, + /* 390 */ 1301, 1301, 1301, 1301, 1301, 1301, 1057, 1301, 1301, 1301, + /* 400 */ 1276, 1301, 1301, 1301, 1301, 1301, 1301, 1301, 1102, 1301, + /* 410 */ 1095, 1301, 1280, 1301, 1301, 1301, 1301, 1301, 1301, 1301, + /* 420 */ 1301, 1301, 1301, 1242, 1301, 1301, 1301, 1241, 1301, 1301, + /* 430 */ 1301, 1301, 1301, 1127, 1301, 1126, 1130, 1301, 985, 1301, }; +/********** End of lemon-generated parsing tables *****************************/ -/* The next table maps tokens into fallback tokens. If a construct -** like the following: +/* The next table maps tokens (terminal symbols) into fallback tokens. +** If a construct like the following: ** ** %fallback ID X Y Z. ** @@ -126780,79 +131540,109 @@ static const YYACTIONTYPE yy_default[] = { ** and Z. Whenever one of the tokens X, Y, or Z is input to the parser ** but it does not parse, the type of the token is changed to ID and ** the parse is retried before an error is thrown. +** +** This feature can be used, for example, to cause some keywords in a language +** to revert to identifiers if they keyword does not apply in the context where +** it appears. */ #ifdef YYFALLBACK static const YYCODETYPE yyFallback[] = { 0, /* $ => nothing */ 0, /* SEMI => nothing */ - 27, /* EXPLAIN => ID */ - 27, /* QUERY => ID */ - 27, /* PLAN => ID */ - 27, /* BEGIN => ID */ + 55, /* EXPLAIN => ID */ + 55, /* QUERY => ID */ + 55, /* PLAN => ID */ + 55, /* BEGIN => ID */ 0, /* TRANSACTION => nothing */ - 27, /* DEFERRED => ID */ - 27, /* IMMEDIATE => ID */ - 27, /* EXCLUSIVE => ID */ + 55, /* DEFERRED => ID */ + 55, /* IMMEDIATE => ID */ + 55, /* EXCLUSIVE => ID */ 0, /* COMMIT => nothing */ - 27, /* END => ID */ - 27, /* ROLLBACK => ID */ - 27, /* SAVEPOINT => ID */ - 27, /* RELEASE => ID */ + 55, /* END => ID */ + 55, /* ROLLBACK => ID */ + 55, /* SAVEPOINT => ID */ + 55, /* RELEASE => ID */ 0, /* TO => nothing */ 0, /* TABLE => nothing */ 0, /* CREATE => nothing */ - 27, /* IF => ID */ + 55, /* IF => ID */ 0, /* NOT => nothing */ 0, /* EXISTS => nothing */ - 27, /* TEMP => ID */ + 55, /* TEMP => ID */ 0, /* LP => nothing */ 0, /* RP => nothing */ 0, /* AS => nothing */ - 27, /* WITHOUT => ID */ + 55, /* WITHOUT => ID */ 0, /* COMMA => nothing */ + 0, /* OR => nothing */ + 0, /* AND => nothing */ + 0, /* IS => nothing */ + 55, /* MATCH => ID */ + 55, /* LIKE_KW => ID */ + 0, /* BETWEEN => nothing */ + 0, /* IN => nothing */ + 0, /* ISNULL => nothing */ + 0, /* NOTNULL => nothing */ + 0, /* NE => nothing */ + 0, /* EQ => nothing */ + 0, /* GT => nothing */ + 0, /* LE => nothing */ + 0, /* LT => nothing */ + 0, /* GE => nothing */ + 0, /* ESCAPE => nothing */ + 0, /* BITAND => nothing */ + 0, /* BITOR => nothing */ + 0, /* LSHIFT => nothing */ + 0, /* RSHIFT => nothing */ + 0, /* PLUS => nothing */ + 0, /* MINUS => nothing */ + 0, /* STAR => nothing */ + 0, /* SLASH => nothing */ + 0, /* REM => nothing */ + 0, /* CONCAT => nothing */ + 0, /* COLLATE => nothing */ + 0, /* BITNOT => nothing */ 0, /* ID => nothing */ 0, /* INDEXED => nothing */ - 27, /* ABORT => ID */ - 27, /* ACTION => ID */ - 27, /* AFTER => ID */ - 27, /* ANALYZE => ID */ - 27, /* ASC => ID */ - 27, /* ATTACH => ID */ - 27, /* BEFORE => ID */ - 27, /* BY => ID */ - 27, /* CASCADE => ID */ - 27, /* CAST => ID */ - 27, /* COLUMNKW => ID */ - 27, /* CONFLICT => ID */ - 27, /* DATABASE => ID */ - 27, /* DESC => ID */ - 27, /* DETACH => ID */ - 27, /* EACH => ID */ - 27, /* FAIL => ID */ - 27, /* FOR => ID */ - 27, /* IGNORE => ID */ - 27, /* INITIALLY => ID */ - 27, /* INSTEAD => ID */ - 27, /* LIKE_KW => ID */ - 27, /* MATCH => ID */ - 27, /* NO => ID */ - 27, /* KEY => ID */ - 27, /* OF => ID */ - 27, /* OFFSET => ID */ - 27, /* PRAGMA => ID */ - 27, /* RAISE => ID */ - 27, /* RECURSIVE => ID */ - 27, /* REPLACE => ID */ - 27, /* RESTRICT => ID */ - 27, /* ROW => ID */ - 27, /* TRIGGER => ID */ - 27, /* VACUUM => ID */ - 27, /* VIEW => ID */ - 27, /* VIRTUAL => ID */ - 27, /* WITH => ID */ - 27, /* REINDEX => ID */ - 27, /* RENAME => ID */ - 27, /* CTIME_KW => ID */ + 55, /* ABORT => ID */ + 55, /* ACTION => ID */ + 55, /* AFTER => ID */ + 55, /* ANALYZE => ID */ + 55, /* ASC => ID */ + 55, /* ATTACH => ID */ + 55, /* BEFORE => ID */ + 55, /* BY => ID */ + 55, /* CASCADE => ID */ + 55, /* CAST => ID */ + 55, /* COLUMNKW => ID */ + 55, /* CONFLICT => ID */ + 55, /* DATABASE => ID */ + 55, /* DESC => ID */ + 55, /* DETACH => ID */ + 55, /* EACH => ID */ + 55, /* FAIL => ID */ + 55, /* FOR => ID */ + 55, /* IGNORE => ID */ + 55, /* INITIALLY => ID */ + 55, /* INSTEAD => ID */ + 55, /* NO => ID */ + 55, /* KEY => ID */ + 55, /* OF => ID */ + 55, /* OFFSET => ID */ + 55, /* PRAGMA => ID */ + 55, /* RAISE => ID */ + 55, /* RECURSIVE => ID */ + 55, /* REPLACE => ID */ + 55, /* RESTRICT => ID */ + 55, /* ROW => ID */ + 55, /* TRIGGER => ID */ + 55, /* VACUUM => ID */ + 55, /* VIEW => ID */ + 55, /* VIRTUAL => ID */ + 55, /* WITH => ID */ + 55, /* REINDEX => ID */ + 55, /* RENAME => ID */ + 55, /* CTIME_KW => ID */ }; #endif /* YYFALLBACK */ @@ -126888,7 +131678,9 @@ struct yyParser { #ifdef YYTRACKMAXSTACKDEPTH int yyidxMax; /* Maximum value of yyidx */ #endif +#ifndef YYNOERRORRECOVERY int yyerrcnt; /* Shifts left before out of the error */ +#endif sqlite3ParserARG_SDECL /* A place to hold %extra_argument */ #if YYSTACKDEPTH<=0 int yystksz; /* Current side of the stack */ @@ -126941,25 +131733,25 @@ static const char *const yyTokenName[] = { "ROLLBACK", "SAVEPOINT", "RELEASE", "TO", "TABLE", "CREATE", "IF", "NOT", "EXISTS", "TEMP", "LP", "RP", - "AS", "WITHOUT", "COMMA", "ID", + "AS", "WITHOUT", "COMMA", "OR", + "AND", "IS", "MATCH", "LIKE_KW", + "BETWEEN", "IN", "ISNULL", "NOTNULL", + "NE", "EQ", "GT", "LE", + "LT", "GE", "ESCAPE", "BITAND", + "BITOR", "LSHIFT", "RSHIFT", "PLUS", + "MINUS", "STAR", "SLASH", "REM", + "CONCAT", "COLLATE", "BITNOT", "ID", "INDEXED", "ABORT", "ACTION", "AFTER", "ANALYZE", "ASC", "ATTACH", "BEFORE", "BY", "CASCADE", "CAST", "COLUMNKW", "CONFLICT", "DATABASE", "DESC", "DETACH", "EACH", "FAIL", "FOR", "IGNORE", - "INITIALLY", "INSTEAD", "LIKE_KW", "MATCH", - "NO", "KEY", "OF", "OFFSET", - "PRAGMA", "RAISE", "RECURSIVE", "REPLACE", - "RESTRICT", "ROW", "TRIGGER", "VACUUM", - "VIEW", "VIRTUAL", "WITH", "REINDEX", - "RENAME", "CTIME_KW", "ANY", "OR", - "AND", "IS", "BETWEEN", "IN", - "ISNULL", "NOTNULL", "NE", "EQ", - "GT", "LE", "LT", "GE", - "ESCAPE", "BITAND", "BITOR", "LSHIFT", - "RSHIFT", "PLUS", "MINUS", "STAR", - "SLASH", "REM", "CONCAT", "COLLATE", - "BITNOT", "STRING", "JOIN_KW", "CONSTRAINT", + "INITIALLY", "INSTEAD", "NO", "KEY", + "OF", "OFFSET", "PRAGMA", "RAISE", + "RECURSIVE", "REPLACE", "RESTRICT", "ROW", + "TRIGGER", "VACUUM", "VIEW", "VIRTUAL", + "WITH", "REINDEX", "RENAME", "CTIME_KW", + "ANY", "STRING", "JOIN_KW", "CONSTRAINT", "DEFAULT", "NULL", "PRIMARY", "UNIQUE", "CHECK", "REFERENCES", "AUTOINCR", "ON", "INSERT", "DELETE", "UPDATE", "SET", @@ -126976,29 +131768,28 @@ static const char *const yyTokenName[] = { "nm", "savepoint_opt", "create_table", "create_table_args", "createkw", "temp", "ifnotexists", "dbnm", "columnlist", "conslist_opt", "table_options", "select", - "column", "columnid", "type", "carglist", - "typetoken", "typename", "signed", "plus_num", - "minus_num", "ccons", "term", "expr", - "onconf", "sortorder", "autoinc", "eidlist_opt", - "refargs", "defer_subclause", "refarg", "refact", - "init_deferred_pred_opt", "conslist", "tconscomma", "tcons", - "sortlist", "eidlist", "defer_subclause_opt", "orconf", - "resolvetype", "raisetype", "ifexists", "fullname", - "selectnowith", "oneselect", "with", "multiselect_op", - "distinct", "selcollist", "from", "where_opt", - "groupby_opt", "having_opt", "orderby_opt", "limit_opt", - "values", "nexprlist", "exprlist", "sclp", - "as", "seltablist", "stl_prefix", "joinop", - "indexed_opt", "on_opt", "using_opt", "joinop2", - "idlist", "setlist", "insert_cmd", "idlist_opt", - "likeop", "between_op", "in_op", "case_operand", - "case_exprlist", "case_else", "uniqueflag", "collate", - "nmnum", "trigger_decl", "trigger_cmd_list", "trigger_time", - "trigger_event", "foreach_clause", "when_clause", "trigger_cmd", - "trnm", "tridxby", "database_kw_opt", "key_opt", - "add_column_fullname", "kwcolumn_opt", "create_vtab", "vtabarglist", - "vtabarg", "vtabargtoken", "lp", "anylist", - "wqlist", + "columnname", "carglist", "typetoken", "typename", + "signed", "plus_num", "minus_num", "ccons", + "term", "expr", "onconf", "sortorder", + "autoinc", "eidlist_opt", "refargs", "defer_subclause", + "refarg", "refact", "init_deferred_pred_opt", "conslist", + "tconscomma", "tcons", "sortlist", "eidlist", + "defer_subclause_opt", "orconf", "resolvetype", "raisetype", + "ifexists", "fullname", "selectnowith", "oneselect", + "with", "multiselect_op", "distinct", "selcollist", + "from", "where_opt", "groupby_opt", "having_opt", + "orderby_opt", "limit_opt", "values", "nexprlist", + "exprlist", "sclp", "as", "seltablist", + "stl_prefix", "joinop", "indexed_opt", "on_opt", + "using_opt", "idlist", "setlist", "insert_cmd", + "idlist_opt", "likeop", "between_op", "in_op", + "case_operand", "case_exprlist", "case_else", "uniqueflag", + "collate", "nmnum", "trigger_decl", "trigger_cmd_list", + "trigger_time", "trigger_event", "foreach_clause", "when_clause", + "trigger_cmd", "trnm", "tridxby", "database_kw_opt", + "key_opt", "add_column_fullname", "kwcolumn_opt", "create_vtab", + "vtabarglist", "vtabarg", "vtabargtoken", "lp", + "anylist", "wqlist", }; #endif /* NDEBUG */ @@ -127006,334 +131797,332 @@ static const char *const yyTokenName[] = { /* For tracing reduce actions, the names of all rules are required. */ static const char *const yyRuleName[] = { - /* 0 */ "input ::= cmdlist", - /* 1 */ "cmdlist ::= cmdlist ecmd", - /* 2 */ "cmdlist ::= ecmd", - /* 3 */ "ecmd ::= SEMI", - /* 4 */ "ecmd ::= explain cmdx SEMI", - /* 5 */ "explain ::=", - /* 6 */ "explain ::= EXPLAIN", - /* 7 */ "explain ::= EXPLAIN QUERY PLAN", - /* 8 */ "cmdx ::= cmd", - /* 9 */ "cmd ::= BEGIN transtype trans_opt", - /* 10 */ "trans_opt ::=", - /* 11 */ "trans_opt ::= TRANSACTION", - /* 12 */ "trans_opt ::= TRANSACTION nm", - /* 13 */ "transtype ::=", - /* 14 */ "transtype ::= DEFERRED", - /* 15 */ "transtype ::= IMMEDIATE", - /* 16 */ "transtype ::= EXCLUSIVE", - /* 17 */ "cmd ::= COMMIT trans_opt", - /* 18 */ "cmd ::= END trans_opt", - /* 19 */ "cmd ::= ROLLBACK trans_opt", - /* 20 */ "savepoint_opt ::= SAVEPOINT", - /* 21 */ "savepoint_opt ::=", - /* 22 */ "cmd ::= SAVEPOINT nm", - /* 23 */ "cmd ::= RELEASE savepoint_opt nm", - /* 24 */ "cmd ::= ROLLBACK trans_opt TO savepoint_opt nm", - /* 25 */ "cmd ::= create_table create_table_args", - /* 26 */ "create_table ::= createkw temp TABLE ifnotexists nm dbnm", - /* 27 */ "createkw ::= CREATE", - /* 28 */ "ifnotexists ::=", - /* 29 */ "ifnotexists ::= IF NOT EXISTS", - /* 30 */ "temp ::= TEMP", - /* 31 */ "temp ::=", - /* 32 */ "create_table_args ::= LP columnlist conslist_opt RP table_options", - /* 33 */ "create_table_args ::= AS select", - /* 34 */ "table_options ::=", - /* 35 */ "table_options ::= WITHOUT nm", - /* 36 */ "columnlist ::= columnlist COMMA column", - /* 37 */ "columnlist ::= column", - /* 38 */ "column ::= columnid type carglist", - /* 39 */ "columnid ::= nm", - /* 40 */ "nm ::= ID|INDEXED", - /* 41 */ "nm ::= STRING", - /* 42 */ "nm ::= JOIN_KW", - /* 43 */ "type ::=", - /* 44 */ "type ::= typetoken", - /* 45 */ "typetoken ::= typename", - /* 46 */ "typetoken ::= typename LP signed RP", - /* 47 */ "typetoken ::= typename LP signed COMMA signed RP", - /* 48 */ "typename ::= ID|STRING", - /* 49 */ "typename ::= typename ID|STRING", - /* 50 */ "signed ::= plus_num", - /* 51 */ "signed ::= minus_num", - /* 52 */ "carglist ::= carglist ccons", - /* 53 */ "carglist ::=", - /* 54 */ "ccons ::= CONSTRAINT nm", - /* 55 */ "ccons ::= DEFAULT term", - /* 56 */ "ccons ::= DEFAULT LP expr RP", - /* 57 */ "ccons ::= DEFAULT PLUS term", - /* 58 */ "ccons ::= DEFAULT MINUS term", - /* 59 */ "ccons ::= DEFAULT ID|INDEXED", - /* 60 */ "ccons ::= NULL onconf", - /* 61 */ "ccons ::= NOT NULL onconf", - /* 62 */ "ccons ::= PRIMARY KEY sortorder onconf autoinc", - /* 63 */ "ccons ::= UNIQUE onconf", - /* 64 */ "ccons ::= CHECK LP expr RP", - /* 65 */ "ccons ::= REFERENCES nm eidlist_opt refargs", - /* 66 */ "ccons ::= defer_subclause", - /* 67 */ "ccons ::= COLLATE ID|STRING", - /* 68 */ "autoinc ::=", - /* 69 */ "autoinc ::= AUTOINCR", - /* 70 */ "refargs ::=", - /* 71 */ "refargs ::= refargs refarg", - /* 72 */ "refarg ::= MATCH nm", - /* 73 */ "refarg ::= ON INSERT refact", - /* 74 */ "refarg ::= ON DELETE refact", - /* 75 */ "refarg ::= ON UPDATE refact", - /* 76 */ "refact ::= SET NULL", - /* 77 */ "refact ::= SET DEFAULT", - /* 78 */ "refact ::= CASCADE", - /* 79 */ "refact ::= RESTRICT", - /* 80 */ "refact ::= NO ACTION", - /* 81 */ "defer_subclause ::= NOT DEFERRABLE init_deferred_pred_opt", - /* 82 */ "defer_subclause ::= DEFERRABLE init_deferred_pred_opt", - /* 83 */ "init_deferred_pred_opt ::=", - /* 84 */ "init_deferred_pred_opt ::= INITIALLY DEFERRED", - /* 85 */ "init_deferred_pred_opt ::= INITIALLY IMMEDIATE", - /* 86 */ "conslist_opt ::=", - /* 87 */ "conslist_opt ::= COMMA conslist", - /* 88 */ "conslist ::= conslist tconscomma tcons", - /* 89 */ "conslist ::= tcons", - /* 90 */ "tconscomma ::= COMMA", - /* 91 */ "tconscomma ::=", - /* 92 */ "tcons ::= CONSTRAINT nm", - /* 93 */ "tcons ::= PRIMARY KEY LP sortlist autoinc RP onconf", - /* 94 */ "tcons ::= UNIQUE LP sortlist RP onconf", - /* 95 */ "tcons ::= CHECK LP expr RP onconf", - /* 96 */ "tcons ::= FOREIGN KEY LP eidlist RP REFERENCES nm eidlist_opt refargs defer_subclause_opt", - /* 97 */ "defer_subclause_opt ::=", - /* 98 */ "defer_subclause_opt ::= defer_subclause", - /* 99 */ "onconf ::=", - /* 100 */ "onconf ::= ON CONFLICT resolvetype", - /* 101 */ "orconf ::=", - /* 102 */ "orconf ::= OR resolvetype", - /* 103 */ "resolvetype ::= raisetype", - /* 104 */ "resolvetype ::= IGNORE", - /* 105 */ "resolvetype ::= REPLACE", - /* 106 */ "cmd ::= DROP TABLE ifexists fullname", - /* 107 */ "ifexists ::= IF EXISTS", - /* 108 */ "ifexists ::=", - /* 109 */ "cmd ::= createkw temp VIEW ifnotexists nm dbnm eidlist_opt AS select", - /* 110 */ "cmd ::= DROP VIEW ifexists fullname", - /* 111 */ "cmd ::= select", - /* 112 */ "select ::= with selectnowith", - /* 113 */ "selectnowith ::= oneselect", - /* 114 */ "selectnowith ::= selectnowith multiselect_op oneselect", - /* 115 */ "multiselect_op ::= UNION", - /* 116 */ "multiselect_op ::= UNION ALL", - /* 117 */ "multiselect_op ::= EXCEPT|INTERSECT", - /* 118 */ "oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt orderby_opt limit_opt", - /* 119 */ "oneselect ::= values", - /* 120 */ "values ::= VALUES LP nexprlist RP", - /* 121 */ "values ::= values COMMA LP exprlist RP", - /* 122 */ "distinct ::= DISTINCT", - /* 123 */ "distinct ::= ALL", - /* 124 */ "distinct ::=", - /* 125 */ "sclp ::= selcollist COMMA", - /* 126 */ "sclp ::=", - /* 127 */ "selcollist ::= sclp expr as", - /* 128 */ "selcollist ::= sclp STAR", - /* 129 */ "selcollist ::= sclp nm DOT STAR", - /* 130 */ "as ::= AS nm", - /* 131 */ "as ::= ID|STRING", - /* 132 */ "as ::=", - /* 133 */ "from ::=", - /* 134 */ "from ::= FROM seltablist", - /* 135 */ "stl_prefix ::= seltablist joinop", - /* 136 */ "stl_prefix ::=", - /* 137 */ "seltablist ::= stl_prefix nm dbnm as indexed_opt on_opt using_opt", - /* 138 */ "seltablist ::= stl_prefix nm dbnm LP exprlist RP as on_opt using_opt", - /* 139 */ "seltablist ::= stl_prefix LP select RP as on_opt using_opt", - /* 140 */ "seltablist ::= stl_prefix LP seltablist RP as on_opt using_opt", - /* 141 */ "dbnm ::=", - /* 142 */ "dbnm ::= DOT nm", - /* 143 */ "fullname ::= nm dbnm", - /* 144 */ "joinop ::= COMMA|JOIN", - /* 145 */ "joinop ::= JOIN_KW JOIN", - /* 146 */ "joinop ::= JOIN_KW nm JOIN", - /* 147 */ "joinop ::= JOIN_KW nm nm JOIN", - /* 148 */ "on_opt ::= ON expr", - /* 149 */ "on_opt ::=", - /* 150 */ "indexed_opt ::=", - /* 151 */ "indexed_opt ::= INDEXED BY nm", - /* 152 */ "indexed_opt ::= NOT INDEXED", - /* 153 */ "using_opt ::= USING LP idlist RP", - /* 154 */ "using_opt ::=", - /* 155 */ "orderby_opt ::=", - /* 156 */ "orderby_opt ::= ORDER BY sortlist", - /* 157 */ "sortlist ::= sortlist COMMA expr sortorder", - /* 158 */ "sortlist ::= expr sortorder", - /* 159 */ "sortorder ::= ASC", - /* 160 */ "sortorder ::= DESC", - /* 161 */ "sortorder ::=", - /* 162 */ "groupby_opt ::=", - /* 163 */ "groupby_opt ::= GROUP BY nexprlist", - /* 164 */ "having_opt ::=", - /* 165 */ "having_opt ::= HAVING expr", - /* 166 */ "limit_opt ::=", - /* 167 */ "limit_opt ::= LIMIT expr", - /* 168 */ "limit_opt ::= LIMIT expr OFFSET expr", - /* 169 */ "limit_opt ::= LIMIT expr COMMA expr", - /* 170 */ "cmd ::= with DELETE FROM fullname indexed_opt where_opt", - /* 171 */ "where_opt ::=", - /* 172 */ "where_opt ::= WHERE expr", - /* 173 */ "cmd ::= with UPDATE orconf fullname indexed_opt SET setlist where_opt", - /* 174 */ "setlist ::= setlist COMMA nm EQ expr", - /* 175 */ "setlist ::= nm EQ expr", - /* 176 */ "cmd ::= with insert_cmd INTO fullname idlist_opt select", - /* 177 */ "cmd ::= with insert_cmd INTO fullname idlist_opt DEFAULT VALUES", - /* 178 */ "insert_cmd ::= INSERT orconf", - /* 179 */ "insert_cmd ::= REPLACE", - /* 180 */ "idlist_opt ::=", - /* 181 */ "idlist_opt ::= LP idlist RP", - /* 182 */ "idlist ::= idlist COMMA nm", - /* 183 */ "idlist ::= nm", - /* 184 */ "expr ::= term", - /* 185 */ "expr ::= LP expr RP", - /* 186 */ "term ::= NULL", - /* 187 */ "expr ::= ID|INDEXED", - /* 188 */ "expr ::= JOIN_KW", - /* 189 */ "expr ::= nm DOT nm", - /* 190 */ "expr ::= nm DOT nm DOT nm", - /* 191 */ "term ::= INTEGER|FLOAT|BLOB", - /* 192 */ "term ::= STRING", - /* 193 */ "expr ::= VARIABLE", - /* 194 */ "expr ::= expr COLLATE ID|STRING", - /* 195 */ "expr ::= CAST LP expr AS typetoken RP", - /* 196 */ "expr ::= ID|INDEXED LP distinct exprlist RP", - /* 197 */ "expr ::= ID|INDEXED LP STAR RP", - /* 198 */ "term ::= CTIME_KW", - /* 199 */ "expr ::= expr AND expr", - /* 200 */ "expr ::= expr OR expr", - /* 201 */ "expr ::= expr LT|GT|GE|LE expr", - /* 202 */ "expr ::= expr EQ|NE expr", - /* 203 */ "expr ::= expr BITAND|BITOR|LSHIFT|RSHIFT expr", - /* 204 */ "expr ::= expr PLUS|MINUS expr", - /* 205 */ "expr ::= expr STAR|SLASH|REM expr", - /* 206 */ "expr ::= expr CONCAT expr", - /* 207 */ "likeop ::= LIKE_KW|MATCH", - /* 208 */ "likeop ::= NOT LIKE_KW|MATCH", - /* 209 */ "expr ::= expr likeop expr", - /* 210 */ "expr ::= expr likeop expr ESCAPE expr", - /* 211 */ "expr ::= expr ISNULL|NOTNULL", - /* 212 */ "expr ::= expr NOT NULL", - /* 213 */ "expr ::= expr IS expr", - /* 214 */ "expr ::= expr IS NOT expr", - /* 215 */ "expr ::= NOT expr", - /* 216 */ "expr ::= BITNOT expr", - /* 217 */ "expr ::= MINUS expr", - /* 218 */ "expr ::= PLUS expr", - /* 219 */ "between_op ::= BETWEEN", - /* 220 */ "between_op ::= NOT BETWEEN", - /* 221 */ "expr ::= expr between_op expr AND expr", - /* 222 */ "in_op ::= IN", - /* 223 */ "in_op ::= NOT IN", - /* 224 */ "expr ::= expr in_op LP exprlist RP", - /* 225 */ "expr ::= LP select RP", - /* 226 */ "expr ::= expr in_op LP select RP", - /* 227 */ "expr ::= expr in_op nm dbnm", - /* 228 */ "expr ::= EXISTS LP select RP", - /* 229 */ "expr ::= CASE case_operand case_exprlist case_else END", - /* 230 */ "case_exprlist ::= case_exprlist WHEN expr THEN expr", - /* 231 */ "case_exprlist ::= WHEN expr THEN expr", - /* 232 */ "case_else ::= ELSE expr", - /* 233 */ "case_else ::=", - /* 234 */ "case_operand ::= expr", - /* 235 */ "case_operand ::=", - /* 236 */ "exprlist ::= nexprlist", - /* 237 */ "exprlist ::=", - /* 238 */ "nexprlist ::= nexprlist COMMA expr", - /* 239 */ "nexprlist ::= expr", - /* 240 */ "cmd ::= createkw uniqueflag INDEX ifnotexists nm dbnm ON nm LP sortlist RP where_opt", - /* 241 */ "uniqueflag ::= UNIQUE", - /* 242 */ "uniqueflag ::=", - /* 243 */ "eidlist_opt ::=", - /* 244 */ "eidlist_opt ::= LP eidlist RP", - /* 245 */ "eidlist ::= eidlist COMMA nm collate sortorder", - /* 246 */ "eidlist ::= nm collate sortorder", - /* 247 */ "collate ::=", - /* 248 */ "collate ::= COLLATE ID|STRING", - /* 249 */ "cmd ::= DROP INDEX ifexists fullname", - /* 250 */ "cmd ::= VACUUM", - /* 251 */ "cmd ::= VACUUM nm", - /* 252 */ "cmd ::= PRAGMA nm dbnm", - /* 253 */ "cmd ::= PRAGMA nm dbnm EQ nmnum", - /* 254 */ "cmd ::= PRAGMA nm dbnm LP nmnum RP", - /* 255 */ "cmd ::= PRAGMA nm dbnm EQ minus_num", - /* 256 */ "cmd ::= PRAGMA nm dbnm LP minus_num RP", - /* 257 */ "nmnum ::= plus_num", - /* 258 */ "nmnum ::= nm", - /* 259 */ "nmnum ::= ON", - /* 260 */ "nmnum ::= DELETE", - /* 261 */ "nmnum ::= DEFAULT", - /* 262 */ "plus_num ::= PLUS INTEGER|FLOAT", - /* 263 */ "plus_num ::= INTEGER|FLOAT", - /* 264 */ "minus_num ::= MINUS INTEGER|FLOAT", - /* 265 */ "cmd ::= createkw trigger_decl BEGIN trigger_cmd_list END", - /* 266 */ "trigger_decl ::= temp TRIGGER ifnotexists nm dbnm trigger_time trigger_event ON fullname foreach_clause when_clause", - /* 267 */ "trigger_time ::= BEFORE", - /* 268 */ "trigger_time ::= AFTER", - /* 269 */ "trigger_time ::= INSTEAD OF", - /* 270 */ "trigger_time ::=", - /* 271 */ "trigger_event ::= DELETE|INSERT", - /* 272 */ "trigger_event ::= UPDATE", - /* 273 */ "trigger_event ::= UPDATE OF idlist", - /* 274 */ "foreach_clause ::=", - /* 275 */ "foreach_clause ::= FOR EACH ROW", - /* 276 */ "when_clause ::=", - /* 277 */ "when_clause ::= WHEN expr", - /* 278 */ "trigger_cmd_list ::= trigger_cmd_list trigger_cmd SEMI", - /* 279 */ "trigger_cmd_list ::= trigger_cmd SEMI", - /* 280 */ "trnm ::= nm", - /* 281 */ "trnm ::= nm DOT nm", - /* 282 */ "tridxby ::=", - /* 283 */ "tridxby ::= INDEXED BY nm", - /* 284 */ "tridxby ::= NOT INDEXED", - /* 285 */ "trigger_cmd ::= UPDATE orconf trnm tridxby SET setlist where_opt", - /* 286 */ "trigger_cmd ::= insert_cmd INTO trnm idlist_opt select", - /* 287 */ "trigger_cmd ::= DELETE FROM trnm tridxby where_opt", - /* 288 */ "trigger_cmd ::= select", - /* 289 */ "expr ::= RAISE LP IGNORE RP", - /* 290 */ "expr ::= RAISE LP raisetype COMMA nm RP", - /* 291 */ "raisetype ::= ROLLBACK", - /* 292 */ "raisetype ::= ABORT", - /* 293 */ "raisetype ::= FAIL", - /* 294 */ "cmd ::= DROP TRIGGER ifexists fullname", - /* 295 */ "cmd ::= ATTACH database_kw_opt expr AS expr key_opt", - /* 296 */ "cmd ::= DETACH database_kw_opt expr", - /* 297 */ "key_opt ::=", - /* 298 */ "key_opt ::= KEY expr", - /* 299 */ "database_kw_opt ::= DATABASE", - /* 300 */ "database_kw_opt ::=", - /* 301 */ "cmd ::= REINDEX", - /* 302 */ "cmd ::= REINDEX nm dbnm", - /* 303 */ "cmd ::= ANALYZE", - /* 304 */ "cmd ::= ANALYZE nm dbnm", - /* 305 */ "cmd ::= ALTER TABLE fullname RENAME TO nm", - /* 306 */ "cmd ::= ALTER TABLE add_column_fullname ADD kwcolumn_opt column", - /* 307 */ "add_column_fullname ::= fullname", - /* 308 */ "kwcolumn_opt ::=", - /* 309 */ "kwcolumn_opt ::= COLUMNKW", - /* 310 */ "cmd ::= create_vtab", - /* 311 */ "cmd ::= create_vtab LP vtabarglist RP", - /* 312 */ "create_vtab ::= createkw VIRTUAL TABLE ifnotexists nm dbnm USING nm", - /* 313 */ "vtabarglist ::= vtabarg", - /* 314 */ "vtabarglist ::= vtabarglist COMMA vtabarg", - /* 315 */ "vtabarg ::=", - /* 316 */ "vtabarg ::= vtabarg vtabargtoken", - /* 317 */ "vtabargtoken ::= ANY", - /* 318 */ "vtabargtoken ::= lp anylist RP", - /* 319 */ "lp ::= LP", - /* 320 */ "anylist ::=", - /* 321 */ "anylist ::= anylist LP anylist RP", - /* 322 */ "anylist ::= anylist ANY", - /* 323 */ "with ::=", - /* 324 */ "with ::= WITH wqlist", - /* 325 */ "with ::= WITH RECURSIVE wqlist", - /* 326 */ "wqlist ::= nm eidlist_opt AS LP select RP", - /* 327 */ "wqlist ::= wqlist COMMA nm eidlist_opt AS LP select RP", + /* 0 */ "explain ::= EXPLAIN", + /* 1 */ "explain ::= EXPLAIN QUERY PLAN", + /* 2 */ "cmdx ::= cmd", + /* 3 */ "cmd ::= BEGIN transtype trans_opt", + /* 4 */ "transtype ::=", + /* 5 */ "transtype ::= DEFERRED", + /* 6 */ "transtype ::= IMMEDIATE", + /* 7 */ "transtype ::= EXCLUSIVE", + /* 8 */ "cmd ::= COMMIT trans_opt", + /* 9 */ "cmd ::= END trans_opt", + /* 10 */ "cmd ::= ROLLBACK trans_opt", + /* 11 */ "cmd ::= SAVEPOINT nm", + /* 12 */ "cmd ::= RELEASE savepoint_opt nm", + /* 13 */ "cmd ::= ROLLBACK trans_opt TO savepoint_opt nm", + /* 14 */ "create_table ::= createkw temp TABLE ifnotexists nm dbnm", + /* 15 */ "createkw ::= CREATE", + /* 16 */ "ifnotexists ::=", + /* 17 */ "ifnotexists ::= IF NOT EXISTS", + /* 18 */ "temp ::= TEMP", + /* 19 */ "temp ::=", + /* 20 */ "create_table_args ::= LP columnlist conslist_opt RP table_options", + /* 21 */ "create_table_args ::= AS select", + /* 22 */ "table_options ::=", + /* 23 */ "table_options ::= WITHOUT nm", + /* 24 */ "columnname ::= nm typetoken", + /* 25 */ "typetoken ::=", + /* 26 */ "typetoken ::= typename LP signed RP", + /* 27 */ "typetoken ::= typename LP signed COMMA signed RP", + /* 28 */ "typename ::= typename ID|STRING", + /* 29 */ "ccons ::= CONSTRAINT nm", + /* 30 */ "ccons ::= DEFAULT term", + /* 31 */ "ccons ::= DEFAULT LP expr RP", + /* 32 */ "ccons ::= DEFAULT PLUS term", + /* 33 */ "ccons ::= DEFAULT MINUS term", + /* 34 */ "ccons ::= DEFAULT ID|INDEXED", + /* 35 */ "ccons ::= NOT NULL onconf", + /* 36 */ "ccons ::= PRIMARY KEY sortorder onconf autoinc", + /* 37 */ "ccons ::= UNIQUE onconf", + /* 38 */ "ccons ::= CHECK LP expr RP", + /* 39 */ "ccons ::= REFERENCES nm eidlist_opt refargs", + /* 40 */ "ccons ::= defer_subclause", + /* 41 */ "ccons ::= COLLATE ID|STRING", + /* 42 */ "autoinc ::=", + /* 43 */ "autoinc ::= AUTOINCR", + /* 44 */ "refargs ::=", + /* 45 */ "refargs ::= refargs refarg", + /* 46 */ "refarg ::= MATCH nm", + /* 47 */ "refarg ::= ON INSERT refact", + /* 48 */ "refarg ::= ON DELETE refact", + /* 49 */ "refarg ::= ON UPDATE refact", + /* 50 */ "refact ::= SET NULL", + /* 51 */ "refact ::= SET DEFAULT", + /* 52 */ "refact ::= CASCADE", + /* 53 */ "refact ::= RESTRICT", + /* 54 */ "refact ::= NO ACTION", + /* 55 */ "defer_subclause ::= NOT DEFERRABLE init_deferred_pred_opt", + /* 56 */ "defer_subclause ::= DEFERRABLE init_deferred_pred_opt", + /* 57 */ "init_deferred_pred_opt ::=", + /* 58 */ "init_deferred_pred_opt ::= INITIALLY DEFERRED", + /* 59 */ "init_deferred_pred_opt ::= INITIALLY IMMEDIATE", + /* 60 */ "conslist_opt ::=", + /* 61 */ "tconscomma ::= COMMA", + /* 62 */ "tcons ::= CONSTRAINT nm", + /* 63 */ "tcons ::= PRIMARY KEY LP sortlist autoinc RP onconf", + /* 64 */ "tcons ::= UNIQUE LP sortlist RP onconf", + /* 65 */ "tcons ::= CHECK LP expr RP onconf", + /* 66 */ "tcons ::= FOREIGN KEY LP eidlist RP REFERENCES nm eidlist_opt refargs defer_subclause_opt", + /* 67 */ "defer_subclause_opt ::=", + /* 68 */ "onconf ::=", + /* 69 */ "onconf ::= ON CONFLICT resolvetype", + /* 70 */ "orconf ::=", + /* 71 */ "orconf ::= OR resolvetype", + /* 72 */ "resolvetype ::= IGNORE", + /* 73 */ "resolvetype ::= REPLACE", + /* 74 */ "cmd ::= DROP TABLE ifexists fullname", + /* 75 */ "ifexists ::= IF EXISTS", + /* 76 */ "ifexists ::=", + /* 77 */ "cmd ::= createkw temp VIEW ifnotexists nm dbnm eidlist_opt AS select", + /* 78 */ "cmd ::= DROP VIEW ifexists fullname", + /* 79 */ "cmd ::= select", + /* 80 */ "select ::= with selectnowith", + /* 81 */ "selectnowith ::= selectnowith multiselect_op oneselect", + /* 82 */ "multiselect_op ::= UNION", + /* 83 */ "multiselect_op ::= UNION ALL", + /* 84 */ "multiselect_op ::= EXCEPT|INTERSECT", + /* 85 */ "oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt orderby_opt limit_opt", + /* 86 */ "values ::= VALUES LP nexprlist RP", + /* 87 */ "values ::= values COMMA LP exprlist RP", + /* 88 */ "distinct ::= DISTINCT", + /* 89 */ "distinct ::= ALL", + /* 90 */ "distinct ::=", + /* 91 */ "sclp ::=", + /* 92 */ "selcollist ::= sclp expr as", + /* 93 */ "selcollist ::= sclp STAR", + /* 94 */ "selcollist ::= sclp nm DOT STAR", + /* 95 */ "as ::= AS nm", + /* 96 */ "as ::=", + /* 97 */ "from ::=", + /* 98 */ "from ::= FROM seltablist", + /* 99 */ "stl_prefix ::= seltablist joinop", + /* 100 */ "stl_prefix ::=", + /* 101 */ "seltablist ::= stl_prefix nm dbnm as indexed_opt on_opt using_opt", + /* 102 */ "seltablist ::= stl_prefix nm dbnm LP exprlist RP as on_opt using_opt", + /* 103 */ "seltablist ::= stl_prefix LP select RP as on_opt using_opt", + /* 104 */ "seltablist ::= stl_prefix LP seltablist RP as on_opt using_opt", + /* 105 */ "dbnm ::=", + /* 106 */ "dbnm ::= DOT nm", + /* 107 */ "fullname ::= nm dbnm", + /* 108 */ "joinop ::= COMMA|JOIN", + /* 109 */ "joinop ::= JOIN_KW JOIN", + /* 110 */ "joinop ::= JOIN_KW nm JOIN", + /* 111 */ "joinop ::= JOIN_KW nm nm JOIN", + /* 112 */ "on_opt ::= ON expr", + /* 113 */ "on_opt ::=", + /* 114 */ "indexed_opt ::=", + /* 115 */ "indexed_opt ::= INDEXED BY nm", + /* 116 */ "indexed_opt ::= NOT INDEXED", + /* 117 */ "using_opt ::= USING LP idlist RP", + /* 118 */ "using_opt ::=", + /* 119 */ "orderby_opt ::=", + /* 120 */ "orderby_opt ::= ORDER BY sortlist", + /* 121 */ "sortlist ::= sortlist COMMA expr sortorder", + /* 122 */ "sortlist ::= expr sortorder", + /* 123 */ "sortorder ::= ASC", + /* 124 */ "sortorder ::= DESC", + /* 125 */ "sortorder ::=", + /* 126 */ "groupby_opt ::=", + /* 127 */ "groupby_opt ::= GROUP BY nexprlist", + /* 128 */ "having_opt ::=", + /* 129 */ "having_opt ::= HAVING expr", + /* 130 */ "limit_opt ::=", + /* 131 */ "limit_opt ::= LIMIT expr", + /* 132 */ "limit_opt ::= LIMIT expr OFFSET expr", + /* 133 */ "limit_opt ::= LIMIT expr COMMA expr", + /* 134 */ "cmd ::= with DELETE FROM fullname indexed_opt where_opt", + /* 135 */ "where_opt ::=", + /* 136 */ "where_opt ::= WHERE expr", + /* 137 */ "cmd ::= with UPDATE orconf fullname indexed_opt SET setlist where_opt", + /* 138 */ "setlist ::= setlist COMMA nm EQ expr", + /* 139 */ "setlist ::= nm EQ expr", + /* 140 */ "cmd ::= with insert_cmd INTO fullname idlist_opt select", + /* 141 */ "cmd ::= with insert_cmd INTO fullname idlist_opt DEFAULT VALUES", + /* 142 */ "insert_cmd ::= INSERT orconf", + /* 143 */ "insert_cmd ::= REPLACE", + /* 144 */ "idlist_opt ::=", + /* 145 */ "idlist_opt ::= LP idlist RP", + /* 146 */ "idlist ::= idlist COMMA nm", + /* 147 */ "idlist ::= nm", + /* 148 */ "expr ::= LP expr RP", + /* 149 */ "term ::= NULL", + /* 150 */ "expr ::= ID|INDEXED", + /* 151 */ "expr ::= JOIN_KW", + /* 152 */ "expr ::= nm DOT nm", + /* 153 */ "expr ::= nm DOT nm DOT nm", + /* 154 */ "term ::= INTEGER|FLOAT|BLOB", + /* 155 */ "term ::= STRING", + /* 156 */ "expr ::= VARIABLE", + /* 157 */ "expr ::= expr COLLATE ID|STRING", + /* 158 */ "expr ::= CAST LP expr AS typetoken RP", + /* 159 */ "expr ::= ID|INDEXED LP distinct exprlist RP", + /* 160 */ "expr ::= ID|INDEXED LP STAR RP", + /* 161 */ "term ::= CTIME_KW", + /* 162 */ "expr ::= expr AND expr", + /* 163 */ "expr ::= expr OR expr", + /* 164 */ "expr ::= expr LT|GT|GE|LE expr", + /* 165 */ "expr ::= expr EQ|NE expr", + /* 166 */ "expr ::= expr BITAND|BITOR|LSHIFT|RSHIFT expr", + /* 167 */ "expr ::= expr PLUS|MINUS expr", + /* 168 */ "expr ::= expr STAR|SLASH|REM expr", + /* 169 */ "expr ::= expr CONCAT expr", + /* 170 */ "likeop ::= LIKE_KW|MATCH", + /* 171 */ "likeop ::= NOT LIKE_KW|MATCH", + /* 172 */ "expr ::= expr likeop expr", + /* 173 */ "expr ::= expr likeop expr ESCAPE expr", + /* 174 */ "expr ::= expr ISNULL|NOTNULL", + /* 175 */ "expr ::= expr NOT NULL", + /* 176 */ "expr ::= expr IS expr", + /* 177 */ "expr ::= expr IS NOT expr", + /* 178 */ "expr ::= NOT expr", + /* 179 */ "expr ::= BITNOT expr", + /* 180 */ "expr ::= MINUS expr", + /* 181 */ "expr ::= PLUS expr", + /* 182 */ "between_op ::= BETWEEN", + /* 183 */ "between_op ::= NOT BETWEEN", + /* 184 */ "expr ::= expr between_op expr AND expr", + /* 185 */ "in_op ::= IN", + /* 186 */ "in_op ::= NOT IN", + /* 187 */ "expr ::= expr in_op LP exprlist RP", + /* 188 */ "expr ::= LP select RP", + /* 189 */ "expr ::= expr in_op LP select RP", + /* 190 */ "expr ::= expr in_op nm dbnm", + /* 191 */ "expr ::= EXISTS LP select RP", + /* 192 */ "expr ::= CASE case_operand case_exprlist case_else END", + /* 193 */ "case_exprlist ::= case_exprlist WHEN expr THEN expr", + /* 194 */ "case_exprlist ::= WHEN expr THEN expr", + /* 195 */ "case_else ::= ELSE expr", + /* 196 */ "case_else ::=", + /* 197 */ "case_operand ::= expr", + /* 198 */ "case_operand ::=", + /* 199 */ "exprlist ::=", + /* 200 */ "nexprlist ::= nexprlist COMMA expr", + /* 201 */ "nexprlist ::= expr", + /* 202 */ "cmd ::= createkw uniqueflag INDEX ifnotexists nm dbnm ON nm LP sortlist RP where_opt", + /* 203 */ "uniqueflag ::= UNIQUE", + /* 204 */ "uniqueflag ::=", + /* 205 */ "eidlist_opt ::=", + /* 206 */ "eidlist_opt ::= LP eidlist RP", + /* 207 */ "eidlist ::= eidlist COMMA nm collate sortorder", + /* 208 */ "eidlist ::= nm collate sortorder", + /* 209 */ "collate ::=", + /* 210 */ "collate ::= COLLATE ID|STRING", + /* 211 */ "cmd ::= DROP INDEX ifexists fullname", + /* 212 */ "cmd ::= VACUUM", + /* 213 */ "cmd ::= VACUUM nm", + /* 214 */ "cmd ::= PRAGMA nm dbnm", + /* 215 */ "cmd ::= PRAGMA nm dbnm EQ nmnum", + /* 216 */ "cmd ::= PRAGMA nm dbnm LP nmnum RP", + /* 217 */ "cmd ::= PRAGMA nm dbnm EQ minus_num", + /* 218 */ "cmd ::= PRAGMA nm dbnm LP minus_num RP", + /* 219 */ "plus_num ::= PLUS INTEGER|FLOAT", + /* 220 */ "minus_num ::= MINUS INTEGER|FLOAT", + /* 221 */ "cmd ::= createkw trigger_decl BEGIN trigger_cmd_list END", + /* 222 */ "trigger_decl ::= temp TRIGGER ifnotexists nm dbnm trigger_time trigger_event ON fullname foreach_clause when_clause", + /* 223 */ "trigger_time ::= BEFORE", + /* 224 */ "trigger_time ::= AFTER", + /* 225 */ "trigger_time ::= INSTEAD OF", + /* 226 */ "trigger_time ::=", + /* 227 */ "trigger_event ::= DELETE|INSERT", + /* 228 */ "trigger_event ::= UPDATE", + /* 229 */ "trigger_event ::= UPDATE OF idlist", + /* 230 */ "when_clause ::=", + /* 231 */ "when_clause ::= WHEN expr", + /* 232 */ "trigger_cmd_list ::= trigger_cmd_list trigger_cmd SEMI", + /* 233 */ "trigger_cmd_list ::= trigger_cmd SEMI", + /* 234 */ "trnm ::= nm DOT nm", + /* 235 */ "tridxby ::= INDEXED BY nm", + /* 236 */ "tridxby ::= NOT INDEXED", + /* 237 */ "trigger_cmd ::= UPDATE orconf trnm tridxby SET setlist where_opt", + /* 238 */ "trigger_cmd ::= insert_cmd INTO trnm idlist_opt select", + /* 239 */ "trigger_cmd ::= DELETE FROM trnm tridxby where_opt", + /* 240 */ "trigger_cmd ::= select", + /* 241 */ "expr ::= RAISE LP IGNORE RP", + /* 242 */ "expr ::= RAISE LP raisetype COMMA nm RP", + /* 243 */ "raisetype ::= ROLLBACK", + /* 244 */ "raisetype ::= ABORT", + /* 245 */ "raisetype ::= FAIL", + /* 246 */ "cmd ::= DROP TRIGGER ifexists fullname", + /* 247 */ "cmd ::= ATTACH database_kw_opt expr AS expr key_opt", + /* 248 */ "cmd ::= DETACH database_kw_opt expr", + /* 249 */ "key_opt ::=", + /* 250 */ "key_opt ::= KEY expr", + /* 251 */ "cmd ::= REINDEX", + /* 252 */ "cmd ::= REINDEX nm dbnm", + /* 253 */ "cmd ::= ANALYZE", + /* 254 */ "cmd ::= ANALYZE nm dbnm", + /* 255 */ "cmd ::= ALTER TABLE fullname RENAME TO nm", + /* 256 */ "cmd ::= ALTER TABLE add_column_fullname ADD kwcolumn_opt columnname carglist", + /* 257 */ "add_column_fullname ::= fullname", + /* 258 */ "cmd ::= create_vtab", + /* 259 */ "cmd ::= create_vtab LP vtabarglist RP", + /* 260 */ "create_vtab ::= createkw VIRTUAL TABLE ifnotexists nm dbnm USING nm", + /* 261 */ "vtabarg ::=", + /* 262 */ "vtabargtoken ::= ANY", + /* 263 */ "vtabargtoken ::= lp anylist RP", + /* 264 */ "lp ::= LP", + /* 265 */ "with ::=", + /* 266 */ "with ::= WITH wqlist", + /* 267 */ "with ::= WITH RECURSIVE wqlist", + /* 268 */ "wqlist ::= nm eidlist_opt AS LP select RP", + /* 269 */ "wqlist ::= wqlist COMMA nm eidlist_opt AS LP select RP", + /* 270 */ "input ::= cmdlist", + /* 271 */ "cmdlist ::= cmdlist ecmd", + /* 272 */ "cmdlist ::= ecmd", + /* 273 */ "ecmd ::= SEMI", + /* 274 */ "ecmd ::= explain cmdx SEMI", + /* 275 */ "explain ::=", + /* 276 */ "trans_opt ::=", + /* 277 */ "trans_opt ::= TRANSACTION", + /* 278 */ "trans_opt ::= TRANSACTION nm", + /* 279 */ "savepoint_opt ::= SAVEPOINT", + /* 280 */ "savepoint_opt ::=", + /* 281 */ "cmd ::= create_table create_table_args", + /* 282 */ "columnlist ::= columnlist COMMA columnname carglist", + /* 283 */ "columnlist ::= columnname carglist", + /* 284 */ "nm ::= ID|INDEXED", + /* 285 */ "nm ::= STRING", + /* 286 */ "nm ::= JOIN_KW", + /* 287 */ "typetoken ::= typename", + /* 288 */ "typename ::= ID|STRING", + /* 289 */ "signed ::= plus_num", + /* 290 */ "signed ::= minus_num", + /* 291 */ "carglist ::= carglist ccons", + /* 292 */ "carglist ::=", + /* 293 */ "ccons ::= NULL onconf", + /* 294 */ "conslist_opt ::= COMMA conslist", + /* 295 */ "conslist ::= conslist tconscomma tcons", + /* 296 */ "conslist ::= tcons", + /* 297 */ "tconscomma ::=", + /* 298 */ "defer_subclause_opt ::= defer_subclause", + /* 299 */ "resolvetype ::= raisetype", + /* 300 */ "selectnowith ::= oneselect", + /* 301 */ "oneselect ::= values", + /* 302 */ "sclp ::= selcollist COMMA", + /* 303 */ "as ::= ID|STRING", + /* 304 */ "expr ::= term", + /* 305 */ "exprlist ::= nexprlist", + /* 306 */ "nmnum ::= plus_num", + /* 307 */ "nmnum ::= nm", + /* 308 */ "nmnum ::= ON", + /* 309 */ "nmnum ::= DELETE", + /* 310 */ "nmnum ::= DEFAULT", + /* 311 */ "plus_num ::= INTEGER|FLOAT", + /* 312 */ "foreach_clause ::=", + /* 313 */ "foreach_clause ::= FOR EACH ROW", + /* 314 */ "trnm ::= nm", + /* 315 */ "tridxby ::=", + /* 316 */ "database_kw_opt ::= DATABASE", + /* 317 */ "database_kw_opt ::=", + /* 318 */ "kwcolumn_opt ::=", + /* 319 */ "kwcolumn_opt ::= COLUMNKW", + /* 320 */ "vtabarglist ::= vtabarg", + /* 321 */ "vtabarglist ::= vtabarglist COMMA vtabarg", + /* 322 */ "vtabarg ::= vtabarg vtabargtoken", + /* 323 */ "anylist ::=", + /* 324 */ "anylist ::= anylist LP anylist RP", + /* 325 */ "anylist ::= anylist ANY", }; #endif /* NDEBUG */ @@ -127361,6 +132150,15 @@ static void yyGrowStack(yyParser *p){ } #endif +/* Datatype of the argument to the memory allocated passed as the +** second argument to sqlite3ParserAlloc() below. This can be changed by +** putting an appropriate #define in the %include section of the input +** grammar. +*/ +#ifndef YYMALLOCARGTYPE +# define YYMALLOCARGTYPE size_t +#endif + /* ** This function allocates a new parser. ** The only argument is a pointer to a function which works like @@ -127373,9 +132171,9 @@ static void yyGrowStack(yyParser *p){ ** A pointer to a parser. This pointer is used in subsequent calls ** to sqlite3Parser and sqlite3ParserFree. */ -SQLITE_PRIVATE void *sqlite3ParserAlloc(void *(*mallocProc)(u64)){ +SQLITE_PRIVATE void *sqlite3ParserAlloc(void *(*mallocProc)(YYMALLOCARGTYPE)){ yyParser *pParser; - pParser = (yyParser*)(*mallocProc)( (u64)sizeof(yyParser) ); + pParser = (yyParser*)(*mallocProc)( (YYMALLOCARGTYPE)sizeof(yyParser) ); if( pParser ){ pParser->yyidx = -1; #ifdef YYTRACKMAXSTACKDEPTH @@ -127390,10 +132188,12 @@ SQLITE_PRIVATE void *sqlite3ParserAlloc(void *(*mallocProc)(u64)){ return pParser; } -/* The following function deletes the value associated with a -** symbol. The symbol can be either a terminal or nonterminal. -** "yymajor" is the symbol code, and "yypminor" is a pointer to -** the value. +/* The following function deletes the "minor type" or semantic value +** associated with a symbol. The symbol can be either a terminal +** or nonterminal. "yymajor" is the symbol code, and "yypminor" is +** a pointer to the value to be deleted. The code used to do the +** deletions is derived from the %destructor and/or %token_destructor +** directives of the input grammar. */ static void yy_destructor( yyParser *yypParser, /* The parser */ @@ -127409,81 +132209,83 @@ static void yy_destructor( ** being destroyed before it is finished parsing. ** ** Note: during a reduce, the only symbols destroyed are those - ** which appear on the RHS of the rule, but which are not used + ** which appear on the RHS of the rule, but which are *not* used ** inside the C code. */ +/********* Begin destructor definitions ***************************************/ case 163: /* select */ - case 196: /* selectnowith */ - case 197: /* oneselect */ - case 208: /* values */ + case 194: /* selectnowith */ + case 195: /* oneselect */ + case 206: /* values */ { -sqlite3SelectDelete(pParse->db, (yypminor->yy3)); +sqlite3SelectDelete(pParse->db, (yypminor->yy159)); } break; - case 174: /* term */ - case 175: /* expr */ + case 172: /* term */ + case 173: /* expr */ { -sqlite3ExprDelete(pParse->db, (yypminor->yy346).pExpr); +sqlite3ExprDelete(pParse->db, (yypminor->yy342).pExpr); } break; - case 179: /* eidlist_opt */ - case 188: /* sortlist */ - case 189: /* eidlist */ - case 201: /* selcollist */ - case 204: /* groupby_opt */ - case 206: /* orderby_opt */ - case 209: /* nexprlist */ - case 210: /* exprlist */ - case 211: /* sclp */ - case 221: /* setlist */ - case 228: /* case_exprlist */ + case 177: /* eidlist_opt */ + case 186: /* sortlist */ + case 187: /* eidlist */ + case 199: /* selcollist */ + case 202: /* groupby_opt */ + case 204: /* orderby_opt */ + case 207: /* nexprlist */ + case 208: /* exprlist */ + case 209: /* sclp */ + case 218: /* setlist */ + case 225: /* case_exprlist */ { -sqlite3ExprListDelete(pParse->db, (yypminor->yy14)); +sqlite3ExprListDelete(pParse->db, (yypminor->yy442)); } break; - case 195: /* fullname */ - case 202: /* from */ - case 213: /* seltablist */ - case 214: /* stl_prefix */ + case 193: /* fullname */ + case 200: /* from */ + case 211: /* seltablist */ + case 212: /* stl_prefix */ { -sqlite3SrcListDelete(pParse->db, (yypminor->yy65)); +sqlite3SrcListDelete(pParse->db, (yypminor->yy347)); } break; - case 198: /* with */ - case 252: /* wqlist */ + case 196: /* with */ + case 249: /* wqlist */ { -sqlite3WithDelete(pParse->db, (yypminor->yy59)); +sqlite3WithDelete(pParse->db, (yypminor->yy331)); } break; - case 203: /* where_opt */ - case 205: /* having_opt */ - case 217: /* on_opt */ - case 227: /* case_operand */ - case 229: /* case_else */ - case 238: /* when_clause */ - case 243: /* key_opt */ + case 201: /* where_opt */ + case 203: /* having_opt */ + case 215: /* on_opt */ + case 224: /* case_operand */ + case 226: /* case_else */ + case 235: /* when_clause */ + case 240: /* key_opt */ { -sqlite3ExprDelete(pParse->db, (yypminor->yy132)); +sqlite3ExprDelete(pParse->db, (yypminor->yy122)); } break; - case 218: /* using_opt */ - case 220: /* idlist */ - case 223: /* idlist_opt */ + case 216: /* using_opt */ + case 217: /* idlist */ + case 220: /* idlist_opt */ { -sqlite3IdListDelete(pParse->db, (yypminor->yy408)); +sqlite3IdListDelete(pParse->db, (yypminor->yy180)); } break; - case 234: /* trigger_cmd_list */ - case 239: /* trigger_cmd */ + case 231: /* trigger_cmd_list */ + case 236: /* trigger_cmd */ { -sqlite3DeleteTriggerStep(pParse->db, (yypminor->yy473)); +sqlite3DeleteTriggerStep(pParse->db, (yypminor->yy327)); } break; - case 236: /* trigger_event */ + case 233: /* trigger_event */ { -sqlite3IdListDelete(pParse->db, (yypminor->yy378).b); +sqlite3IdListDelete(pParse->db, (yypminor->yy410).b); } break; +/********* End destructor definitions *****************************************/ default: break; /* If no destructor action specified: do nothing */ } } @@ -127493,49 +132295,37 @@ sqlite3IdListDelete(pParse->db, (yypminor->yy378).b); ** ** If there is a destructor routine associated with the token which ** is popped from the stack, then call it. -** -** Return the major token number for the symbol popped. */ -static int yy_pop_parser_stack(yyParser *pParser){ - YYCODETYPE yymajor; - yyStackEntry *yytos = &pParser->yystack[pParser->yyidx]; - - /* There is no mechanism by which the parser stack can be popped below - ** empty in SQLite. */ +static void yy_pop_parser_stack(yyParser *pParser){ + yyStackEntry *yytos; assert( pParser->yyidx>=0 ); + yytos = &pParser->yystack[pParser->yyidx--]; #ifndef NDEBUG - if( yyTraceFILE && pParser->yyidx>=0 ){ + if( yyTraceFILE ){ fprintf(yyTraceFILE,"%sPopping %s\n", yyTracePrompt, yyTokenName[yytos->major]); } #endif - yymajor = yytos->major; - yy_destructor(pParser, yymajor, &yytos->minor); - pParser->yyidx--; - return yymajor; + yy_destructor(pParser, yytos->major, &yytos->minor); } /* -** Deallocate and destroy a parser. Destructors are all called for +** Deallocate and destroy a parser. Destructors are called for ** all stack elements before shutting the parser down. ** -** Inputs: -**
      -**
    • A pointer to the parser. This should be a pointer -** obtained from sqlite3ParserAlloc. -**
    • A pointer to a function used to reclaim memory obtained -** from malloc. -**
    +** If the YYPARSEFREENEVERNULL macro exists (for example because it +** is defined in a %include section of the input grammar) then it is +** assumed that the input pointer is never NULL. */ SQLITE_PRIVATE void sqlite3ParserFree( void *p, /* The parser to be deleted */ void (*freeProc)(void*) /* Function used to reclaim memory */ ){ yyParser *pParser = (yyParser*)p; - /* In SQLite, we never try to destroy a parser that was not successfully - ** created in the first place. */ - if( NEVER(pParser==0) ) return; +#ifndef YYPARSEFREENEVERNULL + if( pParser==0 ) return; +#endif while( pParser->yyidx>=0 ) yy_pop_parser_stack(pParser); #if YYSTACKDEPTH<=0 free(pParser->yystack); @@ -127556,12 +132346,8 @@ SQLITE_PRIVATE int sqlite3ParserStackPeak(void *p){ /* ** Find the appropriate action for a parser given the terminal ** look-ahead token iLookAhead. -** -** If the look-ahead token is YYNOCODE, then check to see if the action is -** independent of the look-ahead. If it is, return the action, otherwise -** return YY_NO_ACTION. */ -static int yy_find_shift_action( +static unsigned int yy_find_shift_action( yyParser *pParser, /* The parser */ YYCODETYPE iLookAhead /* The look-ahead token */ ){ @@ -127570,61 +132356,62 @@ static int yy_find_shift_action( if( stateno>=YY_MIN_REDUCE ) return stateno; assert( stateno <= YY_SHIFT_COUNT ); - i = yy_shift_ofst[stateno]; - if( i==YY_SHIFT_USE_DFLT ) return yy_default[stateno]; - assert( iLookAhead!=YYNOCODE ); - i += iLookAhead; - if( i<0 || i>=YY_ACTTAB_COUNT || yy_lookahead[i]!=iLookAhead ){ - if( iLookAhead>0 ){ + do{ + i = yy_shift_ofst[stateno]; + if( i==YY_SHIFT_USE_DFLT ) return yy_default[stateno]; + assert( iLookAhead!=YYNOCODE ); + i += iLookAhead; + if( i<0 || i>=YY_ACTTAB_COUNT || yy_lookahead[i]!=iLookAhead ){ + if( iLookAhead>0 ){ #ifdef YYFALLBACK - YYCODETYPE iFallback; /* Fallback token */ - if( iLookAhead %s\n", - yyTracePrompt, yyTokenName[iLookAhead], yyTokenName[iFallback]); - } -#endif - return yy_find_shift_action(pParser, iFallback); - } -#endif -#ifdef YYWILDCARD - { - int j = i - iLookAhead + YYWILDCARD; - if( -#if YY_SHIFT_MIN+YYWILDCARD<0 - j>=0 && -#endif -#if YY_SHIFT_MAX+YYWILDCARD>=YY_ACTTAB_COUNT - j %s\n", - yyTracePrompt, yyTokenName[iLookAhead], yyTokenName[YYWILDCARD]); + fprintf(yyTraceFILE, "%sFALLBACK %s => %s\n", + yyTracePrompt, yyTokenName[iLookAhead], yyTokenName[iFallback]); + } +#endif + assert( yyFallback[iFallback]==0 ); /* Fallback loop must terminate */ + iLookAhead = iFallback; + continue; + } +#endif +#ifdef YYWILDCARD + { + int j = i - iLookAhead + YYWILDCARD; + if( +#if YY_SHIFT_MIN+YYWILDCARD<0 + j>=0 && +#endif +#if YY_SHIFT_MAX+YYWILDCARD>=YY_ACTTAB_COUNT + j %s\n", + yyTracePrompt, yyTokenName[iLookAhead], + yyTokenName[YYWILDCARD]); + } +#endif /* NDEBUG */ + return yy_action[j]; } -#endif /* NDEBUG */ - return yy_action[j]; } - } #endif /* YYWILDCARD */ + } + return yy_default[stateno]; + }else{ + return yy_action[i]; } - return yy_default[stateno]; - }else{ - return yy_action[i]; - } + }while(1); } /* ** Find the appropriate action for a parser given the non-terminal ** look-ahead token iLookAhead. -** -** If the look-ahead token is YYNOCODE, then check to see if the action is -** independent of the look-ahead. If it is, return the action, otherwise -** return YY_NO_ACTION. */ static int yy_find_reduce_action( int stateno, /* Current state number */ @@ -127656,7 +132443,7 @@ static int yy_find_reduce_action( /* ** The following routine is called if the stack overflows. */ -static void yyStackOverflow(yyParser *yypParser, YYMINORTYPE *yypMinor){ +static void yyStackOverflow(yyParser *yypParser){ sqlite3ParserARG_FETCH; yypParser->yyidx--; #ifndef NDEBUG @@ -127667,9 +132454,10 @@ static void yyStackOverflow(yyParser *yypParser, YYMINORTYPE *yypMinor){ while( yypParser->yyidx>=0 ) yy_pop_parser_stack(yypParser); /* Here code is inserted which will execute if the parser ** stack every overflows */ +/******** Begin %stack_overflow code ******************************************/ - UNUSED_PARAMETER(yypMinor); /* Silence some compiler warnings */ sqlite3ErrorMsg(pParse, "parser stack overflow"); +/******** End %stack_overflow code ********************************************/ sqlite3ParserARG_STORE; /* Suppress warning about unused %extra_argument var */ } @@ -127679,15 +132467,13 @@ static void yyStackOverflow(yyParser *yypParser, YYMINORTYPE *yypMinor){ #ifndef NDEBUG static void yyTraceShift(yyParser *yypParser, int yyNewState){ if( yyTraceFILE ){ - int i; if( yyNewStateyyidx; i++) - fprintf(yyTraceFILE," %s",yyTokenName[yypParser->yystack[i].major]); - fprintf(yyTraceFILE,"\n"); + fprintf(yyTraceFILE,"%sShift '%s', go to state %d\n", + yyTracePrompt,yyTokenName[yypParser->yystack[yypParser->yyidx].major], + yyNewState); }else{ - fprintf(yyTraceFILE,"%sShift *\n",yyTracePrompt); + fprintf(yyTraceFILE,"%sShift '%s'\n", + yyTracePrompt,yyTokenName[yypParser->yystack[yypParser->yyidx].major]); } } } @@ -127696,13 +132482,13 @@ static void yyTraceShift(yyParser *yypParser, int yyNewState){ #endif /* -** Perform a shift action. Return the number of errors. +** Perform a shift action. */ static void yy_shift( yyParser *yypParser, /* The parser to be shifted */ int yyNewState, /* The new state to shift in */ int yyMajor, /* The major token to shift in */ - YYMINORTYPE *yypMinor /* Pointer to the minor token to shift in */ + sqlite3ParserTOKENTYPE yyMinor /* The minor token to shift in */ ){ yyStackEntry *yytos; yypParser->yyidx++; @@ -127713,14 +132499,14 @@ static void yy_shift( #endif #if YYSTACKDEPTH>0 if( yypParser->yyidx>=YYSTACKDEPTH ){ - yyStackOverflow(yypParser, yypMinor); + yyStackOverflow(yypParser); return; } #else if( yypParser->yyidx>=yypParser->yystksz ){ yyGrowStack(yypParser); if( yypParser->yyidx>=yypParser->yystksz ){ - yyStackOverflow(yypParser, yypMinor); + yyStackOverflow(yypParser); return; } } @@ -127728,7 +132514,7 @@ static void yy_shift( yytos = &yypParser->yystack[yypParser->yyidx]; yytos->stateno = (YYACTIONTYPE)yyNewState; yytos->major = (YYCODETYPE)yyMajor; - yytos->minor = *yypMinor; + yytos->minor.yy0 = yyMinor; yyTraceShift(yypParser, yyNewState); } @@ -127739,19 +132525,10 @@ static const struct { YYCODETYPE lhs; /* Symbol on the left-hand side of the rule */ unsigned char nrhs; /* Number of right-hand side symbols in the rule */ } yyRuleInfo[] = { - { 144, 1 }, - { 145, 2 }, - { 145, 1 }, - { 146, 1 }, - { 146, 3 }, - { 147, 0 }, { 147, 1 }, { 147, 3 }, { 148, 1 }, { 149, 3 }, - { 151, 0 }, - { 151, 1 }, - { 151, 2 }, { 150, 0 }, { 150, 1 }, { 150, 1 }, @@ -127759,12 +132536,9 @@ static const struct { { 149, 2 }, { 149, 2 }, { 149, 2 }, - { 153, 1 }, - { 153, 0 }, { 149, 2 }, { 149, 3 }, { 149, 5 }, - { 149, 2 }, { 154, 6 }, { 156, 1 }, { 158, 0 }, @@ -127775,219 +132549,193 @@ static const struct { { 155, 2 }, { 162, 0 }, { 162, 2 }, - { 160, 3 }, - { 160, 1 }, - { 164, 3 }, - { 165, 1 }, - { 152, 1 }, - { 152, 1 }, - { 152, 1 }, + { 164, 2 }, { 166, 0 }, - { 166, 1 }, - { 168, 1 }, - { 168, 4 }, - { 168, 6 }, - { 169, 1 }, - { 169, 2 }, - { 170, 1 }, - { 170, 1 }, + { 166, 4 }, + { 166, 6 }, { 167, 2 }, - { 167, 0 }, - { 173, 2 }, - { 173, 2 }, - { 173, 4 }, - { 173, 3 }, - { 173, 3 }, - { 173, 2 }, - { 173, 2 }, - { 173, 3 }, - { 173, 5 }, - { 173, 2 }, - { 173, 4 }, - { 173, 4 }, - { 173, 1 }, - { 173, 2 }, - { 178, 0 }, - { 178, 1 }, - { 180, 0 }, - { 180, 2 }, - { 182, 2 }, - { 182, 3 }, - { 182, 3 }, - { 182, 3 }, - { 183, 2 }, - { 183, 2 }, - { 183, 1 }, - { 183, 1 }, - { 183, 2 }, - { 181, 3 }, - { 181, 2 }, - { 184, 0 }, - { 184, 2 }, - { 184, 2 }, - { 161, 0 }, - { 161, 2 }, - { 185, 3 }, - { 185, 1 }, - { 186, 1 }, - { 186, 0 }, - { 187, 2 }, - { 187, 7 }, - { 187, 5 }, - { 187, 5 }, - { 187, 10 }, - { 190, 0 }, - { 190, 1 }, + { 171, 2 }, + { 171, 2 }, + { 171, 4 }, + { 171, 3 }, + { 171, 3 }, + { 171, 2 }, + { 171, 3 }, + { 171, 5 }, + { 171, 2 }, + { 171, 4 }, + { 171, 4 }, + { 171, 1 }, + { 171, 2 }, { 176, 0 }, - { 176, 3 }, - { 191, 0 }, - { 191, 2 }, - { 192, 1 }, - { 192, 1 }, - { 192, 1 }, + { 176, 1 }, + { 178, 0 }, + { 178, 2 }, + { 180, 2 }, + { 180, 3 }, + { 180, 3 }, + { 180, 3 }, + { 181, 2 }, + { 181, 2 }, + { 181, 1 }, + { 181, 1 }, + { 181, 2 }, + { 179, 3 }, + { 179, 2 }, + { 182, 0 }, + { 182, 2 }, + { 182, 2 }, + { 161, 0 }, + { 184, 1 }, + { 185, 2 }, + { 185, 7 }, + { 185, 5 }, + { 185, 5 }, + { 185, 10 }, + { 188, 0 }, + { 174, 0 }, + { 174, 3 }, + { 189, 0 }, + { 189, 2 }, + { 190, 1 }, + { 190, 1 }, { 149, 4 }, - { 194, 2 }, - { 194, 0 }, + { 192, 2 }, + { 192, 0 }, { 149, 9 }, { 149, 4 }, { 149, 1 }, { 163, 2 }, - { 196, 1 }, - { 196, 3 }, - { 199, 1 }, - { 199, 2 }, - { 199, 1 }, - { 197, 9 }, + { 194, 3 }, { 197, 1 }, - { 208, 4 }, - { 208, 5 }, - { 200, 1 }, - { 200, 1 }, + { 197, 2 }, + { 197, 1 }, + { 195, 9 }, + { 206, 4 }, + { 206, 5 }, + { 198, 1 }, + { 198, 1 }, + { 198, 0 }, + { 209, 0 }, + { 199, 3 }, + { 199, 2 }, + { 199, 4 }, + { 210, 2 }, + { 210, 0 }, { 200, 0 }, - { 211, 2 }, - { 211, 0 }, - { 201, 3 }, - { 201, 2 }, - { 201, 4 }, + { 200, 2 }, { 212, 2 }, - { 212, 1 }, { 212, 0 }, - { 202, 0 }, - { 202, 2 }, - { 214, 2 }, - { 214, 0 }, - { 213, 7 }, - { 213, 9 }, - { 213, 7 }, - { 213, 7 }, + { 211, 7 }, + { 211, 9 }, + { 211, 7 }, + { 211, 7 }, { 159, 0 }, { 159, 2 }, - { 195, 2 }, - { 215, 1 }, + { 193, 2 }, + { 213, 1 }, + { 213, 2 }, + { 213, 3 }, + { 213, 4 }, { 215, 2 }, - { 215, 3 }, - { 215, 4 }, - { 217, 2 }, - { 217, 0 }, + { 215, 0 }, + { 214, 0 }, + { 214, 3 }, + { 214, 2 }, + { 216, 4 }, { 216, 0 }, - { 216, 3 }, - { 216, 2 }, - { 218, 4 }, - { 218, 0 }, - { 206, 0 }, - { 206, 3 }, - { 188, 4 }, - { 188, 2 }, - { 177, 1 }, - { 177, 1 }, - { 177, 0 }, { 204, 0 }, { 204, 3 }, - { 205, 0 }, - { 205, 2 }, - { 207, 0 }, - { 207, 2 }, - { 207, 4 }, - { 207, 4 }, - { 149, 6 }, + { 186, 4 }, + { 186, 2 }, + { 175, 1 }, + { 175, 1 }, + { 175, 0 }, + { 202, 0 }, + { 202, 3 }, { 203, 0 }, { 203, 2 }, + { 205, 0 }, + { 205, 2 }, + { 205, 4 }, + { 205, 4 }, + { 149, 6 }, + { 201, 0 }, + { 201, 2 }, { 149, 8 }, - { 221, 5 }, - { 221, 3 }, + { 218, 5 }, + { 218, 3 }, { 149, 6 }, { 149, 7 }, - { 222, 2 }, - { 222, 1 }, - { 223, 0 }, - { 223, 3 }, + { 219, 2 }, + { 219, 1 }, + { 220, 0 }, { 220, 3 }, - { 220, 1 }, - { 175, 1 }, - { 175, 3 }, - { 174, 1 }, - { 175, 1 }, - { 175, 1 }, - { 175, 3 }, - { 175, 5 }, - { 174, 1 }, - { 174, 1 }, - { 175, 1 }, - { 175, 3 }, - { 175, 6 }, - { 175, 5 }, - { 175, 4 }, - { 174, 1 }, - { 175, 3 }, - { 175, 3 }, - { 175, 3 }, - { 175, 3 }, - { 175, 3 }, - { 175, 3 }, - { 175, 3 }, - { 175, 3 }, - { 224, 1 }, - { 224, 2 }, - { 175, 3 }, - { 175, 5 }, - { 175, 2 }, - { 175, 3 }, - { 175, 3 }, - { 175, 4 }, - { 175, 2 }, - { 175, 2 }, - { 175, 2 }, - { 175, 2 }, - { 225, 1 }, - { 225, 2 }, - { 175, 5 }, - { 226, 1 }, + { 217, 3 }, + { 217, 1 }, + { 173, 3 }, + { 172, 1 }, + { 173, 1 }, + { 173, 1 }, + { 173, 3 }, + { 173, 5 }, + { 172, 1 }, + { 172, 1 }, + { 173, 1 }, + { 173, 3 }, + { 173, 6 }, + { 173, 5 }, + { 173, 4 }, + { 172, 1 }, + { 173, 3 }, + { 173, 3 }, + { 173, 3 }, + { 173, 3 }, + { 173, 3 }, + { 173, 3 }, + { 173, 3 }, + { 173, 3 }, + { 221, 1 }, + { 221, 2 }, + { 173, 3 }, + { 173, 5 }, + { 173, 2 }, + { 173, 3 }, + { 173, 3 }, + { 173, 4 }, + { 173, 2 }, + { 173, 2 }, + { 173, 2 }, + { 173, 2 }, + { 222, 1 }, + { 222, 2 }, + { 173, 5 }, + { 223, 1 }, + { 223, 2 }, + { 173, 5 }, + { 173, 3 }, + { 173, 5 }, + { 173, 4 }, + { 173, 4 }, + { 173, 5 }, + { 225, 5 }, + { 225, 4 }, { 226, 2 }, - { 175, 5 }, - { 175, 3 }, - { 175, 5 }, - { 175, 4 }, - { 175, 4 }, - { 175, 5 }, - { 228, 5 }, - { 228, 4 }, - { 229, 2 }, - { 229, 0 }, + { 226, 0 }, + { 224, 1 }, + { 224, 0 }, + { 208, 0 }, + { 207, 3 }, + { 207, 1 }, + { 149, 12 }, { 227, 1 }, { 227, 0 }, - { 210, 1 }, - { 210, 0 }, - { 209, 3 }, - { 209, 1 }, - { 149, 12 }, - { 230, 1 }, - { 230, 0 }, - { 179, 0 }, - { 179, 3 }, - { 189, 5 }, - { 189, 3 }, - { 231, 0 }, - { 231, 2 }, + { 177, 0 }, + { 177, 3 }, + { 187, 5 }, + { 187, 3 }, + { 228, 0 }, + { 228, 2 }, { 149, 4 }, { 149, 1 }, { 149, 2 }, @@ -127996,77 +132744,113 @@ static const struct { { 149, 6 }, { 149, 5 }, { 149, 6 }, - { 232, 1 }, - { 232, 1 }, - { 232, 1 }, - { 232, 1 }, - { 232, 1 }, - { 171, 2 }, - { 171, 1 }, - { 172, 2 }, + { 169, 2 }, + { 170, 2 }, { 149, 5 }, - { 233, 11 }, - { 235, 1 }, - { 235, 1 }, - { 235, 2 }, + { 230, 11 }, + { 232, 1 }, + { 232, 1 }, + { 232, 2 }, + { 232, 0 }, + { 233, 1 }, + { 233, 1 }, + { 233, 3 }, { 235, 0 }, - { 236, 1 }, - { 236, 1 }, - { 236, 3 }, - { 237, 0 }, + { 235, 2 }, + { 231, 3 }, + { 231, 2 }, { 237, 3 }, - { 238, 0 }, + { 238, 3 }, { 238, 2 }, - { 234, 3 }, - { 234, 2 }, - { 240, 1 }, - { 240, 3 }, - { 241, 0 }, - { 241, 3 }, - { 241, 2 }, - { 239, 7 }, - { 239, 5 }, - { 239, 5 }, - { 239, 1 }, - { 175, 4 }, - { 175, 6 }, - { 193, 1 }, - { 193, 1 }, - { 193, 1 }, + { 236, 7 }, + { 236, 5 }, + { 236, 5 }, + { 236, 1 }, + { 173, 4 }, + { 173, 6 }, + { 191, 1 }, + { 191, 1 }, + { 191, 1 }, { 149, 4 }, { 149, 6 }, { 149, 3 }, - { 243, 0 }, - { 243, 2 }, - { 242, 1 }, - { 242, 0 }, + { 240, 0 }, + { 240, 2 }, { 149, 1 }, { 149, 3 }, { 149, 1 }, { 149, 3 }, { 149, 6 }, - { 149, 6 }, - { 244, 1 }, + { 149, 7 }, + { 241, 1 }, + { 149, 1 }, + { 149, 4 }, + { 243, 8 }, { 245, 0 }, - { 245, 1 }, - { 149, 1 }, - { 149, 4 }, - { 246, 8 }, + { 246, 1 }, + { 246, 3 }, { 247, 1 }, - { 247, 3 }, + { 196, 0 }, + { 196, 2 }, + { 196, 3 }, + { 249, 6 }, + { 249, 8 }, + { 144, 1 }, + { 145, 2 }, + { 145, 1 }, + { 146, 1 }, + { 146, 3 }, + { 147, 0 }, + { 151, 0 }, + { 151, 1 }, + { 151, 2 }, + { 153, 1 }, + { 153, 0 }, + { 149, 2 }, + { 160, 4 }, + { 160, 2 }, + { 152, 1 }, + { 152, 1 }, + { 152, 1 }, + { 166, 1 }, + { 167, 1 }, + { 168, 1 }, + { 168, 1 }, + { 165, 2 }, + { 165, 0 }, + { 171, 2 }, + { 161, 2 }, + { 183, 3 }, + { 183, 1 }, + { 184, 0 }, + { 188, 1 }, + { 190, 1 }, + { 194, 1 }, + { 195, 1 }, + { 209, 2 }, + { 210, 1 }, + { 173, 1 }, + { 208, 1 }, + { 229, 1 }, + { 229, 1 }, + { 229, 1 }, + { 229, 1 }, + { 229, 1 }, + { 169, 1 }, + { 234, 0 }, + { 234, 3 }, + { 237, 1 }, + { 238, 0 }, + { 239, 1 }, + { 239, 0 }, + { 242, 0 }, + { 242, 1 }, + { 244, 1 }, + { 244, 3 }, + { 245, 2 }, { 248, 0 }, + { 248, 4 }, { 248, 2 }, - { 249, 1 }, - { 249, 3 }, - { 250, 1 }, - { 251, 0 }, - { 251, 4 }, - { 251, 2 }, - { 198, 0 }, - { 198, 2 }, - { 198, 3 }, - { 252, 6 }, - { 252, 8 }, }; static void yy_accept(yyParser*); /* Forward Declaration */ @@ -128077,41 +132861,46 @@ static void yy_accept(yyParser*); /* Forward Declaration */ */ static void yy_reduce( yyParser *yypParser, /* The parser */ - int yyruleno /* Number of the rule by which to reduce */ + unsigned int yyruleno /* Number of the rule by which to reduce */ ){ int yygoto; /* The next state */ int yyact; /* The next action */ - YYMINORTYPE yygotominor; /* The LHS of the rule reduced */ yyStackEntry *yymsp; /* The top of the parser's stack */ int yysize; /* Amount to pop the stack */ sqlite3ParserARG_FETCH; yymsp = &yypParser->yystack[yypParser->yyidx]; #ifndef NDEBUG - if( yyTraceFILE && yyruleno>=0 - && yyruleno<(int)(sizeof(yyRuleName)/sizeof(yyRuleName[0])) ){ + if( yyTraceFILE && yyruleno<(int)(sizeof(yyRuleName)/sizeof(yyRuleName[0])) ){ yysize = yyRuleInfo[yyruleno].nrhs; - fprintf(yyTraceFILE, "%sReduce [%s] -> state %d.\n", yyTracePrompt, + fprintf(yyTraceFILE, "%sReduce [%s], go to state %d.\n", yyTracePrompt, yyRuleName[yyruleno], yymsp[-yysize].stateno); } #endif /* NDEBUG */ - /* Silence complaints from purify about yygotominor being uninitialized - ** in some cases when it is copied into the stack after the following - ** switch. yygotominor is uninitialized when a rule reduces that does - ** not set the value of its left-hand side nonterminal. Leaving the - ** value of the nonterminal uninitialized is utterly harmless as long - ** as the value is never used. So really the only thing this code - ** accomplishes is to quieten purify. - ** - ** 2007-01-16: The wireshark project (www.wireshark.org) reports that - ** without this code, their parser segfaults. I'm not sure what there - ** parser is doing to make this happen. This is the second bug report - ** from wireshark this week. Clearly they are stressing Lemon in ways - ** that it has not been previously stressed... (SQLite ticket #2172) - */ - /*memset(&yygotominor, 0, sizeof(yygotominor));*/ - yygotominor = yyzerominor; - + /* Check that the stack is large enough to grow by a single entry + ** if the RHS of the rule is empty. This ensures that there is room + ** enough on the stack to push the LHS value */ + if( yyRuleInfo[yyruleno].nrhs==0 ){ +#ifdef YYTRACKMAXSTACKDEPTH + if( yypParser->yyidx>yypParser->yyidxMax ){ + yypParser->yyidxMax = yypParser->yyidx; + } +#endif +#if YYSTACKDEPTH>0 + if( yypParser->yyidx>=YYSTACKDEPTH-1 ){ + yyStackOverflow(yypParser); + return; + } +#else + if( yypParser->yyidx>=yypParser->yystksz-1 ){ + yyGrowStack(yypParser); + if( yypParser->yyidx>=yypParser->yystksz-1 ){ + yyStackOverflow(yypParser); + return; + } + } +#endif + } switch( yyruleno ){ /* Beginning here are the reduction cases. A typical example @@ -128122,328 +132911,287 @@ static void yy_reduce( ** #line ** break; */ - case 5: /* explain ::= */ -{ sqlite3BeginParse(pParse, 0); } +/********** Begin reduce actions **********************************************/ + YYMINORTYPE yylhsminor; + case 0: /* explain ::= EXPLAIN */ +{ pParse->explain = 1; } break; - case 6: /* explain ::= EXPLAIN */ -{ sqlite3BeginParse(pParse, 1); } + case 1: /* explain ::= EXPLAIN QUERY PLAN */ +{ pParse->explain = 2; } break; - case 7: /* explain ::= EXPLAIN QUERY PLAN */ -{ sqlite3BeginParse(pParse, 2); } - break; - case 8: /* cmdx ::= cmd */ + case 2: /* cmdx ::= cmd */ { sqlite3FinishCoding(pParse); } break; - case 9: /* cmd ::= BEGIN transtype trans_opt */ -{sqlite3BeginTransaction(pParse, yymsp[-1].minor.yy328);} + case 3: /* cmd ::= BEGIN transtype trans_opt */ +{sqlite3BeginTransaction(pParse, yymsp[-1].minor.yy392);} break; - case 13: /* transtype ::= */ -{yygotominor.yy328 = TK_DEFERRED;} + case 4: /* transtype ::= */ +{yymsp[1].minor.yy392 = TK_DEFERRED;} break; - case 14: /* transtype ::= DEFERRED */ - case 15: /* transtype ::= IMMEDIATE */ yytestcase(yyruleno==15); - case 16: /* transtype ::= EXCLUSIVE */ yytestcase(yyruleno==16); - case 115: /* multiselect_op ::= UNION */ yytestcase(yyruleno==115); - case 117: /* multiselect_op ::= EXCEPT|INTERSECT */ yytestcase(yyruleno==117); -{yygotominor.yy328 = yymsp[0].major;} + case 5: /* transtype ::= DEFERRED */ + case 6: /* transtype ::= IMMEDIATE */ yytestcase(yyruleno==6); + case 7: /* transtype ::= EXCLUSIVE */ yytestcase(yyruleno==7); +{yymsp[0].minor.yy392 = yymsp[0].major; /*A-overwrites-X*/} break; - case 17: /* cmd ::= COMMIT trans_opt */ - case 18: /* cmd ::= END trans_opt */ yytestcase(yyruleno==18); + case 8: /* cmd ::= COMMIT trans_opt */ + case 9: /* cmd ::= END trans_opt */ yytestcase(yyruleno==9); {sqlite3CommitTransaction(pParse);} break; - case 19: /* cmd ::= ROLLBACK trans_opt */ + case 10: /* cmd ::= ROLLBACK trans_opt */ {sqlite3RollbackTransaction(pParse);} break; - case 22: /* cmd ::= SAVEPOINT nm */ + case 11: /* cmd ::= SAVEPOINT nm */ { sqlite3Savepoint(pParse, SAVEPOINT_BEGIN, &yymsp[0].minor.yy0); } break; - case 23: /* cmd ::= RELEASE savepoint_opt nm */ + case 12: /* cmd ::= RELEASE savepoint_opt nm */ { sqlite3Savepoint(pParse, SAVEPOINT_RELEASE, &yymsp[0].minor.yy0); } break; - case 24: /* cmd ::= ROLLBACK trans_opt TO savepoint_opt nm */ + case 13: /* cmd ::= ROLLBACK trans_opt TO savepoint_opt nm */ { sqlite3Savepoint(pParse, SAVEPOINT_ROLLBACK, &yymsp[0].minor.yy0); } break; - case 26: /* create_table ::= createkw temp TABLE ifnotexists nm dbnm */ + case 14: /* create_table ::= createkw temp TABLE ifnotexists nm dbnm */ { - sqlite3StartTable(pParse,&yymsp[-1].minor.yy0,&yymsp[0].minor.yy0,yymsp[-4].minor.yy328,0,0,yymsp[-2].minor.yy328); + sqlite3StartTable(pParse,&yymsp[-1].minor.yy0,&yymsp[0].minor.yy0,yymsp[-4].minor.yy392,0,0,yymsp[-2].minor.yy392); } break; - case 27: /* createkw ::= CREATE */ + case 15: /* createkw ::= CREATE */ +{disableLookaside(pParse);} + break; + case 16: /* ifnotexists ::= */ + case 19: /* temp ::= */ yytestcase(yyruleno==19); + case 22: /* table_options ::= */ yytestcase(yyruleno==22); + case 42: /* autoinc ::= */ yytestcase(yyruleno==42); + case 57: /* init_deferred_pred_opt ::= */ yytestcase(yyruleno==57); + case 67: /* defer_subclause_opt ::= */ yytestcase(yyruleno==67); + case 76: /* ifexists ::= */ yytestcase(yyruleno==76); + case 90: /* distinct ::= */ yytestcase(yyruleno==90); + case 209: /* collate ::= */ yytestcase(yyruleno==209); +{yymsp[1].minor.yy392 = 0;} + break; + case 17: /* ifnotexists ::= IF NOT EXISTS */ +{yymsp[-2].minor.yy392 = 1;} + break; + case 18: /* temp ::= TEMP */ + case 43: /* autoinc ::= AUTOINCR */ yytestcase(yyruleno==43); +{yymsp[0].minor.yy392 = 1;} + break; + case 20: /* create_table_args ::= LP columnlist conslist_opt RP table_options */ { - pParse->db->lookaside.bEnabled = 0; - yygotominor.yy0 = yymsp[0].minor.yy0; + sqlite3EndTable(pParse,&yymsp[-2].minor.yy0,&yymsp[-1].minor.yy0,yymsp[0].minor.yy392,0); } break; - case 28: /* ifnotexists ::= */ - case 31: /* temp ::= */ yytestcase(yyruleno==31); - case 68: /* autoinc ::= */ yytestcase(yyruleno==68); - case 81: /* defer_subclause ::= NOT DEFERRABLE init_deferred_pred_opt */ yytestcase(yyruleno==81); - case 83: /* init_deferred_pred_opt ::= */ yytestcase(yyruleno==83); - case 85: /* init_deferred_pred_opt ::= INITIALLY IMMEDIATE */ yytestcase(yyruleno==85); - case 97: /* defer_subclause_opt ::= */ yytestcase(yyruleno==97); - case 108: /* ifexists ::= */ yytestcase(yyruleno==108); - case 219: /* between_op ::= BETWEEN */ yytestcase(yyruleno==219); - case 222: /* in_op ::= IN */ yytestcase(yyruleno==222); - case 247: /* collate ::= */ yytestcase(yyruleno==247); -{yygotominor.yy328 = 0;} - break; - case 29: /* ifnotexists ::= IF NOT EXISTS */ - case 30: /* temp ::= TEMP */ yytestcase(yyruleno==30); - case 69: /* autoinc ::= AUTOINCR */ yytestcase(yyruleno==69); - case 84: /* init_deferred_pred_opt ::= INITIALLY DEFERRED */ yytestcase(yyruleno==84); - case 107: /* ifexists ::= IF EXISTS */ yytestcase(yyruleno==107); - case 220: /* between_op ::= NOT BETWEEN */ yytestcase(yyruleno==220); - case 223: /* in_op ::= NOT IN */ yytestcase(yyruleno==223); - case 248: /* collate ::= COLLATE ID|STRING */ yytestcase(yyruleno==248); -{yygotominor.yy328 = 1;} - break; - case 32: /* create_table_args ::= LP columnlist conslist_opt RP table_options */ + case 21: /* create_table_args ::= AS select */ { - sqlite3EndTable(pParse,&yymsp[-2].minor.yy0,&yymsp[-1].minor.yy0,yymsp[0].minor.yy186,0); + sqlite3EndTable(pParse,0,0,0,yymsp[0].minor.yy159); + sqlite3SelectDelete(pParse->db, yymsp[0].minor.yy159); } break; - case 33: /* create_table_args ::= AS select */ -{ - sqlite3EndTable(pParse,0,0,0,yymsp[0].minor.yy3); - sqlite3SelectDelete(pParse->db, yymsp[0].minor.yy3); -} - break; - case 34: /* table_options ::= */ -{yygotominor.yy186 = 0;} - break; - case 35: /* table_options ::= WITHOUT nm */ + case 23: /* table_options ::= WITHOUT nm */ { if( yymsp[0].minor.yy0.n==5 && sqlite3_strnicmp(yymsp[0].minor.yy0.z,"rowid",5)==0 ){ - yygotominor.yy186 = TF_WithoutRowid | TF_NoVisibleRowid; + yymsp[-1].minor.yy392 = TF_WithoutRowid | TF_NoVisibleRowid; }else{ - yygotominor.yy186 = 0; + yymsp[-1].minor.yy392 = 0; sqlite3ErrorMsg(pParse, "unknown table option: %.*s", yymsp[0].minor.yy0.n, yymsp[0].minor.yy0.z); } } break; - case 38: /* column ::= columnid type carglist */ + case 24: /* columnname ::= nm typetoken */ +{sqlite3AddColumn(pParse,&yymsp[-1].minor.yy0,&yymsp[0].minor.yy0);} + break; + case 25: /* typetoken ::= */ + case 60: /* conslist_opt ::= */ yytestcase(yyruleno==60); + case 96: /* as ::= */ yytestcase(yyruleno==96); +{yymsp[1].minor.yy0.n = 0; yymsp[1].minor.yy0.z = 0;} + break; + case 26: /* typetoken ::= typename LP signed RP */ { - yygotominor.yy0.z = yymsp[-2].minor.yy0.z; - yygotominor.yy0.n = (int)(pParse->sLastToken.z-yymsp[-2].minor.yy0.z) + pParse->sLastToken.n; + yymsp[-3].minor.yy0.n = (int)(&yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n] - yymsp[-3].minor.yy0.z); } break; - case 39: /* columnid ::= nm */ + case 27: /* typetoken ::= typename LP signed COMMA signed RP */ { - sqlite3AddColumn(pParse,&yymsp[0].minor.yy0); - yygotominor.yy0 = yymsp[0].minor.yy0; - pParse->constraintName.n = 0; + yymsp[-5].minor.yy0.n = (int)(&yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n] - yymsp[-5].minor.yy0.z); } break; - case 40: /* nm ::= ID|INDEXED */ - case 41: /* nm ::= STRING */ yytestcase(yyruleno==41); - case 42: /* nm ::= JOIN_KW */ yytestcase(yyruleno==42); - case 45: /* typetoken ::= typename */ yytestcase(yyruleno==45); - case 48: /* typename ::= ID|STRING */ yytestcase(yyruleno==48); - case 130: /* as ::= AS nm */ yytestcase(yyruleno==130); - case 131: /* as ::= ID|STRING */ yytestcase(yyruleno==131); - case 142: /* dbnm ::= DOT nm */ yytestcase(yyruleno==142); - case 151: /* indexed_opt ::= INDEXED BY nm */ yytestcase(yyruleno==151); - case 257: /* nmnum ::= plus_num */ yytestcase(yyruleno==257); - case 258: /* nmnum ::= nm */ yytestcase(yyruleno==258); - case 259: /* nmnum ::= ON */ yytestcase(yyruleno==259); - case 260: /* nmnum ::= DELETE */ yytestcase(yyruleno==260); - case 261: /* nmnum ::= DEFAULT */ yytestcase(yyruleno==261); - case 262: /* plus_num ::= PLUS INTEGER|FLOAT */ yytestcase(yyruleno==262); - case 263: /* plus_num ::= INTEGER|FLOAT */ yytestcase(yyruleno==263); - case 264: /* minus_num ::= MINUS INTEGER|FLOAT */ yytestcase(yyruleno==264); - case 280: /* trnm ::= nm */ yytestcase(yyruleno==280); -{yygotominor.yy0 = yymsp[0].minor.yy0;} + case 28: /* typename ::= typename ID|STRING */ +{yymsp[-1].minor.yy0.n=yymsp[0].minor.yy0.n+(int)(yymsp[0].minor.yy0.z-yymsp[-1].minor.yy0.z);} break; - case 44: /* type ::= typetoken */ -{sqlite3AddColumnType(pParse,&yymsp[0].minor.yy0);} - break; - case 46: /* typetoken ::= typename LP signed RP */ -{ - yygotominor.yy0.z = yymsp[-3].minor.yy0.z; - yygotominor.yy0.n = (int)(&yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n] - yymsp[-3].minor.yy0.z); -} - break; - case 47: /* typetoken ::= typename LP signed COMMA signed RP */ -{ - yygotominor.yy0.z = yymsp[-5].minor.yy0.z; - yygotominor.yy0.n = (int)(&yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n] - yymsp[-5].minor.yy0.z); -} - break; - case 49: /* typename ::= typename ID|STRING */ -{yygotominor.yy0.z=yymsp[-1].minor.yy0.z; yygotominor.yy0.n=yymsp[0].minor.yy0.n+(int)(yymsp[0].minor.yy0.z-yymsp[-1].minor.yy0.z);} - break; - case 54: /* ccons ::= CONSTRAINT nm */ - case 92: /* tcons ::= CONSTRAINT nm */ yytestcase(yyruleno==92); + case 29: /* ccons ::= CONSTRAINT nm */ + case 62: /* tcons ::= CONSTRAINT nm */ yytestcase(yyruleno==62); {pParse->constraintName = yymsp[0].minor.yy0;} break; - case 55: /* ccons ::= DEFAULT term */ - case 57: /* ccons ::= DEFAULT PLUS term */ yytestcase(yyruleno==57); -{sqlite3AddDefaultValue(pParse,&yymsp[0].minor.yy346);} + case 30: /* ccons ::= DEFAULT term */ + case 32: /* ccons ::= DEFAULT PLUS term */ yytestcase(yyruleno==32); +{sqlite3AddDefaultValue(pParse,&yymsp[0].minor.yy342);} break; - case 56: /* ccons ::= DEFAULT LP expr RP */ -{sqlite3AddDefaultValue(pParse,&yymsp[-1].minor.yy346);} + case 31: /* ccons ::= DEFAULT LP expr RP */ +{sqlite3AddDefaultValue(pParse,&yymsp[-1].minor.yy342);} break; - case 58: /* ccons ::= DEFAULT MINUS term */ + case 33: /* ccons ::= DEFAULT MINUS term */ { ExprSpan v; - v.pExpr = sqlite3PExpr(pParse, TK_UMINUS, yymsp[0].minor.yy346.pExpr, 0, 0); + v.pExpr = sqlite3PExpr(pParse, TK_UMINUS, yymsp[0].minor.yy342.pExpr, 0, 0); v.zStart = yymsp[-1].minor.yy0.z; - v.zEnd = yymsp[0].minor.yy346.zEnd; + v.zEnd = yymsp[0].minor.yy342.zEnd; sqlite3AddDefaultValue(pParse,&v); } break; - case 59: /* ccons ::= DEFAULT ID|INDEXED */ + case 34: /* ccons ::= DEFAULT ID|INDEXED */ { ExprSpan v; - spanExpr(&v, pParse, TK_STRING, &yymsp[0].minor.yy0); + spanExpr(&v, pParse, TK_STRING, yymsp[0].minor.yy0); sqlite3AddDefaultValue(pParse,&v); } break; - case 61: /* ccons ::= NOT NULL onconf */ -{sqlite3AddNotNull(pParse, yymsp[0].minor.yy328);} + case 35: /* ccons ::= NOT NULL onconf */ +{sqlite3AddNotNull(pParse, yymsp[0].minor.yy392);} break; - case 62: /* ccons ::= PRIMARY KEY sortorder onconf autoinc */ -{sqlite3AddPrimaryKey(pParse,0,yymsp[-1].minor.yy328,yymsp[0].minor.yy328,yymsp[-2].minor.yy328);} + case 36: /* ccons ::= PRIMARY KEY sortorder onconf autoinc */ +{sqlite3AddPrimaryKey(pParse,0,yymsp[-1].minor.yy392,yymsp[0].minor.yy392,yymsp[-2].minor.yy392);} break; - case 63: /* ccons ::= UNIQUE onconf */ -{sqlite3CreateIndex(pParse,0,0,0,0,yymsp[0].minor.yy328,0,0,0,0);} + case 37: /* ccons ::= UNIQUE onconf */ +{sqlite3CreateIndex(pParse,0,0,0,0,yymsp[0].minor.yy392,0,0,0,0);} break; - case 64: /* ccons ::= CHECK LP expr RP */ -{sqlite3AddCheckConstraint(pParse,yymsp[-1].minor.yy346.pExpr);} + case 38: /* ccons ::= CHECK LP expr RP */ +{sqlite3AddCheckConstraint(pParse,yymsp[-1].minor.yy342.pExpr);} break; - case 65: /* ccons ::= REFERENCES nm eidlist_opt refargs */ -{sqlite3CreateForeignKey(pParse,0,&yymsp[-2].minor.yy0,yymsp[-1].minor.yy14,yymsp[0].minor.yy328);} + case 39: /* ccons ::= REFERENCES nm eidlist_opt refargs */ +{sqlite3CreateForeignKey(pParse,0,&yymsp[-2].minor.yy0,yymsp[-1].minor.yy442,yymsp[0].minor.yy392);} break; - case 66: /* ccons ::= defer_subclause */ -{sqlite3DeferForeignKey(pParse,yymsp[0].minor.yy328);} + case 40: /* ccons ::= defer_subclause */ +{sqlite3DeferForeignKey(pParse,yymsp[0].minor.yy392);} break; - case 67: /* ccons ::= COLLATE ID|STRING */ + case 41: /* ccons ::= COLLATE ID|STRING */ {sqlite3AddCollateType(pParse, &yymsp[0].minor.yy0);} break; - case 70: /* refargs ::= */ -{ yygotominor.yy328 = OE_None*0x0101; /* EV: R-19803-45884 */} + case 44: /* refargs ::= */ +{ yymsp[1].minor.yy392 = OE_None*0x0101; /* EV: R-19803-45884 */} break; - case 71: /* refargs ::= refargs refarg */ -{ yygotominor.yy328 = (yymsp[-1].minor.yy328 & ~yymsp[0].minor.yy429.mask) | yymsp[0].minor.yy429.value; } + case 45: /* refargs ::= refargs refarg */ +{ yymsp[-1].minor.yy392 = (yymsp[-1].minor.yy392 & ~yymsp[0].minor.yy207.mask) | yymsp[0].minor.yy207.value; } break; - case 72: /* refarg ::= MATCH nm */ - case 73: /* refarg ::= ON INSERT refact */ yytestcase(yyruleno==73); -{ yygotominor.yy429.value = 0; yygotominor.yy429.mask = 0x000000; } + case 46: /* refarg ::= MATCH nm */ +{ yymsp[-1].minor.yy207.value = 0; yymsp[-1].minor.yy207.mask = 0x000000; } break; - case 74: /* refarg ::= ON DELETE refact */ -{ yygotominor.yy429.value = yymsp[0].minor.yy328; yygotominor.yy429.mask = 0x0000ff; } + case 47: /* refarg ::= ON INSERT refact */ +{ yymsp[-2].minor.yy207.value = 0; yymsp[-2].minor.yy207.mask = 0x000000; } break; - case 75: /* refarg ::= ON UPDATE refact */ -{ yygotominor.yy429.value = yymsp[0].minor.yy328<<8; yygotominor.yy429.mask = 0x00ff00; } + case 48: /* refarg ::= ON DELETE refact */ +{ yymsp[-2].minor.yy207.value = yymsp[0].minor.yy392; yymsp[-2].minor.yy207.mask = 0x0000ff; } break; - case 76: /* refact ::= SET NULL */ -{ yygotominor.yy328 = OE_SetNull; /* EV: R-33326-45252 */} + case 49: /* refarg ::= ON UPDATE refact */ +{ yymsp[-2].minor.yy207.value = yymsp[0].minor.yy392<<8; yymsp[-2].minor.yy207.mask = 0x00ff00; } break; - case 77: /* refact ::= SET DEFAULT */ -{ yygotominor.yy328 = OE_SetDflt; /* EV: R-33326-45252 */} + case 50: /* refact ::= SET NULL */ +{ yymsp[-1].minor.yy392 = OE_SetNull; /* EV: R-33326-45252 */} break; - case 78: /* refact ::= CASCADE */ -{ yygotominor.yy328 = OE_Cascade; /* EV: R-33326-45252 */} + case 51: /* refact ::= SET DEFAULT */ +{ yymsp[-1].minor.yy392 = OE_SetDflt; /* EV: R-33326-45252 */} break; - case 79: /* refact ::= RESTRICT */ -{ yygotominor.yy328 = OE_Restrict; /* EV: R-33326-45252 */} + case 52: /* refact ::= CASCADE */ +{ yymsp[0].minor.yy392 = OE_Cascade; /* EV: R-33326-45252 */} break; - case 80: /* refact ::= NO ACTION */ -{ yygotominor.yy328 = OE_None; /* EV: R-33326-45252 */} + case 53: /* refact ::= RESTRICT */ +{ yymsp[0].minor.yy392 = OE_Restrict; /* EV: R-33326-45252 */} break; - case 82: /* defer_subclause ::= DEFERRABLE init_deferred_pred_opt */ - case 98: /* defer_subclause_opt ::= defer_subclause */ yytestcase(yyruleno==98); - case 100: /* onconf ::= ON CONFLICT resolvetype */ yytestcase(yyruleno==100); - case 103: /* resolvetype ::= raisetype */ yytestcase(yyruleno==103); -{yygotominor.yy328 = yymsp[0].minor.yy328;} + case 54: /* refact ::= NO ACTION */ +{ yymsp[-1].minor.yy392 = OE_None; /* EV: R-33326-45252 */} break; - case 86: /* conslist_opt ::= */ -{yygotominor.yy0.n = 0; yygotominor.yy0.z = 0;} + case 55: /* defer_subclause ::= NOT DEFERRABLE init_deferred_pred_opt */ +{yymsp[-2].minor.yy392 = 0;} break; - case 87: /* conslist_opt ::= COMMA conslist */ -{yygotominor.yy0 = yymsp[-1].minor.yy0;} + case 56: /* defer_subclause ::= DEFERRABLE init_deferred_pred_opt */ + case 71: /* orconf ::= OR resolvetype */ yytestcase(yyruleno==71); + case 142: /* insert_cmd ::= INSERT orconf */ yytestcase(yyruleno==142); +{yymsp[-1].minor.yy392 = yymsp[0].minor.yy392;} break; - case 90: /* tconscomma ::= COMMA */ + case 58: /* init_deferred_pred_opt ::= INITIALLY DEFERRED */ + case 75: /* ifexists ::= IF EXISTS */ yytestcase(yyruleno==75); + case 183: /* between_op ::= NOT BETWEEN */ yytestcase(yyruleno==183); + case 186: /* in_op ::= NOT IN */ yytestcase(yyruleno==186); + case 210: /* collate ::= COLLATE ID|STRING */ yytestcase(yyruleno==210); +{yymsp[-1].minor.yy392 = 1;} + break; + case 59: /* init_deferred_pred_opt ::= INITIALLY IMMEDIATE */ +{yymsp[-1].minor.yy392 = 0;} + break; + case 61: /* tconscomma ::= COMMA */ {pParse->constraintName.n = 0;} break; - case 93: /* tcons ::= PRIMARY KEY LP sortlist autoinc RP onconf */ -{sqlite3AddPrimaryKey(pParse,yymsp[-3].minor.yy14,yymsp[0].minor.yy328,yymsp[-2].minor.yy328,0);} + case 63: /* tcons ::= PRIMARY KEY LP sortlist autoinc RP onconf */ +{sqlite3AddPrimaryKey(pParse,yymsp[-3].minor.yy442,yymsp[0].minor.yy392,yymsp[-2].minor.yy392,0);} break; - case 94: /* tcons ::= UNIQUE LP sortlist RP onconf */ -{sqlite3CreateIndex(pParse,0,0,0,yymsp[-2].minor.yy14,yymsp[0].minor.yy328,0,0,0,0);} + case 64: /* tcons ::= UNIQUE LP sortlist RP onconf */ +{sqlite3CreateIndex(pParse,0,0,0,yymsp[-2].minor.yy442,yymsp[0].minor.yy392,0,0,0,0);} break; - case 95: /* tcons ::= CHECK LP expr RP onconf */ -{sqlite3AddCheckConstraint(pParse,yymsp[-2].minor.yy346.pExpr);} + case 65: /* tcons ::= CHECK LP expr RP onconf */ +{sqlite3AddCheckConstraint(pParse,yymsp[-2].minor.yy342.pExpr);} break; - case 96: /* tcons ::= FOREIGN KEY LP eidlist RP REFERENCES nm eidlist_opt refargs defer_subclause_opt */ + case 66: /* tcons ::= FOREIGN KEY LP eidlist RP REFERENCES nm eidlist_opt refargs defer_subclause_opt */ { - sqlite3CreateForeignKey(pParse, yymsp[-6].minor.yy14, &yymsp[-3].minor.yy0, yymsp[-2].minor.yy14, yymsp[-1].minor.yy328); - sqlite3DeferForeignKey(pParse, yymsp[0].minor.yy328); + sqlite3CreateForeignKey(pParse, yymsp[-6].minor.yy442, &yymsp[-3].minor.yy0, yymsp[-2].minor.yy442, yymsp[-1].minor.yy392); + sqlite3DeferForeignKey(pParse, yymsp[0].minor.yy392); } break; - case 99: /* onconf ::= */ -{yygotominor.yy328 = OE_Default;} + case 68: /* onconf ::= */ + case 70: /* orconf ::= */ yytestcase(yyruleno==70); +{yymsp[1].minor.yy392 = OE_Default;} break; - case 101: /* orconf ::= */ -{yygotominor.yy186 = OE_Default;} + case 69: /* onconf ::= ON CONFLICT resolvetype */ +{yymsp[-2].minor.yy392 = yymsp[0].minor.yy392;} break; - case 102: /* orconf ::= OR resolvetype */ -{yygotominor.yy186 = (u8)yymsp[0].minor.yy328;} + case 72: /* resolvetype ::= IGNORE */ +{yymsp[0].minor.yy392 = OE_Ignore;} break; - case 104: /* resolvetype ::= IGNORE */ -{yygotominor.yy328 = OE_Ignore;} + case 73: /* resolvetype ::= REPLACE */ + case 143: /* insert_cmd ::= REPLACE */ yytestcase(yyruleno==143); +{yymsp[0].minor.yy392 = OE_Replace;} break; - case 105: /* resolvetype ::= REPLACE */ -{yygotominor.yy328 = OE_Replace;} - break; - case 106: /* cmd ::= DROP TABLE ifexists fullname */ + case 74: /* cmd ::= DROP TABLE ifexists fullname */ { - sqlite3DropTable(pParse, yymsp[0].minor.yy65, 0, yymsp[-1].minor.yy328); + sqlite3DropTable(pParse, yymsp[0].minor.yy347, 0, yymsp[-1].minor.yy392); } break; - case 109: /* cmd ::= createkw temp VIEW ifnotexists nm dbnm eidlist_opt AS select */ + case 77: /* cmd ::= createkw temp VIEW ifnotexists nm dbnm eidlist_opt AS select */ { - sqlite3CreateView(pParse, &yymsp[-8].minor.yy0, &yymsp[-4].minor.yy0, &yymsp[-3].minor.yy0, yymsp[-2].minor.yy14, yymsp[0].minor.yy3, yymsp[-7].minor.yy328, yymsp[-5].minor.yy328); + sqlite3CreateView(pParse, &yymsp[-8].minor.yy0, &yymsp[-4].minor.yy0, &yymsp[-3].minor.yy0, yymsp[-2].minor.yy442, yymsp[0].minor.yy159, yymsp[-7].minor.yy392, yymsp[-5].minor.yy392); } break; - case 110: /* cmd ::= DROP VIEW ifexists fullname */ + case 78: /* cmd ::= DROP VIEW ifexists fullname */ { - sqlite3DropTable(pParse, yymsp[0].minor.yy65, 1, yymsp[-1].minor.yy328); + sqlite3DropTable(pParse, yymsp[0].minor.yy347, 1, yymsp[-1].minor.yy392); } break; - case 111: /* cmd ::= select */ + case 79: /* cmd ::= select */ { SelectDest dest = {SRT_Output, 0, 0, 0, 0, 0}; - sqlite3Select(pParse, yymsp[0].minor.yy3, &dest); - sqlite3SelectDelete(pParse->db, yymsp[0].minor.yy3); + sqlite3Select(pParse, yymsp[0].minor.yy159, &dest); + sqlite3SelectDelete(pParse->db, yymsp[0].minor.yy159); } break; - case 112: /* select ::= with selectnowith */ + case 80: /* select ::= with selectnowith */ { - Select *p = yymsp[0].minor.yy3; + Select *p = yymsp[0].minor.yy159; if( p ){ - p->pWith = yymsp[-1].minor.yy59; + p->pWith = yymsp[-1].minor.yy331; parserDoubleLinkSelect(pParse, p); }else{ - sqlite3WithDelete(pParse->db, yymsp[-1].minor.yy59); + sqlite3WithDelete(pParse->db, yymsp[-1].minor.yy331); } - yygotominor.yy3 = p; + yymsp[-1].minor.yy159 = p; /*A-overwrites-W*/ } break; - case 113: /* selectnowith ::= oneselect */ - case 119: /* oneselect ::= values */ yytestcase(yyruleno==119); -{yygotominor.yy3 = yymsp[0].minor.yy3;} - break; - case 114: /* selectnowith ::= selectnowith multiselect_op oneselect */ + case 81: /* selectnowith ::= selectnowith multiselect_op oneselect */ { - Select *pRhs = yymsp[0].minor.yy3; - Select *pLhs = yymsp[-2].minor.yy3; + Select *pRhs = yymsp[0].minor.yy159; + Select *pLhs = yymsp[-2].minor.yy159; if( pRhs && pRhs->pPrior ){ SrcList *pFrom; Token x; @@ -128453,23 +133201,30 @@ static void yy_reduce( pRhs = sqlite3SelectNew(pParse,0,pFrom,0,0,0,0,0,0,0); } if( pRhs ){ - pRhs->op = (u8)yymsp[-1].minor.yy328; + pRhs->op = (u8)yymsp[-1].minor.yy392; pRhs->pPrior = pLhs; if( ALWAYS(pLhs) ) pLhs->selFlags &= ~SF_MultiValue; pRhs->selFlags &= ~SF_MultiValue; - if( yymsp[-1].minor.yy328!=TK_ALL ) pParse->hasCompound = 1; + if( yymsp[-1].minor.yy392!=TK_ALL ) pParse->hasCompound = 1; }else{ sqlite3SelectDelete(pParse->db, pLhs); } - yygotominor.yy3 = pRhs; + yymsp[-2].minor.yy159 = pRhs; } break; - case 116: /* multiselect_op ::= UNION ALL */ -{yygotominor.yy328 = TK_ALL;} + case 82: /* multiselect_op ::= UNION */ + case 84: /* multiselect_op ::= EXCEPT|INTERSECT */ yytestcase(yyruleno==84); +{yymsp[0].minor.yy392 = yymsp[0].major; /*A-overwrites-OP*/} break; - case 118: /* oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt orderby_opt limit_opt */ + case 83: /* multiselect_op ::= UNION ALL */ +{yymsp[-1].minor.yy392 = TK_ALL;} + break; + case 85: /* oneselect ::= SELECT distinct selcollist from where_opt groupby_opt having_opt orderby_opt limit_opt */ { - yygotominor.yy3 = sqlite3SelectNew(pParse,yymsp[-6].minor.yy14,yymsp[-5].minor.yy65,yymsp[-4].minor.yy132,yymsp[-3].minor.yy14,yymsp[-2].minor.yy132,yymsp[-1].minor.yy14,yymsp[-7].minor.yy381,yymsp[0].minor.yy476.pLimit,yymsp[0].minor.yy476.pOffset); +#if SELECTTRACE_ENABLED + Token s = yymsp[-8].minor.yy0; /*A-overwrites-S*/ +#endif + yymsp[-8].minor.yy159 = sqlite3SelectNew(pParse,yymsp[-6].minor.yy442,yymsp[-5].minor.yy347,yymsp[-4].minor.yy122,yymsp[-3].minor.yy442,yymsp[-2].minor.yy122,yymsp[-1].minor.yy442,yymsp[-7].minor.yy392,yymsp[0].minor.yy64.pLimit,yymsp[0].minor.yy64.pOffset); #if SELECTTRACE_ENABLED /* Populate the Select.zSelName[] string that is used to help with ** query planner debugging, to differentiate between multiple Select @@ -128480,455 +133235,449 @@ static void yy_reduce( ** comment to be the zSelName value. Otherwise, the label is #N where ** is an integer that is incremented with each SELECT statement seen. */ - if( yygotominor.yy3!=0 ){ - const char *z = yymsp[-8].minor.yy0.z+6; + if( yymsp[-8].minor.yy159!=0 ){ + const char *z = s.z+6; int i; - sqlite3_snprintf(sizeof(yygotominor.yy3->zSelName), yygotominor.yy3->zSelName, "#%d", + sqlite3_snprintf(sizeof(yymsp[-8].minor.yy159->zSelName), yymsp[-8].minor.yy159->zSelName, "#%d", ++pParse->nSelect); while( z[0]==' ' ) z++; if( z[0]=='/' && z[1]=='*' ){ z += 2; while( z[0]==' ' ) z++; for(i=0; sqlite3Isalnum(z[i]); i++){} - sqlite3_snprintf(sizeof(yygotominor.yy3->zSelName), yygotominor.yy3->zSelName, "%.*s", i, z); + sqlite3_snprintf(sizeof(yymsp[-8].minor.yy159->zSelName), yymsp[-8].minor.yy159->zSelName, "%.*s", i, z); } } #endif /* SELECTRACE_ENABLED */ } break; - case 120: /* values ::= VALUES LP nexprlist RP */ + case 86: /* values ::= VALUES LP nexprlist RP */ { - yygotominor.yy3 = sqlite3SelectNew(pParse,yymsp[-1].minor.yy14,0,0,0,0,0,SF_Values,0,0); + yymsp[-3].minor.yy159 = sqlite3SelectNew(pParse,yymsp[-1].minor.yy442,0,0,0,0,0,SF_Values,0,0); } break; - case 121: /* values ::= values COMMA LP exprlist RP */ + case 87: /* values ::= values COMMA LP exprlist RP */ { - Select *pRight, *pLeft = yymsp[-4].minor.yy3; - pRight = sqlite3SelectNew(pParse,yymsp[-1].minor.yy14,0,0,0,0,0,SF_Values|SF_MultiValue,0,0); + Select *pRight, *pLeft = yymsp[-4].minor.yy159; + pRight = sqlite3SelectNew(pParse,yymsp[-1].minor.yy442,0,0,0,0,0,SF_Values|SF_MultiValue,0,0); if( ALWAYS(pLeft) ) pLeft->selFlags &= ~SF_MultiValue; if( pRight ){ pRight->op = TK_ALL; - pLeft = yymsp[-4].minor.yy3; pRight->pPrior = pLeft; - yygotominor.yy3 = pRight; + yymsp[-4].minor.yy159 = pRight; }else{ - yygotominor.yy3 = pLeft; + yymsp[-4].minor.yy159 = pLeft; } } break; - case 122: /* distinct ::= DISTINCT */ -{yygotominor.yy381 = SF_Distinct;} + case 88: /* distinct ::= DISTINCT */ +{yymsp[0].minor.yy392 = SF_Distinct;} break; - case 123: /* distinct ::= ALL */ -{yygotominor.yy381 = SF_All;} + case 89: /* distinct ::= ALL */ +{yymsp[0].minor.yy392 = SF_All;} break; - case 124: /* distinct ::= */ -{yygotominor.yy381 = 0;} + case 91: /* sclp ::= */ + case 119: /* orderby_opt ::= */ yytestcase(yyruleno==119); + case 126: /* groupby_opt ::= */ yytestcase(yyruleno==126); + case 199: /* exprlist ::= */ yytestcase(yyruleno==199); + case 205: /* eidlist_opt ::= */ yytestcase(yyruleno==205); +{yymsp[1].minor.yy442 = 0;} break; - case 125: /* sclp ::= selcollist COMMA */ - case 244: /* eidlist_opt ::= LP eidlist RP */ yytestcase(yyruleno==244); -{yygotominor.yy14 = yymsp[-1].minor.yy14;} - break; - case 126: /* sclp ::= */ - case 155: /* orderby_opt ::= */ yytestcase(yyruleno==155); - case 162: /* groupby_opt ::= */ yytestcase(yyruleno==162); - case 237: /* exprlist ::= */ yytestcase(yyruleno==237); - case 243: /* eidlist_opt ::= */ yytestcase(yyruleno==243); -{yygotominor.yy14 = 0;} - break; - case 127: /* selcollist ::= sclp expr as */ + case 92: /* selcollist ::= sclp expr as */ { - yygotominor.yy14 = sqlite3ExprListAppend(pParse, yymsp[-2].minor.yy14, yymsp[-1].minor.yy346.pExpr); - if( yymsp[0].minor.yy0.n>0 ) sqlite3ExprListSetName(pParse, yygotominor.yy14, &yymsp[0].minor.yy0, 1); - sqlite3ExprListSetSpan(pParse,yygotominor.yy14,&yymsp[-1].minor.yy346); + yymsp[-2].minor.yy442 = sqlite3ExprListAppend(pParse, yymsp[-2].minor.yy442, yymsp[-1].minor.yy342.pExpr); + if( yymsp[0].minor.yy0.n>0 ) sqlite3ExprListSetName(pParse, yymsp[-2].minor.yy442, &yymsp[0].minor.yy0, 1); + sqlite3ExprListSetSpan(pParse,yymsp[-2].minor.yy442,&yymsp[-1].minor.yy342); } break; - case 128: /* selcollist ::= sclp STAR */ + case 93: /* selcollist ::= sclp STAR */ { - Expr *p = sqlite3Expr(pParse->db, TK_ALL, 0); - yygotominor.yy14 = sqlite3ExprListAppend(pParse, yymsp[-1].minor.yy14, p); + Expr *p = sqlite3Expr(pParse->db, TK_ASTERISK, 0); + yymsp[-1].minor.yy442 = sqlite3ExprListAppend(pParse, yymsp[-1].minor.yy442, p); } break; - case 129: /* selcollist ::= sclp nm DOT STAR */ + case 94: /* selcollist ::= sclp nm DOT STAR */ { - Expr *pRight = sqlite3PExpr(pParse, TK_ALL, 0, 0, &yymsp[0].minor.yy0); + Expr *pRight = sqlite3PExpr(pParse, TK_ASTERISK, 0, 0, &yymsp[0].minor.yy0); Expr *pLeft = sqlite3PExpr(pParse, TK_ID, 0, 0, &yymsp[-2].minor.yy0); Expr *pDot = sqlite3PExpr(pParse, TK_DOT, pLeft, pRight, 0); - yygotominor.yy14 = sqlite3ExprListAppend(pParse,yymsp[-3].minor.yy14, pDot); + yymsp[-3].minor.yy442 = sqlite3ExprListAppend(pParse,yymsp[-3].minor.yy442, pDot); } break; - case 132: /* as ::= */ -{yygotominor.yy0.n = 0;} + case 95: /* as ::= AS nm */ + case 106: /* dbnm ::= DOT nm */ yytestcase(yyruleno==106); + case 219: /* plus_num ::= PLUS INTEGER|FLOAT */ yytestcase(yyruleno==219); + case 220: /* minus_num ::= MINUS INTEGER|FLOAT */ yytestcase(yyruleno==220); +{yymsp[-1].minor.yy0 = yymsp[0].minor.yy0;} break; - case 133: /* from ::= */ -{yygotominor.yy65 = sqlite3DbMallocZero(pParse->db, sizeof(*yygotominor.yy65));} + case 97: /* from ::= */ +{yymsp[1].minor.yy347 = sqlite3DbMallocZero(pParse->db, sizeof(*yymsp[1].minor.yy347));} break; - case 134: /* from ::= FROM seltablist */ + case 98: /* from ::= FROM seltablist */ { - yygotominor.yy65 = yymsp[0].minor.yy65; - sqlite3SrcListShiftJoinType(yygotominor.yy65); + yymsp[-1].minor.yy347 = yymsp[0].minor.yy347; + sqlite3SrcListShiftJoinType(yymsp[-1].minor.yy347); } break; - case 135: /* stl_prefix ::= seltablist joinop */ + case 99: /* stl_prefix ::= seltablist joinop */ { - yygotominor.yy65 = yymsp[-1].minor.yy65; - if( ALWAYS(yygotominor.yy65 && yygotominor.yy65->nSrc>0) ) yygotominor.yy65->a[yygotominor.yy65->nSrc-1].fg.jointype = (u8)yymsp[0].minor.yy328; + if( ALWAYS(yymsp[-1].minor.yy347 && yymsp[-1].minor.yy347->nSrc>0) ) yymsp[-1].minor.yy347->a[yymsp[-1].minor.yy347->nSrc-1].fg.jointype = (u8)yymsp[0].minor.yy392; } break; - case 136: /* stl_prefix ::= */ -{yygotominor.yy65 = 0;} + case 100: /* stl_prefix ::= */ +{yymsp[1].minor.yy347 = 0;} break; - case 137: /* seltablist ::= stl_prefix nm dbnm as indexed_opt on_opt using_opt */ + case 101: /* seltablist ::= stl_prefix nm dbnm as indexed_opt on_opt using_opt */ { - yygotominor.yy65 = sqlite3SrcListAppendFromTerm(pParse,yymsp[-6].minor.yy65,&yymsp[-5].minor.yy0,&yymsp[-4].minor.yy0,&yymsp[-3].minor.yy0,0,yymsp[-1].minor.yy132,yymsp[0].minor.yy408); - sqlite3SrcListIndexedBy(pParse, yygotominor.yy65, &yymsp[-2].minor.yy0); + yymsp[-6].minor.yy347 = sqlite3SrcListAppendFromTerm(pParse,yymsp[-6].minor.yy347,&yymsp[-5].minor.yy0,&yymsp[-4].minor.yy0,&yymsp[-3].minor.yy0,0,yymsp[-1].minor.yy122,yymsp[0].minor.yy180); + sqlite3SrcListIndexedBy(pParse, yymsp[-6].minor.yy347, &yymsp[-2].minor.yy0); } break; - case 138: /* seltablist ::= stl_prefix nm dbnm LP exprlist RP as on_opt using_opt */ + case 102: /* seltablist ::= stl_prefix nm dbnm LP exprlist RP as on_opt using_opt */ { - yygotominor.yy65 = sqlite3SrcListAppendFromTerm(pParse,yymsp[-8].minor.yy65,&yymsp[-7].minor.yy0,&yymsp[-6].minor.yy0,&yymsp[-2].minor.yy0,0,yymsp[-1].minor.yy132,yymsp[0].minor.yy408); - sqlite3SrcListFuncArgs(pParse, yygotominor.yy65, yymsp[-4].minor.yy14); + yymsp[-8].minor.yy347 = sqlite3SrcListAppendFromTerm(pParse,yymsp[-8].minor.yy347,&yymsp[-7].minor.yy0,&yymsp[-6].minor.yy0,&yymsp[-2].minor.yy0,0,yymsp[-1].minor.yy122,yymsp[0].minor.yy180); + sqlite3SrcListFuncArgs(pParse, yymsp[-8].minor.yy347, yymsp[-4].minor.yy442); } break; - case 139: /* seltablist ::= stl_prefix LP select RP as on_opt using_opt */ + case 103: /* seltablist ::= stl_prefix LP select RP as on_opt using_opt */ { - yygotominor.yy65 = sqlite3SrcListAppendFromTerm(pParse,yymsp[-6].minor.yy65,0,0,&yymsp[-2].minor.yy0,yymsp[-4].minor.yy3,yymsp[-1].minor.yy132,yymsp[0].minor.yy408); + yymsp[-6].minor.yy347 = sqlite3SrcListAppendFromTerm(pParse,yymsp[-6].minor.yy347,0,0,&yymsp[-2].minor.yy0,yymsp[-4].minor.yy159,yymsp[-1].minor.yy122,yymsp[0].minor.yy180); } break; - case 140: /* seltablist ::= stl_prefix LP seltablist RP as on_opt using_opt */ + case 104: /* seltablist ::= stl_prefix LP seltablist RP as on_opt using_opt */ { - if( yymsp[-6].minor.yy65==0 && yymsp[-2].minor.yy0.n==0 && yymsp[-1].minor.yy132==0 && yymsp[0].minor.yy408==0 ){ - yygotominor.yy65 = yymsp[-4].minor.yy65; - }else if( yymsp[-4].minor.yy65->nSrc==1 ){ - yygotominor.yy65 = sqlite3SrcListAppendFromTerm(pParse,yymsp[-6].minor.yy65,0,0,&yymsp[-2].minor.yy0,0,yymsp[-1].minor.yy132,yymsp[0].minor.yy408); - if( yygotominor.yy65 ){ - struct SrcList_item *pNew = &yygotominor.yy65->a[yygotominor.yy65->nSrc-1]; - struct SrcList_item *pOld = yymsp[-4].minor.yy65->a; + if( yymsp[-6].minor.yy347==0 && yymsp[-2].minor.yy0.n==0 && yymsp[-1].minor.yy122==0 && yymsp[0].minor.yy180==0 ){ + yymsp[-6].minor.yy347 = yymsp[-4].minor.yy347; + }else if( yymsp[-4].minor.yy347->nSrc==1 ){ + yymsp[-6].minor.yy347 = sqlite3SrcListAppendFromTerm(pParse,yymsp[-6].minor.yy347,0,0,&yymsp[-2].minor.yy0,0,yymsp[-1].minor.yy122,yymsp[0].minor.yy180); + if( yymsp[-6].minor.yy347 ){ + struct SrcList_item *pNew = &yymsp[-6].minor.yy347->a[yymsp[-6].minor.yy347->nSrc-1]; + struct SrcList_item *pOld = yymsp[-4].minor.yy347->a; pNew->zName = pOld->zName; pNew->zDatabase = pOld->zDatabase; pNew->pSelect = pOld->pSelect; pOld->zName = pOld->zDatabase = 0; pOld->pSelect = 0; } - sqlite3SrcListDelete(pParse->db, yymsp[-4].minor.yy65); + sqlite3SrcListDelete(pParse->db, yymsp[-4].minor.yy347); }else{ Select *pSubquery; - sqlite3SrcListShiftJoinType(yymsp[-4].minor.yy65); - pSubquery = sqlite3SelectNew(pParse,0,yymsp[-4].minor.yy65,0,0,0,0,SF_NestedFrom,0,0); - yygotominor.yy65 = sqlite3SrcListAppendFromTerm(pParse,yymsp[-6].minor.yy65,0,0,&yymsp[-2].minor.yy0,pSubquery,yymsp[-1].minor.yy132,yymsp[0].minor.yy408); + sqlite3SrcListShiftJoinType(yymsp[-4].minor.yy347); + pSubquery = sqlite3SelectNew(pParse,0,yymsp[-4].minor.yy347,0,0,0,0,SF_NestedFrom,0,0); + yymsp[-6].minor.yy347 = sqlite3SrcListAppendFromTerm(pParse,yymsp[-6].minor.yy347,0,0,&yymsp[-2].minor.yy0,pSubquery,yymsp[-1].minor.yy122,yymsp[0].minor.yy180); } } break; - case 141: /* dbnm ::= */ - case 150: /* indexed_opt ::= */ yytestcase(yyruleno==150); -{yygotominor.yy0.z=0; yygotominor.yy0.n=0;} + case 105: /* dbnm ::= */ + case 114: /* indexed_opt ::= */ yytestcase(yyruleno==114); +{yymsp[1].minor.yy0.z=0; yymsp[1].minor.yy0.n=0;} break; - case 143: /* fullname ::= nm dbnm */ -{yygotominor.yy65 = sqlite3SrcListAppend(pParse->db,0,&yymsp[-1].minor.yy0,&yymsp[0].minor.yy0);} + case 107: /* fullname ::= nm dbnm */ +{yymsp[-1].minor.yy347 = sqlite3SrcListAppend(pParse->db,0,&yymsp[-1].minor.yy0,&yymsp[0].minor.yy0); /*A-overwrites-X*/} break; - case 144: /* joinop ::= COMMA|JOIN */ -{ yygotominor.yy328 = JT_INNER; } + case 108: /* joinop ::= COMMA|JOIN */ +{ yymsp[0].minor.yy392 = JT_INNER; } break; - case 145: /* joinop ::= JOIN_KW JOIN */ -{ yygotominor.yy328 = sqlite3JoinType(pParse,&yymsp[-1].minor.yy0,0,0); } + case 109: /* joinop ::= JOIN_KW JOIN */ +{yymsp[-1].minor.yy392 = sqlite3JoinType(pParse,&yymsp[-1].minor.yy0,0,0); /*X-overwrites-A*/} break; - case 146: /* joinop ::= JOIN_KW nm JOIN */ -{ yygotominor.yy328 = sqlite3JoinType(pParse,&yymsp[-2].minor.yy0,&yymsp[-1].minor.yy0,0); } + case 110: /* joinop ::= JOIN_KW nm JOIN */ +{yymsp[-2].minor.yy392 = sqlite3JoinType(pParse,&yymsp[-2].minor.yy0,&yymsp[-1].minor.yy0,0); /*X-overwrites-A*/} break; - case 147: /* joinop ::= JOIN_KW nm nm JOIN */ -{ yygotominor.yy328 = sqlite3JoinType(pParse,&yymsp[-3].minor.yy0,&yymsp[-2].minor.yy0,&yymsp[-1].minor.yy0); } + case 111: /* joinop ::= JOIN_KW nm nm JOIN */ +{yymsp[-3].minor.yy392 = sqlite3JoinType(pParse,&yymsp[-3].minor.yy0,&yymsp[-2].minor.yy0,&yymsp[-1].minor.yy0);/*X-overwrites-A*/} break; - case 148: /* on_opt ::= ON expr */ - case 165: /* having_opt ::= HAVING expr */ yytestcase(yyruleno==165); - case 172: /* where_opt ::= WHERE expr */ yytestcase(yyruleno==172); - case 232: /* case_else ::= ELSE expr */ yytestcase(yyruleno==232); - case 234: /* case_operand ::= expr */ yytestcase(yyruleno==234); -{yygotominor.yy132 = yymsp[0].minor.yy346.pExpr;} + case 112: /* on_opt ::= ON expr */ + case 129: /* having_opt ::= HAVING expr */ yytestcase(yyruleno==129); + case 136: /* where_opt ::= WHERE expr */ yytestcase(yyruleno==136); + case 195: /* case_else ::= ELSE expr */ yytestcase(yyruleno==195); +{yymsp[-1].minor.yy122 = yymsp[0].minor.yy342.pExpr;} break; - case 149: /* on_opt ::= */ - case 164: /* having_opt ::= */ yytestcase(yyruleno==164); - case 171: /* where_opt ::= */ yytestcase(yyruleno==171); - case 233: /* case_else ::= */ yytestcase(yyruleno==233); - case 235: /* case_operand ::= */ yytestcase(yyruleno==235); -{yygotominor.yy132 = 0;} + case 113: /* on_opt ::= */ + case 128: /* having_opt ::= */ yytestcase(yyruleno==128); + case 135: /* where_opt ::= */ yytestcase(yyruleno==135); + case 196: /* case_else ::= */ yytestcase(yyruleno==196); + case 198: /* case_operand ::= */ yytestcase(yyruleno==198); +{yymsp[1].minor.yy122 = 0;} break; - case 152: /* indexed_opt ::= NOT INDEXED */ -{yygotominor.yy0.z=0; yygotominor.yy0.n=1;} + case 115: /* indexed_opt ::= INDEXED BY nm */ +{yymsp[-2].minor.yy0 = yymsp[0].minor.yy0;} break; - case 153: /* using_opt ::= USING LP idlist RP */ - case 181: /* idlist_opt ::= LP idlist RP */ yytestcase(yyruleno==181); -{yygotominor.yy408 = yymsp[-1].minor.yy408;} + case 116: /* indexed_opt ::= NOT INDEXED */ +{yymsp[-1].minor.yy0.z=0; yymsp[-1].minor.yy0.n=1;} break; - case 154: /* using_opt ::= */ - case 180: /* idlist_opt ::= */ yytestcase(yyruleno==180); -{yygotominor.yy408 = 0;} + case 117: /* using_opt ::= USING LP idlist RP */ +{yymsp[-3].minor.yy180 = yymsp[-1].minor.yy180;} break; - case 156: /* orderby_opt ::= ORDER BY sortlist */ - case 163: /* groupby_opt ::= GROUP BY nexprlist */ yytestcase(yyruleno==163); - case 236: /* exprlist ::= nexprlist */ yytestcase(yyruleno==236); -{yygotominor.yy14 = yymsp[0].minor.yy14;} + case 118: /* using_opt ::= */ + case 144: /* idlist_opt ::= */ yytestcase(yyruleno==144); +{yymsp[1].minor.yy180 = 0;} break; - case 157: /* sortlist ::= sortlist COMMA expr sortorder */ + case 120: /* orderby_opt ::= ORDER BY sortlist */ + case 127: /* groupby_opt ::= GROUP BY nexprlist */ yytestcase(yyruleno==127); +{yymsp[-2].minor.yy442 = yymsp[0].minor.yy442;} + break; + case 121: /* sortlist ::= sortlist COMMA expr sortorder */ { - yygotominor.yy14 = sqlite3ExprListAppend(pParse,yymsp[-3].minor.yy14,yymsp[-1].minor.yy346.pExpr); - sqlite3ExprListSetSortOrder(yygotominor.yy14,yymsp[0].minor.yy328); + yymsp[-3].minor.yy442 = sqlite3ExprListAppend(pParse,yymsp[-3].minor.yy442,yymsp[-1].minor.yy342.pExpr); + sqlite3ExprListSetSortOrder(yymsp[-3].minor.yy442,yymsp[0].minor.yy392); } break; - case 158: /* sortlist ::= expr sortorder */ + case 122: /* sortlist ::= expr sortorder */ { - yygotominor.yy14 = sqlite3ExprListAppend(pParse,0,yymsp[-1].minor.yy346.pExpr); - sqlite3ExprListSetSortOrder(yygotominor.yy14,yymsp[0].minor.yy328); + yymsp[-1].minor.yy442 = sqlite3ExprListAppend(pParse,0,yymsp[-1].minor.yy342.pExpr); /*A-overwrites-Y*/ + sqlite3ExprListSetSortOrder(yymsp[-1].minor.yy442,yymsp[0].minor.yy392); } break; - case 159: /* sortorder ::= ASC */ -{yygotominor.yy328 = SQLITE_SO_ASC;} + case 123: /* sortorder ::= ASC */ +{yymsp[0].minor.yy392 = SQLITE_SO_ASC;} break; - case 160: /* sortorder ::= DESC */ -{yygotominor.yy328 = SQLITE_SO_DESC;} + case 124: /* sortorder ::= DESC */ +{yymsp[0].minor.yy392 = SQLITE_SO_DESC;} break; - case 161: /* sortorder ::= */ -{yygotominor.yy328 = SQLITE_SO_UNDEFINED;} + case 125: /* sortorder ::= */ +{yymsp[1].minor.yy392 = SQLITE_SO_UNDEFINED;} break; - case 166: /* limit_opt ::= */ -{yygotominor.yy476.pLimit = 0; yygotominor.yy476.pOffset = 0;} + case 130: /* limit_opt ::= */ +{yymsp[1].minor.yy64.pLimit = 0; yymsp[1].minor.yy64.pOffset = 0;} break; - case 167: /* limit_opt ::= LIMIT expr */ -{yygotominor.yy476.pLimit = yymsp[0].minor.yy346.pExpr; yygotominor.yy476.pOffset = 0;} + case 131: /* limit_opt ::= LIMIT expr */ +{yymsp[-1].minor.yy64.pLimit = yymsp[0].minor.yy342.pExpr; yymsp[-1].minor.yy64.pOffset = 0;} break; - case 168: /* limit_opt ::= LIMIT expr OFFSET expr */ -{yygotominor.yy476.pLimit = yymsp[-2].minor.yy346.pExpr; yygotominor.yy476.pOffset = yymsp[0].minor.yy346.pExpr;} + case 132: /* limit_opt ::= LIMIT expr OFFSET expr */ +{yymsp[-3].minor.yy64.pLimit = yymsp[-2].minor.yy342.pExpr; yymsp[-3].minor.yy64.pOffset = yymsp[0].minor.yy342.pExpr;} break; - case 169: /* limit_opt ::= LIMIT expr COMMA expr */ -{yygotominor.yy476.pOffset = yymsp[-2].minor.yy346.pExpr; yygotominor.yy476.pLimit = yymsp[0].minor.yy346.pExpr;} + case 133: /* limit_opt ::= LIMIT expr COMMA expr */ +{yymsp[-3].minor.yy64.pOffset = yymsp[-2].minor.yy342.pExpr; yymsp[-3].minor.yy64.pLimit = yymsp[0].minor.yy342.pExpr;} break; - case 170: /* cmd ::= with DELETE FROM fullname indexed_opt where_opt */ + case 134: /* cmd ::= with DELETE FROM fullname indexed_opt where_opt */ { - sqlite3WithPush(pParse, yymsp[-5].minor.yy59, 1); - sqlite3SrcListIndexedBy(pParse, yymsp[-2].minor.yy65, &yymsp[-1].minor.yy0); - sqlite3DeleteFrom(pParse,yymsp[-2].minor.yy65,yymsp[0].minor.yy132); + sqlite3WithPush(pParse, yymsp[-5].minor.yy331, 1); + sqlite3SrcListIndexedBy(pParse, yymsp[-2].minor.yy347, &yymsp[-1].minor.yy0); + sqlite3DeleteFrom(pParse,yymsp[-2].minor.yy347,yymsp[0].minor.yy122); } break; - case 173: /* cmd ::= with UPDATE orconf fullname indexed_opt SET setlist where_opt */ + case 137: /* cmd ::= with UPDATE orconf fullname indexed_opt SET setlist where_opt */ { - sqlite3WithPush(pParse, yymsp[-7].minor.yy59, 1); - sqlite3SrcListIndexedBy(pParse, yymsp[-4].minor.yy65, &yymsp[-3].minor.yy0); - sqlite3ExprListCheckLength(pParse,yymsp[-1].minor.yy14,"set list"); - sqlite3Update(pParse,yymsp[-4].minor.yy65,yymsp[-1].minor.yy14,yymsp[0].minor.yy132,yymsp[-5].minor.yy186); + sqlite3WithPush(pParse, yymsp[-7].minor.yy331, 1); + sqlite3SrcListIndexedBy(pParse, yymsp[-4].minor.yy347, &yymsp[-3].minor.yy0); + sqlite3ExprListCheckLength(pParse,yymsp[-1].minor.yy442,"set list"); + sqlite3Update(pParse,yymsp[-4].minor.yy347,yymsp[-1].minor.yy442,yymsp[0].minor.yy122,yymsp[-5].minor.yy392); } break; - case 174: /* setlist ::= setlist COMMA nm EQ expr */ + case 138: /* setlist ::= setlist COMMA nm EQ expr */ { - yygotominor.yy14 = sqlite3ExprListAppend(pParse, yymsp[-4].minor.yy14, yymsp[0].minor.yy346.pExpr); - sqlite3ExprListSetName(pParse, yygotominor.yy14, &yymsp[-2].minor.yy0, 1); + yymsp[-4].minor.yy442 = sqlite3ExprListAppend(pParse, yymsp[-4].minor.yy442, yymsp[0].minor.yy342.pExpr); + sqlite3ExprListSetName(pParse, yymsp[-4].minor.yy442, &yymsp[-2].minor.yy0, 1); } break; - case 175: /* setlist ::= nm EQ expr */ + case 139: /* setlist ::= nm EQ expr */ { - yygotominor.yy14 = sqlite3ExprListAppend(pParse, 0, yymsp[0].minor.yy346.pExpr); - sqlite3ExprListSetName(pParse, yygotominor.yy14, &yymsp[-2].minor.yy0, 1); + yylhsminor.yy442 = sqlite3ExprListAppend(pParse, 0, yymsp[0].minor.yy342.pExpr); + sqlite3ExprListSetName(pParse, yylhsminor.yy442, &yymsp[-2].minor.yy0, 1); +} + yymsp[-2].minor.yy442 = yylhsminor.yy442; + break; + case 140: /* cmd ::= with insert_cmd INTO fullname idlist_opt select */ +{ + sqlite3WithPush(pParse, yymsp[-5].minor.yy331, 1); + sqlite3Insert(pParse, yymsp[-2].minor.yy347, yymsp[0].minor.yy159, yymsp[-1].minor.yy180, yymsp[-4].minor.yy392); } break; - case 176: /* cmd ::= with insert_cmd INTO fullname idlist_opt select */ + case 141: /* cmd ::= with insert_cmd INTO fullname idlist_opt DEFAULT VALUES */ { - sqlite3WithPush(pParse, yymsp[-5].minor.yy59, 1); - sqlite3Insert(pParse, yymsp[-2].minor.yy65, yymsp[0].minor.yy3, yymsp[-1].minor.yy408, yymsp[-4].minor.yy186); + sqlite3WithPush(pParse, yymsp[-6].minor.yy331, 1); + sqlite3Insert(pParse, yymsp[-3].minor.yy347, 0, yymsp[-2].minor.yy180, yymsp[-5].minor.yy392); } break; - case 177: /* cmd ::= with insert_cmd INTO fullname idlist_opt DEFAULT VALUES */ -{ - sqlite3WithPush(pParse, yymsp[-6].minor.yy59, 1); - sqlite3Insert(pParse, yymsp[-3].minor.yy65, 0, yymsp[-2].minor.yy408, yymsp[-5].minor.yy186); -} + case 145: /* idlist_opt ::= LP idlist RP */ +{yymsp[-2].minor.yy180 = yymsp[-1].minor.yy180;} break; - case 178: /* insert_cmd ::= INSERT orconf */ -{yygotominor.yy186 = yymsp[0].minor.yy186;} + case 146: /* idlist ::= idlist COMMA nm */ +{yymsp[-2].minor.yy180 = sqlite3IdListAppend(pParse->db,yymsp[-2].minor.yy180,&yymsp[0].minor.yy0);} break; - case 179: /* insert_cmd ::= REPLACE */ -{yygotominor.yy186 = OE_Replace;} + case 147: /* idlist ::= nm */ +{yymsp[0].minor.yy180 = sqlite3IdListAppend(pParse->db,0,&yymsp[0].minor.yy0); /*A-overwrites-Y*/} break; - case 182: /* idlist ::= idlist COMMA nm */ -{yygotominor.yy408 = sqlite3IdListAppend(pParse->db,yymsp[-2].minor.yy408,&yymsp[0].minor.yy0);} + case 148: /* expr ::= LP expr RP */ +{spanSet(&yymsp[-2].minor.yy342,&yymsp[-2].minor.yy0,&yymsp[0].minor.yy0); /*A-overwrites-B*/ yymsp[-2].minor.yy342.pExpr = yymsp[-1].minor.yy342.pExpr;} break; - case 183: /* idlist ::= nm */ -{yygotominor.yy408 = sqlite3IdListAppend(pParse->db,0,&yymsp[0].minor.yy0);} + case 149: /* term ::= NULL */ + case 154: /* term ::= INTEGER|FLOAT|BLOB */ yytestcase(yyruleno==154); + case 155: /* term ::= STRING */ yytestcase(yyruleno==155); +{spanExpr(&yymsp[0].minor.yy342,pParse,yymsp[0].major,yymsp[0].minor.yy0);/*A-overwrites-X*/} break; - case 184: /* expr ::= term */ -{yygotominor.yy346 = yymsp[0].minor.yy346;} + case 150: /* expr ::= ID|INDEXED */ + case 151: /* expr ::= JOIN_KW */ yytestcase(yyruleno==151); +{spanExpr(&yymsp[0].minor.yy342,pParse,TK_ID,yymsp[0].minor.yy0); /*A-overwrites-X*/} break; - case 185: /* expr ::= LP expr RP */ -{yygotominor.yy346.pExpr = yymsp[-1].minor.yy346.pExpr; spanSet(&yygotominor.yy346,&yymsp[-2].minor.yy0,&yymsp[0].minor.yy0);} - break; - case 186: /* term ::= NULL */ - case 191: /* term ::= INTEGER|FLOAT|BLOB */ yytestcase(yyruleno==191); - case 192: /* term ::= STRING */ yytestcase(yyruleno==192); -{spanExpr(&yygotominor.yy346, pParse, yymsp[0].major, &yymsp[0].minor.yy0);} - break; - case 187: /* expr ::= ID|INDEXED */ - case 188: /* expr ::= JOIN_KW */ yytestcase(yyruleno==188); -{spanExpr(&yygotominor.yy346, pParse, TK_ID, &yymsp[0].minor.yy0);} - break; - case 189: /* expr ::= nm DOT nm */ + case 152: /* expr ::= nm DOT nm */ { Expr *temp1 = sqlite3PExpr(pParse, TK_ID, 0, 0, &yymsp[-2].minor.yy0); Expr *temp2 = sqlite3PExpr(pParse, TK_ID, 0, 0, &yymsp[0].minor.yy0); - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_DOT, temp1, temp2, 0); - spanSet(&yygotominor.yy346,&yymsp[-2].minor.yy0,&yymsp[0].minor.yy0); + spanSet(&yymsp[-2].minor.yy342,&yymsp[-2].minor.yy0,&yymsp[0].minor.yy0); /*A-overwrites-X*/ + yymsp[-2].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_DOT, temp1, temp2, 0); } break; - case 190: /* expr ::= nm DOT nm DOT nm */ + case 153: /* expr ::= nm DOT nm DOT nm */ { Expr *temp1 = sqlite3PExpr(pParse, TK_ID, 0, 0, &yymsp[-4].minor.yy0); Expr *temp2 = sqlite3PExpr(pParse, TK_ID, 0, 0, &yymsp[-2].minor.yy0); Expr *temp3 = sqlite3PExpr(pParse, TK_ID, 0, 0, &yymsp[0].minor.yy0); Expr *temp4 = sqlite3PExpr(pParse, TK_DOT, temp2, temp3, 0); - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_DOT, temp1, temp4, 0); - spanSet(&yygotominor.yy346,&yymsp[-4].minor.yy0,&yymsp[0].minor.yy0); + spanSet(&yymsp[-4].minor.yy342,&yymsp[-4].minor.yy0,&yymsp[0].minor.yy0); /*A-overwrites-X*/ + yymsp[-4].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_DOT, temp1, temp4, 0); } break; - case 193: /* expr ::= VARIABLE */ + case 156: /* expr ::= VARIABLE */ { - if( yymsp[0].minor.yy0.n>=2 && yymsp[0].minor.yy0.z[0]=='#' && sqlite3Isdigit(yymsp[0].minor.yy0.z[1]) ){ + if( !(yymsp[0].minor.yy0.z[0]=='#' && sqlite3Isdigit(yymsp[0].minor.yy0.z[1])) ){ + spanExpr(&yymsp[0].minor.yy342, pParse, TK_VARIABLE, yymsp[0].minor.yy0); + sqlite3ExprAssignVarNumber(pParse, yymsp[0].minor.yy342.pExpr); + }else{ /* When doing a nested parse, one can include terms in an expression ** that look like this: #1 #2 ... These terms refer to registers ** in the virtual machine. #N is the N-th register. */ + Token t = yymsp[0].minor.yy0; /*A-overwrites-X*/ + assert( t.n>=2 ); + spanSet(&yymsp[0].minor.yy342, &t, &t); if( pParse->nested==0 ){ - sqlite3ErrorMsg(pParse, "near \"%T\": syntax error", &yymsp[0].minor.yy0); - yygotominor.yy346.pExpr = 0; + sqlite3ErrorMsg(pParse, "near \"%T\": syntax error", &t); + yymsp[0].minor.yy342.pExpr = 0; }else{ - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_REGISTER, 0, 0, &yymsp[0].minor.yy0); - if( yygotominor.yy346.pExpr ) sqlite3GetInt32(&yymsp[0].minor.yy0.z[1], &yygotominor.yy346.pExpr->iTable); + yymsp[0].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_REGISTER, 0, 0, &t); + if( yymsp[0].minor.yy342.pExpr ) sqlite3GetInt32(&t.z[1], &yymsp[0].minor.yy342.pExpr->iTable); } - }else{ - spanExpr(&yygotominor.yy346, pParse, TK_VARIABLE, &yymsp[0].minor.yy0); - sqlite3ExprAssignVarNumber(pParse, yygotominor.yy346.pExpr); } - spanSet(&yygotominor.yy346, &yymsp[0].minor.yy0, &yymsp[0].minor.yy0); } break; - case 194: /* expr ::= expr COLLATE ID|STRING */ + case 157: /* expr ::= expr COLLATE ID|STRING */ { - yygotominor.yy346.pExpr = sqlite3ExprAddCollateToken(pParse, yymsp[-2].minor.yy346.pExpr, &yymsp[0].minor.yy0, 1); - yygotominor.yy346.zStart = yymsp[-2].minor.yy346.zStart; - yygotominor.yy346.zEnd = &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n]; + yymsp[-2].minor.yy342.pExpr = sqlite3ExprAddCollateToken(pParse, yymsp[-2].minor.yy342.pExpr, &yymsp[0].minor.yy0, 1); + yymsp[-2].minor.yy342.zEnd = &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n]; } break; - case 195: /* expr ::= CAST LP expr AS typetoken RP */ + case 158: /* expr ::= CAST LP expr AS typetoken RP */ { - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_CAST, yymsp[-3].minor.yy346.pExpr, 0, &yymsp[-1].minor.yy0); - spanSet(&yygotominor.yy346,&yymsp[-5].minor.yy0,&yymsp[0].minor.yy0); + spanSet(&yymsp[-5].minor.yy342,&yymsp[-5].minor.yy0,&yymsp[0].minor.yy0); /*A-overwrites-X*/ + yymsp[-5].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_CAST, yymsp[-3].minor.yy342.pExpr, 0, &yymsp[-1].minor.yy0); } break; - case 196: /* expr ::= ID|INDEXED LP distinct exprlist RP */ + case 159: /* expr ::= ID|INDEXED LP distinct exprlist RP */ { - if( yymsp[-1].minor.yy14 && yymsp[-1].minor.yy14->nExpr>pParse->db->aLimit[SQLITE_LIMIT_FUNCTION_ARG] ){ + if( yymsp[-1].minor.yy442 && yymsp[-1].minor.yy442->nExpr>pParse->db->aLimit[SQLITE_LIMIT_FUNCTION_ARG] ){ sqlite3ErrorMsg(pParse, "too many arguments on function %T", &yymsp[-4].minor.yy0); } - yygotominor.yy346.pExpr = sqlite3ExprFunction(pParse, yymsp[-1].minor.yy14, &yymsp[-4].minor.yy0); - spanSet(&yygotominor.yy346,&yymsp[-4].minor.yy0,&yymsp[0].minor.yy0); - if( yymsp[-2].minor.yy381==SF_Distinct && yygotominor.yy346.pExpr ){ - yygotominor.yy346.pExpr->flags |= EP_Distinct; + yylhsminor.yy342.pExpr = sqlite3ExprFunction(pParse, yymsp[-1].minor.yy442, &yymsp[-4].minor.yy0); + spanSet(&yylhsminor.yy342,&yymsp[-4].minor.yy0,&yymsp[0].minor.yy0); + if( yymsp[-2].minor.yy392==SF_Distinct && yylhsminor.yy342.pExpr ){ + yylhsminor.yy342.pExpr->flags |= EP_Distinct; } } + yymsp[-4].minor.yy342 = yylhsminor.yy342; break; - case 197: /* expr ::= ID|INDEXED LP STAR RP */ + case 160: /* expr ::= ID|INDEXED LP STAR RP */ { - yygotominor.yy346.pExpr = sqlite3ExprFunction(pParse, 0, &yymsp[-3].minor.yy0); - spanSet(&yygotominor.yy346,&yymsp[-3].minor.yy0,&yymsp[0].minor.yy0); + yylhsminor.yy342.pExpr = sqlite3ExprFunction(pParse, 0, &yymsp[-3].minor.yy0); + spanSet(&yylhsminor.yy342,&yymsp[-3].minor.yy0,&yymsp[0].minor.yy0); } + yymsp[-3].minor.yy342 = yylhsminor.yy342; break; - case 198: /* term ::= CTIME_KW */ + case 161: /* term ::= CTIME_KW */ { - yygotominor.yy346.pExpr = sqlite3ExprFunction(pParse, 0, &yymsp[0].minor.yy0); - spanSet(&yygotominor.yy346, &yymsp[0].minor.yy0, &yymsp[0].minor.yy0); + yylhsminor.yy342.pExpr = sqlite3ExprFunction(pParse, 0, &yymsp[0].minor.yy0); + spanSet(&yylhsminor.yy342, &yymsp[0].minor.yy0, &yymsp[0].minor.yy0); } + yymsp[0].minor.yy342 = yylhsminor.yy342; break; - case 199: /* expr ::= expr AND expr */ - case 200: /* expr ::= expr OR expr */ yytestcase(yyruleno==200); - case 201: /* expr ::= expr LT|GT|GE|LE expr */ yytestcase(yyruleno==201); - case 202: /* expr ::= expr EQ|NE expr */ yytestcase(yyruleno==202); - case 203: /* expr ::= expr BITAND|BITOR|LSHIFT|RSHIFT expr */ yytestcase(yyruleno==203); - case 204: /* expr ::= expr PLUS|MINUS expr */ yytestcase(yyruleno==204); - case 205: /* expr ::= expr STAR|SLASH|REM expr */ yytestcase(yyruleno==205); - case 206: /* expr ::= expr CONCAT expr */ yytestcase(yyruleno==206); -{spanBinaryExpr(&yygotominor.yy346,pParse,yymsp[-1].major,&yymsp[-2].minor.yy346,&yymsp[0].minor.yy346);} + case 162: /* expr ::= expr AND expr */ + case 163: /* expr ::= expr OR expr */ yytestcase(yyruleno==163); + case 164: /* expr ::= expr LT|GT|GE|LE expr */ yytestcase(yyruleno==164); + case 165: /* expr ::= expr EQ|NE expr */ yytestcase(yyruleno==165); + case 166: /* expr ::= expr BITAND|BITOR|LSHIFT|RSHIFT expr */ yytestcase(yyruleno==166); + case 167: /* expr ::= expr PLUS|MINUS expr */ yytestcase(yyruleno==167); + case 168: /* expr ::= expr STAR|SLASH|REM expr */ yytestcase(yyruleno==168); + case 169: /* expr ::= expr CONCAT expr */ yytestcase(yyruleno==169); +{spanBinaryExpr(pParse,yymsp[-1].major,&yymsp[-2].minor.yy342,&yymsp[0].minor.yy342);} break; - case 207: /* likeop ::= LIKE_KW|MATCH */ -{yygotominor.yy96.eOperator = yymsp[0].minor.yy0; yygotominor.yy96.bNot = 0;} + case 170: /* likeop ::= LIKE_KW|MATCH */ +{yymsp[0].minor.yy318.eOperator = yymsp[0].minor.yy0; yymsp[0].minor.yy318.bNot = 0;/*A-overwrites-X*/} break; - case 208: /* likeop ::= NOT LIKE_KW|MATCH */ -{yygotominor.yy96.eOperator = yymsp[0].minor.yy0; yygotominor.yy96.bNot = 1;} + case 171: /* likeop ::= NOT LIKE_KW|MATCH */ +{yymsp[-1].minor.yy318.eOperator = yymsp[0].minor.yy0; yymsp[-1].minor.yy318.bNot = 1;} break; - case 209: /* expr ::= expr likeop expr */ + case 172: /* expr ::= expr likeop expr */ { ExprList *pList; - pList = sqlite3ExprListAppend(pParse,0, yymsp[0].minor.yy346.pExpr); - pList = sqlite3ExprListAppend(pParse,pList, yymsp[-2].minor.yy346.pExpr); - yygotominor.yy346.pExpr = sqlite3ExprFunction(pParse, pList, &yymsp[-1].minor.yy96.eOperator); - if( yymsp[-1].minor.yy96.bNot ) yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_NOT, yygotominor.yy346.pExpr, 0, 0); - yygotominor.yy346.zStart = yymsp[-2].minor.yy346.zStart; - yygotominor.yy346.zEnd = yymsp[0].minor.yy346.zEnd; - if( yygotominor.yy346.pExpr ) yygotominor.yy346.pExpr->flags |= EP_InfixFunc; + pList = sqlite3ExprListAppend(pParse,0, yymsp[0].minor.yy342.pExpr); + pList = sqlite3ExprListAppend(pParse,pList, yymsp[-2].minor.yy342.pExpr); + yymsp[-2].minor.yy342.pExpr = sqlite3ExprFunction(pParse, pList, &yymsp[-1].minor.yy318.eOperator); + exprNot(pParse, yymsp[-1].minor.yy318.bNot, &yymsp[-2].minor.yy342); + yymsp[-2].minor.yy342.zEnd = yymsp[0].minor.yy342.zEnd; + if( yymsp[-2].minor.yy342.pExpr ) yymsp[-2].minor.yy342.pExpr->flags |= EP_InfixFunc; } break; - case 210: /* expr ::= expr likeop expr ESCAPE expr */ + case 173: /* expr ::= expr likeop expr ESCAPE expr */ { ExprList *pList; - pList = sqlite3ExprListAppend(pParse,0, yymsp[-2].minor.yy346.pExpr); - pList = sqlite3ExprListAppend(pParse,pList, yymsp[-4].minor.yy346.pExpr); - pList = sqlite3ExprListAppend(pParse,pList, yymsp[0].minor.yy346.pExpr); - yygotominor.yy346.pExpr = sqlite3ExprFunction(pParse, pList, &yymsp[-3].minor.yy96.eOperator); - if( yymsp[-3].minor.yy96.bNot ) yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_NOT, yygotominor.yy346.pExpr, 0, 0); - yygotominor.yy346.zStart = yymsp[-4].minor.yy346.zStart; - yygotominor.yy346.zEnd = yymsp[0].minor.yy346.zEnd; - if( yygotominor.yy346.pExpr ) yygotominor.yy346.pExpr->flags |= EP_InfixFunc; + pList = sqlite3ExprListAppend(pParse,0, yymsp[-2].minor.yy342.pExpr); + pList = sqlite3ExprListAppend(pParse,pList, yymsp[-4].minor.yy342.pExpr); + pList = sqlite3ExprListAppend(pParse,pList, yymsp[0].minor.yy342.pExpr); + yymsp[-4].minor.yy342.pExpr = sqlite3ExprFunction(pParse, pList, &yymsp[-3].minor.yy318.eOperator); + exprNot(pParse, yymsp[-3].minor.yy318.bNot, &yymsp[-4].minor.yy342); + yymsp[-4].minor.yy342.zEnd = yymsp[0].minor.yy342.zEnd; + if( yymsp[-4].minor.yy342.pExpr ) yymsp[-4].minor.yy342.pExpr->flags |= EP_InfixFunc; } break; - case 211: /* expr ::= expr ISNULL|NOTNULL */ -{spanUnaryPostfix(&yygotominor.yy346,pParse,yymsp[0].major,&yymsp[-1].minor.yy346,&yymsp[0].minor.yy0);} + case 174: /* expr ::= expr ISNULL|NOTNULL */ +{spanUnaryPostfix(pParse,yymsp[0].major,&yymsp[-1].minor.yy342,&yymsp[0].minor.yy0);} break; - case 212: /* expr ::= expr NOT NULL */ -{spanUnaryPostfix(&yygotominor.yy346,pParse,TK_NOTNULL,&yymsp[-2].minor.yy346,&yymsp[0].minor.yy0);} + case 175: /* expr ::= expr NOT NULL */ +{spanUnaryPostfix(pParse,TK_NOTNULL,&yymsp[-2].minor.yy342,&yymsp[0].minor.yy0);} break; - case 213: /* expr ::= expr IS expr */ + case 176: /* expr ::= expr IS expr */ { - spanBinaryExpr(&yygotominor.yy346,pParse,TK_IS,&yymsp[-2].minor.yy346,&yymsp[0].minor.yy346); - binaryToUnaryIfNull(pParse, yymsp[0].minor.yy346.pExpr, yygotominor.yy346.pExpr, TK_ISNULL); + spanBinaryExpr(pParse,TK_IS,&yymsp[-2].minor.yy342,&yymsp[0].minor.yy342); + binaryToUnaryIfNull(pParse, yymsp[0].minor.yy342.pExpr, yymsp[-2].minor.yy342.pExpr, TK_ISNULL); } break; - case 214: /* expr ::= expr IS NOT expr */ + case 177: /* expr ::= expr IS NOT expr */ { - spanBinaryExpr(&yygotominor.yy346,pParse,TK_ISNOT,&yymsp[-3].minor.yy346,&yymsp[0].minor.yy346); - binaryToUnaryIfNull(pParse, yymsp[0].minor.yy346.pExpr, yygotominor.yy346.pExpr, TK_NOTNULL); + spanBinaryExpr(pParse,TK_ISNOT,&yymsp[-3].minor.yy342,&yymsp[0].minor.yy342); + binaryToUnaryIfNull(pParse, yymsp[0].minor.yy342.pExpr, yymsp[-3].minor.yy342.pExpr, TK_NOTNULL); } break; - case 215: /* expr ::= NOT expr */ - case 216: /* expr ::= BITNOT expr */ yytestcase(yyruleno==216); -{spanUnaryPrefix(&yygotominor.yy346,pParse,yymsp[-1].major,&yymsp[0].minor.yy346,&yymsp[-1].minor.yy0);} + case 178: /* expr ::= NOT expr */ + case 179: /* expr ::= BITNOT expr */ yytestcase(yyruleno==179); +{spanUnaryPrefix(&yymsp[-1].minor.yy342,pParse,yymsp[-1].major,&yymsp[0].minor.yy342,&yymsp[-1].minor.yy0);/*A-overwrites-B*/} break; - case 217: /* expr ::= MINUS expr */ -{spanUnaryPrefix(&yygotominor.yy346,pParse,TK_UMINUS,&yymsp[0].minor.yy346,&yymsp[-1].minor.yy0);} + case 180: /* expr ::= MINUS expr */ +{spanUnaryPrefix(&yymsp[-1].minor.yy342,pParse,TK_UMINUS,&yymsp[0].minor.yy342,&yymsp[-1].minor.yy0);/*A-overwrites-B*/} break; - case 218: /* expr ::= PLUS expr */ -{spanUnaryPrefix(&yygotominor.yy346,pParse,TK_UPLUS,&yymsp[0].minor.yy346,&yymsp[-1].minor.yy0);} + case 181: /* expr ::= PLUS expr */ +{spanUnaryPrefix(&yymsp[-1].minor.yy342,pParse,TK_UPLUS,&yymsp[0].minor.yy342,&yymsp[-1].minor.yy0);/*A-overwrites-B*/} break; - case 221: /* expr ::= expr between_op expr AND expr */ + case 182: /* between_op ::= BETWEEN */ + case 185: /* in_op ::= IN */ yytestcase(yyruleno==185); +{yymsp[0].minor.yy392 = 0;} + break; + case 184: /* expr ::= expr between_op expr AND expr */ { - ExprList *pList = sqlite3ExprListAppend(pParse,0, yymsp[-2].minor.yy346.pExpr); - pList = sqlite3ExprListAppend(pParse,pList, yymsp[0].minor.yy346.pExpr); - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_BETWEEN, yymsp[-4].minor.yy346.pExpr, 0, 0); - if( yygotominor.yy346.pExpr ){ - yygotominor.yy346.pExpr->x.pList = pList; + ExprList *pList = sqlite3ExprListAppend(pParse,0, yymsp[-2].minor.yy342.pExpr); + pList = sqlite3ExprListAppend(pParse,pList, yymsp[0].minor.yy342.pExpr); + yymsp[-4].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_BETWEEN, yymsp[-4].minor.yy342.pExpr, 0, 0); + if( yymsp[-4].minor.yy342.pExpr ){ + yymsp[-4].minor.yy342.pExpr->x.pList = pList; }else{ sqlite3ExprListDelete(pParse->db, pList); } - if( yymsp[-3].minor.yy328 ) yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_NOT, yygotominor.yy346.pExpr, 0, 0); - yygotominor.yy346.zStart = yymsp[-4].minor.yy346.zStart; - yygotominor.yy346.zEnd = yymsp[0].minor.yy346.zEnd; + exprNot(pParse, yymsp[-3].minor.yy392, &yymsp[-4].minor.yy342); + yymsp[-4].minor.yy342.zEnd = yymsp[0].minor.yy342.zEnd; } break; - case 224: /* expr ::= expr in_op LP exprlist RP */ + case 187: /* expr ::= expr in_op LP exprlist RP */ { - if( yymsp[-1].minor.yy14==0 ){ + if( yymsp[-1].minor.yy442==0 ){ /* Expressions of the form ** ** expr1 IN () @@ -128937,9 +133686,9 @@ static void yy_reduce( ** simplify to constants 0 (false) and 1 (true), respectively, ** regardless of the value of expr1. */ - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_INTEGER, 0, 0, &sqlite3IntTokens[yymsp[-3].minor.yy328]); - sqlite3ExprDelete(pParse->db, yymsp[-4].minor.yy346.pExpr); - }else if( yymsp[-1].minor.yy14->nExpr==1 ){ + sqlite3ExprDelete(pParse->db, yymsp[-4].minor.yy342.pExpr); + yymsp[-4].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_INTEGER, 0, 0, &sqlite3IntTokens[yymsp[-3].minor.yy392]); + }else if( yymsp[-1].minor.yy442->nExpr==1 ){ /* Expressions of the form: ** ** expr1 IN (?1) @@ -128956,421 +133705,413 @@ static void yy_reduce( ** affinity or the collating sequence to use for comparison. Otherwise, ** the semantics would be subtly different from IN or NOT IN. */ - Expr *pRHS = yymsp[-1].minor.yy14->a[0].pExpr; - yymsp[-1].minor.yy14->a[0].pExpr = 0; - sqlite3ExprListDelete(pParse->db, yymsp[-1].minor.yy14); + Expr *pRHS = yymsp[-1].minor.yy442->a[0].pExpr; + yymsp[-1].minor.yy442->a[0].pExpr = 0; + sqlite3ExprListDelete(pParse->db, yymsp[-1].minor.yy442); /* pRHS cannot be NULL because a malloc error would have been detected ** before now and control would have never reached this point */ if( ALWAYS(pRHS) ){ pRHS->flags &= ~EP_Collate; pRHS->flags |= EP_Generic; } - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, yymsp[-3].minor.yy328 ? TK_NE : TK_EQ, yymsp[-4].minor.yy346.pExpr, pRHS, 0); + yymsp[-4].minor.yy342.pExpr = sqlite3PExpr(pParse, yymsp[-3].minor.yy392 ? TK_NE : TK_EQ, yymsp[-4].minor.yy342.pExpr, pRHS, 0); }else{ - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_IN, yymsp[-4].minor.yy346.pExpr, 0, 0); - if( yygotominor.yy346.pExpr ){ - yygotominor.yy346.pExpr->x.pList = yymsp[-1].minor.yy14; - sqlite3ExprSetHeightAndFlags(pParse, yygotominor.yy346.pExpr); + yymsp[-4].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_IN, yymsp[-4].minor.yy342.pExpr, 0, 0); + if( yymsp[-4].minor.yy342.pExpr ){ + yymsp[-4].minor.yy342.pExpr->x.pList = yymsp[-1].minor.yy442; + sqlite3ExprSetHeightAndFlags(pParse, yymsp[-4].minor.yy342.pExpr); }else{ - sqlite3ExprListDelete(pParse->db, yymsp[-1].minor.yy14); + sqlite3ExprListDelete(pParse->db, yymsp[-1].minor.yy442); } - if( yymsp[-3].minor.yy328 ) yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_NOT, yygotominor.yy346.pExpr, 0, 0); + exprNot(pParse, yymsp[-3].minor.yy392, &yymsp[-4].minor.yy342); } - yygotominor.yy346.zStart = yymsp[-4].minor.yy346.zStart; - yygotominor.yy346.zEnd = &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n]; + yymsp[-4].minor.yy342.zEnd = &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n]; } break; - case 225: /* expr ::= LP select RP */ + case 188: /* expr ::= LP select RP */ { - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_SELECT, 0, 0, 0); - if( yygotominor.yy346.pExpr ){ - yygotominor.yy346.pExpr->x.pSelect = yymsp[-1].minor.yy3; - ExprSetProperty(yygotominor.yy346.pExpr, EP_xIsSelect|EP_Subquery); - sqlite3ExprSetHeightAndFlags(pParse, yygotominor.yy346.pExpr); - }else{ - sqlite3SelectDelete(pParse->db, yymsp[-1].minor.yy3); - } - yygotominor.yy346.zStart = yymsp[-2].minor.yy0.z; - yygotominor.yy346.zEnd = &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n]; + spanSet(&yymsp[-2].minor.yy342,&yymsp[-2].minor.yy0,&yymsp[0].minor.yy0); /*A-overwrites-B*/ + yymsp[-2].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_SELECT, 0, 0, 0); + sqlite3PExprAddSelect(pParse, yymsp[-2].minor.yy342.pExpr, yymsp[-1].minor.yy159); } break; - case 226: /* expr ::= expr in_op LP select RP */ + case 189: /* expr ::= expr in_op LP select RP */ { - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_IN, yymsp[-4].minor.yy346.pExpr, 0, 0); - if( yygotominor.yy346.pExpr ){ - yygotominor.yy346.pExpr->x.pSelect = yymsp[-1].minor.yy3; - ExprSetProperty(yygotominor.yy346.pExpr, EP_xIsSelect|EP_Subquery); - sqlite3ExprSetHeightAndFlags(pParse, yygotominor.yy346.pExpr); - }else{ - sqlite3SelectDelete(pParse->db, yymsp[-1].minor.yy3); - } - if( yymsp[-3].minor.yy328 ) yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_NOT, yygotominor.yy346.pExpr, 0, 0); - yygotominor.yy346.zStart = yymsp[-4].minor.yy346.zStart; - yygotominor.yy346.zEnd = &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n]; + yymsp[-4].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_IN, yymsp[-4].minor.yy342.pExpr, 0, 0); + sqlite3PExprAddSelect(pParse, yymsp[-4].minor.yy342.pExpr, yymsp[-1].minor.yy159); + exprNot(pParse, yymsp[-3].minor.yy392, &yymsp[-4].minor.yy342); + yymsp[-4].minor.yy342.zEnd = &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n]; } break; - case 227: /* expr ::= expr in_op nm dbnm */ + case 190: /* expr ::= expr in_op nm dbnm */ { SrcList *pSrc = sqlite3SrcListAppend(pParse->db, 0,&yymsp[-1].minor.yy0,&yymsp[0].minor.yy0); - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_IN, yymsp[-3].minor.yy346.pExpr, 0, 0); - if( yygotominor.yy346.pExpr ){ - yygotominor.yy346.pExpr->x.pSelect = sqlite3SelectNew(pParse, 0,pSrc,0,0,0,0,0,0,0); - ExprSetProperty(yygotominor.yy346.pExpr, EP_xIsSelect|EP_Subquery); - sqlite3ExprSetHeightAndFlags(pParse, yygotominor.yy346.pExpr); - }else{ - sqlite3SrcListDelete(pParse->db, pSrc); - } - if( yymsp[-2].minor.yy328 ) yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_NOT, yygotominor.yy346.pExpr, 0, 0); - yygotominor.yy346.zStart = yymsp[-3].minor.yy346.zStart; - yygotominor.yy346.zEnd = yymsp[0].minor.yy0.z ? &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n] : &yymsp[-1].minor.yy0.z[yymsp[-1].minor.yy0.n]; + Select *pSelect = sqlite3SelectNew(pParse, 0,pSrc,0,0,0,0,0,0,0); + yymsp[-3].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_IN, yymsp[-3].minor.yy342.pExpr, 0, 0); + sqlite3PExprAddSelect(pParse, yymsp[-3].minor.yy342.pExpr, pSelect); + exprNot(pParse, yymsp[-2].minor.yy392, &yymsp[-3].minor.yy342); + yymsp[-3].minor.yy342.zEnd = yymsp[0].minor.yy0.z ? &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n] : &yymsp[-1].minor.yy0.z[yymsp[-1].minor.yy0.n]; } break; - case 228: /* expr ::= EXISTS LP select RP */ + case 191: /* expr ::= EXISTS LP select RP */ { - Expr *p = yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_EXISTS, 0, 0, 0); - if( p ){ - p->x.pSelect = yymsp[-1].minor.yy3; - ExprSetProperty(p, EP_xIsSelect|EP_Subquery); - sqlite3ExprSetHeightAndFlags(pParse, p); - }else{ - sqlite3SelectDelete(pParse->db, yymsp[-1].minor.yy3); - } - yygotominor.yy346.zStart = yymsp[-3].minor.yy0.z; - yygotominor.yy346.zEnd = &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n]; + Expr *p; + spanSet(&yymsp[-3].minor.yy342,&yymsp[-3].minor.yy0,&yymsp[0].minor.yy0); /*A-overwrites-B*/ + p = yymsp[-3].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_EXISTS, 0, 0, 0); + sqlite3PExprAddSelect(pParse, p, yymsp[-1].minor.yy159); } break; - case 229: /* expr ::= CASE case_operand case_exprlist case_else END */ + case 192: /* expr ::= CASE case_operand case_exprlist case_else END */ { - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_CASE, yymsp[-3].minor.yy132, 0, 0); - if( yygotominor.yy346.pExpr ){ - yygotominor.yy346.pExpr->x.pList = yymsp[-1].minor.yy132 ? sqlite3ExprListAppend(pParse,yymsp[-2].minor.yy14,yymsp[-1].minor.yy132) : yymsp[-2].minor.yy14; - sqlite3ExprSetHeightAndFlags(pParse, yygotominor.yy346.pExpr); + spanSet(&yymsp[-4].minor.yy342,&yymsp[-4].minor.yy0,&yymsp[0].minor.yy0); /*A-overwrites-C*/ + yymsp[-4].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_CASE, yymsp[-3].minor.yy122, 0, 0); + if( yymsp[-4].minor.yy342.pExpr ){ + yymsp[-4].minor.yy342.pExpr->x.pList = yymsp[-1].minor.yy122 ? sqlite3ExprListAppend(pParse,yymsp[-2].minor.yy442,yymsp[-1].minor.yy122) : yymsp[-2].minor.yy442; + sqlite3ExprSetHeightAndFlags(pParse, yymsp[-4].minor.yy342.pExpr); }else{ - sqlite3ExprListDelete(pParse->db, yymsp[-2].minor.yy14); - sqlite3ExprDelete(pParse->db, yymsp[-1].minor.yy132); + sqlite3ExprListDelete(pParse->db, yymsp[-2].minor.yy442); + sqlite3ExprDelete(pParse->db, yymsp[-1].minor.yy122); } - yygotominor.yy346.zStart = yymsp[-4].minor.yy0.z; - yygotominor.yy346.zEnd = &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n]; } break; - case 230: /* case_exprlist ::= case_exprlist WHEN expr THEN expr */ + case 193: /* case_exprlist ::= case_exprlist WHEN expr THEN expr */ { - yygotominor.yy14 = sqlite3ExprListAppend(pParse,yymsp[-4].minor.yy14, yymsp[-2].minor.yy346.pExpr); - yygotominor.yy14 = sqlite3ExprListAppend(pParse,yygotominor.yy14, yymsp[0].minor.yy346.pExpr); + yymsp[-4].minor.yy442 = sqlite3ExprListAppend(pParse,yymsp[-4].minor.yy442, yymsp[-2].minor.yy342.pExpr); + yymsp[-4].minor.yy442 = sqlite3ExprListAppend(pParse,yymsp[-4].minor.yy442, yymsp[0].minor.yy342.pExpr); } break; - case 231: /* case_exprlist ::= WHEN expr THEN expr */ + case 194: /* case_exprlist ::= WHEN expr THEN expr */ { - yygotominor.yy14 = sqlite3ExprListAppend(pParse,0, yymsp[-2].minor.yy346.pExpr); - yygotominor.yy14 = sqlite3ExprListAppend(pParse,yygotominor.yy14, yymsp[0].minor.yy346.pExpr); + yymsp[-3].minor.yy442 = sqlite3ExprListAppend(pParse,0, yymsp[-2].minor.yy342.pExpr); + yymsp[-3].minor.yy442 = sqlite3ExprListAppend(pParse,yymsp[-3].minor.yy442, yymsp[0].minor.yy342.pExpr); } break; - case 238: /* nexprlist ::= nexprlist COMMA expr */ -{yygotominor.yy14 = sqlite3ExprListAppend(pParse,yymsp[-2].minor.yy14,yymsp[0].minor.yy346.pExpr);} + case 197: /* case_operand ::= expr */ +{yymsp[0].minor.yy122 = yymsp[0].minor.yy342.pExpr; /*A-overwrites-X*/} break; - case 239: /* nexprlist ::= expr */ -{yygotominor.yy14 = sqlite3ExprListAppend(pParse,0,yymsp[0].minor.yy346.pExpr);} + case 200: /* nexprlist ::= nexprlist COMMA expr */ +{yymsp[-2].minor.yy442 = sqlite3ExprListAppend(pParse,yymsp[-2].minor.yy442,yymsp[0].minor.yy342.pExpr);} break; - case 240: /* cmd ::= createkw uniqueflag INDEX ifnotexists nm dbnm ON nm LP sortlist RP where_opt */ + case 201: /* nexprlist ::= expr */ +{yymsp[0].minor.yy442 = sqlite3ExprListAppend(pParse,0,yymsp[0].minor.yy342.pExpr); /*A-overwrites-Y*/} + break; + case 202: /* cmd ::= createkw uniqueflag INDEX ifnotexists nm dbnm ON nm LP sortlist RP where_opt */ { sqlite3CreateIndex(pParse, &yymsp[-7].minor.yy0, &yymsp[-6].minor.yy0, - sqlite3SrcListAppend(pParse->db,0,&yymsp[-4].minor.yy0,0), yymsp[-2].minor.yy14, yymsp[-10].minor.yy328, - &yymsp[-11].minor.yy0, yymsp[0].minor.yy132, SQLITE_SO_ASC, yymsp[-8].minor.yy328); + sqlite3SrcListAppend(pParse->db,0,&yymsp[-4].minor.yy0,0), yymsp[-2].minor.yy442, yymsp[-10].minor.yy392, + &yymsp[-11].minor.yy0, yymsp[0].minor.yy122, SQLITE_SO_ASC, yymsp[-8].minor.yy392); } break; - case 241: /* uniqueflag ::= UNIQUE */ - case 292: /* raisetype ::= ABORT */ yytestcase(yyruleno==292); -{yygotominor.yy328 = OE_Abort;} + case 203: /* uniqueflag ::= UNIQUE */ + case 244: /* raisetype ::= ABORT */ yytestcase(yyruleno==244); +{yymsp[0].minor.yy392 = OE_Abort;} break; - case 242: /* uniqueflag ::= */ -{yygotominor.yy328 = OE_None;} + case 204: /* uniqueflag ::= */ +{yymsp[1].minor.yy392 = OE_None;} break; - case 245: /* eidlist ::= eidlist COMMA nm collate sortorder */ + case 206: /* eidlist_opt ::= LP eidlist RP */ +{yymsp[-2].minor.yy442 = yymsp[-1].minor.yy442;} + break; + case 207: /* eidlist ::= eidlist COMMA nm collate sortorder */ { - yygotominor.yy14 = parserAddExprIdListTerm(pParse, yymsp[-4].minor.yy14, &yymsp[-2].minor.yy0, yymsp[-1].minor.yy328, yymsp[0].minor.yy328); + yymsp[-4].minor.yy442 = parserAddExprIdListTerm(pParse, yymsp[-4].minor.yy442, &yymsp[-2].minor.yy0, yymsp[-1].minor.yy392, yymsp[0].minor.yy392); } break; - case 246: /* eidlist ::= nm collate sortorder */ + case 208: /* eidlist ::= nm collate sortorder */ { - yygotominor.yy14 = parserAddExprIdListTerm(pParse, 0, &yymsp[-2].minor.yy0, yymsp[-1].minor.yy328, yymsp[0].minor.yy328); + yymsp[-2].minor.yy442 = parserAddExprIdListTerm(pParse, 0, &yymsp[-2].minor.yy0, yymsp[-1].minor.yy392, yymsp[0].minor.yy392); /*A-overwrites-Y*/ } break; - case 249: /* cmd ::= DROP INDEX ifexists fullname */ -{sqlite3DropIndex(pParse, yymsp[0].minor.yy65, yymsp[-1].minor.yy328);} + case 211: /* cmd ::= DROP INDEX ifexists fullname */ +{sqlite3DropIndex(pParse, yymsp[0].minor.yy347, yymsp[-1].minor.yy392);} break; - case 250: /* cmd ::= VACUUM */ - case 251: /* cmd ::= VACUUM nm */ yytestcase(yyruleno==251); + case 212: /* cmd ::= VACUUM */ + case 213: /* cmd ::= VACUUM nm */ yytestcase(yyruleno==213); {sqlite3Vacuum(pParse);} break; - case 252: /* cmd ::= PRAGMA nm dbnm */ + case 214: /* cmd ::= PRAGMA nm dbnm */ {sqlite3Pragma(pParse,&yymsp[-1].minor.yy0,&yymsp[0].minor.yy0,0,0);} break; - case 253: /* cmd ::= PRAGMA nm dbnm EQ nmnum */ + case 215: /* cmd ::= PRAGMA nm dbnm EQ nmnum */ {sqlite3Pragma(pParse,&yymsp[-3].minor.yy0,&yymsp[-2].minor.yy0,&yymsp[0].minor.yy0,0);} break; - case 254: /* cmd ::= PRAGMA nm dbnm LP nmnum RP */ + case 216: /* cmd ::= PRAGMA nm dbnm LP nmnum RP */ {sqlite3Pragma(pParse,&yymsp[-4].minor.yy0,&yymsp[-3].minor.yy0,&yymsp[-1].minor.yy0,0);} break; - case 255: /* cmd ::= PRAGMA nm dbnm EQ minus_num */ + case 217: /* cmd ::= PRAGMA nm dbnm EQ minus_num */ {sqlite3Pragma(pParse,&yymsp[-3].minor.yy0,&yymsp[-2].minor.yy0,&yymsp[0].minor.yy0,1);} break; - case 256: /* cmd ::= PRAGMA nm dbnm LP minus_num RP */ + case 218: /* cmd ::= PRAGMA nm dbnm LP minus_num RP */ {sqlite3Pragma(pParse,&yymsp[-4].minor.yy0,&yymsp[-3].minor.yy0,&yymsp[-1].minor.yy0,1);} break; - case 265: /* cmd ::= createkw trigger_decl BEGIN trigger_cmd_list END */ + case 221: /* cmd ::= createkw trigger_decl BEGIN trigger_cmd_list END */ { Token all; all.z = yymsp[-3].minor.yy0.z; all.n = (int)(yymsp[0].minor.yy0.z - yymsp[-3].minor.yy0.z) + yymsp[0].minor.yy0.n; - sqlite3FinishTrigger(pParse, yymsp[-1].minor.yy473, &all); + sqlite3FinishTrigger(pParse, yymsp[-1].minor.yy327, &all); } break; - case 266: /* trigger_decl ::= temp TRIGGER ifnotexists nm dbnm trigger_time trigger_event ON fullname foreach_clause when_clause */ + case 222: /* trigger_decl ::= temp TRIGGER ifnotexists nm dbnm trigger_time trigger_event ON fullname foreach_clause when_clause */ { - sqlite3BeginTrigger(pParse, &yymsp[-7].minor.yy0, &yymsp[-6].minor.yy0, yymsp[-5].minor.yy328, yymsp[-4].minor.yy378.a, yymsp[-4].minor.yy378.b, yymsp[-2].minor.yy65, yymsp[0].minor.yy132, yymsp[-10].minor.yy328, yymsp[-8].minor.yy328); - yygotominor.yy0 = (yymsp[-6].minor.yy0.n==0?yymsp[-7].minor.yy0:yymsp[-6].minor.yy0); + sqlite3BeginTrigger(pParse, &yymsp[-7].minor.yy0, &yymsp[-6].minor.yy0, yymsp[-5].minor.yy392, yymsp[-4].minor.yy410.a, yymsp[-4].minor.yy410.b, yymsp[-2].minor.yy347, yymsp[0].minor.yy122, yymsp[-10].minor.yy392, yymsp[-8].minor.yy392); + yymsp[-10].minor.yy0 = (yymsp[-6].minor.yy0.n==0?yymsp[-7].minor.yy0:yymsp[-6].minor.yy0); /*A-overwrites-T*/ } break; - case 267: /* trigger_time ::= BEFORE */ - case 270: /* trigger_time ::= */ yytestcase(yyruleno==270); -{ yygotominor.yy328 = TK_BEFORE; } + case 223: /* trigger_time ::= BEFORE */ +{ yymsp[0].minor.yy392 = TK_BEFORE; } break; - case 268: /* trigger_time ::= AFTER */ -{ yygotominor.yy328 = TK_AFTER; } + case 224: /* trigger_time ::= AFTER */ +{ yymsp[0].minor.yy392 = TK_AFTER; } break; - case 269: /* trigger_time ::= INSTEAD OF */ -{ yygotominor.yy328 = TK_INSTEAD;} + case 225: /* trigger_time ::= INSTEAD OF */ +{ yymsp[-1].minor.yy392 = TK_INSTEAD;} break; - case 271: /* trigger_event ::= DELETE|INSERT */ - case 272: /* trigger_event ::= UPDATE */ yytestcase(yyruleno==272); -{yygotominor.yy378.a = yymsp[0].major; yygotominor.yy378.b = 0;} + case 226: /* trigger_time ::= */ +{ yymsp[1].minor.yy392 = TK_BEFORE; } break; - case 273: /* trigger_event ::= UPDATE OF idlist */ -{yygotominor.yy378.a = TK_UPDATE; yygotominor.yy378.b = yymsp[0].minor.yy408;} + case 227: /* trigger_event ::= DELETE|INSERT */ + case 228: /* trigger_event ::= UPDATE */ yytestcase(yyruleno==228); +{yymsp[0].minor.yy410.a = yymsp[0].major; /*A-overwrites-X*/ yymsp[0].minor.yy410.b = 0;} break; - case 276: /* when_clause ::= */ - case 297: /* key_opt ::= */ yytestcase(yyruleno==297); -{ yygotominor.yy132 = 0; } + case 229: /* trigger_event ::= UPDATE OF idlist */ +{yymsp[-2].minor.yy410.a = TK_UPDATE; yymsp[-2].minor.yy410.b = yymsp[0].minor.yy180;} break; - case 277: /* when_clause ::= WHEN expr */ - case 298: /* key_opt ::= KEY expr */ yytestcase(yyruleno==298); -{ yygotominor.yy132 = yymsp[0].minor.yy346.pExpr; } + case 230: /* when_clause ::= */ + case 249: /* key_opt ::= */ yytestcase(yyruleno==249); +{ yymsp[1].minor.yy122 = 0; } break; - case 278: /* trigger_cmd_list ::= trigger_cmd_list trigger_cmd SEMI */ + case 231: /* when_clause ::= WHEN expr */ + case 250: /* key_opt ::= KEY expr */ yytestcase(yyruleno==250); +{ yymsp[-1].minor.yy122 = yymsp[0].minor.yy342.pExpr; } + break; + case 232: /* trigger_cmd_list ::= trigger_cmd_list trigger_cmd SEMI */ { - assert( yymsp[-2].minor.yy473!=0 ); - yymsp[-2].minor.yy473->pLast->pNext = yymsp[-1].minor.yy473; - yymsp[-2].minor.yy473->pLast = yymsp[-1].minor.yy473; - yygotominor.yy473 = yymsp[-2].minor.yy473; + assert( yymsp[-2].minor.yy327!=0 ); + yymsp[-2].minor.yy327->pLast->pNext = yymsp[-1].minor.yy327; + yymsp[-2].minor.yy327->pLast = yymsp[-1].minor.yy327; } break; - case 279: /* trigger_cmd_list ::= trigger_cmd SEMI */ + case 233: /* trigger_cmd_list ::= trigger_cmd SEMI */ { - assert( yymsp[-1].minor.yy473!=0 ); - yymsp[-1].minor.yy473->pLast = yymsp[-1].minor.yy473; - yygotominor.yy473 = yymsp[-1].minor.yy473; + assert( yymsp[-1].minor.yy327!=0 ); + yymsp[-1].minor.yy327->pLast = yymsp[-1].minor.yy327; } break; - case 281: /* trnm ::= nm DOT nm */ + case 234: /* trnm ::= nm DOT nm */ { - yygotominor.yy0 = yymsp[0].minor.yy0; + yymsp[-2].minor.yy0 = yymsp[0].minor.yy0; sqlite3ErrorMsg(pParse, "qualified table names are not allowed on INSERT, UPDATE, and DELETE " "statements within triggers"); } break; - case 283: /* tridxby ::= INDEXED BY nm */ + case 235: /* tridxby ::= INDEXED BY nm */ { sqlite3ErrorMsg(pParse, "the INDEXED BY clause is not allowed on UPDATE or DELETE statements " "within triggers"); } break; - case 284: /* tridxby ::= NOT INDEXED */ + case 236: /* tridxby ::= NOT INDEXED */ { sqlite3ErrorMsg(pParse, "the NOT INDEXED clause is not allowed on UPDATE or DELETE statements " "within triggers"); } break; - case 285: /* trigger_cmd ::= UPDATE orconf trnm tridxby SET setlist where_opt */ -{ yygotominor.yy473 = sqlite3TriggerUpdateStep(pParse->db, &yymsp[-4].minor.yy0, yymsp[-1].minor.yy14, yymsp[0].minor.yy132, yymsp[-5].minor.yy186); } + case 237: /* trigger_cmd ::= UPDATE orconf trnm tridxby SET setlist where_opt */ +{yymsp[-6].minor.yy327 = sqlite3TriggerUpdateStep(pParse->db, &yymsp[-4].minor.yy0, yymsp[-1].minor.yy442, yymsp[0].minor.yy122, yymsp[-5].minor.yy392);} break; - case 286: /* trigger_cmd ::= insert_cmd INTO trnm idlist_opt select */ -{yygotominor.yy473 = sqlite3TriggerInsertStep(pParse->db, &yymsp[-2].minor.yy0, yymsp[-1].minor.yy408, yymsp[0].minor.yy3, yymsp[-4].minor.yy186);} + case 238: /* trigger_cmd ::= insert_cmd INTO trnm idlist_opt select */ +{yymsp[-4].minor.yy327 = sqlite3TriggerInsertStep(pParse->db, &yymsp[-2].minor.yy0, yymsp[-1].minor.yy180, yymsp[0].minor.yy159, yymsp[-4].minor.yy392);/*A-overwrites-R*/} break; - case 287: /* trigger_cmd ::= DELETE FROM trnm tridxby where_opt */ -{yygotominor.yy473 = sqlite3TriggerDeleteStep(pParse->db, &yymsp[-2].minor.yy0, yymsp[0].minor.yy132);} + case 239: /* trigger_cmd ::= DELETE FROM trnm tridxby where_opt */ +{yymsp[-4].minor.yy327 = sqlite3TriggerDeleteStep(pParse->db, &yymsp[-2].minor.yy0, yymsp[0].minor.yy122);} break; - case 288: /* trigger_cmd ::= select */ -{yygotominor.yy473 = sqlite3TriggerSelectStep(pParse->db, yymsp[0].minor.yy3); } + case 240: /* trigger_cmd ::= select */ +{yymsp[0].minor.yy327 = sqlite3TriggerSelectStep(pParse->db, yymsp[0].minor.yy159); /*A-overwrites-X*/} break; - case 289: /* expr ::= RAISE LP IGNORE RP */ + case 241: /* expr ::= RAISE LP IGNORE RP */ { - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_RAISE, 0, 0, 0); - if( yygotominor.yy346.pExpr ){ - yygotominor.yy346.pExpr->affinity = OE_Ignore; + spanSet(&yymsp[-3].minor.yy342,&yymsp[-3].minor.yy0,&yymsp[0].minor.yy0); /*A-overwrites-X*/ + yymsp[-3].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_RAISE, 0, 0, 0); + if( yymsp[-3].minor.yy342.pExpr ){ + yymsp[-3].minor.yy342.pExpr->affinity = OE_Ignore; } - yygotominor.yy346.zStart = yymsp[-3].minor.yy0.z; - yygotominor.yy346.zEnd = &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n]; } break; - case 290: /* expr ::= RAISE LP raisetype COMMA nm RP */ + case 242: /* expr ::= RAISE LP raisetype COMMA nm RP */ { - yygotominor.yy346.pExpr = sqlite3PExpr(pParse, TK_RAISE, 0, 0, &yymsp[-1].minor.yy0); - if( yygotominor.yy346.pExpr ) { - yygotominor.yy346.pExpr->affinity = (char)yymsp[-3].minor.yy328; + spanSet(&yymsp[-5].minor.yy342,&yymsp[-5].minor.yy0,&yymsp[0].minor.yy0); /*A-overwrites-X*/ + yymsp[-5].minor.yy342.pExpr = sqlite3PExpr(pParse, TK_RAISE, 0, 0, &yymsp[-1].minor.yy0); + if( yymsp[-5].minor.yy342.pExpr ) { + yymsp[-5].minor.yy342.pExpr->affinity = (char)yymsp[-3].minor.yy392; } - yygotominor.yy346.zStart = yymsp[-5].minor.yy0.z; - yygotominor.yy346.zEnd = &yymsp[0].minor.yy0.z[yymsp[0].minor.yy0.n]; } break; - case 291: /* raisetype ::= ROLLBACK */ -{yygotominor.yy328 = OE_Rollback;} + case 243: /* raisetype ::= ROLLBACK */ +{yymsp[0].minor.yy392 = OE_Rollback;} break; - case 293: /* raisetype ::= FAIL */ -{yygotominor.yy328 = OE_Fail;} + case 245: /* raisetype ::= FAIL */ +{yymsp[0].minor.yy392 = OE_Fail;} break; - case 294: /* cmd ::= DROP TRIGGER ifexists fullname */ + case 246: /* cmd ::= DROP TRIGGER ifexists fullname */ { - sqlite3DropTrigger(pParse,yymsp[0].minor.yy65,yymsp[-1].minor.yy328); + sqlite3DropTrigger(pParse,yymsp[0].minor.yy347,yymsp[-1].minor.yy392); } break; - case 295: /* cmd ::= ATTACH database_kw_opt expr AS expr key_opt */ + case 247: /* cmd ::= ATTACH database_kw_opt expr AS expr key_opt */ { - sqlite3Attach(pParse, yymsp[-3].minor.yy346.pExpr, yymsp[-1].minor.yy346.pExpr, yymsp[0].minor.yy132); + sqlite3Attach(pParse, yymsp[-3].minor.yy342.pExpr, yymsp[-1].minor.yy342.pExpr, yymsp[0].minor.yy122); } break; - case 296: /* cmd ::= DETACH database_kw_opt expr */ + case 248: /* cmd ::= DETACH database_kw_opt expr */ { - sqlite3Detach(pParse, yymsp[0].minor.yy346.pExpr); + sqlite3Detach(pParse, yymsp[0].minor.yy342.pExpr); } break; - case 301: /* cmd ::= REINDEX */ + case 251: /* cmd ::= REINDEX */ {sqlite3Reindex(pParse, 0, 0);} break; - case 302: /* cmd ::= REINDEX nm dbnm */ + case 252: /* cmd ::= REINDEX nm dbnm */ {sqlite3Reindex(pParse, &yymsp[-1].minor.yy0, &yymsp[0].minor.yy0);} break; - case 303: /* cmd ::= ANALYZE */ + case 253: /* cmd ::= ANALYZE */ {sqlite3Analyze(pParse, 0, 0);} break; - case 304: /* cmd ::= ANALYZE nm dbnm */ + case 254: /* cmd ::= ANALYZE nm dbnm */ {sqlite3Analyze(pParse, &yymsp[-1].minor.yy0, &yymsp[0].minor.yy0);} break; - case 305: /* cmd ::= ALTER TABLE fullname RENAME TO nm */ + case 255: /* cmd ::= ALTER TABLE fullname RENAME TO nm */ { - sqlite3AlterRenameTable(pParse,yymsp[-3].minor.yy65,&yymsp[0].minor.yy0); + sqlite3AlterRenameTable(pParse,yymsp[-3].minor.yy347,&yymsp[0].minor.yy0); } break; - case 306: /* cmd ::= ALTER TABLE add_column_fullname ADD kwcolumn_opt column */ + case 256: /* cmd ::= ALTER TABLE add_column_fullname ADD kwcolumn_opt columnname carglist */ { - sqlite3AlterFinishAddColumn(pParse, &yymsp[0].minor.yy0); + yymsp[-1].minor.yy0.n = (int)(pParse->sLastToken.z-yymsp[-1].minor.yy0.z) + pParse->sLastToken.n; + sqlite3AlterFinishAddColumn(pParse, &yymsp[-1].minor.yy0); } break; - case 307: /* add_column_fullname ::= fullname */ + case 257: /* add_column_fullname ::= fullname */ { - pParse->db->lookaside.bEnabled = 0; - sqlite3AlterBeginAddColumn(pParse, yymsp[0].minor.yy65); + disableLookaside(pParse); + sqlite3AlterBeginAddColumn(pParse, yymsp[0].minor.yy347); } break; - case 310: /* cmd ::= create_vtab */ + case 258: /* cmd ::= create_vtab */ {sqlite3VtabFinishParse(pParse,0);} break; - case 311: /* cmd ::= create_vtab LP vtabarglist RP */ + case 259: /* cmd ::= create_vtab LP vtabarglist RP */ {sqlite3VtabFinishParse(pParse,&yymsp[0].minor.yy0);} break; - case 312: /* create_vtab ::= createkw VIRTUAL TABLE ifnotexists nm dbnm USING nm */ + case 260: /* create_vtab ::= createkw VIRTUAL TABLE ifnotexists nm dbnm USING nm */ { - sqlite3VtabBeginParse(pParse, &yymsp[-3].minor.yy0, &yymsp[-2].minor.yy0, &yymsp[0].minor.yy0, yymsp[-4].minor.yy328); + sqlite3VtabBeginParse(pParse, &yymsp[-3].minor.yy0, &yymsp[-2].minor.yy0, &yymsp[0].minor.yy0, yymsp[-4].minor.yy392); } break; - case 315: /* vtabarg ::= */ + case 261: /* vtabarg ::= */ {sqlite3VtabArgInit(pParse);} break; - case 317: /* vtabargtoken ::= ANY */ - case 318: /* vtabargtoken ::= lp anylist RP */ yytestcase(yyruleno==318); - case 319: /* lp ::= LP */ yytestcase(yyruleno==319); + case 262: /* vtabargtoken ::= ANY */ + case 263: /* vtabargtoken ::= lp anylist RP */ yytestcase(yyruleno==263); + case 264: /* lp ::= LP */ yytestcase(yyruleno==264); {sqlite3VtabArgExtend(pParse,&yymsp[0].minor.yy0);} break; - case 323: /* with ::= */ -{yygotominor.yy59 = 0;} + case 265: /* with ::= */ +{yymsp[1].minor.yy331 = 0;} break; - case 324: /* with ::= WITH wqlist */ - case 325: /* with ::= WITH RECURSIVE wqlist */ yytestcase(yyruleno==325); -{ yygotominor.yy59 = yymsp[0].minor.yy59; } + case 266: /* with ::= WITH wqlist */ +{ yymsp[-1].minor.yy331 = yymsp[0].minor.yy331; } break; - case 326: /* wqlist ::= nm eidlist_opt AS LP select RP */ + case 267: /* with ::= WITH RECURSIVE wqlist */ +{ yymsp[-2].minor.yy331 = yymsp[0].minor.yy331; } + break; + case 268: /* wqlist ::= nm eidlist_opt AS LP select RP */ { - yygotominor.yy59 = sqlite3WithAdd(pParse, 0, &yymsp[-5].minor.yy0, yymsp[-4].minor.yy14, yymsp[-1].minor.yy3); + yymsp[-5].minor.yy331 = sqlite3WithAdd(pParse, 0, &yymsp[-5].minor.yy0, yymsp[-4].minor.yy442, yymsp[-1].minor.yy159); /*A-overwrites-X*/ } break; - case 327: /* wqlist ::= wqlist COMMA nm eidlist_opt AS LP select RP */ + case 269: /* wqlist ::= wqlist COMMA nm eidlist_opt AS LP select RP */ { - yygotominor.yy59 = sqlite3WithAdd(pParse, yymsp[-7].minor.yy59, &yymsp[-5].minor.yy0, yymsp[-4].minor.yy14, yymsp[-1].minor.yy3); + yymsp[-7].minor.yy331 = sqlite3WithAdd(pParse, yymsp[-7].minor.yy331, &yymsp[-5].minor.yy0, yymsp[-4].minor.yy442, yymsp[-1].minor.yy159); } break; default: - /* (0) input ::= cmdlist */ yytestcase(yyruleno==0); - /* (1) cmdlist ::= cmdlist ecmd */ yytestcase(yyruleno==1); - /* (2) cmdlist ::= ecmd */ yytestcase(yyruleno==2); - /* (3) ecmd ::= SEMI */ yytestcase(yyruleno==3); - /* (4) ecmd ::= explain cmdx SEMI */ yytestcase(yyruleno==4); - /* (10) trans_opt ::= */ yytestcase(yyruleno==10); - /* (11) trans_opt ::= TRANSACTION */ yytestcase(yyruleno==11); - /* (12) trans_opt ::= TRANSACTION nm */ yytestcase(yyruleno==12); - /* (20) savepoint_opt ::= SAVEPOINT */ yytestcase(yyruleno==20); - /* (21) savepoint_opt ::= */ yytestcase(yyruleno==21); - /* (25) cmd ::= create_table create_table_args */ yytestcase(yyruleno==25); - /* (36) columnlist ::= columnlist COMMA column */ yytestcase(yyruleno==36); - /* (37) columnlist ::= column */ yytestcase(yyruleno==37); - /* (43) type ::= */ yytestcase(yyruleno==43); - /* (50) signed ::= plus_num */ yytestcase(yyruleno==50); - /* (51) signed ::= minus_num */ yytestcase(yyruleno==51); - /* (52) carglist ::= carglist ccons */ yytestcase(yyruleno==52); - /* (53) carglist ::= */ yytestcase(yyruleno==53); - /* (60) ccons ::= NULL onconf */ yytestcase(yyruleno==60); - /* (88) conslist ::= conslist tconscomma tcons */ yytestcase(yyruleno==88); - /* (89) conslist ::= tcons */ yytestcase(yyruleno==89); - /* (91) tconscomma ::= */ yytestcase(yyruleno==91); - /* (274) foreach_clause ::= */ yytestcase(yyruleno==274); - /* (275) foreach_clause ::= FOR EACH ROW */ yytestcase(yyruleno==275); - /* (282) tridxby ::= */ yytestcase(yyruleno==282); - /* (299) database_kw_opt ::= DATABASE */ yytestcase(yyruleno==299); - /* (300) database_kw_opt ::= */ yytestcase(yyruleno==300); - /* (308) kwcolumn_opt ::= */ yytestcase(yyruleno==308); - /* (309) kwcolumn_opt ::= COLUMNKW */ yytestcase(yyruleno==309); - /* (313) vtabarglist ::= vtabarg */ yytestcase(yyruleno==313); - /* (314) vtabarglist ::= vtabarglist COMMA vtabarg */ yytestcase(yyruleno==314); - /* (316) vtabarg ::= vtabarg vtabargtoken */ yytestcase(yyruleno==316); - /* (320) anylist ::= */ yytestcase(yyruleno==320); - /* (321) anylist ::= anylist LP anylist RP */ yytestcase(yyruleno==321); - /* (322) anylist ::= anylist ANY */ yytestcase(yyruleno==322); + /* (270) input ::= cmdlist */ yytestcase(yyruleno==270); + /* (271) cmdlist ::= cmdlist ecmd */ yytestcase(yyruleno==271); + /* (272) cmdlist ::= ecmd */ yytestcase(yyruleno==272); + /* (273) ecmd ::= SEMI */ yytestcase(yyruleno==273); + /* (274) ecmd ::= explain cmdx SEMI */ yytestcase(yyruleno==274); + /* (275) explain ::= */ yytestcase(yyruleno==275); + /* (276) trans_opt ::= */ yytestcase(yyruleno==276); + /* (277) trans_opt ::= TRANSACTION */ yytestcase(yyruleno==277); + /* (278) trans_opt ::= TRANSACTION nm */ yytestcase(yyruleno==278); + /* (279) savepoint_opt ::= SAVEPOINT */ yytestcase(yyruleno==279); + /* (280) savepoint_opt ::= */ yytestcase(yyruleno==280); + /* (281) cmd ::= create_table create_table_args */ yytestcase(yyruleno==281); + /* (282) columnlist ::= columnlist COMMA columnname carglist */ yytestcase(yyruleno==282); + /* (283) columnlist ::= columnname carglist */ yytestcase(yyruleno==283); + /* (284) nm ::= ID|INDEXED */ yytestcase(yyruleno==284); + /* (285) nm ::= STRING */ yytestcase(yyruleno==285); + /* (286) nm ::= JOIN_KW */ yytestcase(yyruleno==286); + /* (287) typetoken ::= typename */ yytestcase(yyruleno==287); + /* (288) typename ::= ID|STRING */ yytestcase(yyruleno==288); + /* (289) signed ::= plus_num */ yytestcase(yyruleno==289); + /* (290) signed ::= minus_num */ yytestcase(yyruleno==290); + /* (291) carglist ::= carglist ccons */ yytestcase(yyruleno==291); + /* (292) carglist ::= */ yytestcase(yyruleno==292); + /* (293) ccons ::= NULL onconf */ yytestcase(yyruleno==293); + /* (294) conslist_opt ::= COMMA conslist */ yytestcase(yyruleno==294); + /* (295) conslist ::= conslist tconscomma tcons */ yytestcase(yyruleno==295); + /* (296) conslist ::= tcons */ yytestcase(yyruleno==296); + /* (297) tconscomma ::= */ yytestcase(yyruleno==297); + /* (298) defer_subclause_opt ::= defer_subclause */ yytestcase(yyruleno==298); + /* (299) resolvetype ::= raisetype */ yytestcase(yyruleno==299); + /* (300) selectnowith ::= oneselect */ yytestcase(yyruleno==300); + /* (301) oneselect ::= values */ yytestcase(yyruleno==301); + /* (302) sclp ::= selcollist COMMA */ yytestcase(yyruleno==302); + /* (303) as ::= ID|STRING */ yytestcase(yyruleno==303); + /* (304) expr ::= term */ yytestcase(yyruleno==304); + /* (305) exprlist ::= nexprlist */ yytestcase(yyruleno==305); + /* (306) nmnum ::= plus_num */ yytestcase(yyruleno==306); + /* (307) nmnum ::= nm */ yytestcase(yyruleno==307); + /* (308) nmnum ::= ON */ yytestcase(yyruleno==308); + /* (309) nmnum ::= DELETE */ yytestcase(yyruleno==309); + /* (310) nmnum ::= DEFAULT */ yytestcase(yyruleno==310); + /* (311) plus_num ::= INTEGER|FLOAT */ yytestcase(yyruleno==311); + /* (312) foreach_clause ::= */ yytestcase(yyruleno==312); + /* (313) foreach_clause ::= FOR EACH ROW */ yytestcase(yyruleno==313); + /* (314) trnm ::= nm */ yytestcase(yyruleno==314); + /* (315) tridxby ::= */ yytestcase(yyruleno==315); + /* (316) database_kw_opt ::= DATABASE */ yytestcase(yyruleno==316); + /* (317) database_kw_opt ::= */ yytestcase(yyruleno==317); + /* (318) kwcolumn_opt ::= */ yytestcase(yyruleno==318); + /* (319) kwcolumn_opt ::= COLUMNKW */ yytestcase(yyruleno==319); + /* (320) vtabarglist ::= vtabarg */ yytestcase(yyruleno==320); + /* (321) vtabarglist ::= vtabarglist COMMA vtabarg */ yytestcase(yyruleno==321); + /* (322) vtabarg ::= vtabarg vtabargtoken */ yytestcase(yyruleno==322); + /* (323) anylist ::= */ yytestcase(yyruleno==323); + /* (324) anylist ::= anylist LP anylist RP */ yytestcase(yyruleno==324); + /* (325) anylist ::= anylist ANY */ yytestcase(yyruleno==325); break; +/********** End reduce actions ************************************************/ }; - assert( yyruleno>=0 && yyrulenoyyidx -= yysize; yyact = yy_find_reduce_action(yymsp[-yysize].stateno,(YYCODETYPE)yygoto); if( yyact <= YY_MAX_SHIFTREDUCE ){ if( yyact>YY_MAX_SHIFT ) yyact += YY_MIN_REDUCE - YY_MIN_SHIFTREDUCE; - /* If the reduce action popped at least - ** one element off the stack, then we can push the new element back - ** onto the stack here, and skip the stack overflow test in yy_shift(). - ** That gives a significant speed improvement. */ - if( yysize ){ - yypParser->yyidx++; - yymsp -= yysize-1; - yymsp->stateno = (YYACTIONTYPE)yyact; - yymsp->major = (YYCODETYPE)yygoto; - yymsp->minor = yygotominor; - yyTraceShift(yypParser, yyact); - }else{ - yy_shift(yypParser,yyact,yygoto,&yygotominor); - } + yypParser->yyidx -= yysize - 1; + yymsp -= yysize-1; + yymsp->stateno = (YYACTIONTYPE)yyact; + yymsp->major = (YYCODETYPE)yygoto; + yyTraceShift(yypParser, yyact); }else{ assert( yyact == YY_ACCEPT_ACTION ); + yypParser->yyidx -= yysize; yy_accept(yypParser); } } @@ -129391,6 +134132,8 @@ static void yy_parse_failed( while( yypParser->yyidx>=0 ) yy_pop_parser_stack(yypParser); /* Here code is inserted which will be executed whenever the ** parser fails */ +/************ Begin %parse_failure code ***************************************/ +/************ End %parse_failure code *****************************************/ sqlite3ParserARG_STORE; /* Suppress warning about unused %extra_argument variable */ } #endif /* YYNOERRORRECOVERY */ @@ -129401,14 +134144,16 @@ static void yy_parse_failed( static void yy_syntax_error( yyParser *yypParser, /* The parser */ int yymajor, /* The major type of the error token */ - YYMINORTYPE yyminor /* The minor type of the error token */ + sqlite3ParserTOKENTYPE yyminor /* The minor type of the error token */ ){ sqlite3ParserARG_FETCH; -#define TOKEN (yyminor.yy0) +#define TOKEN yyminor +/************ Begin %syntax_error code ****************************************/ UNUSED_PARAMETER(yymajor); /* Silence some compiler warnings */ assert( TOKEN.z[0] ); /* The tokenizer always gives us a token */ sqlite3ErrorMsg(pParse, "near \"%T\": syntax error", &TOKEN); +/************ End %syntax_error code ******************************************/ sqlite3ParserARG_STORE; /* Suppress warning about unused %extra_argument variable */ } @@ -129427,6 +134172,8 @@ static void yy_accept( while( yypParser->yyidx>=0 ) yy_pop_parser_stack(yypParser); /* Here code is inserted which will be executed whenever the ** parser accepts */ +/*********** Begin %parse_accept code *****************************************/ +/*********** End %parse_accept code *******************************************/ sqlite3ParserARG_STORE; /* Suppress warning about unused %extra_argument variable */ } @@ -129456,7 +134203,7 @@ SQLITE_PRIVATE void sqlite3Parser( sqlite3ParserARG_PDECL /* Optional %extra_argument parameter */ ){ YYMINORTYPE yyminorunion; - int yyact; /* The parser action. */ + unsigned int yyact; /* The parser action. */ #if !defined(YYERRORSYMBOL) && !defined(YYNOERRORRECOVERY) int yyendofinput; /* True if we are at the end of input */ #endif @@ -129470,18 +134217,23 @@ SQLITE_PRIVATE void sqlite3Parser( if( yypParser->yyidx<0 ){ #if YYSTACKDEPTH<=0 if( yypParser->yystksz <=0 ){ - /*memset(&yyminorunion, 0, sizeof(yyminorunion));*/ - yyminorunion = yyzerominor; - yyStackOverflow(yypParser, &yyminorunion); + yyStackOverflow(yypParser); return; } #endif yypParser->yyidx = 0; +#ifndef YYNOERRORRECOVERY yypParser->yyerrcnt = -1; +#endif yypParser->yystack[0].stateno = 0; yypParser->yystack[0].major = 0; +#ifndef NDEBUG + if( yyTraceFILE ){ + fprintf(yyTraceFILE,"%sInitialize. Empty stack. State 0\n", + yyTracePrompt); + } +#endif } - yyminorunion.yy0 = yyminor; #if !defined(YYERRORSYMBOL) && !defined(YYNOERRORRECOVERY) yyendofinput = (yymajor==0); #endif @@ -129489,7 +134241,7 @@ SQLITE_PRIVATE void sqlite3Parser( #ifndef NDEBUG if( yyTraceFILE ){ - fprintf(yyTraceFILE,"%sInput %s\n",yyTracePrompt,yyTokenName[yymajor]); + fprintf(yyTraceFILE,"%sInput '%s'\n",yyTracePrompt,yyTokenName[yymajor]); } #endif @@ -129497,13 +134249,16 @@ SQLITE_PRIVATE void sqlite3Parser( yyact = yy_find_shift_action(yypParser,(YYCODETYPE)yymajor); if( yyact <= YY_MAX_SHIFTREDUCE ){ if( yyact > YY_MAX_SHIFT ) yyact += YY_MIN_REDUCE - YY_MIN_SHIFTREDUCE; - yy_shift(yypParser,yyact,yymajor,&yyminorunion); + yy_shift(yypParser,yyact,yymajor,yyminor); +#ifndef YYNOERRORRECOVERY yypParser->yyerrcnt--; +#endif yymajor = YYNOCODE; }else if( yyact <= YY_MAX_REDUCE ){ yy_reduce(yypParser,yyact-YY_MIN_REDUCE); }else{ assert( yyact == YY_ERROR_ACTION ); + yyminorunion.yy0 = yyminor; #ifdef YYERRORSYMBOL int yymx; #endif @@ -129533,7 +134288,7 @@ SQLITE_PRIVATE void sqlite3Parser( ** */ if( yypParser->yyerrcnt<0 ){ - yy_syntax_error(yypParser,yymajor,yyminorunion); + yy_syntax_error(yypParser,yymajor,yyminor); } yymx = yypParser->yystack[yypParser->yyidx].major; if( yymx==YYERRORSYMBOL || yyerrorhit ){ @@ -129543,10 +134298,10 @@ SQLITE_PRIVATE void sqlite3Parser( yyTracePrompt,yyTokenName[yymajor]); } #endif - yy_destructor(yypParser, (YYCODETYPE)yymajor,&yyminorunion); + yy_destructor(yypParser, (YYCODETYPE)yymajor, &yyminorunion); yymajor = YYNOCODE; }else{ - while( + while( yypParser->yyidx >= 0 && yymx != YYERRORSYMBOL && (yyact = yy_find_reduce_action( @@ -129560,9 +134315,7 @@ SQLITE_PRIVATE void sqlite3Parser( yy_parse_failed(yypParser); yymajor = YYNOCODE; }else if( yymx!=YYERRORSYMBOL ){ - YYMINORTYPE u2; - u2.YYERRSYMDT = 0; - yy_shift(yypParser,yyact,YYERRORSYMBOL,&u2); + yy_shift(yypParser,yyact,YYERRORSYMBOL,yyminor); } } yypParser->yyerrcnt = 3; @@ -129575,7 +134328,7 @@ SQLITE_PRIVATE void sqlite3Parser( ** Applications can set this macro (for example inside %include) if ** they intend to abandon the parse upon the first syntax error seen. */ - yy_syntax_error(yypParser,yymajor,yyminorunion); + yy_syntax_error(yypParser,yymajor, yyminor); yy_destructor(yypParser,(YYCODETYPE)yymajor,&yyminorunion); yymajor = YYNOCODE; @@ -129590,7 +134343,7 @@ SQLITE_PRIVATE void sqlite3Parser( ** three input tokens have been successfully shifted. */ if( yypParser->yyerrcnt<=0 ){ - yy_syntax_error(yypParser,yymajor,yyminorunion); + yy_syntax_error(yypParser,yymajor, yyminor); } yypParser->yyerrcnt = 3; yy_destructor(yypParser,(YYCODETYPE)yymajor,&yyminorunion); @@ -129603,7 +134356,12 @@ SQLITE_PRIVATE void sqlite3Parser( }while( yymajor!=YYNOCODE && yypParser->yyidx>=0 ); #ifndef NDEBUG if( yyTraceFILE ){ - fprintf(yyTraceFILE,"%sReturn\n",yyTracePrompt); + int i; + fprintf(yyTraceFILE,"%sReturn. Stack=",yyTracePrompt); + for(i=1; i<=yypParser->yyidx; i++) + fprintf(yyTraceFILE,"%c%s", i==1 ? '[' : ' ', + yyTokenName[yypParser->yystack[i].major]); + fprintf(yyTraceFILE,"]\n"); } #endif return; @@ -129631,12 +134389,92 @@ SQLITE_PRIVATE void sqlite3Parser( /* #include "sqliteInt.h" */ /* #include */ +/* Character classes for tokenizing +** +** In the sqlite3GetToken() function, a switch() on aiClass[c] is implemented +** using a lookup table, whereas a switch() directly on c uses a binary search. +** The lookup table is much faster. To maximize speed, and to ensure that +** a lookup table is used, all of the classes need to be small integers and +** all of them need to be used within the switch. +*/ +#define CC_X 0 /* The letter 'x', or start of BLOB literal */ +#define CC_KYWD 1 /* Alphabetics or '_'. Usable in a keyword */ +#define CC_ID 2 /* unicode characters usable in IDs */ +#define CC_DIGIT 3 /* Digits */ +#define CC_DOLLAR 4 /* '$' */ +#define CC_VARALPHA 5 /* '@', '#', ':'. Alphabetic SQL variables */ +#define CC_VARNUM 6 /* '?'. Numeric SQL variables */ +#define CC_SPACE 7 /* Space characters */ +#define CC_QUOTE 8 /* '"', '\'', or '`'. String literals, quoted ids */ +#define CC_QUOTE2 9 /* '['. [...] style quoted ids */ +#define CC_PIPE 10 /* '|'. Bitwise OR or concatenate */ +#define CC_MINUS 11 /* '-'. Minus or SQL-style comment */ +#define CC_LT 12 /* '<'. Part of < or <= or <> */ +#define CC_GT 13 /* '>'. Part of > or >= */ +#define CC_EQ 14 /* '='. Part of = or == */ +#define CC_BANG 15 /* '!'. Part of != */ +#define CC_SLASH 16 /* '/'. / or c-style comment */ +#define CC_LP 17 /* '(' */ +#define CC_RP 18 /* ')' */ +#define CC_SEMI 19 /* ';' */ +#define CC_PLUS 20 /* '+' */ +#define CC_STAR 21 /* '*' */ +#define CC_PERCENT 22 /* '%' */ +#define CC_COMMA 23 /* ',' */ +#define CC_AND 24 /* '&' */ +#define CC_TILDA 25 /* '~' */ +#define CC_DOT 26 /* '.' */ +#define CC_ILLEGAL 27 /* Illegal character */ + +static const unsigned char aiClass[] = { +#ifdef SQLITE_ASCII +/* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xa xb xc xd xe xf */ +/* 0x */ 27, 27, 27, 27, 27, 27, 27, 27, 27, 7, 7, 27, 7, 7, 27, 27, +/* 1x */ 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, +/* 2x */ 7, 15, 8, 5, 4, 22, 24, 8, 17, 18, 21, 20, 23, 11, 26, 16, +/* 3x */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 19, 12, 14, 13, 6, +/* 4x */ 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +/* 5x */ 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 9, 27, 27, 27, 1, +/* 6x */ 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +/* 7x */ 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 27, 10, 27, 25, 27, +/* 8x */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, +/* 9x */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, +/* Ax */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, +/* Bx */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, +/* Cx */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, +/* Dx */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, +/* Ex */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, +/* Fx */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 +#endif +#ifdef SQLITE_EBCDIC +/* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xa xb xc xd xe xf */ +/* 0x */ 27, 27, 27, 27, 27, 7, 27, 27, 27, 27, 27, 27, 7, 7, 27, 27, +/* 1x */ 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, +/* 2x */ 27, 27, 27, 27, 27, 7, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, +/* 3x */ 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, +/* 4x */ 7, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 12, 17, 20, 10, +/* 5x */ 24, 27, 27, 27, 27, 27, 27, 27, 27, 27, 15, 4, 21, 18, 19, 27, +/* 6x */ 11, 16, 27, 27, 27, 27, 27, 27, 27, 27, 27, 23, 22, 1, 13, 7, +/* 7x */ 27, 27, 27, 27, 27, 27, 27, 27, 27, 8, 5, 5, 5, 8, 14, 8, +/* 8x */ 27, 1, 1, 1, 1, 1, 1, 1, 1, 1, 27, 27, 27, 27, 27, 27, +/* 9x */ 27, 1, 1, 1, 1, 1, 1, 1, 1, 1, 27, 27, 27, 27, 27, 27, +/* 9x */ 25, 1, 1, 1, 1, 1, 1, 0, 1, 1, 27, 27, 27, 27, 27, 27, +/* Bx */ 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 9, 27, 27, 27, 27, 27, +/* Cx */ 27, 1, 1, 1, 1, 1, 1, 1, 1, 1, 27, 27, 27, 27, 27, 27, +/* Dx */ 27, 1, 1, 1, 1, 1, 1, 1, 1, 1, 27, 27, 27, 27, 27, 27, +/* Ex */ 27, 27, 1, 1, 1, 1, 1, 0, 1, 1, 27, 27, 27, 27, 27, 27, +/* Fx */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 27, 27, 27, 27, 27, 27, +#endif +}; + /* -** The charMap() macro maps alphabetic characters into their +** The charMap() macro maps alphabetic characters (only) into their ** lower-case ASCII equivalent. On ASCII machines, this is just ** an upper-to-lower case map. On EBCDIC machines we also need -** to adjust the encoding. Only alphabetic characters and underscores -** need to be translated. +** to adjust the encoding. The mapping is only valid for alphabetics +** which are the only characters for which this feature is used. +** +** Used by keywordhash.h */ #ifdef SQLITE_ASCII # define charMap(X) sqlite3UpperToLower[(unsigned char)X] @@ -129670,7 +134508,7 @@ const unsigned char ebcdicToAscii[] = { ** returned. If the input is not a keyword, TK_ID is returned. ** ** The implementation of this routine was generated by a program, -** mkkeywordhash.h, located in the tool subdirectory of the distribution. +** mkkeywordhash.c, located in the tool subdirectory of the distribution. ** The output of the mkkeywordhash.c program is written into a file ** named keywordhash.h and then included into this source file by ** the #include below. @@ -129691,7 +134529,7 @@ const unsigned char ebcdicToAscii[] = { ** on platforms with limited memory. */ /* Hash score: 182 */ -static int keywordCode(const char *z, int n){ +static int keywordCode(const char *z, int n, int *pType){ /* zText[] encodes 834 bytes of keywords in 554 bytes */ /* REINDEXEDESCAPEACHECKEYBEFOREIGNOREGEXPLAINSTEADDATABASELECT */ /* ABLEFTHENDEFERRABLELSEXCEPTRANSACTIONATURALTERAISEXCLUSIVE */ @@ -129811,13 +134649,21 @@ static int keywordCode(const char *z, int n){ TK_JOIN_KW, TK_ROLLBACK, TK_ROW, TK_UNION, TK_USING, TK_VACUUM, TK_VIEW, TK_INITIALLY, TK_ALL, }; - int h, i; - if( n<2 ) return TK_ID; - h = ((charMap(z[0])*4) ^ - (charMap(z[n-1])*3) ^ - n) % 127; - for(i=((int)aHash[h])-1; i>=0; i=((int)aNext[i])-1){ - if( aLen[i]==n && sqlite3StrNICmp(&zText[aOffset[i]],z,n)==0 ){ + int i, j; + const char *zKW; + if( n>=2 ){ + i = ((charMap(z[0])*4) ^ (charMap(z[n-1])*3) ^ n) % 127; + for(i=((int)aHash[i])-1; i>=0; i=((int)aNext[i])-1){ + if( aLen[i]!=n ) continue; + j = 0; + zKW = &zText[aOffset[i]]; +#ifdef SQLITE_ASCII + while( j': { + case CC_GT: { if( (c=z[1])=='=' ){ *tokenType = TK_GE; return 2; @@ -130091,16 +134942,16 @@ SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *z, int *tokenType){ return 1; } } - case '!': { + case CC_BANG: { if( z[1]!='=' ){ *tokenType = TK_ILLEGAL; - return 2; + return 1; }else{ *tokenType = TK_NE; return 2; } } - case '|': { + case CC_PIPE: { if( z[1]!='|' ){ *tokenType = TK_BITOR; return 1; @@ -130109,21 +134960,19 @@ SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *z, int *tokenType){ return 2; } } - case ',': { + case CC_COMMA: { *tokenType = TK_COMMA; return 1; } - case '&': { + case CC_AND: { *tokenType = TK_BITAND; return 1; } - case '~': { + case CC_TILDA: { *tokenType = TK_BITNOT; return 1; } - case '`': - case '\'': - case '"': { + case CC_QUOTE: { int delim = z[0]; testcase( delim=='`' ); testcase( delim=='\'' ); @@ -130148,7 +134997,7 @@ SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *z, int *tokenType){ return i; } } - case '.': { + case CC_DOT: { #ifndef SQLITE_OMIT_FLOATING_POINT if( !sqlite3Isdigit(z[1]) ) #endif @@ -130159,8 +135008,7 @@ SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *z, int *tokenType){ /* If the next character is a digit, this is a floating point ** number that begins with ".". Fall thru into the next case */ } - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': { + case CC_DIGIT: { testcase( z[0]=='0' ); testcase( z[0]=='1' ); testcase( z[0]=='2' ); testcase( z[0]=='3' ); testcase( z[0]=='4' ); testcase( z[0]=='5' ); testcase( z[0]=='6' ); testcase( z[0]=='7' ); testcase( z[0]=='8' ); @@ -130195,22 +135043,18 @@ SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *z, int *tokenType){ } return i; } - case '[': { + case CC_QUOTE2: { for(i=1, c=z[0]; c!=']' && (c=z[i])!=0; i++){} *tokenType = c==']' ? TK_ID : TK_ILLEGAL; return i; } - case '?': { + case CC_VARNUM: { *tokenType = TK_VARIABLE; for(i=1; sqlite3Isdigit(z[i]); i++){} return i; } -#ifndef SQLITE_OMIT_TCL_VARIABLE - case '$': -#endif - case '@': /* For compatibility with MS SQL Server */ - case '#': - case ':': { + case CC_DOLLAR: + case CC_VARALPHA: { int n = 0; testcase( z[0]=='$' ); testcase( z[0]=='@' ); testcase( z[0]==':' ); testcase( z[0]=='#' ); @@ -130239,8 +135083,20 @@ SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *z, int *tokenType){ if( n==0 ) *tokenType = TK_ILLEGAL; return i; } + case CC_KYWD: { + for(i=1; aiClass[z[i]]<=CC_KYWD; i++){} + if( IdChar(z[i]) ){ + /* This token started out using characters that can appear in keywords, + ** but z[i] is a character not allowed within keywords, so this must + ** be an identifier instead */ + i++; + break; + } + *tokenType = TK_ID; + return keywordCode((char*)z, i, tokenType); + } + case CC_X: { #ifndef SQLITE_OMIT_BLOB_LITERAL - case 'x': case 'X': { testcase( z[0]=='x' ); testcase( z[0]=='X' ); if( z[1]=='\'' ){ *tokenType = TK_BLOB; @@ -130252,20 +135108,22 @@ SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *z, int *tokenType){ if( z[i] ) i++; return i; } - /* Otherwise fall through to the next case */ - } #endif + /* If it is not a BLOB literal, then it must be an ID, since no + ** SQL keywords start with the letter 'x'. Fall through */ + } + case CC_ID: { + i = 1; + break; + } default: { - if( !IdChar(*z) ){ - break; - } - for(i=1; IdChar(z[i]); i++){} - *tokenType = keywordCode((char*)z, i); - return i; + *tokenType = TK_ILLEGAL; + return 1; } } - *tokenType = TK_ILLEGAL; - return 1; + while( IdChar(z[i]) ){ i++; } + *tokenType = TK_ID; + return i; } /* @@ -130281,7 +135139,6 @@ SQLITE_PRIVATE int sqlite3RunParser(Parse *pParse, const char *zSql, char **pzEr void *pEngine; /* The LEMON-generated LALR(1) parser */ int tokenType; /* type of the next token */ int lastTokenParsed = -1; /* type of the previous token */ - u8 enableLookaside; /* Saved value of db->lookaside.bEnabled */ sqlite3 *db = pParse->db; /* The database connection */ int mxSqlLen; /* Max length of an SQL string */ @@ -130297,17 +135154,15 @@ SQLITE_PRIVATE int sqlite3RunParser(Parse *pParse, const char *zSql, char **pzEr /* sqlite3ParserTrace(stdout, "parser: "); */ pEngine = sqlite3ParserAlloc(sqlite3Malloc); if( pEngine==0 ){ - db->mallocFailed = 1; - return SQLITE_NOMEM; + sqlite3OomFault(db); + return SQLITE_NOMEM_BKPT; } assert( pParse->pNewTable==0 ); assert( pParse->pNewTrigger==0 ); assert( pParse->nVar==0 ); assert( pParse->nzVar==0 ); assert( pParse->azVar==0 ); - enableLookaside = db->lookaside.bEnabled; - if( db->lookaside.pStart ) db->lookaside.bEnabled = 1; - while( !db->mallocFailed && zSql[i]!=0 ){ + while( zSql[i]!=0 ){ assert( i>=0 ); pParse->sLastToken.z = &zSql[i]; pParse->sLastToken.n = sqlite3GetToken((unsigned char*)&zSql[i],&tokenType); @@ -130316,41 +135171,29 @@ SQLITE_PRIVATE int sqlite3RunParser(Parse *pParse, const char *zSql, char **pzEr pParse->rc = SQLITE_TOOBIG; break; } - switch( tokenType ){ - case TK_SPACE: { - if( db->u1.isInterrupted ){ - sqlite3ErrorMsg(pParse, "interrupt"); - pParse->rc = SQLITE_INTERRUPT; - goto abort_parse; - } + if( tokenType>=TK_SPACE ){ + assert( tokenType==TK_SPACE || tokenType==TK_ILLEGAL ); + if( db->u1.isInterrupted ){ + pParse->rc = SQLITE_INTERRUPT; break; } - case TK_ILLEGAL: { + if( tokenType==TK_ILLEGAL ){ sqlite3ErrorMsg(pParse, "unrecognized token: \"%T\"", &pParse->sLastToken); - goto abort_parse; - } - case TK_SEMI: { - pParse->zTail = &zSql[i]; - /* Fall thru into the default case */ - } - default: { - sqlite3Parser(pEngine, tokenType, pParse->sLastToken, pParse); - lastTokenParsed = tokenType; - if( pParse->rc!=SQLITE_OK ){ - goto abort_parse; - } break; } + }else{ + sqlite3Parser(pEngine, tokenType, pParse->sLastToken, pParse); + lastTokenParsed = tokenType; + if( pParse->rc!=SQLITE_OK || db->mallocFailed ) break; } } -abort_parse: assert( nErr==0 ); + pParse->zTail = &zSql[i]; if( pParse->rc==SQLITE_OK && db->mallocFailed==0 ){ assert( zSql[i]==0 ); if( lastTokenParsed!=TK_SEMI ){ sqlite3Parser(pEngine, TK_SEMI, pParse->sLastToken, pParse); - pParse->zTail = &zSql[i]; } if( pParse->rc==SQLITE_OK && db->mallocFailed==0 ){ sqlite3Parser(pEngine, 0, pParse->sLastToken, pParse); @@ -130358,15 +135201,14 @@ abort_parse: } #ifdef YYTRACKMAXSTACKDEPTH sqlite3_mutex_enter(sqlite3MallocMutex()); - sqlite3StatusSet(SQLITE_STATUS_PARSER_STACK, + sqlite3StatusHighwater(SQLITE_STATUS_PARSER_STACK, sqlite3ParserStackPeak(pEngine) ); sqlite3_mutex_leave(sqlite3MallocMutex()); #endif /* YYDEBUG */ sqlite3ParserFree(pEngine, sqlite3_free); - db->lookaside.bEnabled = enableLookaside; if( db->mallocFailed ){ - pParse->rc = SQLITE_NOMEM; + pParse->rc = SQLITE_NOMEM_BKPT; } if( pParse->rc!=SQLITE_OK && pParse->rc!=SQLITE_DONE && pParse->zErrMsg==0 ){ pParse->zErrMsg = sqlite3MPrintf(db, "%s", sqlite3ErrStr(pParse->rc)); @@ -130401,7 +135243,7 @@ abort_parse: sqlite3DeleteTable(db, pParse->pNewTable); } - if( pParse->bFreeWith ) sqlite3WithDelete(db, pParse->pWith); + if( pParse->pWithToFree ) sqlite3WithDelete(db, pParse->pWithToFree); sqlite3DeleteTrigger(db, pParse->pNewTrigger); for(i=pParse->nzVar-1; i>=0; i--) sqlite3DbFree(db, pParse->azVar[i]); sqlite3DbFree(db, pParse->azVar); @@ -130704,7 +135546,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_complete16(const void *zSql){ if( zSql8 ){ rc = sqlite3_complete(zSql8); }else{ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } sqlite3ValueFree(pVal); return rc & 0xff; @@ -130994,7 +135836,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_initialize(void){ sqlite3GlobalConfig.pInitMutex = sqlite3MutexAlloc(SQLITE_MUTEX_RECURSIVE); if( sqlite3GlobalConfig.bCoreMutex && !sqlite3GlobalConfig.pInitMutex ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } } } @@ -131025,10 +135867,15 @@ SQLITE_API int SQLITE_STDCALL sqlite3_initialize(void){ */ sqlite3_mutex_enter(sqlite3GlobalConfig.pInitMutex); if( sqlite3GlobalConfig.isInit==0 && sqlite3GlobalConfig.inProgress==0 ){ - FuncDefHash *pHash = &GLOBAL(FuncDefHash, sqlite3GlobalFunctions); sqlite3GlobalConfig.inProgress = 1; - memset(pHash, 0, sizeof(sqlite3GlobalFunctions)); - sqlite3RegisterGlobalFunctions(); +#ifdef SQLITE_ENABLE_SQLLOG + { + extern void sqlite3_init_sqllog(void); + sqlite3_init_sqllog(); + } +#endif + memset(&sqlite3BuiltinFunctions, 0, sizeof(sqlite3BuiltinFunctions)); + sqlite3RegisterBuiltinFunctions(); if( sqlite3GlobalConfig.isPCacheInit==0 ){ rc = sqlite3PcacheInitialize(); } @@ -131246,9 +136093,10 @@ SQLITE_API int SQLITE_CDECL sqlite3_config(int op, ...){ break; } case SQLITE_CONFIG_PAGECACHE: { - /* EVIDENCE-OF: R-31408-40510 There are three arguments to - ** SQLITE_CONFIG_PAGECACHE: A pointer to 8-byte aligned memory, the size - ** of each page buffer (sz), and the number of pages (N). */ + /* EVIDENCE-OF: R-18761-36601 There are three arguments to + ** SQLITE_CONFIG_PAGECACHE: A pointer to 8-byte aligned memory (pMem), + ** the size of each page cache line (sz), and the number of cache lines + ** (N). */ sqlite3GlobalConfig.pPage = va_arg(ap, void*); sqlite3GlobalConfig.szPage = va_arg(ap, int); sqlite3GlobalConfig.nPage = va_arg(ap, int); @@ -131434,6 +136282,11 @@ SQLITE_API int SQLITE_CDECL sqlite3_config(int op, ...){ break; } + case SQLITE_CONFIG_STMTJRNL_SPILL: { + sqlite3GlobalConfig.nStmtSpill = va_arg(ap, int); + break; + } + default: { rc = SQLITE_ERROR; break; @@ -131498,12 +136351,12 @@ static int setupLookaside(sqlite3 *db, void *pBuf, int sz, int cnt){ p = (LookasideSlot*)&((u8*)p)[sz]; } db->lookaside.pEnd = p; - db->lookaside.bEnabled = 1; + db->lookaside.bDisable = 0; db->lookaside.bMalloced = pBuf==0 ?1:0; }else{ db->lookaside.pStart = db; db->lookaside.pEnd = db; - db->lookaside.bEnabled = 0; + db->lookaside.bDisable = 1; db->lookaside.bMalloced = 0; } #endif /* SQLITE_OMIT_LOOKASIDE */ @@ -131547,6 +136400,36 @@ SQLITE_API int SQLITE_STDCALL sqlite3_db_release_memory(sqlite3 *db){ return SQLITE_OK; } +/* +** Flush any dirty pages in the pager-cache for any attached database +** to disk. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_db_cacheflush(sqlite3 *db){ + int i; + int rc = SQLITE_OK; + int bSeenBusy = 0; + +#ifdef SQLITE_ENABLE_API_ARMOR + if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT; +#endif + sqlite3_mutex_enter(db->mutex); + sqlite3BtreeEnterAll(db); + for(i=0; rc==SQLITE_OK && inDb; i++){ + Btree *pBt = db->aDb[i].pBt; + if( pBt && sqlite3BtreeIsInTrans(pBt) ){ + Pager *pPager = sqlite3BtreePager(pBt); + rc = sqlite3PagerFlush(pPager); + if( rc==SQLITE_BUSY ){ + bSeenBusy = 1; + rc = SQLITE_OK; + } + } + } + sqlite3BtreeLeaveAll(db); + sqlite3_mutex_leave(db->mutex); + return ((rc==SQLITE_OK && bSeenBusy) ? SQLITE_BUSY : rc); +} + /* ** Configuration settings for an individual database connection */ @@ -131567,8 +136450,10 @@ SQLITE_API int SQLITE_CDECL sqlite3_db_config(sqlite3 *db, int op, ...){ int op; /* The opcode */ u32 mask; /* Mask of the bit in sqlite3.flags to set/clear */ } aFlagOp[] = { - { SQLITE_DBCONFIG_ENABLE_FKEY, SQLITE_ForeignKeys }, - { SQLITE_DBCONFIG_ENABLE_TRIGGER, SQLITE_EnableTrigger }, + { SQLITE_DBCONFIG_ENABLE_FKEY, SQLITE_ForeignKeys }, + { SQLITE_DBCONFIG_ENABLE_TRIGGER, SQLITE_EnableTrigger }, + { SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, SQLITE_Fts3Tokenizer }, + { SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, SQLITE_LoadExtension }, }; unsigned int i; rc = SQLITE_ERROR; /* IMP: R-42790-23372 */ @@ -131728,7 +136613,7 @@ SQLITE_PRIVATE void sqlite3CloseSavepoints(sqlite3 *db){ ** with SQLITE_ANY as the encoding. */ static void functionDestroy(sqlite3 *db, FuncDef *p){ - FuncDestructor *pDestructor = p->pDestructor; + FuncDestructor *pDestructor = p->u.pDestructor; if( pDestructor ){ pDestructor->nRef--; if( pDestructor->nRef==0 ){ @@ -131910,18 +136795,17 @@ SQLITE_PRIVATE void sqlite3LeaveMutexAndCloseZombie(sqlite3 *db){ */ sqlite3ConnectionClosed(db); - for(j=0; jaFunc.a); j++){ - FuncDef *pNext, *pHash, *p; - for(p=db->aFunc.a[j]; p; p=pHash){ - pHash = p->pHash; - while( p ){ - functionDestroy(db, p); - pNext = p->pNext; - sqlite3DbFree(db, p); - p = pNext; - } - } + for(i=sqliteHashFirst(&db->aFunc); i; i=sqliteHashNext(i)){ + FuncDef *pNext, *p; + p = sqliteHashData(i); + do{ + functionDestroy(db, p); + pNext = p->pNext; + sqlite3DbFree(db, p); + p = pNext; + }while( p ); } + sqlite3HashClear(&db->aFunc); for(i=sqliteHashFirst(&db->aCollSeq); i; i=sqliteHashNext(i)){ CollSeq *pColl = (CollSeq *)sqliteHashData(i); /* Invoke any destructors registered for collation sequence user data. */ @@ -132345,7 +137229,7 @@ SQLITE_PRIVATE int sqlite3CreateFunc( int nArg, int enc, void *pUserData, - void (*xFunc)(sqlite3_context*,int,sqlite3_value **), + void (*xSFunc)(sqlite3_context*,int,sqlite3_value **), void (*xStep)(sqlite3_context*,int,sqlite3_value **), void (*xFinal)(sqlite3_context*), FuncDestructor *pDestructor @@ -132356,9 +137240,9 @@ SQLITE_PRIVATE int sqlite3CreateFunc( assert( sqlite3_mutex_held(db->mutex) ); if( zFunctionName==0 || - (xFunc && (xFinal || xStep)) || - (!xFunc && (xFinal && !xStep)) || - (!xFunc && (!xFinal && xStep)) || + (xSFunc && (xFinal || xStep)) || + (!xSFunc && (xFinal && !xStep)) || + (!xSFunc && (!xFinal && xStep)) || (nArg<-1 || nArg>SQLITE_MAX_FUNCTION_ARG) || (255<(nName = sqlite3Strlen30( zFunctionName))) ){ return SQLITE_MISUSE_BKPT; @@ -132381,10 +137265,10 @@ SQLITE_PRIVATE int sqlite3CreateFunc( }else if( enc==SQLITE_ANY ){ int rc; rc = sqlite3CreateFunc(db, zFunctionName, nArg, SQLITE_UTF8|extraFlags, - pUserData, xFunc, xStep, xFinal, pDestructor); + pUserData, xSFunc, xStep, xFinal, pDestructor); if( rc==SQLITE_OK ){ rc = sqlite3CreateFunc(db, zFunctionName, nArg, SQLITE_UTF16LE|extraFlags, - pUserData, xFunc, xStep, xFinal, pDestructor); + pUserData, xSFunc, xStep, xFinal, pDestructor); } if( rc!=SQLITE_OK ){ return rc; @@ -132400,7 +137284,7 @@ SQLITE_PRIVATE int sqlite3CreateFunc( ** is being overridden/deleted but there are no active VMs, allow the ** operation to continue but invalidate all precompiled statements. */ - p = sqlite3FindFunction(db, zFunctionName, nName, nArg, (u8)enc, 0); + p = sqlite3FindFunction(db, zFunctionName, nArg, (u8)enc, 0); if( p && (p->funcFlags & SQLITE_FUNC_ENCMASK)==enc && p->nArg==nArg ){ if( db->nVdbeActive ){ sqlite3ErrorWithMsg(db, SQLITE_BUSY, @@ -132412,10 +137296,10 @@ SQLITE_PRIVATE int sqlite3CreateFunc( } } - p = sqlite3FindFunction(db, zFunctionName, nName, nArg, (u8)enc, 1); + p = sqlite3FindFunction(db, zFunctionName, nArg, (u8)enc, 1); assert(p || db->mallocFailed); if( !p ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } /* If an older version of the function with a configured destructor is @@ -132425,11 +137309,10 @@ SQLITE_PRIVATE int sqlite3CreateFunc( if( pDestructor ){ pDestructor->nRef++; } - p->pDestructor = pDestructor; + p->u.pDestructor = pDestructor; p->funcFlags = (p->funcFlags & SQLITE_FUNC_ENCMASK) | extraFlags; testcase( p->funcFlags & SQLITE_DETERMINISTIC ); - p->xFunc = xFunc; - p->xStep = xStep; + p->xSFunc = xSFunc ? xSFunc : xStep; p->xFinalize = xFinal; p->pUserData = pUserData; p->nArg = (u16)nArg; @@ -132445,11 +137328,11 @@ SQLITE_API int SQLITE_STDCALL sqlite3_create_function( int nArg, int enc, void *p, - void (*xFunc)(sqlite3_context*,int,sqlite3_value **), + void (*xSFunc)(sqlite3_context*,int,sqlite3_value **), void (*xStep)(sqlite3_context*,int,sqlite3_value **), void (*xFinal)(sqlite3_context*) ){ - return sqlite3_create_function_v2(db, zFunc, nArg, enc, p, xFunc, xStep, + return sqlite3_create_function_v2(db, zFunc, nArg, enc, p, xSFunc, xStep, xFinal, 0); } @@ -132459,7 +137342,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_create_function_v2( int nArg, int enc, void *p, - void (*xFunc)(sqlite3_context*,int,sqlite3_value **), + void (*xSFunc)(sqlite3_context*,int,sqlite3_value **), void (*xStep)(sqlite3_context*,int,sqlite3_value **), void (*xFinal)(sqlite3_context*), void (*xDestroy)(void *) @@ -132482,7 +137365,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_create_function_v2( pArg->xDestroy = xDestroy; pArg->pUserData = p; } - rc = sqlite3CreateFunc(db, zFunc, nArg, enc, p, xFunc, xStep, xFinal, pArg); + rc = sqlite3CreateFunc(db, zFunc, nArg, enc, p, xSFunc, xStep, xFinal, pArg); if( pArg && pArg->nRef==0 ){ assert( rc!=SQLITE_OK ); xDestroy(p); @@ -132502,7 +137385,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_create_function16( int nArg, int eTextRep, void *p, - void (*xFunc)(sqlite3_context*,int,sqlite3_value**), + void (*xSFunc)(sqlite3_context*,int,sqlite3_value**), void (*xStep)(sqlite3_context*,int,sqlite3_value**), void (*xFinal)(sqlite3_context*) ){ @@ -132515,7 +137398,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_create_function16( sqlite3_mutex_enter(db->mutex); assert( !db->mallocFailed ); zFunc8 = sqlite3Utf16to8(db, zFunctionName, -1, SQLITE_UTF16NATIVE); - rc = sqlite3CreateFunc(db, zFunc8, nArg, eTextRep, p, xFunc, xStep, xFinal,0); + rc = sqlite3CreateFunc(db, zFunc8, nArg, eTextRep, p, xSFunc,xStep,xFinal,0); sqlite3DbFree(db, zFunc8); rc = sqlite3ApiExit(db, rc); sqlite3_mutex_leave(db->mutex); @@ -132541,7 +137424,6 @@ SQLITE_API int SQLITE_STDCALL sqlite3_overload_function( const char *zName, int nArg ){ - int nName = sqlite3Strlen30(zName); int rc = SQLITE_OK; #ifdef SQLITE_ENABLE_API_ARMOR @@ -132550,7 +137432,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_overload_function( } #endif sqlite3_mutex_enter(db->mutex); - if( sqlite3FindFunction(db, zName, nName, nArg, SQLITE_UTF8, 0)==0 ){ + if( sqlite3FindFunction(db, zName, nArg, SQLITE_UTF8, 0)==0 ){ rc = sqlite3CreateFunc(db, zName, nArg, SQLITE_UTF8, 0, sqlite3InvalidFunction, 0, 0, 0); } @@ -132690,6 +137572,27 @@ SQLITE_API void *SQLITE_STDCALL sqlite3_rollback_hook( return pRet; } +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** Register a callback to be invoked each time a row is updated, +** inserted or deleted using this database connection. +*/ +SQLITE_API void *SQLITE_STDCALL sqlite3_preupdate_hook( + sqlite3 *db, /* Attach the hook to this database */ + void(*xCallback)( /* Callback function */ + void*,sqlite3*,int,char const*,char const*,sqlite3_int64,sqlite3_int64), + void *pArg /* First callback argument */ +){ + void *pRet; + sqlite3_mutex_enter(db->mutex); + pRet = db->pPreUpdateArg; + db->xPreUpdateCallback = xCallback; + db->pPreUpdateArg = pArg; + sqlite3_mutex_leave(db->mutex); + return pRet; +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + #ifndef SQLITE_OMIT_WAL /* ** The sqlite3_wal_hook() callback registered by sqlite3_wal_autocheckpoint(). @@ -132920,14 +137823,14 @@ SQLITE_PRIVATE int sqlite3TempInMemory(const sqlite3 *db){ SQLITE_API const char *SQLITE_STDCALL sqlite3_errmsg(sqlite3 *db){ const char *z; if( !db ){ - return sqlite3ErrStr(SQLITE_NOMEM); + return sqlite3ErrStr(SQLITE_NOMEM_BKPT); } if( !sqlite3SafetyCheckSickOrOk(db) ){ return sqlite3ErrStr(SQLITE_MISUSE_BKPT); } sqlite3_mutex_enter(db->mutex); if( db->mallocFailed ){ - z = sqlite3ErrStr(SQLITE_NOMEM); + z = sqlite3ErrStr(SQLITE_NOMEM_BKPT); }else{ testcase( db->pErr==0 ); z = (char*)sqlite3_value_text(db->pErr); @@ -132979,7 +137882,7 @@ SQLITE_API const void *SQLITE_STDCALL sqlite3_errmsg16(sqlite3 *db){ ** be cleared before returning. Do this directly, instead of via ** sqlite3ApiExit(), to avoid setting the database handle error message. */ - db->mallocFailed = 0; + sqlite3OomClear(db); } sqlite3_mutex_leave(db->mutex); return z; @@ -132995,7 +137898,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_errcode(sqlite3 *db){ return SQLITE_MISUSE_BKPT; } if( !db || db->mallocFailed ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } return db->errCode & db->errMask; } @@ -133004,10 +137907,13 @@ SQLITE_API int SQLITE_STDCALL sqlite3_extended_errcode(sqlite3 *db){ return SQLITE_MISUSE_BKPT; } if( !db || db->mallocFailed ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; } return db->errCode; } +SQLITE_API int SQLITE_STDCALL sqlite3_system_errno(sqlite3 *db){ + return db ? db->iSysErrno : 0; +} /* ** Return a string that describes the kind of error specified in the @@ -133084,7 +137990,7 @@ static int createCollation( } pColl = sqlite3FindCollSeq(db, (u8)enc2, zName, 1); - if( pColl==0 ) return SQLITE_NOMEM; + if( pColl==0 ) return SQLITE_NOMEM_BKPT; pColl->xCmp = xCompare; pColl->pUser = pCtx; pColl->xDel = xDel; @@ -133132,8 +138038,8 @@ static const int aHardLimit[] = { #if SQLITE_MAX_VDBE_OP<40 # error SQLITE_MAX_VDBE_OP must be at least 40 #endif -#if SQLITE_MAX_FUNCTION_ARG<0 || SQLITE_MAX_FUNCTION_ARG>1000 -# error SQLITE_MAX_FUNCTION_ARG must be between 0 and 1000 +#if SQLITE_MAX_FUNCTION_ARG<0 || SQLITE_MAX_FUNCTION_ARG>127 +# error SQLITE_MAX_FUNCTION_ARG must be between 0 and 127 #endif #if SQLITE_MAX_ATTACHED<0 || SQLITE_MAX_ATTACHED>125 # error SQLITE_MAX_ATTACHED must be between 0 and 125 @@ -133263,7 +138169,7 @@ SQLITE_PRIVATE int sqlite3ParseUri( for(iIn=0; iInaCollSeq); @@ -133599,9 +138508,9 @@ static int openDatabase( ** EVIDENCE-OF: R-52786-44878 SQLite defines three built-in collating ** functions: */ - createCollation(db, "BINARY", SQLITE_UTF8, 0, binCollFunc, 0); - createCollation(db, "BINARY", SQLITE_UTF16BE, 0, binCollFunc, 0); - createCollation(db, "BINARY", SQLITE_UTF16LE, 0, binCollFunc, 0); + createCollation(db, sqlite3StrBINARY, SQLITE_UTF8, 0, binCollFunc, 0); + createCollation(db, sqlite3StrBINARY, SQLITE_UTF16BE, 0, binCollFunc, 0); + createCollation(db, sqlite3StrBINARY, SQLITE_UTF16LE, 0, binCollFunc, 0); createCollation(db, "NOCASE", SQLITE_UTF8, 0, nocaseCollatingFunc, 0); createCollation(db, "RTRIM", SQLITE_UTF8, (void*)1, binCollFunc, 0); if( db->mallocFailed ){ @@ -133610,14 +138519,14 @@ static int openDatabase( /* EVIDENCE-OF: R-08308-17224 The default collating function for all ** strings is BINARY. */ - db->pDfltColl = sqlite3FindCollSeq(db, SQLITE_UTF8, "BINARY", 0); + db->pDfltColl = sqlite3FindCollSeq(db, SQLITE_UTF8, sqlite3StrBINARY, 0); assert( db->pDfltColl!=0 ); /* Parse the filename/URI argument. */ db->openFlags = flags; rc = sqlite3ParseUri(zVfs, zFilename, &flags, &db->pVfs, &zOpen, &zErrMsg); if( rc!=SQLITE_OK ){ - if( rc==SQLITE_NOMEM ) db->mallocFailed = 1; + if( rc==SQLITE_NOMEM ) sqlite3OomFault(db); sqlite3ErrorWithMsg(db, rc, zErrMsg ? "%s" : 0, zErrMsg); sqlite3_free(zErrMsg); goto opendb_out; @@ -133628,7 +138537,7 @@ static int openDatabase( flags | SQLITE_OPEN_MAIN_DB); if( rc!=SQLITE_OK ){ if( rc==SQLITE_IOERR_NOMEM ){ - rc = SQLITE_NOMEM; + rc = SQLITE_NOMEM_BKPT; } sqlite3Error(db, rc); goto opendb_out; @@ -133639,13 +138548,13 @@ static int openDatabase( sqlite3BtreeLeave(db->aDb[0].pBt); db->aDb[1].pSchema = sqlite3SchemaGet(db, 0); - /* The default safety_level for the main database is 'full'; for the temp - ** database it is 'NONE'. This matches the pager layer defaults. + /* The default safety_level for the main database is FULL; for the temp + ** database it is OFF. This matches the pager layer defaults. */ db->aDb[0].zName = "main"; - db->aDb[0].safety_level = 3; + db->aDb[0].safety_level = SQLITE_DEFAULT_SYNCHRONOUS+1; db->aDb[1].zName = "temp"; - db->aDb[1].safety_level = 1; + db->aDb[1].safety_level = PAGER_SYNCHRONOUS_OFF; db->magic = SQLITE_MAGIC_OPEN; if( db->mallocFailed ){ @@ -133657,7 +138566,7 @@ static int openDatabase( ** is accessed. */ sqlite3Error(db, SQLITE_OK); - sqlite3RegisterBuiltinFunctions(db); + sqlite3RegisterPerConnectionBuiltinFunctions(db); /* Load automatic extensions - extensions that have been registered ** using the sqlite3_automatic_extension() API. @@ -133740,7 +138649,6 @@ static int openDatabase( sqlite3_wal_autocheckpoint(db, SQLITE_DEFAULT_WAL_AUTOCHECKPOINT); opendb_out: - sqlite3_free(zOpen); if( db ){ assert( db->mutex!=0 || isThreadsafe==0 || sqlite3GlobalConfig.bFullMutex==0 ); @@ -133762,6 +138670,22 @@ opendb_out: sqlite3GlobalConfig.xSqllog(pArg, db, zFilename, 0); } #endif +#if defined(SQLITE_HAS_CODEC) + if( rc==SQLITE_OK ){ + const char *zHexKey = sqlite3_uri_parameter(zOpen, "hexkey"); + if( zHexKey && zHexKey[0] ){ + u8 iByte; + int i; + char zKey[40]; + for(i=0, iByte=0; izType; + zDataType = sqlite3ColumnType(pCol,0); zCollSeq = pCol->zColl; notnull = pCol->notNull!=0; primarykey = (pCol->colFlags & COLFLAG_PRIMKEY)!=0; @@ -134095,7 +139024,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_table_column_metadata( primarykey = 1; } if( !zCollSeq ){ - zCollSeq = "BINARY"; + zCollSeq = sqlite3StrBINARY; } error_out: @@ -134176,6 +139105,12 @@ SQLITE_API int SQLITE_STDCALL sqlite3_file_control(sqlite3 *db, const char *zDbN if( op==SQLITE_FCNTL_FILE_POINTER ){ *(sqlite3_file**)pArg = fd; rc = SQLITE_OK; + }else if( op==SQLITE_FCNTL_VFS_POINTER ){ + *(sqlite3_vfs**)pArg = sqlite3PagerVfs(pPager); + rc = SQLITE_OK; + }else if( op==SQLITE_FCNTL_JOURNAL_POINTER ){ + *(sqlite3_file**)pArg = sqlite3PagerJrnlFile(pPager); + rc = SQLITE_OK; }else if( fd->pMethods ){ rc = sqlite3OsFileControl(fd, op, pArg); }else{ @@ -134316,7 +139251,7 @@ SQLITE_API int SQLITE_CDECL sqlite3_test_control(int op, ...){ */ case SQLITE_TESTCTRL_ASSERT: { volatile int x = 0; - assert( (x = va_arg(ap,int))!=0 ); + assert( /*side-effects-ok*/ (x = va_arg(ap,int))!=0 ); rc = x; break; } @@ -134619,6 +139554,88 @@ SQLITE_API int SQLITE_STDCALL sqlite3_db_readonly(sqlite3 *db, const char *zDbNa return pBt ? sqlite3BtreeIsReadonly(pBt) : -1; } +#ifdef SQLITE_ENABLE_SNAPSHOT +/* +** Obtain a snapshot handle for the snapshot of database zDb currently +** being read by handle db. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_snapshot_get( + sqlite3 *db, + const char *zDb, + sqlite3_snapshot **ppSnapshot +){ + int rc = SQLITE_ERROR; +#ifndef SQLITE_OMIT_WAL + int iDb; + +#ifdef SQLITE_ENABLE_API_ARMOR + if( !sqlite3SafetyCheckOk(db) ){ + return SQLITE_MISUSE_BKPT; + } +#endif + sqlite3_mutex_enter(db->mutex); + + iDb = sqlite3FindDbName(db, zDb); + if( iDb==0 || iDb>1 ){ + Btree *pBt = db->aDb[iDb].pBt; + if( 0==sqlite3BtreeIsInTrans(pBt) ){ + rc = sqlite3BtreeBeginTrans(pBt, 0); + if( rc==SQLITE_OK ){ + rc = sqlite3PagerSnapshotGet(sqlite3BtreePager(pBt), ppSnapshot); + } + } + } + + sqlite3_mutex_leave(db->mutex); +#endif /* SQLITE_OMIT_WAL */ + return rc; +} + +/* +** Open a read-transaction on the snapshot idendified by pSnapshot. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_snapshot_open( + sqlite3 *db, + const char *zDb, + sqlite3_snapshot *pSnapshot +){ + int rc = SQLITE_ERROR; +#ifndef SQLITE_OMIT_WAL + +#ifdef SQLITE_ENABLE_API_ARMOR + if( !sqlite3SafetyCheckOk(db) ){ + return SQLITE_MISUSE_BKPT; + } +#endif + sqlite3_mutex_enter(db->mutex); + if( db->autoCommit==0 ){ + int iDb; + iDb = sqlite3FindDbName(db, zDb); + if( iDb==0 || iDb>1 ){ + Btree *pBt = db->aDb[iDb].pBt; + if( 0==sqlite3BtreeIsInReadTrans(pBt) ){ + rc = sqlite3PagerSnapshotOpen(sqlite3BtreePager(pBt), pSnapshot); + if( rc==SQLITE_OK ){ + rc = sqlite3BtreeBeginTrans(pBt, 0); + sqlite3PagerSnapshotOpen(sqlite3BtreePager(pBt), 0); + } + } + } + } + + sqlite3_mutex_leave(db->mutex); +#endif /* SQLITE_OMIT_WAL */ + return rc; +} + +/* +** Free a snapshot handle obtained from sqlite3_snapshot_get(). +*/ +SQLITE_API void SQLITE_STDCALL sqlite3_snapshot_free(sqlite3_snapshot *pSnapshot){ + sqlite3_free(pSnapshot); +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ + /************** End of main.c ************************************************/ /************** Begin file notify.c ******************************************/ /* @@ -135268,6 +140285,12 @@ SQLITE_PRIVATE void sqlite3ConnectionClosed(sqlite3 *db){ # define NDEBUG 1 #endif +/* FTS3/FTS4 require virtual tables */ +#ifdef SQLITE_OMIT_VIRTUALTABLE +# undef SQLITE_ENABLE_FTS3 +# undef SQLITE_ENABLE_FTS4 +#endif + /* ** FTS4 is really an extension for FTS3. It is enabled using the ** SQLITE_ENABLE_FTS3 macro. But to avoid confusion we also all @@ -144726,6 +149749,18 @@ SQLITE_PRIVATE void sqlite3Fts3PorterTokenizerModule( /* #include */ /* #include */ +/* +** Return true if the two-argument version of fts3_tokenizer() +** has been activated via a prior call to sqlite3_db_config(db, +** SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, 1, 0); +*/ +static int fts3TokenizerEnabled(sqlite3_context *context){ + sqlite3 *db = sqlite3_context_db_handle(context); + int isEnabled = 0; + sqlite3_db_config(db,SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER,-1,&isEnabled); + return isEnabled; +} + /* ** Implementation of the SQL scalar function for accessing the underlying ** hash table. This function may be called as follows: @@ -144746,7 +149781,7 @@ SQLITE_PRIVATE void sqlite3Fts3PorterTokenizerModule( ** is a blob containing the pointer stored as the hash data corresponding ** to string (after the hash-table is updated, if applicable). */ -static void scalarFunc( +static void fts3TokenizerFunc( sqlite3_context *context, int argc, sqlite3_value **argv @@ -144764,16 +149799,20 @@ static void scalarFunc( nName = sqlite3_value_bytes(argv[0])+1; if( argc==2 ){ - void *pOld; - int n = sqlite3_value_bytes(argv[1]); - if( zName==0 || n!=sizeof(pPtr) ){ - sqlite3_result_error(context, "argument type mismatch", -1); - return; - } - pPtr = *(void **)sqlite3_value_blob(argv[1]); - pOld = sqlite3Fts3HashInsert(pHash, (void *)zName, nName, pPtr); - if( pOld==pPtr ){ - sqlite3_result_error(context, "out of memory", -1); + if( fts3TokenizerEnabled(context) ){ + void *pOld; + int n = sqlite3_value_bytes(argv[1]); + if( zName==0 || n!=sizeof(pPtr) ){ + sqlite3_result_error(context, "argument type mismatch", -1); + return; + } + pPtr = *(void **)sqlite3_value_blob(argv[1]); + pOld = sqlite3Fts3HashInsert(pHash, (void *)zName, nName, pPtr); + if( pOld==pPtr ){ + sqlite3_result_error(context, "out of memory", -1); + } + }else{ + sqlite3_result_error(context, "fts3tokenize disabled", -1); return; } }else{ @@ -144787,7 +149826,6 @@ static void scalarFunc( return; } } - sqlite3_result_blob(context, (void *)&pPtr, sizeof(pPtr), SQLITE_TRANSIENT); } @@ -145047,6 +150085,7 @@ int registerTokenizer( return sqlite3_finalize(pStmt); } + static int queryTokenizer( sqlite3 *db, @@ -145117,11 +150156,13 @@ static void intTestFunc( assert( 0==strcmp(sqlite3_errmsg(db), "unknown tokenizer: nosuchtokenizer") ); /* Test the storage function */ - rc = registerTokenizer(db, "nosuchtokenizer", p1); - assert( rc==SQLITE_OK ); - rc = queryTokenizer(db, "nosuchtokenizer", &p2); - assert( rc==SQLITE_OK ); - assert( p2==p1 ); + if( fts3TokenizerEnabled(context) ){ + rc = registerTokenizer(db, "nosuchtokenizer", p1); + assert( rc==SQLITE_OK ); + rc = queryTokenizer(db, "nosuchtokenizer", &p2); + assert( rc==SQLITE_OK ); + assert( p2==p1 ); + } sqlite3_result_text(context, "ok", -1, SQLITE_STATIC); } @@ -145137,7 +150178,7 @@ static void intTestFunc( ** sqlite3Fts3HashInit(pHash, FTS3_HASH_STRING, 1); ** ** This function adds a scalar function (see header comment above -** scalarFunc() in this file for details) and, if ENABLE_TABLE is +** fts3TokenizerFunc() in this file for details) and, if ENABLE_TABLE is ** defined at compilation time, a temporary virtual table (see header ** comment above struct HashTableVtab) to the database schema. Both ** provide read/write access to the contents of *pHash. @@ -145166,10 +150207,10 @@ SQLITE_PRIVATE int sqlite3Fts3InitHashTable( #endif if( SQLITE_OK==rc ){ - rc = sqlite3_create_function(db, zName, 1, any, p, scalarFunc, 0, 0); + rc = sqlite3_create_function(db, zName, 1, any, p, fts3TokenizerFunc, 0, 0); } if( SQLITE_OK==rc ){ - rc = sqlite3_create_function(db, zName, 2, any, p, scalarFunc, 0, 0); + rc = sqlite3_create_function(db, zName, 2, any, p, fts3TokenizerFunc, 0, 0); } #ifdef SQLITE_TEST if( SQLITE_OK==rc ){ @@ -146221,7 +151262,8 @@ static int fts3SqlStmt( ** of the oldest level in the db that contains at least ? segments. Or, ** if no level in the FTS index contains more than ? segments, the statement ** returns zero rows. */ -/* 28 */ "SELECT level FROM %Q.'%q_segdir' GROUP BY level HAVING count(*)>=?" +/* 28 */ "SELECT level, count(*) AS cnt FROM %Q.'%q_segdir' " + " GROUP BY level HAVING cnt>=?" " ORDER BY (level %% 1024) ASC LIMIT 1", /* Estimate the upper limit on the number of leaf nodes in a new segment @@ -149082,7 +154124,7 @@ static int fts3SegmentMerge( ** segment. The level of the new segment is equal to the numerically ** greatest segment level currently present in the database for this ** index. The idx of the new segment is always 0. */ - if( csr.nSegment==1 ){ + if( csr.nSegment==1 && 0==fts3SegReaderIsPending(csr.apSegment[0]) ){ rc = SQLITE_DONE; goto finished; } @@ -150724,10 +155766,11 @@ SQLITE_PRIVATE int sqlite3Fts3Incrmerge(Fts3Table *p, int nMerge, int nMin){ ** set nSeg to -1. */ rc = fts3SqlStmt(p, SQL_FIND_MERGE_LEVEL, &pFindLevel, 0); - sqlite3_bind_int(pFindLevel, 1, nMin); + sqlite3_bind_int(pFindLevel, 1, MAX(2, nMin)); if( sqlite3_step(pFindLevel)==SQLITE_ROW ){ iAbsLevel = sqlite3_column_int64(pFindLevel, 0); - nSeg = nMin; + nSeg = sqlite3_column_int(pFindLevel, 1); + assert( nSeg>=2 ); }else{ nSeg = -1; } @@ -155781,7 +160824,7 @@ static int rtreeBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pIdxInfo){ return SQLITE_NOMEM; } - nRow = pRtree->nRowEst / (iIdx + 1); + nRow = pRtree->nRowEst >> (iIdx/2); pIdxInfo->estimatedCost = (double)6.0 * (double)nRow; setEstimatedRows(pIdxInfo, nRow); @@ -157615,6 +162658,38 @@ static void xFree(void *p){ sqlite3_free(p); } +/* +** This lookup table is used to help decode the first byte of +** a multi-byte UTF8 character. It is copied here from SQLite source +** code file utf8.c. +*/ +static const unsigned char icuUtf8Trans1[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x00, 0x00, +}; + +#define SQLITE_ICU_READ_UTF8(zIn, c) \ + c = *(zIn++); \ + if( c>=0xc0 ){ \ + c = icuUtf8Trans1[c-0xc0]; \ + while( (*zIn & 0xc0)==0x80 ){ \ + c = (c<<6) + (0x3f & *(zIn++)); \ + } \ + } + +#define SQLITE_ICU_SKIP_UTF8(zIn) \ + assert( *zIn ); \ + if( *(zIn++)>=0xc0 ){ \ + while( (*zIn & 0xc0)==0x80 ){zIn++;} \ + } + + /* ** Compare two UTF-8 strings for equality where the first string is ** a "LIKE" expression. Return true (1) if they are the same and @@ -157628,16 +162703,14 @@ static int icuLikeCompare( static const int MATCH_ONE = (UChar32)'_'; static const int MATCH_ALL = (UChar32)'%'; - int iPattern = 0; /* Current byte index in zPattern */ - int iString = 0; /* Current byte index in zString */ - int prevEscape = 0; /* True if the previous character was uEsc */ - while( zPattern[iPattern]!=0 ){ + while( 1 ){ /* Read (and consume) the next character from the input pattern. */ UChar32 uPattern; - U8_NEXT_UNSAFE(zPattern, iPattern, uPattern); + SQLITE_ICU_READ_UTF8(zPattern, uPattern); + if( uPattern==0 ) break; /* There are now 4 possibilities: ** @@ -157654,28 +162727,28 @@ static int icuLikeCompare( ** MATCH_ALL. For each MATCH_ONE, skip one character in the ** test string. */ - while( (c=zPattern[iPattern]) == MATCH_ALL || c == MATCH_ONE ){ + while( (c=*zPattern) == MATCH_ALL || c == MATCH_ONE ){ if( c==MATCH_ONE ){ - if( zString[iString]==0 ) return 0; - U8_FWD_1_UNSAFE(zString, iString); + if( *zString==0 ) return 0; + SQLITE_ICU_SKIP_UTF8(zString); } - iPattern++; + zPattern++; } - if( zPattern[iPattern]==0 ) return 1; + if( *zPattern==0 ) return 1; - while( zString[iString] ){ - if( icuLikeCompare(&zPattern[iPattern], &zString[iString], uEsc) ){ + while( *zString ){ + if( icuLikeCompare(zPattern, zString, uEsc) ){ return 1; } - U8_FWD_1_UNSAFE(zString, iString); + SQLITE_ICU_SKIP_UTF8(zString); } return 0; }else if( !prevEscape && uPattern==MATCH_ONE ){ /* Case 2. */ - if( zString[iString]==0 ) return 0; - U8_FWD_1_UNSAFE(zString, iString); + if( *zString==0 ) return 0; + SQLITE_ICU_SKIP_UTF8(zString); }else if( !prevEscape && uPattern==uEsc){ /* Case 3. */ @@ -157684,7 +162757,7 @@ static int icuLikeCompare( }else{ /* Case 4. */ UChar32 uString; - U8_NEXT_UNSAFE(zString, iString, uString); + SQLITE_ICU_READ_UTF8(zString, uString); uString = u_foldCase(uString, U_FOLD_CASE_DEFAULT); uPattern = u_foldCase(uPattern, U_FOLD_CASE_DEFAULT); if( uString!=uPattern ){ @@ -157694,7 +162767,7 @@ static int icuLikeCompare( } } - return zString[iString]==0; + return *zString==0; } /* @@ -157879,15 +162952,17 @@ static void icuRegexpFunc(sqlite3_context *p, int nArg, sqlite3_value **apArg){ ** http://www.icu-project.org/userguide/posix.html#case_mappings */ static void icuCaseFunc16(sqlite3_context *p, int nArg, sqlite3_value **apArg){ - const UChar *zInput; - UChar *zOutput; - int nInput; - int nOutput; - - UErrorCode status = U_ZERO_ERROR; + const UChar *zInput; /* Pointer to input string */ + UChar *zOutput = 0; /* Pointer to output buffer */ + int nInput; /* Size of utf-16 input string in bytes */ + int nOut; /* Size of output buffer in bytes */ + int cnt; + int bToUpper; /* True for toupper(), false for tolower() */ + UErrorCode status; const char *zLocale = 0; assert(nArg==1 || nArg==2); + bToUpper = (sqlite3_user_data(p)!=0); if( nArg==2 ){ zLocale = (const char *)sqlite3_value_text(apArg[1]); } @@ -157896,26 +162971,38 @@ static void icuCaseFunc16(sqlite3_context *p, int nArg, sqlite3_value **apArg){ if( !zInput ){ return; } - nInput = sqlite3_value_bytes16(apArg[0]); - - nOutput = nInput * 2 + 2; - zOutput = sqlite3_malloc(nOutput); - if( !zOutput ){ + nOut = nInput = sqlite3_value_bytes16(apArg[0]); + if( nOut==0 ){ + sqlite3_result_text16(p, "", 0, SQLITE_STATIC); return; } - if( sqlite3_user_data(p) ){ - u_strToUpper(zOutput, nOutput/2, zInput, nInput/2, zLocale, &status); - }else{ - u_strToLower(zOutput, nOutput/2, zInput, nInput/2, zLocale, &status); - } + for(cnt=0; cnt<2; cnt++){ + UChar *zNew = sqlite3_realloc(zOutput, nOut); + if( zNew==0 ){ + sqlite3_free(zOutput); + sqlite3_result_error_nomem(p); + return; + } + zOutput = zNew; + status = U_ZERO_ERROR; + if( bToUpper ){ + nOut = 2*u_strToUpper(zOutput,nOut/2,zInput,nInput/2,zLocale,&status); + }else{ + nOut = 2*u_strToLower(zOutput,nOut/2,zInput,nInput/2,zLocale,&status); + } - if( !U_SUCCESS(status) ){ - icuFunctionError(p, "u_strToLower()/u_strToUpper", status); + if( U_SUCCESS(status) ){ + sqlite3_result_text16(p, zOutput, nOut, xFree); + }else if( status==U_BUFFER_OVERFLOW_ERROR ){ + assert( cnt==0 ); + continue; + }else{ + icuFunctionError(p, bToUpper ? "u_strToUpper" : "u_strToLower", status); + } return; } - - sqlite3_result_text16(p, zOutput, -1, xFree); + assert( 0 ); /* Unreachable */ } /* @@ -158732,6 +163819,38 @@ SQLITE_API sqlite3rbu *SQLITE_STDCALL sqlite3rbu_open( const char *zState ); +/* +** Open an RBU handle to perform an RBU vacuum on database file zTarget. +** An RBU vacuum is similar to SQLite's built-in VACUUM command, except +** that it can be suspended and resumed like an RBU update. +** +** The second argument to this function, which may not be NULL, identifies +** a database in which to store the state of the RBU vacuum operation if +** it is suspended. The first time sqlite3rbu_vacuum() is called, to start +** an RBU vacuum operation, the state database should either not exist or +** be empty (contain no tables). If an RBU vacuum is suspended by calling +** sqlite3rbu_close() on the RBU handle before sqlite3rbu_step() has +** returned SQLITE_DONE, the vacuum state is stored in the state database. +** The vacuum can be resumed by calling this function to open a new RBU +** handle specifying the same target and state databases. +** +** This function does not delete the state database after an RBU vacuum +** is completed, even if it created it. However, if the call to +** sqlite3rbu_close() returns any value other than SQLITE_OK, the contents +** of the state tables within the state database are zeroed. This way, +** the next call to sqlite3rbu_vacuum() opens a handle that starts a +** new RBU vacuum operation. +** +** As with sqlite3rbu_open(), Zipvfs users should rever to the comment +** describing the sqlite3rbu_create_vfs() API function below for +** a description of the complications associated with using RBU with +** zipvfs databases. +*/ +SQLITE_API sqlite3rbu *SQLITE_STDCALL sqlite3rbu_vacuum( + const char *zTarget, + const char *zState +); + /* ** Internally, each RBU connection uses a separate SQLite database ** connection to access the target and rbu update databases. This @@ -158759,6 +163878,9 @@ SQLITE_API sqlite3rbu *SQLITE_STDCALL sqlite3rbu_open( ** If an error has occurred, either while opening or stepping the RBU object, ** this function may return NULL. The error code and message may be collected ** when sqlite3rbu_close() is called. +** +** Database handles returned by this function remain valid until the next +** call to any sqlite3rbu_xxx() function other than sqlite3rbu_db(). */ SQLITE_API sqlite3 *SQLITE_STDCALL sqlite3rbu_db(sqlite3rbu*, int bRbu); @@ -158815,6 +163937,48 @@ SQLITE_API int SQLITE_STDCALL sqlite3rbu_close(sqlite3rbu *pRbu, char **pzErrmsg */ SQLITE_API sqlite3_int64 SQLITE_STDCALL sqlite3rbu_progress(sqlite3rbu *pRbu); +/* +** Obtain permyriadage (permyriadage is to 10000 as percentage is to 100) +** progress indications for the two stages of an RBU update. This API may +** be useful for driving GUI progress indicators and similar. +** +** An RBU update is divided into two stages: +** +** * Stage 1, in which changes are accumulated in an oal/wal file, and +** * Stage 2, in which the contents of the wal file are copied into the +** main database. +** +** The update is visible to non-RBU clients during stage 2. During stage 1 +** non-RBU reader clients may see the original database. +** +** If this API is called during stage 2 of the update, output variable +** (*pnOne) is set to 10000 to indicate that stage 1 has finished and (*pnTwo) +** to a value between 0 and 10000 to indicate the permyriadage progress of +** stage 2. A value of 5000 indicates that stage 2 is half finished, +** 9000 indicates that it is 90% finished, and so on. +** +** If this API is called during stage 1 of the update, output variable +** (*pnTwo) is set to 0 to indicate that stage 2 has not yet started. The +** value to which (*pnOne) is set depends on whether or not the RBU +** database contains an "rbu_count" table. The rbu_count table, if it +** exists, must contain the same columns as the following: +** +** CREATE TABLE rbu_count(tbl TEXT PRIMARY KEY, cnt INTEGER) WITHOUT ROWID; +** +** There must be one row in the table for each source (data_xxx) table within +** the RBU database. The 'tbl' column should contain the name of the source +** table. The 'cnt' column should contain the number of rows within the +** source table. +** +** If the rbu_count table is present and populated correctly and this +** API is called during stage 1, the *pnOne output variable is set to the +** permyriadage progress of the same stage. If the rbu_count table does +** not exist, then (*pnOne) is set to -1 during stage 1. If the rbu_count +** table exists but is not correctly populated, the value of the *pnOne +** output variable during stage 1 is undefined. +*/ +SQLITE_API void SQLITE_STDCALL sqlite3rbu_bp_progress(sqlite3rbu *pRbu, int *pnOne, int *pnTwo); + /* ** Create an RBU VFS named zName that accesses the underlying file-system ** via existing VFS zParent. Or, if the zParent parameter is passed NULL, @@ -158936,14 +164100,15 @@ SQLITE_API void SQLITE_STDCALL sqlite3rbu_destroy_vfs(const char *zName); ** RBU_STATE_OALSZ: ** Valid if STAGE==1. The size in bytes of the *-oal file. */ -#define RBU_STATE_STAGE 1 -#define RBU_STATE_TBL 2 -#define RBU_STATE_IDX 3 -#define RBU_STATE_ROW 4 -#define RBU_STATE_PROGRESS 5 -#define RBU_STATE_CKPT 6 -#define RBU_STATE_COOKIE 7 -#define RBU_STATE_OALSZ 8 +#define RBU_STATE_STAGE 1 +#define RBU_STATE_TBL 2 +#define RBU_STATE_IDX 3 +#define RBU_STATE_ROW 4 +#define RBU_STATE_PROGRESS 5 +#define RBU_STATE_CKPT 6 +#define RBU_STATE_COOKIE 7 +#define RBU_STATE_OALSZ 8 +#define RBU_STATE_PHASEONESTEP 9 #define RBU_STAGE_OAL 1 #define RBU_STAGE_MOVE 2 @@ -158964,6 +164129,7 @@ typedef struct RbuUpdateStmt RbuUpdateStmt; #if !defined(SQLITE_AMALGAMATION) typedef unsigned int u32; +typedef unsigned short u16; typedef unsigned char u8; typedef sqlite3_int64 i64; #endif @@ -158977,6 +164143,8 @@ typedef sqlite3_int64 i64; #define WAL_LOCK_CKPT 1 #define WAL_LOCK_READ0 3 +#define SQLITE_FCNTL_RBUCNT 5149216 + /* ** A structure to store values read from the rbu_state table in memory. */ @@ -158989,6 +164157,7 @@ struct RbuState { i64 nProgress; u32 iCookie; i64 iOalSz; + i64 nPhaseOneStep; }; struct RbuUpdateStmt { @@ -159033,6 +164202,7 @@ struct RbuObjIter { int iTnum; /* Root page of current object */ int iPkTnum; /* If eType==EXTERNAL, root of PK index */ int bUnique; /* Current index is unique */ + int nIndex; /* Number of aux. indexes on table zTbl */ /* Statements created by rbuObjIterPrepareAll() */ int nCol; /* Number of columns in current object */ @@ -159069,10 +164239,11 @@ struct RbuObjIter { */ #define RBU_INSERT 1 /* Insert on a main table b-tree */ #define RBU_DELETE 2 /* Delete a row from a main table b-tree */ -#define RBU_IDX_DELETE 3 /* Delete a row from an aux. index b-tree */ -#define RBU_IDX_INSERT 4 /* Insert on an aux. index b-tree */ -#define RBU_UPDATE 5 /* Update a row in a main table b-tree */ +#define RBU_REPLACE 3 /* Delete and then insert a row */ +#define RBU_IDX_DELETE 4 /* Delete a row from an aux. index b-tree */ +#define RBU_IDX_INSERT 5 /* Insert on an aux. index b-tree */ +#define RBU_UPDATE 6 /* Update a row in a main table b-tree */ /* ** A single step of an incremental checkpoint - frame iWalFrame of the wal @@ -159085,6 +164256,43 @@ struct RbuFrame { /* ** RBU handle. +** +** nPhaseOneStep: +** If the RBU database contains an rbu_count table, this value is set to +** a running estimate of the number of b-tree operations required to +** finish populating the *-oal file. This allows the sqlite3_bp_progress() +** API to calculate the permyriadage progress of populating the *-oal file +** using the formula: +** +** permyriadage = (10000 * nProgress) / nPhaseOneStep +** +** nPhaseOneStep is initialized to the sum of: +** +** nRow * (nIndex + 1) +** +** for all source tables in the RBU database, where nRow is the number +** of rows in the source table and nIndex the number of indexes on the +** corresponding target database table. +** +** This estimate is accurate if the RBU update consists entirely of +** INSERT operations. However, it is inaccurate if: +** +** * the RBU update contains any UPDATE operations. If the PK specified +** for an UPDATE operation does not exist in the target table, then +** no b-tree operations are required on index b-trees. Or if the +** specified PK does exist, then (nIndex*2) such operations are +** required (one delete and one insert on each index b-tree). +** +** * the RBU update contains any DELETE operations for which the specified +** PK does not exist. In this case no operations are required on index +** b-trees. +** +** * the RBU update contains REPLACE operations. These are similar to +** UPDATE operations. +** +** nPhaseOneStep is updated to account for the conditions above during the +** first pass of each source table. The updated nPhaseOneStep value is +** stored in the rbu_state table if the RBU update is suspended. */ struct sqlite3rbu { int eStage; /* Value of RBU_STATE_STAGE field */ @@ -159102,6 +164310,7 @@ struct sqlite3rbu { const char *zVfsName; /* Name of automatically created rbu vfs */ rbu_file *pTargetFd; /* File handle open on target db */ i64 iOalSz; + i64 nPhaseOneStep; /* The following state variables are used as part of the incremental ** checkpoint stage (eStage==RBU_STAGE_CKPT). See comments surrounding @@ -159114,6 +164323,10 @@ struct sqlite3rbu { int pgsz; u8 *aBuf; i64 iWalCksum; + + /* Used in RBU vacuum mode only */ + int nRbu; /* Number of RBU VFS in the stack */ + rbu_file *pRbuFd; /* Fd for main db of dbRbu */ }; /* @@ -159139,6 +164352,7 @@ struct rbu_file { int openFlags; /* Flags this file was opened with */ u32 iCookie; /* Cookie value for main db files */ u8 iWriteVer; /* "write-version" value for main db files */ + u8 bNolock; /* True to fail EXCLUSIVE locks */ int nShm; /* Number of entries in apShm[] array */ char **apShm; /* Array of mmap'd *-shm regions */ @@ -159149,6 +164363,11 @@ struct rbu_file { rbu_file *pMainNext; /* Next MAIN_DB file */ }; +/* +** True for an RBU vacuum handle, or false otherwise. +*/ +#define rbuIsVacuum(p) ((p)->zTarget==0) + /************************************************************************* ** The following three functions, found below: @@ -159597,8 +164816,11 @@ static int rbuObjIterNext(sqlite3rbu *p, RbuObjIter *pIter){ /* ** The implementation of the rbu_target_name() SQL function. This function -** accepts one argument - the name of a table in the RBU database. If the -** table name matches the pattern: +** accepts one or two arguments. The first argument is the name of a table - +** the name of a table in the RBU database. The second, if it is present, is 1 +** for a view or 0 for a table. +** +** For a non-vacuum RBU handle, if the table name matches the pattern: ** ** data[0-9]_ ** @@ -159609,21 +164831,33 @@ static int rbuObjIterNext(sqlite3rbu *p, RbuObjIter *pIter){ ** "data_t1" -> "t1" ** "data0123_t2" -> "t2" ** "dataAB_t3" -> NULL +** +** For an rbu vacuum handle, a copy of the first argument is returned if +** the second argument is either missing or 0 (not a view). */ static void rbuTargetNameFunc( - sqlite3_context *context, + sqlite3_context *pCtx, int argc, sqlite3_value **argv ){ + sqlite3rbu *p = sqlite3_user_data(pCtx); const char *zIn; - assert( argc==1 ); + assert( argc==1 || argc==2 ); zIn = (const char*)sqlite3_value_text(argv[0]); - if( zIn && strlen(zIn)>4 && memcmp("data", zIn, 4)==0 ){ - int i; - for(i=4; zIn[i]>='0' && zIn[i]<='9'; i++); - if( zIn[i]=='_' && zIn[i+1] ){ - sqlite3_result_text(context, &zIn[i+1], -1, SQLITE_STATIC); + if( zIn ){ + if( rbuIsVacuum(p) ){ + if( argc==1 || 0==sqlite3_value_int(argv[1]) ){ + sqlite3_result_text(pCtx, zIn, -1, SQLITE_STATIC); + } + }else{ + if( strlen(zIn)>4 && memcmp("data", zIn, 4)==0 ){ + int i; + for(i=4; zIn[i]>='0' && zIn[i]<='9'; i++); + if( zIn[i]=='_' && zIn[i+1] ){ + sqlite3_result_text(pCtx, &zIn[i+1], -1, SQLITE_STATIC); + } + } } } } @@ -159641,7 +164875,8 @@ static int rbuObjIterFirst(sqlite3rbu *p, RbuObjIter *pIter){ memset(pIter, 0, sizeof(RbuObjIter)); rc = prepareAndCollectError(p->dbRbu, &pIter->pTblIter, &p->zErrmsg, - "SELECT rbu_target_name(name) AS target, name FROM sqlite_master " + "SELECT rbu_target_name(name, type='view') AS target, name " + "FROM sqlite_master " "WHERE type IN ('table', 'view') AND target IS NOT NULL " "ORDER BY name" ); @@ -159724,7 +164959,7 @@ static void *rbuMalloc(sqlite3rbu *p, int nByte){ void *pRet = 0; if( p->rc==SQLITE_OK ){ assert( nByte>0 ); - pRet = sqlite3_malloc(nByte); + pRet = sqlite3_malloc64(nByte); if( pRet==0 ){ p->rc = SQLITE_NOMEM; }else{ @@ -159770,8 +165005,8 @@ static char *rbuStrndup(const char *zStr, int *pRc){ assert( *pRc==SQLITE_OK ); if( zStr ){ - int nCopy = strlen(zStr) + 1; - zRet = (char*)sqlite3_malloc(nCopy); + size_t nCopy = strlen(zStr) + 1; + zRet = (char*)sqlite3_malloc64(nCopy); if( zRet ){ memcpy(zRet, zStr, nCopy); }else{ @@ -159932,6 +165167,7 @@ static void rbuObjIterCacheIndexedCols(sqlite3rbu *p, RbuObjIter *pIter){ ); } + pIter->nIndex = 0; while( p->rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pList) ){ const char *zIdx = (const char*)sqlite3_column_text(pList, 1); sqlite3_stmt *pXInfo = 0; @@ -159945,6 +165181,12 @@ static void rbuObjIterCacheIndexedCols(sqlite3rbu *p, RbuObjIter *pIter){ } rbuFinalize(p, pXInfo); bIndex = 1; + pIter->nIndex++; + } + + if( pIter->eType==RBU_PK_WITHOUT_ROWID ){ + /* "PRAGMA index_list" includes the main PK b-tree */ + pIter->nIndex--; } rbuFinalize(p, pList); @@ -160010,6 +165252,7 @@ static int rbuObjIterCacheTableInfo(sqlite3rbu *p, RbuObjIter *pIter){ pStmt = 0; if( p->rc==SQLITE_OK + && rbuIsVacuum(p)==0 && bRbuRowid!=(pIter->eType==RBU_PK_VTAB || pIter->eType==RBU_PK_NONE) ){ p->rc = SQLITE_ERROR; @@ -160058,6 +165301,7 @@ static int rbuObjIterCacheTableInfo(sqlite3rbu *p, RbuObjIter *pIter){ rbuFinalize(p, pStmt); rbuObjIterCacheIndexedCols(p, pIter); assert( pIter->eType!=RBU_PK_VTAB || pIter->abIndexed==0 ); + assert( pIter->eType!=RBU_PK_VTAB || pIter->nIndex==0 ); } return p->rc; @@ -160148,6 +165392,8 @@ static char *rbuObjIterGetIndexCols( for(i=0; pIter->abTblPk[i]==0; i++); assert( inTblCol ); zCol = pIter->azTblCol[i]; + }else if( rbuIsVacuum(p) ){ + zCol = "_rowid_"; }else{ zCol = "rbu_rowid"; } @@ -160611,6 +165857,14 @@ static void rbuTmpInsertFunc( int rc = SQLITE_OK; int i; + assert( sqlite3_value_int(apVal[0])!=0 + || p->objiter.eType==RBU_PK_EXTERNAL + || p->objiter.eType==RBU_PK_NONE + ); + if( sqlite3_value_int(apVal[0])!=0 ){ + p->nPhaseOneStep += p->objiter.nIndex; + } + for(i=0; rc==SQLITE_OK && iobjiter.pTmpInsert, i+1, apVal[i]); } @@ -160680,7 +165934,7 @@ static int rbuObjIterPrepareAll( } /* And to delete index entries */ - if( p->rc==SQLITE_OK ){ + if( rbuIsVacuum(p)==0 && p->rc==SQLITE_OK ){ p->rc = prepareFreeAndCollectError( p->dbMain, &pIter->pDelete, &p->zErrmsg, sqlite3_mprintf("DELETE FROM \"rbu_imp_%w\" WHERE %s", zTbl, zWhere) @@ -160690,6 +165944,15 @@ static int rbuObjIterPrepareAll( /* Create the SELECT statement to read keys in sorted order */ if( p->rc==SQLITE_OK ){ char *zSql; + if( rbuIsVacuum(p) ){ + zSql = sqlite3_mprintf( + "SELECT %s, 0 AS rbu_control FROM '%q' ORDER BY %s%s", + zCollist, + pIter->zDataTbl, + zCollist, zLimit + ); + }else + if( pIter->eType==RBU_PK_EXTERNAL || pIter->eType==RBU_PK_NONE ){ zSql = sqlite3_mprintf( "SELECT %s, rbu_control FROM %s.'rbu_tmp_%q' ORDER BY %s%s", @@ -160698,13 +165961,13 @@ static int rbuObjIterPrepareAll( ); }else{ zSql = sqlite3_mprintf( + "SELECT %s, rbu_control FROM %s.'rbu_tmp_%q' " + "UNION ALL " "SELECT %s, rbu_control FROM '%q' " "WHERE typeof(rbu_control)='integer' AND rbu_control!=1 " - "UNION ALL " - "SELECT %s, rbu_control FROM %s.'rbu_tmp_%q' " "ORDER BY %s%s", - zCollist, pIter->zDataTbl, zCollist, p->zStateDb, pIter->zDataTbl, + zCollist, pIter->zDataTbl, zCollist, zLimit ); } @@ -160716,7 +165979,9 @@ static int rbuObjIterPrepareAll( sqlite3_free(zWhere); sqlite3_free(zBind); }else{ - int bRbuRowid = (pIter->eType==RBU_PK_VTAB || pIter->eType==RBU_PK_NONE); + int bRbuRowid = (pIter->eType==RBU_PK_VTAB) + ||(pIter->eType==RBU_PK_NONE) + ||(pIter->eType==RBU_PK_EXTERNAL && rbuIsVacuum(p)); const char *zTbl = pIter->zTbl; /* Table this step applies to */ const char *zWrite; /* Imposter table name */ @@ -160743,8 +166008,10 @@ static int rbuObjIterPrepareAll( ); } - /* Create the DELETE statement to write to the target PK b-tree */ - if( p->rc==SQLITE_OK ){ + /* Create the DELETE statement to write to the target PK b-tree. + ** Because it only performs INSERT operations, this is not required for + ** an rbu vacuum handle. */ + if( rbuIsVacuum(p)==0 && p->rc==SQLITE_OK ){ p->rc = prepareFreeAndCollectError(p->dbMain, &pIter->pDelete, pz, sqlite3_mprintf( "DELETE FROM \"%s%w\" WHERE %s", zWrite, zTbl, zWhere @@ -160752,7 +166019,7 @@ static int rbuObjIterPrepareAll( ); } - if( pIter->abIndexed ){ + if( rbuIsVacuum(p)==0 && pIter->abIndexed ){ const char *zRbuRowid = ""; if( pIter->eType==RBU_PK_EXTERNAL || pIter->eType==RBU_PK_NONE ){ zRbuRowid = ", rbu_rowid"; @@ -160770,17 +166037,17 @@ static int rbuObjIterPrepareAll( rbuMPrintfExec(p, p->dbMain, "CREATE TEMP TRIGGER rbu_delete_tr BEFORE DELETE ON \"%s%w\" " "BEGIN " - " SELECT rbu_tmp_insert(2, %s);" + " SELECT rbu_tmp_insert(3, %s);" "END;" "CREATE TEMP TRIGGER rbu_update1_tr BEFORE UPDATE ON \"%s%w\" " "BEGIN " - " SELECT rbu_tmp_insert(2, %s);" + " SELECT rbu_tmp_insert(3, %s);" "END;" "CREATE TEMP TRIGGER rbu_update2_tr AFTER UPDATE ON \"%s%w\" " "BEGIN " - " SELECT rbu_tmp_insert(3, %s);" + " SELECT rbu_tmp_insert(4, %s);" "END;", zWrite, zTbl, zOldlist, zWrite, zTbl, zOldlist, @@ -160802,10 +166069,16 @@ static int rbuObjIterPrepareAll( /* Create the SELECT statement to read keys from data_xxx */ if( p->rc==SQLITE_OK ){ + const char *zRbuRowid = ""; + if( bRbuRowid ){ + zRbuRowid = rbuIsVacuum(p) ? ",_rowid_ " : ",rbu_rowid"; + } p->rc = prepareFreeAndCollectError(p->dbRbu, &pIter->pSelect, pz, sqlite3_mprintf( - "SELECT %s, rbu_control%s FROM '%q'%s", - zCollist, (bRbuRowid ? ", rbu_rowid" : ""), + "SELECT %s,%s rbu_control%s FROM '%q'%s", + zCollist, + (rbuIsVacuum(p) ? "0 AS " : ""), + zRbuRowid, pIter->zDataTbl, zLimit ) ); @@ -160900,11 +166173,15 @@ static int rbuGetUpdateStmt( return p->rc; } -static sqlite3 *rbuOpenDbhandle(sqlite3rbu *p, const char *zName){ +static sqlite3 *rbuOpenDbhandle( + sqlite3rbu *p, + const char *zName, + int bUseVfs +){ sqlite3 *db = 0; if( p->rc==SQLITE_OK ){ const int flags = SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE|SQLITE_OPEN_URI; - p->rc = sqlite3_open_v2(zName, &db, flags, p->zVfsName); + p->rc = sqlite3_open_v2(zName, &db, flags, bUseVfs ? p->zVfsName : 0); if( p->rc ){ p->zErrmsg = sqlite3_mprintf("%s", sqlite3_errmsg(db)); sqlite3_close(db); @@ -160914,6 +166191,95 @@ static sqlite3 *rbuOpenDbhandle(sqlite3rbu *p, const char *zName){ return db; } +/* +** Free an RbuState object allocated by rbuLoadState(). +*/ +static void rbuFreeState(RbuState *p){ + if( p ){ + sqlite3_free(p->zTbl); + sqlite3_free(p->zIdx); + sqlite3_free(p); + } +} + +/* +** Allocate an RbuState object and load the contents of the rbu_state +** table into it. Return a pointer to the new object. It is the +** responsibility of the caller to eventually free the object using +** sqlite3_free(). +** +** If an error occurs, leave an error code and message in the rbu handle +** and return NULL. +*/ +static RbuState *rbuLoadState(sqlite3rbu *p){ + RbuState *pRet = 0; + sqlite3_stmt *pStmt = 0; + int rc; + int rc2; + + pRet = (RbuState*)rbuMalloc(p, sizeof(RbuState)); + if( pRet==0 ) return 0; + + rc = prepareFreeAndCollectError(p->dbRbu, &pStmt, &p->zErrmsg, + sqlite3_mprintf("SELECT k, v FROM %s.rbu_state", p->zStateDb) + ); + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ + switch( sqlite3_column_int(pStmt, 0) ){ + case RBU_STATE_STAGE: + pRet->eStage = sqlite3_column_int(pStmt, 1); + if( pRet->eStage!=RBU_STAGE_OAL + && pRet->eStage!=RBU_STAGE_MOVE + && pRet->eStage!=RBU_STAGE_CKPT + ){ + p->rc = SQLITE_CORRUPT; + } + break; + + case RBU_STATE_TBL: + pRet->zTbl = rbuStrndup((char*)sqlite3_column_text(pStmt, 1), &rc); + break; + + case RBU_STATE_IDX: + pRet->zIdx = rbuStrndup((char*)sqlite3_column_text(pStmt, 1), &rc); + break; + + case RBU_STATE_ROW: + pRet->nRow = sqlite3_column_int(pStmt, 1); + break; + + case RBU_STATE_PROGRESS: + pRet->nProgress = sqlite3_column_int64(pStmt, 1); + break; + + case RBU_STATE_CKPT: + pRet->iWalCksum = sqlite3_column_int64(pStmt, 1); + break; + + case RBU_STATE_COOKIE: + pRet->iCookie = (u32)sqlite3_column_int64(pStmt, 1); + break; + + case RBU_STATE_OALSZ: + pRet->iOalSz = (u32)sqlite3_column_int64(pStmt, 1); + break; + + case RBU_STATE_PHASEONESTEP: + pRet->nPhaseOneStep = sqlite3_column_int64(pStmt, 1); + break; + + default: + rc = SQLITE_CORRUPT; + break; + } + } + rc2 = sqlite3_finalize(pStmt); + if( rc==SQLITE_OK ) rc = rc2; + + p->rc = rc; + return pRet; +} + + /* ** Open the database handle and attach the RBU database as "rbu". If an ** error occurs, leave an error code and message in the RBU handle. @@ -160921,10 +166287,14 @@ static sqlite3 *rbuOpenDbhandle(sqlite3rbu *p, const char *zName){ static void rbuOpenDatabase(sqlite3rbu *p){ assert( p->rc==SQLITE_OK ); assert( p->dbMain==0 && p->dbRbu==0 ); + assert( rbuIsVacuum(p) || p->zTarget!=0 ); - p->eStage = 0; - p->dbMain = rbuOpenDbhandle(p, p->zTarget); - p->dbRbu = rbuOpenDbhandle(p, p->zRbu); + /* Open the RBU database */ + p->dbRbu = rbuOpenDbhandle(p, p->zRbu, 1); + + if( p->rc==SQLITE_OK && rbuIsVacuum(p) ){ + sqlite3_file_control(p->dbRbu, "main", SQLITE_FCNTL_RBUCNT, (void*)p); + } /* If using separate RBU and state databases, attach the state database to ** the RBU db handle now. */ @@ -160935,6 +166305,96 @@ static void rbuOpenDatabase(sqlite3rbu *p){ memcpy(p->zStateDb, "main", 4); } +#if 0 + if( p->rc==SQLITE_OK && rbuIsVacuum(p) ){ + p->rc = sqlite3_exec(p->dbRbu, "BEGIN", 0, 0, 0); + } +#endif + + /* If it has not already been created, create the rbu_state table */ + rbuMPrintfExec(p, p->dbRbu, RBU_CREATE_STATE, p->zStateDb); + +#if 0 + if( rbuIsVacuum(p) ){ + if( p->rc==SQLITE_OK ){ + int rc2; + int bOk = 0; + sqlite3_stmt *pCnt = 0; + p->rc = prepareAndCollectError(p->dbRbu, &pCnt, &p->zErrmsg, + "SELECT count(*) FROM stat.sqlite_master" + ); + if( p->rc==SQLITE_OK + && sqlite3_step(pCnt)==SQLITE_ROW + && 1==sqlite3_column_int(pCnt, 0) + ){ + bOk = 1; + } + rc2 = sqlite3_finalize(pCnt); + if( p->rc==SQLITE_OK ) p->rc = rc2; + + if( p->rc==SQLITE_OK && bOk==0 ){ + p->rc = SQLITE_ERROR; + p->zErrmsg = sqlite3_mprintf("invalid state database"); + } + + if( p->rc==SQLITE_OK ){ + p->rc = sqlite3_exec(p->dbRbu, "COMMIT", 0, 0, 0); + } + } + } +#endif + + if( p->rc==SQLITE_OK && rbuIsVacuum(p) ){ + int bOpen = 0; + int rc; + p->nRbu = 0; + p->pRbuFd = 0; + rc = sqlite3_file_control(p->dbRbu, "main", SQLITE_FCNTL_RBUCNT, (void*)p); + if( rc!=SQLITE_NOTFOUND ) p->rc = rc; + if( p->eStage>=RBU_STAGE_MOVE ){ + bOpen = 1; + }else{ + RbuState *pState = rbuLoadState(p); + if( pState ){ + bOpen = (pState->eStage>RBU_STAGE_MOVE); + rbuFreeState(pState); + } + } + if( bOpen ) p->dbMain = rbuOpenDbhandle(p, p->zRbu, p->nRbu<=1); + } + + p->eStage = 0; + if( p->rc==SQLITE_OK && p->dbMain==0 ){ + if( !rbuIsVacuum(p) ){ + p->dbMain = rbuOpenDbhandle(p, p->zTarget, 1); + }else if( p->pRbuFd->pWalFd ){ + p->rc = SQLITE_ERROR; + p->zErrmsg = sqlite3_mprintf("cannot vacuum wal mode database"); + }else{ + char *zTarget; + char *zExtra = 0; + if( strlen(p->zRbu)>=5 && 0==memcmp("file:", p->zRbu, 5) ){ + zExtra = &p->zRbu[5]; + while( *zExtra ){ + if( *zExtra++=='?' ) break; + } + if( *zExtra=='\0' ) zExtra = 0; + } + + zTarget = sqlite3_mprintf("file:%s-vacuum?rbu_memory=1%s%s", + sqlite3_db_filename(p->dbRbu, "main"), + (zExtra==0 ? "" : "&"), (zExtra==0 ? "" : zExtra) + ); + + if( zTarget==0 ){ + p->rc = SQLITE_NOMEM; + return; + } + p->dbMain = rbuOpenDbhandle(p, zTarget, p->nRbu<=1); + sqlite3_free(zTarget); + } + } + if( p->rc==SQLITE_OK ){ p->rc = sqlite3_create_function(p->dbMain, "rbu_tmp_insert", -1, SQLITE_UTF8, (void*)p, rbuTmpInsertFunc, 0, 0 @@ -160949,7 +166409,7 @@ static void rbuOpenDatabase(sqlite3rbu *p){ if( p->rc==SQLITE_OK ){ p->rc = sqlite3_create_function(p->dbRbu, - "rbu_target_name", 1, SQLITE_UTF8, (void*)p, rbuTargetNameFunc, 0, 0 + "rbu_target_name", -1, SQLITE_UTF8, (void*)p, rbuTargetNameFunc, 0, 0 ); } @@ -161119,7 +166579,7 @@ static int rbuCaptureWalRead(sqlite3rbu *pRbu, i64 iOff, int iAmt){ if( pRbu->nFrame==pRbu->nFrameAlloc ){ int nNew = (pRbu->nFrameAlloc ? pRbu->nFrameAlloc : 64) * 2; RbuFrame *aNew; - aNew = (RbuFrame*)sqlite3_realloc(pRbu->aFrame, nNew * sizeof(RbuFrame)); + aNew = (RbuFrame*)sqlite3_realloc64(pRbu->aFrame, nNew * sizeof(RbuFrame)); if( aNew==0 ) return SQLITE_NOMEM; pRbu->aFrame = aNew; pRbu->nFrameAlloc = nNew; @@ -161184,7 +166644,7 @@ static LPWSTR rbuWinUtf8ToUnicode(const char *zFilename){ if( nChar==0 ){ return 0; } - zWideFilename = sqlite3_malloc( nChar*sizeof(zWideFilename[0]) ); + zWideFilename = sqlite3_malloc64( nChar*sizeof(zWideFilename[0]) ); if( zWideFilename==0 ){ return 0; } @@ -161208,9 +166668,15 @@ static LPWSTR rbuWinUtf8ToUnicode(const char *zFilename){ */ static void rbuMoveOalFile(sqlite3rbu *p){ const char *zBase = sqlite3_db_filename(p->dbMain, "main"); + const char *zMove = zBase; + char *zOal; + char *zWal; - char *zWal = sqlite3_mprintf("%s-wal", zBase); - char *zOal = sqlite3_mprintf("%s-oal", zBase); + if( rbuIsVacuum(p) ){ + zMove = sqlite3_db_filename(p->dbRbu, "main"); + } + zOal = sqlite3_mprintf("%s-oal", zMove); + zWal = sqlite3_mprintf("%s-wal", zMove); assert( p->eStage==RBU_STAGE_MOVE ); assert( p->rc==SQLITE_OK && p->zErrmsg==0 ); @@ -161231,8 +166697,8 @@ static void rbuMoveOalFile(sqlite3rbu *p){ /* Re-open the databases. */ rbuObjIterFinalize(&p->objiter); - sqlite3_close(p->dbMain); sqlite3_close(p->dbRbu); + sqlite3_close(p->dbMain); p->dbMain = 0; p->dbRbu = 0; @@ -161298,14 +166764,12 @@ static int rbuStepType(sqlite3rbu *p, const char **pzMask){ switch( sqlite3_column_type(p->objiter.pSelect, iCol) ){ case SQLITE_INTEGER: { int iVal = sqlite3_column_int(p->objiter.pSelect, iCol); - if( iVal==0 ){ - res = RBU_INSERT; - }else if( iVal==1 ){ - res = RBU_DELETE; - }else if( iVal==2 ){ - res = RBU_IDX_DELETE; - }else if( iVal==3 ){ - res = RBU_IDX_INSERT; + switch( iVal ){ + case 0: res = RBU_INSERT; break; + case 1: res = RBU_DELETE; break; + case 2: res = RBU_REPLACE; break; + case 3: res = RBU_IDX_DELETE; break; + case 4: res = RBU_IDX_INSERT; break; } break; } @@ -161344,6 +166808,83 @@ static void assertColumnName(sqlite3_stmt *pStmt, int iCol, const char *zName){ # define assertColumnName(x,y,z) #endif +/* +** Argument eType must be one of RBU_INSERT, RBU_DELETE, RBU_IDX_INSERT or +** RBU_IDX_DELETE. This function performs the work of a single +** sqlite3rbu_step() call for the type of operation specified by eType. +*/ +static void rbuStepOneOp(sqlite3rbu *p, int eType){ + RbuObjIter *pIter = &p->objiter; + sqlite3_value *pVal; + sqlite3_stmt *pWriter; + int i; + + assert( p->rc==SQLITE_OK ); + assert( eType!=RBU_DELETE || pIter->zIdx==0 ); + assert( eType==RBU_DELETE || eType==RBU_IDX_DELETE + || eType==RBU_INSERT || eType==RBU_IDX_INSERT + ); + + /* If this is a delete, decrement nPhaseOneStep by nIndex. If the DELETE + ** statement below does actually delete a row, nPhaseOneStep will be + ** incremented by the same amount when SQL function rbu_tmp_insert() + ** is invoked by the trigger. */ + if( eType==RBU_DELETE ){ + p->nPhaseOneStep -= p->objiter.nIndex; + } + + if( eType==RBU_IDX_DELETE || eType==RBU_DELETE ){ + pWriter = pIter->pDelete; + }else{ + pWriter = pIter->pInsert; + } + + for(i=0; inCol; i++){ + /* If this is an INSERT into a table b-tree and the table has an + ** explicit INTEGER PRIMARY KEY, check that this is not an attempt + ** to write a NULL into the IPK column. That is not permitted. */ + if( eType==RBU_INSERT + && pIter->zIdx==0 && pIter->eType==RBU_PK_IPK && pIter->abTblPk[i] + && sqlite3_column_type(pIter->pSelect, i)==SQLITE_NULL + ){ + p->rc = SQLITE_MISMATCH; + p->zErrmsg = sqlite3_mprintf("datatype mismatch"); + return; + } + + if( eType==RBU_DELETE && pIter->abTblPk[i]==0 ){ + continue; + } + + pVal = sqlite3_column_value(pIter->pSelect, i); + p->rc = sqlite3_bind_value(pWriter, i+1, pVal); + if( p->rc ) return; + } + if( pIter->zIdx==0 ){ + if( pIter->eType==RBU_PK_VTAB + || pIter->eType==RBU_PK_NONE + || (pIter->eType==RBU_PK_EXTERNAL && rbuIsVacuum(p)) + ){ + /* For a virtual table, or a table with no primary key, the + ** SELECT statement is: + ** + ** SELECT , rbu_control, rbu_rowid FROM .... + ** + ** Hence column_value(pIter->nCol+1). + */ + assertColumnName(pIter->pSelect, pIter->nCol+1, + rbuIsVacuum(p) ? "rowid" : "rbu_rowid" + ); + pVal = sqlite3_column_value(pIter->pSelect, pIter->nCol+1); + p->rc = sqlite3_bind_value(pWriter, pIter->nCol+1, pVal); + } + } + if( p->rc==SQLITE_OK ){ + sqlite3_step(pWriter); + p->rc = resetAndCollectError(pWriter, &p->zErrmsg); + } +} + /* ** This function does the work for an sqlite3rbu_step() call. ** @@ -161358,78 +166899,36 @@ static void assertColumnName(sqlite3_stmt *pStmt, int iCol, const char *zName){ static int rbuStep(sqlite3rbu *p){ RbuObjIter *pIter = &p->objiter; const char *zMask = 0; - int i; int eType = rbuStepType(p, &zMask); if( eType ){ + assert( eType==RBU_INSERT || eType==RBU_DELETE + || eType==RBU_REPLACE || eType==RBU_IDX_DELETE + || eType==RBU_IDX_INSERT || eType==RBU_UPDATE + ); assert( eType!=RBU_UPDATE || pIter->zIdx==0 ); - if( pIter->zIdx==0 && eType==RBU_IDX_DELETE ){ + if( pIter->zIdx==0 && (eType==RBU_IDX_DELETE || eType==RBU_IDX_INSERT) ){ rbuBadControlError(p); } - else if( - eType==RBU_INSERT - || eType==RBU_DELETE - || eType==RBU_IDX_DELETE - || eType==RBU_IDX_INSERT - ){ - sqlite3_value *pVal; - sqlite3_stmt *pWriter; - - assert( eType!=RBU_UPDATE ); - assert( eType!=RBU_DELETE || pIter->zIdx==0 ); - - if( eType==RBU_IDX_DELETE || eType==RBU_DELETE ){ - pWriter = pIter->pDelete; - }else{ - pWriter = pIter->pInsert; + else if( eType==RBU_REPLACE ){ + if( pIter->zIdx==0 ){ + p->nPhaseOneStep += p->objiter.nIndex; + rbuStepOneOp(p, RBU_DELETE); } - - for(i=0; inCol; i++){ - /* If this is an INSERT into a table b-tree and the table has an - ** explicit INTEGER PRIMARY KEY, check that this is not an attempt - ** to write a NULL into the IPK column. That is not permitted. */ - if( eType==RBU_INSERT - && pIter->zIdx==0 && pIter->eType==RBU_PK_IPK && pIter->abTblPk[i] - && sqlite3_column_type(pIter->pSelect, i)==SQLITE_NULL - ){ - p->rc = SQLITE_MISMATCH; - p->zErrmsg = sqlite3_mprintf("datatype mismatch"); - goto step_out; - } - - if( eType==RBU_DELETE && pIter->abTblPk[i]==0 ){ - continue; - } - - pVal = sqlite3_column_value(pIter->pSelect, i); - p->rc = sqlite3_bind_value(pWriter, i+1, pVal); - if( p->rc ) goto step_out; - } - if( pIter->zIdx==0 - && (pIter->eType==RBU_PK_VTAB || pIter->eType==RBU_PK_NONE) - ){ - /* For a virtual table, or a table with no primary key, the - ** SELECT statement is: - ** - ** SELECT , rbu_control, rbu_rowid FROM .... - ** - ** Hence column_value(pIter->nCol+1). - */ - assertColumnName(pIter->pSelect, pIter->nCol+1, "rbu_rowid"); - pVal = sqlite3_column_value(pIter->pSelect, pIter->nCol+1); - p->rc = sqlite3_bind_value(pWriter, pIter->nCol+1, pVal); - } - if( p->rc==SQLITE_OK ){ - sqlite3_step(pWriter); - p->rc = resetAndCollectError(pWriter, &p->zErrmsg); - } - }else{ + if( p->rc==SQLITE_OK ) rbuStepOneOp(p, RBU_INSERT); + } + else if( eType!=RBU_UPDATE ){ + rbuStepOneOp(p, eType); + } + else{ sqlite3_value *pVal; sqlite3_stmt *pUpdate = 0; assert( eType==RBU_UPDATE ); + p->nPhaseOneStep -= p->objiter.nIndex; rbuGetUpdateStmt(p, pIter, zMask, &pUpdate); if( pUpdate ){ + int i; for(i=0; p->rc==SQLITE_OK && inCol; i++){ char c = zMask[pIter->aiSrcOrder[i]]; pVal = sqlite3_column_value(pIter->pSelect, i); @@ -161452,20 +166951,23 @@ static int rbuStep(sqlite3rbu *p){ } } } - - step_out: return p->rc; } /* ** Increment the schema cookie of the main database opened by p->dbMain. +** +** Or, if this is an RBU vacuum, set the schema cookie of the main db +** opened by p->dbMain to one more than the schema cookie of the main +** db opened by p->dbRbu. */ static void rbuIncrSchemaCookie(sqlite3rbu *p){ if( p->rc==SQLITE_OK ){ + sqlite3 *dbread = (rbuIsVacuum(p) ? p->dbRbu : p->dbMain); int iCookie = 1000000; sqlite3_stmt *pStmt; - p->rc = prepareAndCollectError(p->dbMain, &pStmt, &p->zErrmsg, + p->rc = prepareAndCollectError(dbread, &pStmt, &p->zErrmsg, "PRAGMA schema_version" ); if( p->rc==SQLITE_OK ){ @@ -161493,6 +166995,7 @@ static void rbuIncrSchemaCookie(sqlite3rbu *p){ static void rbuSaveState(sqlite3rbu *p, int eStage){ if( p->rc==SQLITE_OK || p->rc==SQLITE_DONE ){ sqlite3_stmt *pInsert = 0; + rbu_file *pFd = (rbuIsVacuum(p) ? p->pRbuFd : p->pTargetFd); int rc; assert( p->zErrmsg==0 ); @@ -161506,6 +167009,7 @@ static void rbuSaveState(sqlite3rbu *p, int eStage){ "(%d, %d), " "(%d, %lld), " "(%d, %lld), " + "(%d, %lld), " "(%d, %lld) ", p->zStateDb, RBU_STATE_STAGE, eStage, @@ -161514,8 +167018,9 @@ static void rbuSaveState(sqlite3rbu *p, int eStage){ RBU_STATE_ROW, p->nStep, RBU_STATE_PROGRESS, p->nProgress, RBU_STATE_CKPT, p->iWalCksum, - RBU_STATE_COOKIE, (i64)p->pTargetFd->iCookie, - RBU_STATE_OALSZ, p->iOalSz + RBU_STATE_COOKIE, (i64)pFd->iCookie, + RBU_STATE_OALSZ, p->iOalSz, + RBU_STATE_PHASEONESTEP, p->nPhaseOneStep ) ); assert( pInsert==0 || rc==SQLITE_OK ); @@ -161529,6 +167034,92 @@ static void rbuSaveState(sqlite3rbu *p, int eStage){ } +/* +** The second argument passed to this function is the name of a PRAGMA +** setting - "page_size", "auto_vacuum", "user_version" or "application_id". +** This function executes the following on sqlite3rbu.dbRbu: +** +** "PRAGMA main.$zPragma" +** +** where $zPragma is the string passed as the second argument, then +** on sqlite3rbu.dbMain: +** +** "PRAGMA main.$zPragma = $val" +** +** where $val is the value returned by the first PRAGMA invocation. +** +** In short, it copies the value of the specified PRAGMA setting from +** dbRbu to dbMain. +*/ +static void rbuCopyPragma(sqlite3rbu *p, const char *zPragma){ + if( p->rc==SQLITE_OK ){ + sqlite3_stmt *pPragma = 0; + p->rc = prepareFreeAndCollectError(p->dbRbu, &pPragma, &p->zErrmsg, + sqlite3_mprintf("PRAGMA main.%s", zPragma) + ); + if( p->rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pPragma) ){ + p->rc = rbuMPrintfExec(p, p->dbMain, "PRAGMA main.%s = %d", + zPragma, sqlite3_column_int(pPragma, 0) + ); + } + rbuFinalize(p, pPragma); + } +} + +/* +** The RBU handle passed as the only argument has just been opened and +** the state database is empty. If this RBU handle was opened for an +** RBU vacuum operation, create the schema in the target db. +*/ +static void rbuCreateTargetSchema(sqlite3rbu *p){ + sqlite3_stmt *pSql = 0; + sqlite3_stmt *pInsert = 0; + + assert( rbuIsVacuum(p) ); + p->rc = sqlite3_exec(p->dbMain, "PRAGMA writable_schema=1", 0,0, &p->zErrmsg); + if( p->rc==SQLITE_OK ){ + p->rc = prepareAndCollectError(p->dbRbu, &pSql, &p->zErrmsg, + "SELECT sql FROM sqlite_master WHERE sql!='' AND rootpage!=0" + " AND name!='sqlite_sequence' " + " ORDER BY type DESC" + ); + } + + while( p->rc==SQLITE_OK && sqlite3_step(pSql)==SQLITE_ROW ){ + const char *zSql = (const char*)sqlite3_column_text(pSql, 0); + p->rc = sqlite3_exec(p->dbMain, zSql, 0, 0, &p->zErrmsg); + } + rbuFinalize(p, pSql); + if( p->rc!=SQLITE_OK ) return; + + if( p->rc==SQLITE_OK ){ + p->rc = prepareAndCollectError(p->dbRbu, &pSql, &p->zErrmsg, + "SELECT * FROM sqlite_master WHERE rootpage=0 OR rootpage IS NULL" + ); + } + + if( p->rc==SQLITE_OK ){ + p->rc = prepareAndCollectError(p->dbMain, &pInsert, &p->zErrmsg, + "INSERT INTO sqlite_master VALUES(?,?,?,?,?)" + ); + } + + while( p->rc==SQLITE_OK && sqlite3_step(pSql)==SQLITE_ROW ){ + int i; + for(i=0; i<5; i++){ + sqlite3_bind_value(pInsert, i+1, sqlite3_column_value(pSql, i)); + } + sqlite3_step(pInsert); + p->rc = sqlite3_reset(pInsert); + } + if( p->rc==SQLITE_OK ){ + p->rc = sqlite3_exec(p->dbMain, "PRAGMA writable_schema=0",0,0,&p->zErrmsg); + } + + rbuFinalize(p, pSql); + rbuFinalize(p, pInsert); +} + /* ** Step the RBU object. */ @@ -161537,13 +167128,22 @@ SQLITE_API int SQLITE_STDCALL sqlite3rbu_step(sqlite3rbu *p){ switch( p->eStage ){ case RBU_STAGE_OAL: { RbuObjIter *pIter = &p->objiter; + + /* If this is an RBU vacuum operation and the state table was empty + ** when this handle was opened, create the target database schema. */ + if( rbuIsVacuum(p) && p->nProgress==0 && p->rc==SQLITE_OK ){ + rbuCreateTargetSchema(p); + rbuCopyPragma(p, "user_version"); + rbuCopyPragma(p, "application_id"); + } + while( p->rc==SQLITE_OK && pIter->zTbl ){ if( pIter->bCleanup ){ /* Clean up the rbu_tmp_xxx table for the previous table. It ** cannot be dropped as there are currently active SQL statements. ** But the contents can be deleted. */ - if( pIter->abIndexed ){ + if( rbuIsVacuum(p)==0 && pIter->abIndexed ){ rbuMPrintfExec(p, p->dbRbu, "DELETE FROM %s.'rbu_tmp_%q'", p->zStateDb, pIter->zDataTbl ); @@ -161630,90 +167230,6 @@ SQLITE_API int SQLITE_STDCALL sqlite3rbu_step(sqlite3rbu *p){ } } -/* -** Free an RbuState object allocated by rbuLoadState(). -*/ -static void rbuFreeState(RbuState *p){ - if( p ){ - sqlite3_free(p->zTbl); - sqlite3_free(p->zIdx); - sqlite3_free(p); - } -} - -/* -** Allocate an RbuState object and load the contents of the rbu_state -** table into it. Return a pointer to the new object. It is the -** responsibility of the caller to eventually free the object using -** sqlite3_free(). -** -** If an error occurs, leave an error code and message in the rbu handle -** and return NULL. -*/ -static RbuState *rbuLoadState(sqlite3rbu *p){ - RbuState *pRet = 0; - sqlite3_stmt *pStmt = 0; - int rc; - int rc2; - - pRet = (RbuState*)rbuMalloc(p, sizeof(RbuState)); - if( pRet==0 ) return 0; - - rc = prepareFreeAndCollectError(p->dbRbu, &pStmt, &p->zErrmsg, - sqlite3_mprintf("SELECT k, v FROM %s.rbu_state", p->zStateDb) - ); - while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){ - switch( sqlite3_column_int(pStmt, 0) ){ - case RBU_STATE_STAGE: - pRet->eStage = sqlite3_column_int(pStmt, 1); - if( pRet->eStage!=RBU_STAGE_OAL - && pRet->eStage!=RBU_STAGE_MOVE - && pRet->eStage!=RBU_STAGE_CKPT - ){ - p->rc = SQLITE_CORRUPT; - } - break; - - case RBU_STATE_TBL: - pRet->zTbl = rbuStrndup((char*)sqlite3_column_text(pStmt, 1), &rc); - break; - - case RBU_STATE_IDX: - pRet->zIdx = rbuStrndup((char*)sqlite3_column_text(pStmt, 1), &rc); - break; - - case RBU_STATE_ROW: - pRet->nRow = sqlite3_column_int(pStmt, 1); - break; - - case RBU_STATE_PROGRESS: - pRet->nProgress = sqlite3_column_int64(pStmt, 1); - break; - - case RBU_STATE_CKPT: - pRet->iWalCksum = sqlite3_column_int64(pStmt, 1); - break; - - case RBU_STATE_COOKIE: - pRet->iCookie = (u32)sqlite3_column_int64(pStmt, 1); - break; - - case RBU_STATE_OALSZ: - pRet->iOalSz = (u32)sqlite3_column_int64(pStmt, 1); - break; - - default: - rc = SQLITE_CORRUPT; - break; - } - } - rc2 = sqlite3_finalize(pStmt); - if( rc==SQLITE_OK ) rc = rc2; - - p->rc = rc; - return pRet; -} - /* ** Compare strings z1 and z2, returning 0 if they are identical, or non-zero ** otherwise. Either or both argument may be NULL. Two NULL values are @@ -161810,19 +167326,112 @@ static void rbuDeleteVfs(sqlite3rbu *p){ } /* -** Open and return a new RBU handle. +** This user-defined SQL function is invoked with a single argument - the +** name of a table expected to appear in the target database. It returns +** the number of auxilliary indexes on the table. */ -SQLITE_API sqlite3rbu *SQLITE_STDCALL sqlite3rbu_open( +static void rbuIndexCntFunc( + sqlite3_context *pCtx, + int nVal, + sqlite3_value **apVal +){ + sqlite3rbu *p = (sqlite3rbu*)sqlite3_user_data(pCtx); + sqlite3_stmt *pStmt = 0; + char *zErrmsg = 0; + int rc; + + assert( nVal==1 ); + + rc = prepareFreeAndCollectError(p->dbMain, &pStmt, &zErrmsg, + sqlite3_mprintf("SELECT count(*) FROM sqlite_master " + "WHERE type='index' AND tbl_name = %Q", sqlite3_value_text(apVal[0])) + ); + if( rc!=SQLITE_OK ){ + sqlite3_result_error(pCtx, zErrmsg, -1); + }else{ + int nIndex = 0; + if( SQLITE_ROW==sqlite3_step(pStmt) ){ + nIndex = sqlite3_column_int(pStmt, 0); + } + rc = sqlite3_finalize(pStmt); + if( rc==SQLITE_OK ){ + sqlite3_result_int(pCtx, nIndex); + }else{ + sqlite3_result_error(pCtx, sqlite3_errmsg(p->dbMain), -1); + } + } + + sqlite3_free(zErrmsg); +} + +/* +** If the RBU database contains the rbu_count table, use it to initialize +** the sqlite3rbu.nPhaseOneStep variable. The schema of the rbu_count table +** is assumed to contain the same columns as: +** +** CREATE TABLE rbu_count(tbl TEXT PRIMARY KEY, cnt INTEGER) WITHOUT ROWID; +** +** There should be one row in the table for each data_xxx table in the +** database. The 'tbl' column should contain the name of a data_xxx table, +** and the cnt column the number of rows it contains. +** +** sqlite3rbu.nPhaseOneStep is initialized to the sum of (1 + nIndex) * cnt +** for all rows in the rbu_count table, where nIndex is the number of +** indexes on the corresponding target database table. +*/ +static void rbuInitPhaseOneSteps(sqlite3rbu *p){ + if( p->rc==SQLITE_OK ){ + sqlite3_stmt *pStmt = 0; + int bExists = 0; /* True if rbu_count exists */ + + p->nPhaseOneStep = -1; + + p->rc = sqlite3_create_function(p->dbRbu, + "rbu_index_cnt", 1, SQLITE_UTF8, (void*)p, rbuIndexCntFunc, 0, 0 + ); + + /* Check for the rbu_count table. If it does not exist, or if an error + ** occurs, nPhaseOneStep will be left set to -1. */ + if( p->rc==SQLITE_OK ){ + p->rc = prepareAndCollectError(p->dbRbu, &pStmt, &p->zErrmsg, + "SELECT 1 FROM sqlite_master WHERE tbl_name = 'rbu_count'" + ); + } + if( p->rc==SQLITE_OK ){ + if( SQLITE_ROW==sqlite3_step(pStmt) ){ + bExists = 1; + } + p->rc = sqlite3_finalize(pStmt); + } + + if( p->rc==SQLITE_OK && bExists ){ + p->rc = prepareAndCollectError(p->dbRbu, &pStmt, &p->zErrmsg, + "SELECT sum(cnt * (1 + rbu_index_cnt(rbu_target_name(tbl))))" + "FROM rbu_count" + ); + if( p->rc==SQLITE_OK ){ + if( SQLITE_ROW==sqlite3_step(pStmt) ){ + p->nPhaseOneStep = sqlite3_column_int64(pStmt, 0); + } + p->rc = sqlite3_finalize(pStmt); + } + } + } +} + + +static sqlite3rbu *openRbuHandle( const char *zTarget, const char *zRbu, const char *zState ){ sqlite3rbu *p; - int nTarget = strlen(zTarget); - int nRbu = strlen(zRbu); - int nState = zState ? strlen(zState) : 0; + size_t nTarget = zTarget ? strlen(zTarget) : 0; + size_t nRbu = strlen(zRbu); + size_t nState = zState ? strlen(zState) : 0; + size_t nByte = sizeof(sqlite3rbu) + nTarget+1 + nRbu+1+ nState+1; - p = (sqlite3rbu*)sqlite3_malloc(sizeof(sqlite3rbu)+nTarget+1+nRbu+1+nState+1); + p = (sqlite3rbu*)sqlite3_malloc64(nByte); if( p ){ RbuState *pState = 0; @@ -161830,22 +167439,24 @@ SQLITE_API sqlite3rbu *SQLITE_STDCALL sqlite3rbu_open( memset(p, 0, sizeof(sqlite3rbu)); rbuCreateVfs(p); - /* Open the target database */ + /* Open the target, RBU and state databases */ if( p->rc==SQLITE_OK ){ - p->zTarget = (char*)&p[1]; - memcpy(p->zTarget, zTarget, nTarget+1); - p->zRbu = &p->zTarget[nTarget+1]; + char *pCsr = (char*)&p[1]; + if( zTarget ){ + p->zTarget = pCsr; + memcpy(p->zTarget, zTarget, nTarget+1); + pCsr += nTarget+1; + } + p->zRbu = pCsr; memcpy(p->zRbu, zRbu, nRbu+1); + pCsr += nRbu+1; if( zState ){ - p->zState = &p->zRbu[nRbu+1]; + p->zState = pCsr; memcpy(p->zState, zState, nState+1); } rbuOpenDatabase(p); } - /* If it has not already been created, create the rbu_state table */ - rbuMPrintfExec(p, p->dbRbu, RBU_CREATE_STATE, p->zStateDb); - if( p->rc==SQLITE_OK ){ pState = rbuLoadState(p); assert( pState || p->rc!=SQLITE_OK ); @@ -161853,9 +167464,11 @@ SQLITE_API sqlite3rbu *SQLITE_STDCALL sqlite3rbu_open( if( pState->eStage==0 ){ rbuDeleteOalFile(p); + rbuInitPhaseOneSteps(p); p->eStage = RBU_STAGE_OAL; }else{ p->eStage = pState->eStage; + p->nPhaseOneStep = pState->nPhaseOneStep; } p->nProgress = pState->nProgress; p->iOalSz = pState->iOalSz; @@ -161873,27 +167486,39 @@ SQLITE_API sqlite3rbu *SQLITE_STDCALL sqlite3rbu_open( } } - if( p->rc==SQLITE_OK + if( p->rc==SQLITE_OK && (p->eStage==RBU_STAGE_OAL || p->eStage==RBU_STAGE_MOVE) - && pState->eStage!=0 && p->pTargetFd->iCookie!=pState->iCookie - ){ - /* At this point (pTargetFd->iCookie) contains the value of the - ** change-counter cookie (the thing that gets incremented when a - ** transaction is committed in rollback mode) currently stored on - ** page 1 of the database file. */ - p->rc = SQLITE_BUSY; - p->zErrmsg = sqlite3_mprintf("database modified during rbu update"); + && pState->eStage!=0 + ){ + rbu_file *pFd = (rbuIsVacuum(p) ? p->pRbuFd : p->pTargetFd); + if( pFd->iCookie!=pState->iCookie ){ + /* At this point (pTargetFd->iCookie) contains the value of the + ** change-counter cookie (the thing that gets incremented when a + ** transaction is committed in rollback mode) currently stored on + ** page 1 of the database file. */ + p->rc = SQLITE_BUSY; + p->zErrmsg = sqlite3_mprintf("database modified during rbu %s", + (rbuIsVacuum(p) ? "vacuum" : "update") + ); + } } if( p->rc==SQLITE_OK ){ if( p->eStage==RBU_STAGE_OAL ){ sqlite3 *db = p->dbMain; + if( pState->eStage==0 && rbuIsVacuum(p) ){ + rbuCopyPragma(p, "page_size"); + rbuCopyPragma(p, "auto_vacuum"); + } + /* Open transactions both databases. The *-oal file is opened or ** created at this point. */ - p->rc = sqlite3_exec(db, "BEGIN IMMEDIATE", 0, 0, &p->zErrmsg); if( p->rc==SQLITE_OK ){ - p->rc = sqlite3_exec(p->dbRbu, "BEGIN IMMEDIATE", 0, 0, &p->zErrmsg); + p->rc = sqlite3_exec(db, "BEGIN IMMEDIATE", 0, 0, &p->zErrmsg); + } + if( p->rc==SQLITE_OK ){ + p->rc = sqlite3_exec(p->dbRbu, "BEGIN", 0, 0, &p->zErrmsg); } /* Check if the main database is a zipvfs db. If it is, set the upper @@ -161938,6 +167563,28 @@ SQLITE_API sqlite3rbu *SQLITE_STDCALL sqlite3rbu_open( return p; } +/* +** Open and return a new RBU handle. +*/ +SQLITE_API sqlite3rbu *SQLITE_STDCALL sqlite3rbu_open( + const char *zTarget, + const char *zRbu, + const char *zState +){ + /* TODO: Check that zTarget and zRbu are non-NULL */ + return openRbuHandle(zTarget, zRbu, zState); +} + +/* +** Open a handle to begin or resume an RBU VACUUM operation. +*/ +SQLITE_API sqlite3rbu *SQLITE_STDCALL sqlite3rbu_vacuum( + const char *zTarget, + const char *zState +){ + /* TODO: Check that both arguments are non-NULL */ + return openRbuHandle(0, zTarget, zState); +} /* ** Return the database handle used by pRbu. @@ -161958,8 +167605,8 @@ SQLITE_API sqlite3 *SQLITE_STDCALL sqlite3rbu_db(sqlite3rbu *pRbu, int bRbu){ */ static void rbuEditErrmsg(sqlite3rbu *p){ if( p->rc==SQLITE_CONSTRAINT && p->zErrmsg ){ - int i; - int nErrmsg = strlen(p->zErrmsg); + unsigned int i; + size_t nErrmsg = strlen(p->zErrmsg); for(i=0; i<(nErrmsg-8); i++){ if( memcmp(&p->zErrmsg[i], "rbu_imp_", 8)==0 ){ int nDel = 8; @@ -161992,9 +167639,19 @@ SQLITE_API int SQLITE_STDCALL sqlite3rbu_close(sqlite3rbu *p, char **pzErrmsg){ /* Close any open statement handles. */ rbuObjIterFinalize(&p->objiter); + /* If this is an RBU vacuum handle and the vacuum has either finished + ** successfully or encountered an error, delete the contents of the + ** state table. This causes the next call to sqlite3rbu_vacuum() + ** specifying the current target and state databases to start a new + ** vacuum from scratch. */ + if( rbuIsVacuum(p) && p->rc!=SQLITE_OK && p->dbRbu ){ + int rc2 = sqlite3_exec(p->dbRbu, "DELETE FROM stat.rbu_state", 0, 0, 0); + if( p->rc==SQLITE_DONE && rc2!=SQLITE_OK ) p->rc = rc2; + } + /* Close the open database handle and VFS object. */ - sqlite3_close(p->dbMain); sqlite3_close(p->dbRbu); + sqlite3_close(p->dbMain); rbuDeleteVfs(p); sqlite3_free(p->aBuf); sqlite3_free(p->aFrame); @@ -162019,6 +167676,42 @@ SQLITE_API sqlite3_int64 SQLITE_STDCALL sqlite3rbu_progress(sqlite3rbu *pRbu){ return pRbu->nProgress; } +/* +** Return permyriadage progress indications for the two main stages of +** an RBU update. +*/ +SQLITE_API void SQLITE_STDCALL sqlite3rbu_bp_progress(sqlite3rbu *p, int *pnOne, int *pnTwo){ + const int MAX_PROGRESS = 10000; + switch( p->eStage ){ + case RBU_STAGE_OAL: + if( p->nPhaseOneStep>0 ){ + *pnOne = (int)(MAX_PROGRESS * (i64)p->nProgress/(i64)p->nPhaseOneStep); + }else{ + *pnOne = -1; + } + *pnTwo = 0; + break; + + case RBU_STAGE_MOVE: + *pnOne = MAX_PROGRESS; + *pnTwo = 0; + break; + + case RBU_STAGE_CKPT: + *pnOne = MAX_PROGRESS; + *pnTwo = (int)(MAX_PROGRESS * (i64)p->nStep / (i64)p->nFrame); + break; + + case RBU_STAGE_DONE: + *pnOne = MAX_PROGRESS; + *pnTwo = MAX_PROGRESS; + break; + + default: + assert( 0 ); + } +} + SQLITE_API int SQLITE_STDCALL sqlite3rbu_savestate(sqlite3rbu *p){ int rc = p->rc; @@ -162160,6 +167853,22 @@ static u32 rbuGetU32(u8 *aBuf){ + ((u32)aBuf[3]); } +/* +** Write an unsigned 32-bit value in big-endian format to the supplied +** buffer. +*/ +static void rbuPutU32(u8 *aBuf, u32 iVal){ + aBuf[0] = (iVal >> 24) & 0xFF; + aBuf[1] = (iVal >> 16) & 0xFF; + aBuf[2] = (iVal >> 8) & 0xFF; + aBuf[3] = (iVal >> 0) & 0xFF; +} + +static void rbuPutU16(u8 *aBuf, u16 iVal){ + aBuf[0] = (iVal >> 8) & 0xFF; + aBuf[1] = (iVal >> 0) & 0xFF; +} + /* ** Read data from an rbuVfs-file. */ @@ -162185,6 +167894,35 @@ static int rbuVfsRead( memset(zBuf, 0, iAmt); }else{ rc = p->pReal->pMethods->xRead(p->pReal, zBuf, iAmt, iOfst); +#if 1 + /* If this is being called to read the first page of the target + ** database as part of an rbu vacuum operation, synthesize the + ** contents of the first page if it does not yet exist. Otherwise, + ** SQLite will not check for a *-wal file. */ + if( pRbu && rbuIsVacuum(pRbu) + && rc==SQLITE_IOERR_SHORT_READ && iOfst==0 + && (p->openFlags & SQLITE_OPEN_MAIN_DB) + && pRbu->rc==SQLITE_OK + ){ + sqlite3_file *pFd = (sqlite3_file*)pRbu->pRbuFd; + rc = pFd->pMethods->xRead(pFd, zBuf, iAmt, iOfst); + if( rc==SQLITE_OK ){ + u8 *aBuf = (u8*)zBuf; + u32 iRoot = rbuGetU32(&aBuf[52]) ? 1 : 0; + rbuPutU32(&aBuf[52], iRoot); /* largest root page number */ + rbuPutU32(&aBuf[36], 0); /* number of free pages */ + rbuPutU32(&aBuf[32], 0); /* first page on free list trunk */ + rbuPutU32(&aBuf[28], 1); /* size of db file in pages */ + rbuPutU32(&aBuf[24], pRbu->pRbuFd->iCookie+1); /* Change counter */ + + if( iAmt>100 ){ + memset(&aBuf[100], 0, iAmt-100); + rbuPutU16(&aBuf[105], iAmt & 0xFFFF); + aBuf[100] = 0x0D; + } + } + } +#endif } if( rc==SQLITE_OK && iOfst==0 && (p->openFlags & SQLITE_OPEN_MAIN_DB) ){ /* These look like magic numbers. But they are stable, as they are part @@ -162259,7 +167997,20 @@ static int rbuVfsSync(sqlite3_file *pFile, int flags){ */ static int rbuVfsFileSize(sqlite3_file *pFile, sqlite_int64 *pSize){ rbu_file *p = (rbu_file *)pFile; - return p->pReal->pMethods->xFileSize(p->pReal, pSize); + int rc; + rc = p->pReal->pMethods->xFileSize(p->pReal, pSize); + + /* If this is an RBU vacuum operation and this is the target database, + ** pretend that it has at least one page. Otherwise, SQLite will not + ** check for the existance of a *-wal file. rbuVfsRead() contains + ** similar logic. */ + if( rc==SQLITE_OK && *pSize==0 + && p->pRbu && rbuIsVacuum(p->pRbu) + && (p->openFlags & SQLITE_OPEN_MAIN_DB) + ){ + *pSize = 1024; + } + return rc; } /* @@ -162271,7 +168022,9 @@ static int rbuVfsLock(sqlite3_file *pFile, int eLock){ int rc = SQLITE_OK; assert( p->openFlags & (SQLITE_OPEN_MAIN_DB|SQLITE_OPEN_TEMP_DB) ); - if( pRbu && eLock==SQLITE_LOCK_EXCLUSIVE && pRbu->eStage!=RBU_STAGE_DONE ){ + if( eLock==SQLITE_LOCK_EXCLUSIVE + && (p->bNolock || (pRbu && pRbu->eStage!=RBU_STAGE_DONE)) + ){ /* Do not allow EXCLUSIVE locks. Preventing SQLite from taking this ** prevents it from checkpointing the database from sqlite3_close(). */ rc = SQLITE_BUSY; @@ -162334,6 +168087,12 @@ static int rbuVfsFileControl(sqlite3_file *pFile, int op, void *pArg){ } return rc; } + else if( op==SQLITE_FCNTL_RBUCNT ){ + sqlite3rbu *pRbu = (sqlite3rbu*)pArg; + pRbu->nRbu++; + pRbu->pRbuFd = p; + p->bNolock = 1; + } rc = xControl(p->pReal, op, pArg); if( rc==SQLITE_OK && op==SQLITE_FCNTL_VFSNAME ){ @@ -162423,7 +168182,7 @@ static int rbuVfsShmMap( if( eStage==RBU_STAGE_OAL || eStage==RBU_STAGE_MOVE ){ if( iRegion<=p->nShm ){ int nByte = (iRegion+1) * sizeof(char*); - char **apNew = (char**)sqlite3_realloc(p->apShm, nByte); + char **apNew = (char**)sqlite3_realloc64(p->apShm, nByte); if( apNew==0 ){ rc = SQLITE_NOMEM; }else{ @@ -162434,7 +168193,7 @@ static int rbuVfsShmMap( } if( rc==SQLITE_OK && p->apShm[iRegion]==0 ){ - char *pNew = (char*)sqlite3_malloc(szRegion); + char *pNew = (char*)sqlite3_malloc64(szRegion); if( pNew==0 ){ rc = SQLITE_NOMEM; }else{ @@ -162492,11 +168251,38 @@ static int rbuVfsShmUnmap(sqlite3_file *pFile, int delFlag){ static rbu_file *rbuFindMaindb(rbu_vfs *pRbuVfs, const char *zWal){ rbu_file *pDb; sqlite3_mutex_enter(pRbuVfs->mutex); - for(pDb=pRbuVfs->pMain; pDb && pDb->zWal!=zWal; pDb=pDb->pMainNext); + for(pDb=pRbuVfs->pMain; pDb && pDb->zWal!=zWal; pDb=pDb->pMainNext){} sqlite3_mutex_leave(pRbuVfs->mutex); return pDb; } +/* +** A main database named zName has just been opened. The following +** function returns a pointer to a buffer owned by SQLite that contains +** the name of the *-wal file this db connection will use. SQLite +** happens to pass a pointer to this buffer when using xAccess() +** or xOpen() to operate on the *-wal file. +*/ +static const char *rbuMainToWal(const char *zName, int flags){ + int n = (int)strlen(zName); + const char *z = &zName[n]; + if( flags & SQLITE_OPEN_URI ){ + int odd = 0; + while( 1 ){ + if( z[0]==0 ){ + odd = 1 - odd; + if( odd && z[1]==0 ) break; + } + z++; + } + z += 2; + }else{ + while( *z==0 ) z++; + } + z += (n + 8 + 1); + return z; +} + /* ** Open an rbu file handle. */ @@ -162532,6 +168318,7 @@ static int rbuVfsOpen( rbu_file *pFd = (rbu_file *)pFile; int rc = SQLITE_OK; const char *zOpen = zName; + int oflags = flags; memset(pFd, 0, sizeof(rbu_file)); pFd->pReal = (sqlite3_file*)&pFd[1]; @@ -162544,23 +168331,7 @@ static int rbuVfsOpen( ** the name of the *-wal file this db connection will use. SQLite ** happens to pass a pointer to this buffer when using xAccess() ** or xOpen() to operate on the *-wal file. */ - int n = strlen(zName); - const char *z = &zName[n]; - if( flags & SQLITE_OPEN_URI ){ - int odd = 0; - while( 1 ){ - if( z[0]==0 ){ - odd = 1 - odd; - if( odd && z[1]==0 ) break; - } - z++; - } - z += 2; - }else{ - while( *z==0 ) z++; - } - z += (n + 8 + 1); - pFd->zWal = z; + pFd->zWal = rbuMainToWal(zName, flags); } else if( flags & SQLITE_OPEN_WAL ){ rbu_file *pDb = rbuFindMaindb(pRbuVfs, zName); @@ -162570,10 +168341,17 @@ static int rbuVfsOpen( ** code ensures that the string passed to xOpen() is terminated by a ** pair of '\0' bytes in case the VFS attempts to extract a URI ** parameter from it. */ - int nCopy = strlen(zName); - char *zCopy = sqlite3_malloc(nCopy+2); + const char *zBase = zName; + size_t nCopy; + char *zCopy; + if( rbuIsVacuum(pDb->pRbu) ){ + zBase = sqlite3_db_filename(pDb->pRbu->dbRbu, "main"); + zBase = rbuMainToWal(zBase, SQLITE_OPEN_URI); + } + nCopy = strlen(zBase); + zCopy = sqlite3_malloc64(nCopy+2); if( zCopy ){ - memcpy(zCopy, zName, nCopy); + memcpy(zCopy, zBase, nCopy); zCopy[nCopy-3] = 'o'; zCopy[nCopy] = '\0'; zCopy[nCopy+1] = '\0'; @@ -162588,8 +168366,17 @@ static int rbuVfsOpen( } } + if( oflags & SQLITE_OPEN_MAIN_DB + && sqlite3_uri_boolean(zName, "rbu_memory", 0) + ){ + assert( oflags & SQLITE_OPEN_MAIN_DB ); + oflags = SQLITE_OPEN_TEMP_DB | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | + SQLITE_OPEN_EXCLUSIVE | SQLITE_OPEN_DELETEONCLOSE; + zOpen = 0; + } + if( rc==SQLITE_OK ){ - rc = pRealVfs->xOpen(pRealVfs, zOpen, pFd->pReal, flags, pOutFlags); + rc = pRealVfs->xOpen(pRealVfs, zOpen, pFd->pReal, oflags, pOutFlags); } if( pFd->pReal->pMethods ){ /* The xOpen() operation has succeeded. Set the sqlite3_file.pMethods @@ -162800,13 +168587,13 @@ SQLITE_API int SQLITE_STDCALL sqlite3rbu_create_vfs(const char *zName, const cha }; rbu_vfs *pNew = 0; /* Newly allocated VFS */ - int nName; int rc = SQLITE_OK; + size_t nName; + size_t nByte; - int nByte; nName = strlen(zName); nByte = sizeof(rbu_vfs) + nName + 1; - pNew = (rbu_vfs*)sqlite3_malloc(nByte); + pNew = (rbu_vfs*)sqlite3_malloc64(nByte); if( pNew==0 ){ rc = SQLITE_NOMEM; }else{ @@ -163000,7 +168787,9 @@ static int statConnect( int iDb; if( argc>=4 ){ - iDb = sqlite3FindDbName(db, argv[3]); + Token nm; + sqlite3TokenInit(&nm, (char*)argv[3]); + iDb = sqlite3FindDb(db, &nm); if( iDb<0 ){ *pzErr = sqlite3_mprintf("no such database: %s", argv[3]); return SQLITE_ERROR; @@ -163011,7 +168800,7 @@ static int statConnect( rc = sqlite3_declare_vtab(db, VTAB_SCHEMA); if( rc==SQLITE_OK ){ pTab = (StatTable *)sqlite3_malloc64(sizeof(StatTable)); - if( pTab==0 ) rc = SQLITE_NOMEM; + if( pTab==0 ) rc = SQLITE_NOMEM_BKPT; } assert( rc==SQLITE_OK || pTab==0 ); @@ -163092,7 +168881,7 @@ static int statOpen(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor){ pCsr = (StatCursor *)sqlite3_malloc64(sizeof(StatCursor)); if( pCsr==0 ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; }else{ memset(pCsr, 0, sizeof(StatCursor)); pCsr->base.pVtab = pVTab; @@ -163198,7 +168987,7 @@ static int statDecodePage(Btree *pBt, StatPage *p){ nUsable = szPage - sqlite3BtreeGetReserveNoMutex(pBt); sqlite3BtreeLeave(pBt); p->aCell = sqlite3_malloc64((p->nCell+1) * sizeof(StatCell)); - if( p->aCell==0 ) return SQLITE_NOMEM; + if( p->aCell==0 ) return SQLITE_NOMEM_BKPT; memset(p->aCell, 0, (p->nCell+1) * sizeof(StatCell)); for(i=0; inCell; i++){ @@ -163231,13 +169020,13 @@ static int statDecodePage(Btree *pBt, StatPage *p){ pCell->nLastOvfl = (nPayload-nLocal) - (nOvfl-1) * (nUsable-4); pCell->nOvfl = nOvfl; pCell->aOvfl = sqlite3_malloc64(sizeof(u32)*nOvfl); - if( pCell->aOvfl==0 ) return SQLITE_NOMEM; + if( pCell->aOvfl==0 ) return SQLITE_NOMEM_BKPT; pCell->aOvfl[0] = sqlite3Get4byte(&aData[iOff+nLocal]); for(j=1; jaOvfl[j-1]; DbPage *pPg = 0; - rc = sqlite3PagerGet(sqlite3BtreePager(pBt), iPrev, &pPg); + rc = sqlite3PagerGet(sqlite3BtreePager(pBt), iPrev, &pPg, 0); if( rc!=SQLITE_OK ){ assert( pPg==0 ); return rc; @@ -163305,12 +169094,12 @@ statNextRestart: pCsr->isEof = 1; return sqlite3_reset(pCsr->pStmt); } - rc = sqlite3PagerGet(pPager, iRoot, &pCsr->aPage[0].pPg); + rc = sqlite3PagerGet(pPager, iRoot, &pCsr->aPage[0].pPg, 0); pCsr->aPage[0].iPgno = iRoot; pCsr->aPage[0].iCell = 0; pCsr->aPage[0].zPath = z = sqlite3_mprintf("/"); pCsr->iPage = 0; - if( z==0 ) rc = SQLITE_NOMEM; + if( z==0 ) rc = SQLITE_NOMEM_BKPT; }else{ pCsr->isEof = 1; return sqlite3_reset(pCsr->pStmt); @@ -163345,7 +169134,7 @@ statNextRestart: } pCell->iOvfl++; statSizeAndOffset(pCsr); - return z==0 ? SQLITE_NOMEM : SQLITE_OK; + return z==0 ? SQLITE_NOMEM_BKPT : SQLITE_OK; } if( p->iRightChildPg ) break; p->iCell++; @@ -163365,11 +169154,11 @@ statNextRestart: }else{ p[1].iPgno = p->aCell[p->iCell].iChildPg; } - rc = sqlite3PagerGet(pPager, p[1].iPgno, &p[1].pPg); + rc = sqlite3PagerGet(pPager, p[1].iPgno, &p[1].pPg, 0); p[1].iCell = 0; p[1].zPath = z = sqlite3_mprintf("%s%.3x/", p->zPath, p->iCell); p->iCell++; - if( z==0 ) rc = SQLITE_NOMEM; + if( z==0 ) rc = SQLITE_NOMEM_BKPT; } @@ -163403,7 +169192,7 @@ statNextRestart: pCsr->nUnused = p->nUnused; pCsr->nMxPayload = p->nMxPayload; pCsr->zPath = z = sqlite3_mprintf("%s", p->zPath); - if( z==0 ) rc = SQLITE_NOMEM; + if( z==0 ) rc = SQLITE_NOMEM_BKPT; nPayload = 0; for(i=0; inCell; i++){ nPayload += p->aCell[i].nLocal; @@ -163437,7 +169226,7 @@ static int statFilter( if( pCsr->iDb<0 ){ sqlite3_free(pCursor->pVtab->zErrMsg); pCursor->pVtab->zErrMsg = sqlite3_mprintf("no such schema: %s", zDbase); - return pCursor->pVtab->zErrMsg ? SQLITE_ERROR : SQLITE_NOMEM; + return pCursor->pVtab->zErrMsg ? SQLITE_ERROR : SQLITE_NOMEM_BKPT; } }else{ pCsr->iDb = pTab->iDb; @@ -163453,7 +169242,7 @@ static int statFilter( " FROM \"%w\".%s WHERE rootpage!=0" " ORDER BY name", pTab->db->aDb[pCsr->iDb].zName, zMaster); if( zSql==0 ){ - return SQLITE_NOMEM; + return SQLITE_NOMEM_BKPT; }else{ rc = sqlite3_prepare_v2(pTab->db, zSql, -1, &pCsr->pStmt, 0); sqlite3_free(zSql); @@ -163551,6 +169340,4650 @@ SQLITE_PRIVATE int sqlite3DbstatRegister(sqlite3 *db){ return SQLITE_OK; } #endif /* SQLITE_ENABLE_DBSTAT_VTAB */ /************** End of dbstat.c **********************************************/ +/************** Begin file sqlite3session.c **********************************/ + +#if defined(SQLITE_ENABLE_SESSION) && defined(SQLITE_ENABLE_PREUPDATE_HOOK) +/* #include "sqlite3session.h" */ +/* #include */ +/* #include */ + +#ifndef SQLITE_AMALGAMATION +/* # include "sqliteInt.h" */ +/* # include "vdbeInt.h" */ +#endif + +typedef struct SessionTable SessionTable; +typedef struct SessionChange SessionChange; +typedef struct SessionBuffer SessionBuffer; +typedef struct SessionInput SessionInput; + +/* +** Minimum chunk size used by streaming versions of functions. +*/ +#ifndef SESSIONS_STRM_CHUNK_SIZE +# ifdef SQLITE_TEST +# define SESSIONS_STRM_CHUNK_SIZE 64 +# else +# define SESSIONS_STRM_CHUNK_SIZE 1024 +# endif +#endif + +typedef struct SessionHook SessionHook; +struct SessionHook { + void *pCtx; + int (*xOld)(void*,int,sqlite3_value**); + int (*xNew)(void*,int,sqlite3_value**); + int (*xCount)(void*); + int (*xDepth)(void*); +}; + +/* +** Session handle structure. +*/ +struct sqlite3_session { + sqlite3 *db; /* Database handle session is attached to */ + char *zDb; /* Name of database session is attached to */ + int bEnable; /* True if currently recording */ + int bIndirect; /* True if all changes are indirect */ + int bAutoAttach; /* True to auto-attach tables */ + int rc; /* Non-zero if an error has occurred */ + void *pFilterCtx; /* First argument to pass to xTableFilter */ + int (*xTableFilter)(void *pCtx, const char *zTab); + sqlite3_session *pNext; /* Next session object on same db. */ + SessionTable *pTable; /* List of attached tables */ + SessionHook hook; /* APIs to grab new and old data with */ +}; + +/* +** Instances of this structure are used to build strings or binary records. +*/ +struct SessionBuffer { + u8 *aBuf; /* Pointer to changeset buffer */ + int nBuf; /* Size of buffer aBuf */ + int nAlloc; /* Size of allocation containing aBuf */ +}; + +/* +** An object of this type is used internally as an abstraction for +** input data. Input data may be supplied either as a single large buffer +** (e.g. sqlite3changeset_start()) or using a stream function (e.g. +** sqlite3changeset_start_strm()). +*/ +struct SessionInput { + int bNoDiscard; /* If true, discard no data */ + int iCurrent; /* Offset in aData[] of current change */ + int iNext; /* Offset in aData[] of next change */ + u8 *aData; /* Pointer to buffer containing changeset */ + int nData; /* Number of bytes in aData */ + + SessionBuffer buf; /* Current read buffer */ + int (*xInput)(void*, void*, int*); /* Input stream call (or NULL) */ + void *pIn; /* First argument to xInput */ + int bEof; /* Set to true after xInput finished */ +}; + +/* +** Structure for changeset iterators. +*/ +struct sqlite3_changeset_iter { + SessionInput in; /* Input buffer or stream */ + SessionBuffer tblhdr; /* Buffer to hold apValue/zTab/abPK/ */ + int bPatchset; /* True if this is a patchset */ + int rc; /* Iterator error code */ + sqlite3_stmt *pConflict; /* Points to conflicting row, if any */ + char *zTab; /* Current table */ + int nCol; /* Number of columns in zTab */ + int op; /* Current operation */ + int bIndirect; /* True if current change was indirect */ + u8 *abPK; /* Primary key array */ + sqlite3_value **apValue; /* old.* and new.* values */ +}; + +/* +** Each session object maintains a set of the following structures, one +** for each table the session object is monitoring. The structures are +** stored in a linked list starting at sqlite3_session.pTable. +** +** The keys of the SessionTable.aChange[] hash table are all rows that have +** been modified in any way since the session object was attached to the +** table. +** +** The data associated with each hash-table entry is a structure containing +** a subset of the initial values that the modified row contained at the +** start of the session. Or no initial values if the row was inserted. +*/ +struct SessionTable { + SessionTable *pNext; + char *zName; /* Local name of table */ + int nCol; /* Number of columns in table zName */ + const char **azCol; /* Column names */ + u8 *abPK; /* Array of primary key flags */ + int nEntry; /* Total number of entries in hash table */ + int nChange; /* Size of apChange[] array */ + SessionChange **apChange; /* Hash table buckets */ +}; + +/* +** RECORD FORMAT: +** +** The following record format is similar to (but not compatible with) that +** used in SQLite database files. This format is used as part of the +** change-set binary format, and so must be architecture independent. +** +** Unlike the SQLite database record format, each field is self-contained - +** there is no separation of header and data. Each field begins with a +** single byte describing its type, as follows: +** +** 0x00: Undefined value. +** 0x01: Integer value. +** 0x02: Real value. +** 0x03: Text value. +** 0x04: Blob value. +** 0x05: SQL NULL value. +** +** Note that the above match the definitions of SQLITE_INTEGER, SQLITE_TEXT +** and so on in sqlite3.h. For undefined and NULL values, the field consists +** only of the single type byte. For other types of values, the type byte +** is followed by: +** +** Text values: +** A varint containing the number of bytes in the value (encoded using +** UTF-8). Followed by a buffer containing the UTF-8 representation +** of the text value. There is no nul terminator. +** +** Blob values: +** A varint containing the number of bytes in the value, followed by +** a buffer containing the value itself. +** +** Integer values: +** An 8-byte big-endian integer value. +** +** Real values: +** An 8-byte big-endian IEEE 754-2008 real value. +** +** Varint values are encoded in the same way as varints in the SQLite +** record format. +** +** CHANGESET FORMAT: +** +** A changeset is a collection of DELETE, UPDATE and INSERT operations on +** one or more tables. Operations on a single table are grouped together, +** but may occur in any order (i.e. deletes, updates and inserts are all +** mixed together). +** +** Each group of changes begins with a table header: +** +** 1 byte: Constant 0x54 (capital 'T') +** Varint: Number of columns in the table. +** nCol bytes: 0x01 for PK columns, 0x00 otherwise. +** N bytes: Unqualified table name (encoded using UTF-8). Nul-terminated. +** +** Followed by one or more changes to the table. +** +** 1 byte: Either SQLITE_INSERT (0x12), UPDATE (0x17) or DELETE (0x09). +** 1 byte: The "indirect-change" flag. +** old.* record: (delete and update only) +** new.* record: (insert and update only) +** +** The "old.*" and "new.*" records, if present, are N field records in the +** format described above under "RECORD FORMAT", where N is the number of +** columns in the table. The i'th field of each record is associated with +** the i'th column of the table, counting from left to right in the order +** in which columns were declared in the CREATE TABLE statement. +** +** The new.* record that is part of each INSERT change contains the values +** that make up the new row. Similarly, the old.* record that is part of each +** DELETE change contains the values that made up the row that was deleted +** from the database. In the changeset format, the records that are part +** of INSERT or DELETE changes never contain any undefined (type byte 0x00) +** fields. +** +** Within the old.* record associated with an UPDATE change, all fields +** associated with table columns that are not PRIMARY KEY columns and are +** not modified by the UPDATE change are set to "undefined". Other fields +** are set to the values that made up the row before the UPDATE that the +** change records took place. Within the new.* record, fields associated +** with table columns modified by the UPDATE change contain the new +** values. Fields associated with table columns that are not modified +** are set to "undefined". +** +** PATCHSET FORMAT: +** +** A patchset is also a collection of changes. It is similar to a changeset, +** but leaves undefined those fields that are not useful if no conflict +** resolution is required when applying the changeset. +** +** Each group of changes begins with a table header: +** +** 1 byte: Constant 0x50 (capital 'P') +** Varint: Number of columns in the table. +** nCol bytes: 0x01 for PK columns, 0x00 otherwise. +** N bytes: Unqualified table name (encoded using UTF-8). Nul-terminated. +** +** Followed by one or more changes to the table. +** +** 1 byte: Either SQLITE_INSERT (0x12), UPDATE (0x17) or DELETE (0x09). +** 1 byte: The "indirect-change" flag. +** single record: (PK fields for DELETE, PK and modified fields for UPDATE, +** full record for INSERT). +** +** As in the changeset format, each field of the single record that is part +** of a patchset change is associated with the correspondingly positioned +** table column, counting from left to right within the CREATE TABLE +** statement. +** +** For a DELETE change, all fields within the record except those associated +** with PRIMARY KEY columns are set to "undefined". The PRIMARY KEY fields +** contain the values identifying the row to delete. +** +** For an UPDATE change, all fields except those associated with PRIMARY KEY +** columns and columns that are modified by the UPDATE are set to "undefined". +** PRIMARY KEY fields contain the values identifying the table row to update, +** and fields associated with modified columns contain the new column values. +** +** The records associated with INSERT changes are in the same format as for +** changesets. It is not possible for a record associated with an INSERT +** change to contain a field set to "undefined". +*/ + +/* +** For each row modified during a session, there exists a single instance of +** this structure stored in a SessionTable.aChange[] hash table. +*/ +struct SessionChange { + int op; /* One of UPDATE, DELETE, INSERT */ + int bIndirect; /* True if this change is "indirect" */ + int nRecord; /* Number of bytes in buffer aRecord[] */ + u8 *aRecord; /* Buffer containing old.* record */ + SessionChange *pNext; /* For hash-table collisions */ +}; + +/* +** Write a varint with value iVal into the buffer at aBuf. Return the +** number of bytes written. +*/ +static int sessionVarintPut(u8 *aBuf, int iVal){ + return putVarint32(aBuf, iVal); +} + +/* +** Return the number of bytes required to store value iVal as a varint. +*/ +static int sessionVarintLen(int iVal){ + return sqlite3VarintLen(iVal); +} + +/* +** Read a varint value from aBuf[] into *piVal. Return the number of +** bytes read. +*/ +static int sessionVarintGet(u8 *aBuf, int *piVal){ + return getVarint32(aBuf, *piVal); +} + +/* Load an unaligned and unsigned 32-bit integer */ +#define SESSION_UINT32(x) (((u32)(x)[0]<<24)|((x)[1]<<16)|((x)[2]<<8)|(x)[3]) + +/* +** Read a 64-bit big-endian integer value from buffer aRec[]. Return +** the value read. +*/ +static sqlite3_int64 sessionGetI64(u8 *aRec){ + u64 x = SESSION_UINT32(aRec); + u32 y = SESSION_UINT32(aRec+4); + x = (x<<32) + y; + return (sqlite3_int64)x; +} + +/* +** Write a 64-bit big-endian integer value to the buffer aBuf[]. +*/ +static void sessionPutI64(u8 *aBuf, sqlite3_int64 i){ + aBuf[0] = (i>>56) & 0xFF; + aBuf[1] = (i>>48) & 0xFF; + aBuf[2] = (i>>40) & 0xFF; + aBuf[3] = (i>>32) & 0xFF; + aBuf[4] = (i>>24) & 0xFF; + aBuf[5] = (i>>16) & 0xFF; + aBuf[6] = (i>> 8) & 0xFF; + aBuf[7] = (i>> 0) & 0xFF; +} + +/* +** This function is used to serialize the contents of value pValue (see +** comment titled "RECORD FORMAT" above). +** +** If it is non-NULL, the serialized form of the value is written to +** buffer aBuf. *pnWrite is set to the number of bytes written before +** returning. Or, if aBuf is NULL, the only thing this function does is +** set *pnWrite. +** +** If no error occurs, SQLITE_OK is returned. Or, if an OOM error occurs +** within a call to sqlite3_value_text() (may fail if the db is utf-16)) +** SQLITE_NOMEM is returned. +*/ +static int sessionSerializeValue( + u8 *aBuf, /* If non-NULL, write serialized value here */ + sqlite3_value *pValue, /* Value to serialize */ + int *pnWrite /* IN/OUT: Increment by bytes written */ +){ + int nByte; /* Size of serialized value in bytes */ + + if( pValue ){ + int eType; /* Value type (SQLITE_NULL, TEXT etc.) */ + + eType = sqlite3_value_type(pValue); + if( aBuf ) aBuf[0] = eType; + + switch( eType ){ + case SQLITE_NULL: + nByte = 1; + break; + + case SQLITE_INTEGER: + case SQLITE_FLOAT: + if( aBuf ){ + /* TODO: SQLite does something special to deal with mixed-endian + ** floating point values (e.g. ARM7). This code probably should + ** too. */ + u64 i; + if( eType==SQLITE_INTEGER ){ + i = (u64)sqlite3_value_int64(pValue); + }else{ + double r; + assert( sizeof(double)==8 && sizeof(u64)==8 ); + r = sqlite3_value_double(pValue); + memcpy(&i, &r, 8); + } + sessionPutI64(&aBuf[1], i); + } + nByte = 9; + break; + + default: { + u8 *z; + int n; + int nVarint; + + assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB ); + if( eType==SQLITE_TEXT ){ + z = (u8 *)sqlite3_value_text(pValue); + }else{ + z = (u8 *)sqlite3_value_blob(pValue); + } + n = sqlite3_value_bytes(pValue); + if( z==0 && (eType!=SQLITE_BLOB || n>0) ) return SQLITE_NOMEM; + nVarint = sessionVarintLen(n); + + if( aBuf ){ + sessionVarintPut(&aBuf[1], n); + memcpy(&aBuf[nVarint + 1], eType==SQLITE_TEXT ? + sqlite3_value_text(pValue) : sqlite3_value_blob(pValue), n + ); + } + + nByte = 1 + nVarint + n; + break; + } + } + }else{ + nByte = 1; + if( aBuf ) aBuf[0] = '\0'; + } + + if( pnWrite ) *pnWrite += nByte; + return SQLITE_OK; +} + + +/* +** This macro is used to calculate hash key values for data structures. In +** order to use this macro, the entire data structure must be represented +** as a series of unsigned integers. In order to calculate a hash-key value +** for a data structure represented as three such integers, the macro may +** then be used as follows: +** +** int hash_key_value; +** hash_key_value = HASH_APPEND(0, ); +** hash_key_value = HASH_APPEND(hash_key_value, ); +** hash_key_value = HASH_APPEND(hash_key_value, ); +** +** In practice, the data structures this macro is used for are the primary +** key values of modified rows. +*/ +#define HASH_APPEND(hash, add) ((hash) << 3) ^ (hash) ^ (unsigned int)(add) + +/* +** Append the hash of the 64-bit integer passed as the second argument to the +** hash-key value passed as the first. Return the new hash-key value. +*/ +static unsigned int sessionHashAppendI64(unsigned int h, i64 i){ + h = HASH_APPEND(h, i & 0xFFFFFFFF); + return HASH_APPEND(h, (i>>32)&0xFFFFFFFF); +} + +/* +** Append the hash of the blob passed via the second and third arguments to +** the hash-key value passed as the first. Return the new hash-key value. +*/ +static unsigned int sessionHashAppendBlob(unsigned int h, int n, const u8 *z){ + int i; + for(i=0; inCol==pSession->hook.xCount(pSession->hook.pCtx) ); + for(i=0; inCol; i++){ + if( pTab->abPK[i] ){ + int rc; + int eType; + sqlite3_value *pVal; + + if( bNew ){ + rc = pSession->hook.xNew(pSession->hook.pCtx, i, &pVal); + }else{ + rc = pSession->hook.xOld(pSession->hook.pCtx, i, &pVal); + } + if( rc!=SQLITE_OK ) return rc; + + eType = sqlite3_value_type(pVal); + h = sessionHashAppendType(h, eType); + if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + i64 iVal; + if( eType==SQLITE_INTEGER ){ + iVal = sqlite3_value_int64(pVal); + }else{ + double rVal = sqlite3_value_double(pVal); + assert( sizeof(iVal)==8 && sizeof(rVal)==8 ); + memcpy(&iVal, &rVal, 8); + } + h = sessionHashAppendI64(h, iVal); + }else if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){ + const u8 *z; + int n; + if( eType==SQLITE_TEXT ){ + z = (const u8 *)sqlite3_value_text(pVal); + }else{ + z = (const u8 *)sqlite3_value_blob(pVal); + } + n = sqlite3_value_bytes(pVal); + if( !z && (eType!=SQLITE_BLOB || n>0) ) return SQLITE_NOMEM; + h = sessionHashAppendBlob(h, n, z); + }else{ + assert( eType==SQLITE_NULL ); + *pbNullPK = 1; + } + } + } + + *piHash = (h % pTab->nChange); + return SQLITE_OK; +} + +/* +** The buffer that the argument points to contains a serialized SQL value. +** Return the number of bytes of space occupied by the value (including +** the type byte). +*/ +static int sessionSerialLen(u8 *a){ + int e = *a; + int n; + if( e==0 ) return 1; + if( e==SQLITE_NULL ) return 1; + if( e==SQLITE_INTEGER || e==SQLITE_FLOAT ) return 9; + return sessionVarintGet(&a[1], &n) + 1 + n; +} + +/* +** Based on the primary key values stored in change aRecord, calculate a +** hash key. Assume the has table has nBucket buckets. The hash keys +** calculated by this function are compatible with those calculated by +** sessionPreupdateHash(). +** +** The bPkOnly argument is non-zero if the record at aRecord[] is from +** a patchset DELETE. In this case the non-PK fields are omitted entirely. +*/ +static unsigned int sessionChangeHash( + SessionTable *pTab, /* Table handle */ + int bPkOnly, /* Record consists of PK fields only */ + u8 *aRecord, /* Change record */ + int nBucket /* Assume this many buckets in hash table */ +){ + unsigned int h = 0; /* Value to return */ + int i; /* Used to iterate through columns */ + u8 *a = aRecord; /* Used to iterate through change record */ + + for(i=0; inCol; i++){ + int eType = *a; + int isPK = pTab->abPK[i]; + if( bPkOnly && isPK==0 ) continue; + + /* It is not possible for eType to be SQLITE_NULL here. The session + ** module does not record changes for rows with NULL values stored in + ** primary key columns. */ + assert( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT + || eType==SQLITE_TEXT || eType==SQLITE_BLOB + || eType==SQLITE_NULL || eType==0 + ); + assert( !isPK || (eType!=0 && eType!=SQLITE_NULL) ); + + if( isPK ){ + a++; + h = sessionHashAppendType(h, eType); + if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + h = sessionHashAppendI64(h, sessionGetI64(a)); + a += 8; + }else{ + int n; + a += sessionVarintGet(a, &n); + h = sessionHashAppendBlob(h, n, a); + a += n; + } + }else{ + a += sessionSerialLen(a); + } + } + return (h % nBucket); +} + +/* +** Arguments aLeft and aRight are pointers to change records for table pTab. +** This function returns true if the two records apply to the same row (i.e. +** have the same values stored in the primary key columns), or false +** otherwise. +*/ +static int sessionChangeEqual( + SessionTable *pTab, /* Table used for PK definition */ + int bLeftPkOnly, /* True if aLeft[] contains PK fields only */ + u8 *aLeft, /* Change record */ + int bRightPkOnly, /* True if aRight[] contains PK fields only */ + u8 *aRight /* Change record */ +){ + u8 *a1 = aLeft; /* Cursor to iterate through aLeft */ + u8 *a2 = aRight; /* Cursor to iterate through aRight */ + int iCol; /* Used to iterate through table columns */ + + for(iCol=0; iColnCol; iCol++){ + if( pTab->abPK[iCol] ){ + int n1 = sessionSerialLen(a1); + int n2 = sessionSerialLen(a2); + + if( pTab->abPK[iCol] && (n1!=n2 || memcmp(a1, a2, n1)) ){ + return 0; + } + a1 += n1; + a2 += n2; + }else{ + if( bLeftPkOnly==0 ) a1 += sessionSerialLen(a1); + if( bRightPkOnly==0 ) a2 += sessionSerialLen(a2); + } + } + + return 1; +} + +/* +** Arguments aLeft and aRight both point to buffers containing change +** records with nCol columns. This function "merges" the two records into +** a single records which is written to the buffer at *paOut. *paOut is +** then set to point to one byte after the last byte written before +** returning. +** +** The merging of records is done as follows: For each column, if the +** aRight record contains a value for the column, copy the value from +** their. Otherwise, if aLeft contains a value, copy it. If neither +** record contains a value for a given column, then neither does the +** output record. +*/ +static void sessionMergeRecord( + u8 **paOut, + int nCol, + u8 *aLeft, + u8 *aRight +){ + u8 *a1 = aLeft; /* Cursor used to iterate through aLeft */ + u8 *a2 = aRight; /* Cursor used to iterate through aRight */ + u8 *aOut = *paOut; /* Output cursor */ + int iCol; /* Used to iterate from 0 to nCol */ + + for(iCol=0; iColnCol; i++){ + int nOld; + u8 *aOld; + int nNew; + u8 *aNew; + + aOld = sessionMergeValue(&aOld1, &aOld2, &nOld); + aNew = sessionMergeValue(&aNew1, &aNew2, &nNew); + if( pTab->abPK[i] || nOld!=nNew || memcmp(aOld, aNew, nNew) ){ + if( pTab->abPK[i]==0 ) bRequired = 1; + memcpy(aOut, aOld, nOld); + aOut += nOld; + }else{ + *(aOut++) = '\0'; + } + } + + if( !bRequired ) return 0; + } + + /* Write the new.* vector */ + aOld1 = aOldRecord1; + aOld2 = aOldRecord2; + aNew1 = aNewRecord1; + aNew2 = aNewRecord2; + for(i=0; inCol; i++){ + int nOld; + u8 *aOld; + int nNew; + u8 *aNew; + + aOld = sessionMergeValue(&aOld1, &aOld2, &nOld); + aNew = sessionMergeValue(&aNew1, &aNew2, &nNew); + if( bPatchset==0 + && (pTab->abPK[i] || (nOld==nNew && 0==memcmp(aOld, aNew, nNew))) + ){ + *(aOut++) = '\0'; + }else{ + memcpy(aOut, aNew, nNew); + aOut += nNew; + } + } + + *paOut = aOut; + return 1; +} + +/* +** This function is only called from within a pre-update-hook callback. +** It determines if the current pre-update-hook change affects the same row +** as the change stored in argument pChange. If so, it returns true. Otherwise +** if the pre-update-hook does not affect the same row as pChange, it returns +** false. +*/ +static int sessionPreupdateEqual( + sqlite3_session *pSession, /* Session object that owns SessionTable */ + SessionTable *pTab, /* Table associated with change */ + SessionChange *pChange, /* Change to compare to */ + int op /* Current pre-update operation */ +){ + int iCol; /* Used to iterate through columns */ + u8 *a = pChange->aRecord; /* Cursor used to scan change record */ + + assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE ); + for(iCol=0; iColnCol; iCol++){ + if( !pTab->abPK[iCol] ){ + a += sessionSerialLen(a); + }else{ + sqlite3_value *pVal; /* Value returned by preupdate_new/old */ + int rc; /* Error code from preupdate_new/old */ + int eType = *a++; /* Type of value from change record */ + + /* The following calls to preupdate_new() and preupdate_old() can not + ** fail. This is because they cache their return values, and by the + ** time control flows to here they have already been called once from + ** within sessionPreupdateHash(). The first two asserts below verify + ** this (that the method has already been called). */ + if( op==SQLITE_INSERT ){ + /* assert( db->pPreUpdate->pNewUnpacked || db->pPreUpdate->aNew ); */ + rc = pSession->hook.xNew(pSession->hook.pCtx, iCol, &pVal); + }else{ + /* assert( db->pPreUpdate->pUnpacked ); */ + rc = pSession->hook.xOld(pSession->hook.pCtx, iCol, &pVal); + } + assert( rc==SQLITE_OK ); + if( sqlite3_value_type(pVal)!=eType ) return 0; + + /* A SessionChange object never has a NULL value in a PK column */ + assert( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT + || eType==SQLITE_BLOB || eType==SQLITE_TEXT + ); + + if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + i64 iVal = sessionGetI64(a); + a += 8; + if( eType==SQLITE_INTEGER ){ + if( sqlite3_value_int64(pVal)!=iVal ) return 0; + }else{ + double rVal; + assert( sizeof(iVal)==8 && sizeof(rVal)==8 ); + memcpy(&rVal, &iVal, 8); + if( sqlite3_value_double(pVal)!=rVal ) return 0; + } + }else{ + int n; + const u8 *z; + a += sessionVarintGet(a, &n); + if( sqlite3_value_bytes(pVal)!=n ) return 0; + if( eType==SQLITE_TEXT ){ + z = sqlite3_value_text(pVal); + }else{ + z = sqlite3_value_blob(pVal); + } + if( memcmp(a, z, n) ) return 0; + a += n; + break; + } + } + } + + return 1; +} + +/* +** If required, grow the hash table used to store changes on table pTab +** (part of the session pSession). If a fatal OOM error occurs, set the +** session object to failed and return SQLITE_ERROR. Otherwise, return +** SQLITE_OK. +** +** It is possible that a non-fatal OOM error occurs in this function. In +** that case the hash-table does not grow, but SQLITE_OK is returned anyway. +** Growing the hash table in this case is a performance optimization only, +** it is not required for correct operation. +*/ +static int sessionGrowHash(int bPatchset, SessionTable *pTab){ + if( pTab->nChange==0 || pTab->nEntry>=(pTab->nChange/2) ){ + int i; + SessionChange **apNew; + int nNew = (pTab->nChange ? pTab->nChange : 128) * 2; + + apNew = (SessionChange **)sqlite3_malloc(sizeof(SessionChange *) * nNew); + if( apNew==0 ){ + if( pTab->nChange==0 ){ + return SQLITE_ERROR; + } + return SQLITE_OK; + } + memset(apNew, 0, sizeof(SessionChange *) * nNew); + + for(i=0; inChange; i++){ + SessionChange *p; + SessionChange *pNext; + for(p=pTab->apChange[i]; p; p=pNext){ + int bPkOnly = (p->op==SQLITE_DELETE && bPatchset); + int iHash = sessionChangeHash(pTab, bPkOnly, p->aRecord, nNew); + pNext = p->pNext; + p->pNext = apNew[iHash]; + apNew[iHash] = p; + } + } + + sqlite3_free(pTab->apChange); + pTab->nChange = nNew; + pTab->apChange = apNew; + } + + return SQLITE_OK; +} + +/* +** This function queries the database for the names of the columns of table +** zThis, in schema zDb. It is expected that the table has nCol columns. If +** not, SQLITE_SCHEMA is returned and none of the output variables are +** populated. +** +** Otherwise, if they are not NULL, variable *pnCol is set to the number +** of columns in the database table and variable *pzTab is set to point to a +** nul-terminated copy of the table name. *pazCol (if not NULL) is set to +** point to an array of pointers to column names. And *pabPK (again, if not +** NULL) is set to point to an array of booleans - true if the corresponding +** column is part of the primary key. +** +** For example, if the table is declared as: +** +** CREATE TABLE tbl1(w, x, y, z, PRIMARY KEY(w, z)); +** +** Then the four output variables are populated as follows: +** +** *pnCol = 4 +** *pzTab = "tbl1" +** *pazCol = {"w", "x", "y", "z"} +** *pabPK = {1, 0, 0, 1} +** +** All returned buffers are part of the same single allocation, which must +** be freed using sqlite3_free() by the caller. If pazCol was not NULL, then +** pointer *pazCol should be freed to release all memory. Otherwise, pointer +** *pabPK. It is illegal for both pazCol and pabPK to be NULL. +*/ +static int sessionTableInfo( + sqlite3 *db, /* Database connection */ + const char *zDb, /* Name of attached database (e.g. "main") */ + const char *zThis, /* Table name */ + int *pnCol, /* OUT: number of columns */ + const char **pzTab, /* OUT: Copy of zThis */ + const char ***pazCol, /* OUT: Array of column names for table */ + u8 **pabPK /* OUT: Array of booleans - true for PK col */ +){ + char *zPragma; + sqlite3_stmt *pStmt; + int rc; + int nByte; + int nDbCol = 0; + int nThis; + int i; + u8 *pAlloc = 0; + char **azCol = 0; + u8 *abPK = 0; + + assert( pazCol && pabPK ); + + nThis = sqlite3Strlen30(zThis); + zPragma = sqlite3_mprintf("PRAGMA '%q'.table_info('%q')", zDb, zThis); + if( !zPragma ) return SQLITE_NOMEM; + + rc = sqlite3_prepare_v2(db, zPragma, -1, &pStmt, 0); + sqlite3_free(zPragma); + if( rc!=SQLITE_OK ) return rc; + + nByte = nThis + 1; + while( SQLITE_ROW==sqlite3_step(pStmt) ){ + nByte += sqlite3_column_bytes(pStmt, 1); + nDbCol++; + } + rc = sqlite3_reset(pStmt); + + if( rc==SQLITE_OK ){ + nByte += nDbCol * (sizeof(const char *) + sizeof(u8) + 1); + pAlloc = sqlite3_malloc(nByte); + if( pAlloc==0 ){ + rc = SQLITE_NOMEM; + } + } + if( rc==SQLITE_OK ){ + azCol = (char **)pAlloc; + pAlloc = (u8 *)&azCol[nDbCol]; + abPK = (u8 *)pAlloc; + pAlloc = &abPK[nDbCol]; + if( pzTab ){ + memcpy(pAlloc, zThis, nThis+1); + *pzTab = (char *)pAlloc; + pAlloc += nThis+1; + } + + i = 0; + while( SQLITE_ROW==sqlite3_step(pStmt) ){ + int nName = sqlite3_column_bytes(pStmt, 1); + const unsigned char *zName = sqlite3_column_text(pStmt, 1); + if( zName==0 ) break; + memcpy(pAlloc, zName, nName+1); + azCol[i] = (char *)pAlloc; + pAlloc += nName+1; + abPK[i] = sqlite3_column_int(pStmt, 5); + i++; + } + rc = sqlite3_reset(pStmt); + + } + + /* If successful, populate the output variables. Otherwise, zero them and + ** free any allocation made. An error code will be returned in this case. + */ + if( rc==SQLITE_OK ){ + *pazCol = (const char **)azCol; + *pabPK = abPK; + *pnCol = nDbCol; + }else{ + *pazCol = 0; + *pabPK = 0; + *pnCol = 0; + if( pzTab ) *pzTab = 0; + sqlite3_free(azCol); + } + sqlite3_finalize(pStmt); + return rc; +} + +/* +** This function is only called from within a pre-update handler for a +** write to table pTab, part of session pSession. If this is the first +** write to this table, initalize the SessionTable.nCol, azCol[] and +** abPK[] arrays accordingly. +** +** If an error occurs, an error code is stored in sqlite3_session.rc and +** non-zero returned. Or, if no error occurs but the table has no primary +** key, sqlite3_session.rc is left set to SQLITE_OK and non-zero returned to +** indicate that updates on this table should be ignored. SessionTable.abPK +** is set to NULL in this case. +*/ +static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){ + if( pTab->nCol==0 ){ + u8 *abPK; + assert( pTab->azCol==0 || pTab->abPK==0 ); + pSession->rc = sessionTableInfo(pSession->db, pSession->zDb, + pTab->zName, &pTab->nCol, 0, &pTab->azCol, &abPK + ); + if( pSession->rc==SQLITE_OK ){ + int i; + for(i=0; inCol; i++){ + if( abPK[i] ){ + pTab->abPK = abPK; + break; + } + } + } + } + return (pSession->rc || pTab->abPK==0); +} + +/* +** This function is only called from with a pre-update-hook reporting a +** change on table pTab (attached to session pSession). The type of change +** (UPDATE, INSERT, DELETE) is specified by the first argument. +** +** Unless one is already present or an error occurs, an entry is added +** to the changed-rows hash table associated with table pTab. +*/ +static void sessionPreupdateOneChange( + int op, /* One of SQLITE_UPDATE, INSERT, DELETE */ + sqlite3_session *pSession, /* Session object pTab is attached to */ + SessionTable *pTab /* Table that change applies to */ +){ + int iHash; + int bNull = 0; + int rc = SQLITE_OK; + + if( pSession->rc ) return; + + /* Load table details if required */ + if( sessionInitTable(pSession, pTab) ) return; + + /* Check the number of columns in this xPreUpdate call matches the + ** number of columns in the table. */ + if( pTab->nCol!=pSession->hook.xCount(pSession->hook.pCtx) ){ + pSession->rc = SQLITE_SCHEMA; + return; + } + + /* Grow the hash table if required */ + if( sessionGrowHash(0, pTab) ){ + pSession->rc = SQLITE_NOMEM; + return; + } + + /* Calculate the hash-key for this change. If the primary key of the row + ** includes a NULL value, exit early. Such changes are ignored by the + ** session module. */ + rc = sessionPreupdateHash(pSession, pTab, op==SQLITE_INSERT, &iHash, &bNull); + if( rc!=SQLITE_OK ) goto error_out; + + if( bNull==0 ){ + /* Search the hash table for an existing record for this row. */ + SessionChange *pC; + for(pC=pTab->apChange[iHash]; pC; pC=pC->pNext){ + if( sessionPreupdateEqual(pSession, pTab, pC, op) ) break; + } + + if( pC==0 ){ + /* Create a new change object containing all the old values (if + ** this is an SQLITE_UPDATE or SQLITE_DELETE), or just the PK + ** values (if this is an INSERT). */ + SessionChange *pChange; /* New change object */ + int nByte; /* Number of bytes to allocate */ + int i; /* Used to iterate through columns */ + + assert( rc==SQLITE_OK ); + pTab->nEntry++; + + /* Figure out how large an allocation is required */ + nByte = sizeof(SessionChange); + for(i=0; inCol; i++){ + sqlite3_value *p = 0; + if( op!=SQLITE_INSERT ){ + TESTONLY(int trc = ) pSession->hook.xOld(pSession->hook.pCtx, i, &p); + assert( trc==SQLITE_OK ); + }else if( pTab->abPK[i] ){ + TESTONLY(int trc = ) pSession->hook.xNew(pSession->hook.pCtx, i, &p); + assert( trc==SQLITE_OK ); + } + + /* This may fail if SQLite value p contains a utf-16 string that must + ** be converted to utf-8 and an OOM error occurs while doing so. */ + rc = sessionSerializeValue(0, p, &nByte); + if( rc!=SQLITE_OK ) goto error_out; + } + + /* Allocate the change object */ + pChange = (SessionChange *)sqlite3_malloc(nByte); + if( !pChange ){ + rc = SQLITE_NOMEM; + goto error_out; + }else{ + memset(pChange, 0, sizeof(SessionChange)); + pChange->aRecord = (u8 *)&pChange[1]; + } + + /* Populate the change object. None of the preupdate_old(), + ** preupdate_new() or SerializeValue() calls below may fail as all + ** required values and encodings have already been cached in memory. + ** It is not possible for an OOM to occur in this block. */ + nByte = 0; + for(i=0; inCol; i++){ + sqlite3_value *p = 0; + if( op!=SQLITE_INSERT ){ + pSession->hook.xOld(pSession->hook.pCtx, i, &p); + }else if( pTab->abPK[i] ){ + pSession->hook.xNew(pSession->hook.pCtx, i, &p); + } + sessionSerializeValue(&pChange->aRecord[nByte], p, &nByte); + } + + /* Add the change to the hash-table */ + if( pSession->bIndirect || pSession->hook.xDepth(pSession->hook.pCtx) ){ + pChange->bIndirect = 1; + } + pChange->nRecord = nByte; + pChange->op = op; + pChange->pNext = pTab->apChange[iHash]; + pTab->apChange[iHash] = pChange; + + }else if( pC->bIndirect ){ + /* If the existing change is considered "indirect", but this current + ** change is "direct", mark the change object as direct. */ + if( pSession->hook.xDepth(pSession->hook.pCtx)==0 + && pSession->bIndirect==0 + ){ + pC->bIndirect = 0; + } + } + } + + /* If an error has occurred, mark the session object as failed. */ + error_out: + if( rc!=SQLITE_OK ){ + pSession->rc = rc; + } +} + +static int sessionFindTable( + sqlite3_session *pSession, + const char *zName, + SessionTable **ppTab +){ + int rc = SQLITE_OK; + int nName = sqlite3Strlen30(zName); + SessionTable *pRet; + + /* Search for an existing table */ + for(pRet=pSession->pTable; pRet; pRet=pRet->pNext){ + if( 0==sqlite3_strnicmp(pRet->zName, zName, nName+1) ) break; + } + + if( pRet==0 && pSession->bAutoAttach ){ + /* If there is a table-filter configured, invoke it. If it returns 0, + ** do not automatically add the new table. */ + if( pSession->xTableFilter==0 + || pSession->xTableFilter(pSession->pFilterCtx, zName) + ){ + rc = sqlite3session_attach(pSession, zName); + if( rc==SQLITE_OK ){ + for(pRet=pSession->pTable; pRet->pNext; pRet=pRet->pNext); + assert( 0==sqlite3_strnicmp(pRet->zName, zName, nName+1) ); + } + } + } + + assert( rc==SQLITE_OK || pRet==0 ); + *ppTab = pRet; + return rc; +} + +/* +** The 'pre-update' hook registered by this module with SQLite databases. +*/ +static void xPreUpdate( + void *pCtx, /* Copy of third arg to preupdate_hook() */ + sqlite3 *db, /* Database handle */ + int op, /* SQLITE_UPDATE, DELETE or INSERT */ + char const *zDb, /* Database name */ + char const *zName, /* Table name */ + sqlite3_int64 iKey1, /* Rowid of row about to be deleted/updated */ + sqlite3_int64 iKey2 /* New rowid value (for a rowid UPDATE) */ +){ + sqlite3_session *pSession; + int nDb = sqlite3Strlen30(zDb); + + assert( sqlite3_mutex_held(db->mutex) ); + + for(pSession=(sqlite3_session *)pCtx; pSession; pSession=pSession->pNext){ + SessionTable *pTab; + + /* If this session is attached to a different database ("main", "temp" + ** etc.), or if it is not currently enabled, there is nothing to do. Skip + ** to the next session object attached to this database. */ + if( pSession->bEnable==0 ) continue; + if( pSession->rc ) continue; + if( sqlite3_strnicmp(zDb, pSession->zDb, nDb+1) ) continue; + + pSession->rc = sessionFindTable(pSession, zName, &pTab); + if( pTab ){ + assert( pSession->rc==SQLITE_OK ); + sessionPreupdateOneChange(op, pSession, pTab); + if( op==SQLITE_UPDATE ){ + sessionPreupdateOneChange(SQLITE_INSERT, pSession, pTab); + } + } + } +} + +/* +** The pre-update hook implementations. +*/ +static int sessionPreupdateOld(void *pCtx, int iVal, sqlite3_value **ppVal){ + return sqlite3_preupdate_old((sqlite3*)pCtx, iVal, ppVal); +} +static int sessionPreupdateNew(void *pCtx, int iVal, sqlite3_value **ppVal){ + return sqlite3_preupdate_new((sqlite3*)pCtx, iVal, ppVal); +} +static int sessionPreupdateCount(void *pCtx){ + return sqlite3_preupdate_count((sqlite3*)pCtx); +} +static int sessionPreupdateDepth(void *pCtx){ + return sqlite3_preupdate_depth((sqlite3*)pCtx); +} + +/* +** Install the pre-update hooks on the session object passed as the only +** argument. +*/ +static void sessionPreupdateHooks( + sqlite3_session *pSession +){ + pSession->hook.pCtx = (void*)pSession->db; + pSession->hook.xOld = sessionPreupdateOld; + pSession->hook.xNew = sessionPreupdateNew; + pSession->hook.xCount = sessionPreupdateCount; + pSession->hook.xDepth = sessionPreupdateDepth; +} + +typedef struct SessionDiffCtx SessionDiffCtx; +struct SessionDiffCtx { + sqlite3_stmt *pStmt; + int nOldOff; +}; + +/* +** The diff hook implementations. +*/ +static int sessionDiffOld(void *pCtx, int iVal, sqlite3_value **ppVal){ + SessionDiffCtx *p = (SessionDiffCtx*)pCtx; + *ppVal = sqlite3_column_value(p->pStmt, iVal+p->nOldOff); + return SQLITE_OK; +} +static int sessionDiffNew(void *pCtx, int iVal, sqlite3_value **ppVal){ + SessionDiffCtx *p = (SessionDiffCtx*)pCtx; + *ppVal = sqlite3_column_value(p->pStmt, iVal); + return SQLITE_OK; +} +static int sessionDiffCount(void *pCtx){ + SessionDiffCtx *p = (SessionDiffCtx*)pCtx; + return p->nOldOff ? p->nOldOff : sqlite3_column_count(p->pStmt); +} +static int sessionDiffDepth(void *pCtx){ + return 0; +} + +/* +** Install the diff hooks on the session object passed as the only +** argument. +*/ +static void sessionDiffHooks( + sqlite3_session *pSession, + SessionDiffCtx *pDiffCtx +){ + pSession->hook.pCtx = (void*)pDiffCtx; + pSession->hook.xOld = sessionDiffOld; + pSession->hook.xNew = sessionDiffNew; + pSession->hook.xCount = sessionDiffCount; + pSession->hook.xDepth = sessionDiffDepth; +} + +static char *sessionExprComparePK( + int nCol, + const char *zDb1, const char *zDb2, + const char *zTab, + const char **azCol, u8 *abPK +){ + int i; + const char *zSep = ""; + char *zRet = 0; + + for(i=0; inCol, zDb1, zDb2, pTab->zName,zExpr); + + if( zStmt==0 ){ + rc = SQLITE_NOMEM; + }else{ + sqlite3_stmt *pStmt; + rc = sqlite3_prepare(pSession->db, zStmt, -1, &pStmt, 0); + if( rc==SQLITE_OK ){ + SessionDiffCtx *pDiffCtx = (SessionDiffCtx*)pSession->hook.pCtx; + pDiffCtx->pStmt = pStmt; + pDiffCtx->nOldOff = 0; + while( SQLITE_ROW==sqlite3_step(pStmt) ){ + sessionPreupdateOneChange(op, pSession, pTab); + } + rc = sqlite3_finalize(pStmt); + } + sqlite3_free(zStmt); + } + + return rc; +} + +static int sessionDiffFindModified( + sqlite3_session *pSession, + SessionTable *pTab, + const char *zFrom, + const char *zExpr +){ + int rc = SQLITE_OK; + + char *zExpr2 = sessionExprCompareOther(pTab->nCol, + pSession->zDb, zFrom, pTab->zName, pTab->azCol, pTab->abPK + ); + if( zExpr2==0 ){ + rc = SQLITE_NOMEM; + }else{ + char *zStmt = sqlite3_mprintf( + "SELECT * FROM \"%w\".\"%w\", \"%w\".\"%w\" WHERE %s AND (%z)", + pSession->zDb, pTab->zName, zFrom, pTab->zName, zExpr, zExpr2 + ); + if( zStmt==0 ){ + rc = SQLITE_NOMEM; + }else{ + sqlite3_stmt *pStmt; + rc = sqlite3_prepare(pSession->db, zStmt, -1, &pStmt, 0); + + if( rc==SQLITE_OK ){ + SessionDiffCtx *pDiffCtx = (SessionDiffCtx*)pSession->hook.pCtx; + pDiffCtx->pStmt = pStmt; + pDiffCtx->nOldOff = pTab->nCol; + while( SQLITE_ROW==sqlite3_step(pStmt) ){ + sessionPreupdateOneChange(SQLITE_UPDATE, pSession, pTab); + } + rc = sqlite3_finalize(pStmt); + } + sqlite3_free(zStmt); + } + } + + return rc; +} + +SQLITE_API int SQLITE_STDCALL sqlite3session_diff( + sqlite3_session *pSession, + const char *zFrom, + const char *zTbl, + char **pzErrMsg +){ + const char *zDb = pSession->zDb; + int rc = pSession->rc; + SessionDiffCtx d; + + memset(&d, 0, sizeof(d)); + sessionDiffHooks(pSession, &d); + + sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db)); + if( pzErrMsg ) *pzErrMsg = 0; + if( rc==SQLITE_OK ){ + char *zExpr = 0; + sqlite3 *db = pSession->db; + SessionTable *pTo; /* Table zTbl */ + + /* Locate and if necessary initialize the target table object */ + rc = sessionFindTable(pSession, zTbl, &pTo); + if( pTo==0 ) goto diff_out; + if( sessionInitTable(pSession, pTo) ){ + rc = pSession->rc; + goto diff_out; + } + + /* Check the table schemas match */ + if( rc==SQLITE_OK ){ + int bHasPk = 0; + int bMismatch = 0; + int nCol; /* Columns in zFrom.zTbl */ + u8 *abPK; + const char **azCol = 0; + rc = sessionTableInfo(db, zFrom, zTbl, &nCol, 0, &azCol, &abPK); + if( rc==SQLITE_OK ){ + if( pTo->nCol!=nCol ){ + bMismatch = 1; + }else{ + int i; + for(i=0; iabPK[i]!=abPK[i] ) bMismatch = 1; + if( sqlite3_stricmp(azCol[i], pTo->azCol[i]) ) bMismatch = 1; + if( abPK[i] ) bHasPk = 1; + } + } + + } + sqlite3_free((char*)azCol); + if( bMismatch ){ + *pzErrMsg = sqlite3_mprintf("table schemas do not match"); + rc = SQLITE_SCHEMA; + } + if( bHasPk==0 ){ + /* Ignore tables with no primary keys */ + goto diff_out; + } + } + + if( rc==SQLITE_OK ){ + zExpr = sessionExprComparePK(pTo->nCol, + zDb, zFrom, pTo->zName, pTo->azCol, pTo->abPK + ); + } + + /* Find new rows */ + if( rc==SQLITE_OK ){ + rc = sessionDiffFindNew(SQLITE_INSERT, pSession, pTo, zDb, zFrom, zExpr); + } + + /* Find old rows */ + if( rc==SQLITE_OK ){ + rc = sessionDiffFindNew(SQLITE_DELETE, pSession, pTo, zFrom, zDb, zExpr); + } + + /* Find modified rows */ + if( rc==SQLITE_OK ){ + rc = sessionDiffFindModified(pSession, pTo, zFrom, zExpr); + } + + sqlite3_free(zExpr); + } + + diff_out: + sessionPreupdateHooks(pSession); + sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db)); + return rc; +} + +/* +** Create a session object. This session object will record changes to +** database zDb attached to connection db. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3session_create( + sqlite3 *db, /* Database handle */ + const char *zDb, /* Name of db (e.g. "main") */ + sqlite3_session **ppSession /* OUT: New session object */ +){ + sqlite3_session *pNew; /* Newly allocated session object */ + sqlite3_session *pOld; /* Session object already attached to db */ + int nDb = sqlite3Strlen30(zDb); /* Length of zDb in bytes */ + + /* Zero the output value in case an error occurs. */ + *ppSession = 0; + + /* Allocate and populate the new session object. */ + pNew = (sqlite3_session *)sqlite3_malloc(sizeof(sqlite3_session) + nDb + 1); + if( !pNew ) return SQLITE_NOMEM; + memset(pNew, 0, sizeof(sqlite3_session)); + pNew->db = db; + pNew->zDb = (char *)&pNew[1]; + pNew->bEnable = 1; + memcpy(pNew->zDb, zDb, nDb+1); + sessionPreupdateHooks(pNew); + + /* Add the new session object to the linked list of session objects + ** attached to database handle $db. Do this under the cover of the db + ** handle mutex. */ + sqlite3_mutex_enter(sqlite3_db_mutex(db)); + pOld = (sqlite3_session*)sqlite3_preupdate_hook(db, xPreUpdate, (void*)pNew); + pNew->pNext = pOld; + sqlite3_mutex_leave(sqlite3_db_mutex(db)); + + *ppSession = pNew; + return SQLITE_OK; +} + +/* +** Free the list of table objects passed as the first argument. The contents +** of the changed-rows hash tables are also deleted. +*/ +static void sessionDeleteTable(SessionTable *pList){ + SessionTable *pNext; + SessionTable *pTab; + + for(pTab=pList; pTab; pTab=pNext){ + int i; + pNext = pTab->pNext; + for(i=0; inChange; i++){ + SessionChange *p; + SessionChange *pNextChange; + for(p=pTab->apChange[i]; p; p=pNextChange){ + pNextChange = p->pNext; + sqlite3_free(p); + } + } + sqlite3_free((char*)pTab->azCol); /* cast works around VC++ bug */ + sqlite3_free(pTab->apChange); + sqlite3_free(pTab); + } +} + +/* +** Delete a session object previously allocated using sqlite3session_create(). +*/ +SQLITE_API void SQLITE_STDCALL sqlite3session_delete(sqlite3_session *pSession){ + sqlite3 *db = pSession->db; + sqlite3_session *pHead; + sqlite3_session **pp; + + /* Unlink the session from the linked list of sessions attached to the + ** database handle. Hold the db mutex while doing so. */ + sqlite3_mutex_enter(sqlite3_db_mutex(db)); + pHead = (sqlite3_session*)sqlite3_preupdate_hook(db, 0, 0); + for(pp=&pHead; ALWAYS((*pp)!=0); pp=&((*pp)->pNext)){ + if( (*pp)==pSession ){ + *pp = (*pp)->pNext; + if( pHead ) sqlite3_preupdate_hook(db, xPreUpdate, (void*)pHead); + break; + } + } + sqlite3_mutex_leave(sqlite3_db_mutex(db)); + + /* Delete all attached table objects. And the contents of their + ** associated hash-tables. */ + sessionDeleteTable(pSession->pTable); + + /* Free the session object itself. */ + sqlite3_free(pSession); +} + +/* +** Set a table filter on a Session Object. +*/ +SQLITE_API void SQLITE_STDCALL sqlite3session_table_filter( + sqlite3_session *pSession, + int(*xFilter)(void*, const char*), + void *pCtx /* First argument passed to xFilter */ +){ + pSession->bAutoAttach = 1; + pSession->pFilterCtx = pCtx; + pSession->xTableFilter = xFilter; +} + +/* +** Attach a table to a session. All subsequent changes made to the table +** while the session object is enabled will be recorded. +** +** Only tables that have a PRIMARY KEY defined may be attached. It does +** not matter if the PRIMARY KEY is an "INTEGER PRIMARY KEY" (rowid alias) +** or not. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3session_attach( + sqlite3_session *pSession, /* Session object */ + const char *zName /* Table name */ +){ + int rc = SQLITE_OK; + sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db)); + + if( !zName ){ + pSession->bAutoAttach = 1; + }else{ + SessionTable *pTab; /* New table object (if required) */ + int nName; /* Number of bytes in string zName */ + + /* First search for an existing entry. If one is found, this call is + ** a no-op. Return early. */ + nName = sqlite3Strlen30(zName); + for(pTab=pSession->pTable; pTab; pTab=pTab->pNext){ + if( 0==sqlite3_strnicmp(pTab->zName, zName, nName+1) ) break; + } + + if( !pTab ){ + /* Allocate new SessionTable object. */ + pTab = (SessionTable *)sqlite3_malloc(sizeof(SessionTable) + nName + 1); + if( !pTab ){ + rc = SQLITE_NOMEM; + }else{ + /* Populate the new SessionTable object and link it into the list. + ** The new object must be linked onto the end of the list, not + ** simply added to the start of it in order to ensure that tables + ** appear in the correct order when a changeset or patchset is + ** eventually generated. */ + SessionTable **ppTab; + memset(pTab, 0, sizeof(SessionTable)); + pTab->zName = (char *)&pTab[1]; + memcpy(pTab->zName, zName, nName+1); + for(ppTab=&pSession->pTable; *ppTab; ppTab=&(*ppTab)->pNext); + *ppTab = pTab; + } + } + } + + sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db)); + return rc; +} + +/* +** Ensure that there is room in the buffer to append nByte bytes of data. +** If not, use sqlite3_realloc() to grow the buffer so that there is. +** +** If successful, return zero. Otherwise, if an OOM condition is encountered, +** set *pRc to SQLITE_NOMEM and return non-zero. +*/ +static int sessionBufferGrow(SessionBuffer *p, int nByte, int *pRc){ + if( *pRc==SQLITE_OK && p->nAlloc-p->nBufnAlloc ? p->nAlloc : 128; + do { + nNew = nNew*2; + }while( nNew<(p->nBuf+nByte) ); + + aNew = (u8 *)sqlite3_realloc(p->aBuf, nNew); + if( 0==aNew ){ + *pRc = SQLITE_NOMEM; + }else{ + p->aBuf = aNew; + p->nAlloc = nNew; + } + } + return (*pRc!=SQLITE_OK); +} + +/* +** Append the value passed as the second argument to the buffer passed +** as the first. +** +** This function is a no-op if *pRc is non-zero when it is called. +** Otherwise, if an error occurs, *pRc is set to an SQLite error code +** before returning. +*/ +static void sessionAppendValue(SessionBuffer *p, sqlite3_value *pVal, int *pRc){ + int rc = *pRc; + if( rc==SQLITE_OK ){ + int nByte = 0; + rc = sessionSerializeValue(0, pVal, &nByte); + sessionBufferGrow(p, nByte, &rc); + if( rc==SQLITE_OK ){ + rc = sessionSerializeValue(&p->aBuf[p->nBuf], pVal, 0); + p->nBuf += nByte; + }else{ + *pRc = rc; + } + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append a single byte to the buffer. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendByte(SessionBuffer *p, u8 v, int *pRc){ + if( 0==sessionBufferGrow(p, 1, pRc) ){ + p->aBuf[p->nBuf++] = v; + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append a single varint to the buffer. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendVarint(SessionBuffer *p, int v, int *pRc){ + if( 0==sessionBufferGrow(p, 9, pRc) ){ + p->nBuf += sessionVarintPut(&p->aBuf[p->nBuf], v); + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append a blob of data to the buffer. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendBlob( + SessionBuffer *p, + const u8 *aBlob, + int nBlob, + int *pRc +){ + if( 0==sessionBufferGrow(p, nBlob, pRc) ){ + memcpy(&p->aBuf[p->nBuf], aBlob, nBlob); + p->nBuf += nBlob; + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append a string to the buffer. All bytes in the string +** up to (but not including) the nul-terminator are written to the buffer. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendStr( + SessionBuffer *p, + const char *zStr, + int *pRc +){ + int nStr = sqlite3Strlen30(zStr); + if( 0==sessionBufferGrow(p, nStr, pRc) ){ + memcpy(&p->aBuf[p->nBuf], zStr, nStr); + p->nBuf += nStr; + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append the string representation of integer iVal +** to the buffer. No nul-terminator is written. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendInteger( + SessionBuffer *p, /* Buffer to append to */ + int iVal, /* Value to write the string rep. of */ + int *pRc /* IN/OUT: Error code */ +){ + char aBuf[24]; + sqlite3_snprintf(sizeof(aBuf)-1, aBuf, "%d", iVal); + sessionAppendStr(p, aBuf, pRc); +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append the string zStr enclosed in quotes (") and +** with any embedded quote characters escaped to the buffer. No +** nul-terminator byte is written. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendIdent( + SessionBuffer *p, /* Buffer to a append to */ + const char *zStr, /* String to quote, escape and append */ + int *pRc /* IN/OUT: Error code */ +){ + int nStr = sqlite3Strlen30(zStr)*2 + 2 + 1; + if( 0==sessionBufferGrow(p, nStr, pRc) ){ + char *zOut = (char *)&p->aBuf[p->nBuf]; + const char *zIn = zStr; + *zOut++ = '"'; + while( *zIn ){ + if( *zIn=='"' ) *zOut++ = '"'; + *zOut++ = *(zIn++); + } + *zOut++ = '"'; + p->nBuf = (int)((u8 *)zOut - p->aBuf); + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwse, it appends the serialized version of the value stored +** in column iCol of the row that SQL statement pStmt currently points +** to to the buffer. +*/ +static void sessionAppendCol( + SessionBuffer *p, /* Buffer to append to */ + sqlite3_stmt *pStmt, /* Handle pointing to row containing value */ + int iCol, /* Column to read value from */ + int *pRc /* IN/OUT: Error code */ +){ + if( *pRc==SQLITE_OK ){ + int eType = sqlite3_column_type(pStmt, iCol); + sessionAppendByte(p, (u8)eType, pRc); + if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + sqlite3_int64 i; + u8 aBuf[8]; + if( eType==SQLITE_INTEGER ){ + i = sqlite3_column_int64(pStmt, iCol); + }else{ + double r = sqlite3_column_double(pStmt, iCol); + memcpy(&i, &r, 8); + } + sessionPutI64(aBuf, i); + sessionAppendBlob(p, aBuf, 8, pRc); + } + if( eType==SQLITE_BLOB || eType==SQLITE_TEXT ){ + u8 *z; + int nByte; + if( eType==SQLITE_BLOB ){ + z = (u8 *)sqlite3_column_blob(pStmt, iCol); + }else{ + z = (u8 *)sqlite3_column_text(pStmt, iCol); + } + nByte = sqlite3_column_bytes(pStmt, iCol); + if( z || (eType==SQLITE_BLOB && nByte==0) ){ + sessionAppendVarint(p, nByte, pRc); + sessionAppendBlob(p, z, nByte, pRc); + }else{ + *pRc = SQLITE_NOMEM; + } + } + } +} + +/* +** +** This function appends an update change to the buffer (see the comments +** under "CHANGESET FORMAT" at the top of the file). An update change +** consists of: +** +** 1 byte: SQLITE_UPDATE (0x17) +** n bytes: old.* record (see RECORD FORMAT) +** m bytes: new.* record (see RECORD FORMAT) +** +** The SessionChange object passed as the third argument contains the +** values that were stored in the row when the session began (the old.* +** values). The statement handle passed as the second argument points +** at the current version of the row (the new.* values). +** +** If all of the old.* values are equal to their corresponding new.* value +** (i.e. nothing has changed), then no data at all is appended to the buffer. +** +** Otherwise, the old.* record contains all primary key values and the +** original values of any fields that have been modified. The new.* record +** contains the new values of only those fields that have been modified. +*/ +static int sessionAppendUpdate( + SessionBuffer *pBuf, /* Buffer to append to */ + int bPatchset, /* True for "patchset", 0 for "changeset" */ + sqlite3_stmt *pStmt, /* Statement handle pointing at new row */ + SessionChange *p, /* Object containing old values */ + u8 *abPK /* Boolean array - true for PK columns */ +){ + int rc = SQLITE_OK; + SessionBuffer buf2 = {0,0,0}; /* Buffer to accumulate new.* record in */ + int bNoop = 1; /* Set to zero if any values are modified */ + int nRewind = pBuf->nBuf; /* Set to zero if any values are modified */ + int i; /* Used to iterate through columns */ + u8 *pCsr = p->aRecord; /* Used to iterate through old.* values */ + + sessionAppendByte(pBuf, SQLITE_UPDATE, &rc); + sessionAppendByte(pBuf, p->bIndirect, &rc); + for(i=0; inBuf = nRewind; + }else{ + sessionAppendBlob(pBuf, buf2.aBuf, buf2.nBuf, &rc); + } + sqlite3_free(buf2.aBuf); + + return rc; +} + +/* +** Append a DELETE change to the buffer passed as the first argument. Use +** the changeset format if argument bPatchset is zero, or the patchset +** format otherwise. +*/ +static int sessionAppendDelete( + SessionBuffer *pBuf, /* Buffer to append to */ + int bPatchset, /* True for "patchset", 0 for "changeset" */ + SessionChange *p, /* Object containing old values */ + int nCol, /* Number of columns in table */ + u8 *abPK /* Boolean array - true for PK columns */ +){ + int rc = SQLITE_OK; + + sessionAppendByte(pBuf, SQLITE_DELETE, &rc); + sessionAppendByte(pBuf, p->bIndirect, &rc); + + if( bPatchset==0 ){ + sessionAppendBlob(pBuf, p->aRecord, p->nRecord, &rc); + }else{ + int i; + u8 *a = p->aRecord; + for(i=0; iaRecord)==p->nRecord ); + } + + return rc; +} + +/* +** Formulate and prepare a SELECT statement to retrieve a row from table +** zTab in database zDb based on its primary key. i.e. +** +** SELECT * FROM zDb.zTab WHERE pk1 = ? AND pk2 = ? AND ... +*/ +static int sessionSelectStmt( + sqlite3 *db, /* Database handle */ + const char *zDb, /* Database name */ + const char *zTab, /* Table name */ + int nCol, /* Number of columns in table */ + const char **azCol, /* Names of table columns */ + u8 *abPK, /* PRIMARY KEY array */ + sqlite3_stmt **ppStmt /* OUT: Prepared SELECT statement */ +){ + int rc = SQLITE_OK; + int i; + const char *zSep = ""; + SessionBuffer buf = {0, 0, 0}; + + sessionAppendStr(&buf, "SELECT * FROM ", &rc); + sessionAppendIdent(&buf, zDb, &rc); + sessionAppendStr(&buf, ".", &rc); + sessionAppendIdent(&buf, zTab, &rc); + sessionAppendStr(&buf, " WHERE ", &rc); + for(i=0; iaRecord; + + for(i=0; inCol, pRc); + sessionAppendBlob(pBuf, pTab->abPK, pTab->nCol, pRc); + sessionAppendBlob(pBuf, (u8 *)pTab->zName, (int)strlen(pTab->zName)+1, pRc); +} + +/* +** Generate either a changeset (if argument bPatchset is zero) or a patchset +** (if it is non-zero) based on the current contents of the session object +** passed as the first argument. +** +** If no error occurs, SQLITE_OK is returned and the new changeset/patchset +** stored in output variables *pnChangeset and *ppChangeset. Or, if an error +** occurs, an SQLite error code is returned and both output variables set +** to 0. +*/ +static int sessionGenerateChangeset( + sqlite3_session *pSession, /* Session object */ + int bPatchset, /* True for patchset, false for changeset */ + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut, /* First argument for xOutput */ + int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + void **ppChangeset /* OUT: Buffer containing changeset */ +){ + sqlite3 *db = pSession->db; /* Source database handle */ + SessionTable *pTab; /* Used to iterate through attached tables */ + SessionBuffer buf = {0,0,0}; /* Buffer in which to accumlate changeset */ + int rc; /* Return code */ + + assert( xOutput==0 || (pnChangeset==0 && ppChangeset==0 ) ); + + /* Zero the output variables in case an error occurs. If this session + ** object is already in the error state (sqlite3_session.rc != SQLITE_OK), + ** this call will be a no-op. */ + if( xOutput==0 ){ + *pnChangeset = 0; + *ppChangeset = 0; + } + + if( pSession->rc ) return pSession->rc; + rc = sqlite3_exec(pSession->db, "SAVEPOINT changeset", 0, 0, 0); + if( rc!=SQLITE_OK ) return rc; + + sqlite3_mutex_enter(sqlite3_db_mutex(db)); + + for(pTab=pSession->pTable; rc==SQLITE_OK && pTab; pTab=pTab->pNext){ + if( pTab->nEntry ){ + const char *zName = pTab->zName; + int nCol; /* Number of columns in table */ + u8 *abPK; /* Primary key array */ + const char **azCol = 0; /* Table columns */ + int i; /* Used to iterate through hash buckets */ + sqlite3_stmt *pSel = 0; /* SELECT statement to query table pTab */ + int nRewind = buf.nBuf; /* Initial size of write buffer */ + int nNoop; /* Size of buffer after writing tbl header */ + + /* Check the table schema is still Ok. */ + rc = sessionTableInfo(db, pSession->zDb, zName, &nCol, 0, &azCol, &abPK); + if( !rc && (pTab->nCol!=nCol || memcmp(abPK, pTab->abPK, nCol)) ){ + rc = SQLITE_SCHEMA; + } + + /* Write a table header */ + sessionAppendTableHdr(&buf, bPatchset, pTab, &rc); + + /* Build and compile a statement to execute: */ + if( rc==SQLITE_OK ){ + rc = sessionSelectStmt( + db, pSession->zDb, zName, nCol, azCol, abPK, &pSel); + } + + nNoop = buf.nBuf; + for(i=0; inChange && rc==SQLITE_OK; i++){ + SessionChange *p; /* Used to iterate through changes */ + + for(p=pTab->apChange[i]; rc==SQLITE_OK && p; p=p->pNext){ + rc = sessionSelectBind(pSel, nCol, abPK, p); + if( rc!=SQLITE_OK ) continue; + if( sqlite3_step(pSel)==SQLITE_ROW ){ + if( p->op==SQLITE_INSERT ){ + int iCol; + sessionAppendByte(&buf, SQLITE_INSERT, &rc); + sessionAppendByte(&buf, p->bIndirect, &rc); + for(iCol=0; iColop!=SQLITE_INSERT ){ + rc = sessionAppendDelete(&buf, bPatchset, p, nCol, abPK); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_reset(pSel); + } + + /* If the buffer is now larger than SESSIONS_STRM_CHUNK_SIZE, pass + ** its contents to the xOutput() callback. */ + if( xOutput + && rc==SQLITE_OK + && buf.nBuf>nNoop + && buf.nBuf>SESSIONS_STRM_CHUNK_SIZE + ){ + rc = xOutput(pOut, (void*)buf.aBuf, buf.nBuf); + nNoop = -1; + buf.nBuf = 0; + } + + } + } + + sqlite3_finalize(pSel); + if( buf.nBuf==nNoop ){ + buf.nBuf = nRewind; + } + sqlite3_free((char*)azCol); /* cast works around VC++ bug */ + } + } + + if( rc==SQLITE_OK ){ + if( xOutput==0 ){ + *pnChangeset = buf.nBuf; + *ppChangeset = buf.aBuf; + buf.aBuf = 0; + }else if( buf.nBuf>0 ){ + rc = xOutput(pOut, (void*)buf.aBuf, buf.nBuf); + } + } + + sqlite3_free(buf.aBuf); + sqlite3_exec(db, "RELEASE changeset", 0, 0, 0); + sqlite3_mutex_leave(sqlite3_db_mutex(db)); + return rc; +} + +/* +** Obtain a changeset object containing all changes recorded by the +** session object passed as the first argument. +** +** It is the responsibility of the caller to eventually free the buffer +** using sqlite3_free(). +*/ +SQLITE_API int SQLITE_STDCALL sqlite3session_changeset( + sqlite3_session *pSession, /* Session object */ + int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + void **ppChangeset /* OUT: Buffer containing changeset */ +){ + return sessionGenerateChangeset(pSession, 0, 0, 0, pnChangeset, ppChangeset); +} + +/* +** Streaming version of sqlite3session_changeset(). +*/ +SQLITE_API int SQLITE_STDCALL sqlite3session_changeset_strm( + sqlite3_session *pSession, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +){ + return sessionGenerateChangeset(pSession, 0, xOutput, pOut, 0, 0); +} + +/* +** Streaming version of sqlite3session_patchset(). +*/ +SQLITE_API int SQLITE_STDCALL sqlite3session_patchset_strm( + sqlite3_session *pSession, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +){ + return sessionGenerateChangeset(pSession, 1, xOutput, pOut, 0, 0); +} + +/* +** Obtain a patchset object containing all changes recorded by the +** session object passed as the first argument. +** +** It is the responsibility of the caller to eventually free the buffer +** using sqlite3_free(). +*/ +SQLITE_API int SQLITE_STDCALL sqlite3session_patchset( + sqlite3_session *pSession, /* Session object */ + int *pnPatchset, /* OUT: Size of buffer at *ppChangeset */ + void **ppPatchset /* OUT: Buffer containing changeset */ +){ + return sessionGenerateChangeset(pSession, 1, 0, 0, pnPatchset, ppPatchset); +} + +/* +** Enable or disable the session object passed as the first argument. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3session_enable(sqlite3_session *pSession, int bEnable){ + int ret; + sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db)); + if( bEnable>=0 ){ + pSession->bEnable = bEnable; + } + ret = pSession->bEnable; + sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db)); + return ret; +} + +/* +** Enable or disable the session object passed as the first argument. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3session_indirect(sqlite3_session *pSession, int bIndirect){ + int ret; + sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db)); + if( bIndirect>=0 ){ + pSession->bIndirect = bIndirect; + } + ret = pSession->bIndirect; + sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db)); + return ret; +} + +/* +** Return true if there have been no changes to monitored tables recorded +** by the session object passed as the only argument. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3session_isempty(sqlite3_session *pSession){ + int ret = 0; + SessionTable *pTab; + + sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db)); + for(pTab=pSession->pTable; pTab && ret==0; pTab=pTab->pNext){ + ret = (pTab->nEntry>0); + } + sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db)); + + return (ret==0); +} + +/* +** Do the work for either sqlite3changeset_start() or start_strm(). +*/ +static int sessionChangesetStart( + sqlite3_changeset_iter **pp, /* OUT: Changeset iterator handle */ + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn, + int nChangeset, /* Size of buffer pChangeset in bytes */ + void *pChangeset /* Pointer to buffer containing changeset */ +){ + sqlite3_changeset_iter *pRet; /* Iterator to return */ + int nByte; /* Number of bytes to allocate for iterator */ + + assert( xInput==0 || (pChangeset==0 && nChangeset==0) ); + + /* Zero the output variable in case an error occurs. */ + *pp = 0; + + /* Allocate and initialize the iterator structure. */ + nByte = sizeof(sqlite3_changeset_iter); + pRet = (sqlite3_changeset_iter *)sqlite3_malloc(nByte); + if( !pRet ) return SQLITE_NOMEM; + memset(pRet, 0, sizeof(sqlite3_changeset_iter)); + pRet->in.aData = (u8 *)pChangeset; + pRet->in.nData = nChangeset; + pRet->in.xInput = xInput; + pRet->in.pIn = pIn; + pRet->in.bEof = (xInput ? 0 : 1); + + /* Populate the output variable and return success. */ + *pp = pRet; + return SQLITE_OK; +} + +/* +** Create an iterator used to iterate through the contents of a changeset. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_start( + sqlite3_changeset_iter **pp, /* OUT: Changeset iterator handle */ + int nChangeset, /* Size of buffer pChangeset in bytes */ + void *pChangeset /* Pointer to buffer containing changeset */ +){ + return sessionChangesetStart(pp, 0, 0, nChangeset, pChangeset); +} + +/* +** Streaming version of sqlite3changeset_start(). +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_start_strm( + sqlite3_changeset_iter **pp, /* OUT: Changeset iterator handle */ + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn +){ + return sessionChangesetStart(pp, xInput, pIn, 0, 0); +} + +/* +** If the SessionInput object passed as the only argument is a streaming +** object and the buffer is full, discard some data to free up space. +*/ +static void sessionDiscardData(SessionInput *pIn){ + if( pIn->bEof && pIn->xInput && pIn->iNext>=SESSIONS_STRM_CHUNK_SIZE ){ + int nMove = pIn->buf.nBuf - pIn->iNext; + assert( nMove>=0 ); + if( nMove>0 ){ + memmove(pIn->buf.aBuf, &pIn->buf.aBuf[pIn->iNext], nMove); + } + pIn->buf.nBuf -= pIn->iNext; + pIn->iNext = 0; + pIn->nData = pIn->buf.nBuf; + } +} + +/* +** Ensure that there are at least nByte bytes available in the buffer. Or, +** if there are not nByte bytes remaining in the input, that all available +** data is in the buffer. +** +** Return an SQLite error code if an error occurs, or SQLITE_OK otherwise. +*/ +static int sessionInputBuffer(SessionInput *pIn, int nByte){ + int rc = SQLITE_OK; + if( pIn->xInput ){ + while( !pIn->bEof && (pIn->iNext+nByte)>=pIn->nData && rc==SQLITE_OK ){ + int nNew = SESSIONS_STRM_CHUNK_SIZE; + + if( pIn->bNoDiscard==0 ) sessionDiscardData(pIn); + if( SQLITE_OK==sessionBufferGrow(&pIn->buf, nNew, &rc) ){ + rc = pIn->xInput(pIn->pIn, &pIn->buf.aBuf[pIn->buf.nBuf], &nNew); + if( nNew==0 ){ + pIn->bEof = 1; + }else{ + pIn->buf.nBuf += nNew; + } + } + + pIn->aData = pIn->buf.aBuf; + pIn->nData = pIn->buf.nBuf; + } + } + return rc; +} + +/* +** When this function is called, *ppRec points to the start of a record +** that contains nCol values. This function advances the pointer *ppRec +** until it points to the byte immediately following that record. +*/ +static void sessionSkipRecord( + u8 **ppRec, /* IN/OUT: Record pointer */ + int nCol /* Number of values in record */ +){ + u8 *aRec = *ppRec; + int i; + for(i=0; iaData[pIn->iNext++]; + } + + assert( apOut[i]==0 ); + if( eType ){ + apOut[i] = sqlite3ValueNew(0); + if( !apOut[i] ) rc = SQLITE_NOMEM; + } + + if( rc==SQLITE_OK ){ + u8 *aVal = &pIn->aData[pIn->iNext]; + if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){ + int nByte; + pIn->iNext += sessionVarintGet(aVal, &nByte); + rc = sessionInputBuffer(pIn, nByte); + if( rc==SQLITE_OK ){ + u8 enc = (eType==SQLITE_TEXT ? SQLITE_UTF8 : 0); + rc = sessionValueSetStr(apOut[i],&pIn->aData[pIn->iNext],nByte,enc); + } + pIn->iNext += nByte; + } + if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + sqlite3_int64 v = sessionGetI64(aVal); + if( eType==SQLITE_INTEGER ){ + sqlite3VdbeMemSetInt64(apOut[i], v); + }else{ + double d; + memcpy(&d, &v, 8); + sqlite3VdbeMemSetDouble(apOut[i], d); + } + pIn->iNext += 8; + } + } + } + + return rc; +} + +/* +** The input pointer currently points to the second byte of a table-header. +** Specifically, to the following: +** +** + number of columns in table (varint) +** + array of PK flags (1 byte per column), +** + table name (nul terminated). +** +** This function ensures that all of the above is present in the input +** buffer (i.e. that it can be accessed without any calls to xInput()). +** If successful, SQLITE_OK is returned. Otherwise, an SQLite error code. +** The input pointer is not moved. +*/ +static int sessionChangesetBufferTblhdr(SessionInput *pIn, int *pnByte){ + int rc = SQLITE_OK; + int nCol = 0; + int nRead = 0; + + rc = sessionInputBuffer(pIn, 9); + if( rc==SQLITE_OK ){ + nRead += sessionVarintGet(&pIn->aData[pIn->iNext + nRead], &nCol); + rc = sessionInputBuffer(pIn, nRead+nCol+100); + nRead += nCol; + } + + while( rc==SQLITE_OK ){ + while( (pIn->iNext + nRead)nData && pIn->aData[pIn->iNext + nRead] ){ + nRead++; + } + if( (pIn->iNext + nRead)nData ) break; + rc = sessionInputBuffer(pIn, nRead + 100); + } + *pnByte = nRead+1; + return rc; +} + +/* +** The input pointer currently points to the first byte of the first field +** of a record consisting of nCol columns. This function ensures the entire +** record is buffered. It does not move the input pointer. +** +** If successful, SQLITE_OK is returned and *pnByte is set to the size of +** the record in bytes. Otherwise, an SQLite error code is returned. The +** final value of *pnByte is undefined in this case. +*/ +static int sessionChangesetBufferRecord( + SessionInput *pIn, /* Input data */ + int nCol, /* Number of columns in record */ + int *pnByte /* OUT: Size of record in bytes */ +){ + int rc = SQLITE_OK; + int nByte = 0; + int i; + for(i=0; rc==SQLITE_OK && iaData[pIn->iNext + nByte++]; + if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){ + int n; + nByte += sessionVarintGet(&pIn->aData[pIn->iNext+nByte], &n); + nByte += n; + rc = sessionInputBuffer(pIn, nByte); + }else if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + nByte += 8; + } + } + } + *pnByte = nByte; + return rc; +} + +/* +** The input pointer currently points to the second byte of a table-header. +** Specifically, to the following: +** +** + number of columns in table (varint) +** + array of PK flags (1 byte per column), +** + table name (nul terminated). +** +** This function decodes the table-header and populates the p->nCol, +** p->zTab and p->abPK[] variables accordingly. The p->apValue[] array is +** also allocated or resized according to the new value of p->nCol. The +** input pointer is left pointing to the byte following the table header. +** +** If successful, SQLITE_OK is returned. Otherwise, an SQLite error code +** is returned and the final values of the various fields enumerated above +** are undefined. +*/ +static int sessionChangesetReadTblhdr(sqlite3_changeset_iter *p){ + int rc; + int nCopy; + assert( p->rc==SQLITE_OK ); + + rc = sessionChangesetBufferTblhdr(&p->in, &nCopy); + if( rc==SQLITE_OK ){ + int nByte; + int nVarint; + nVarint = sessionVarintGet(&p->in.aData[p->in.iNext], &p->nCol); + nCopy -= nVarint; + p->in.iNext += nVarint; + nByte = p->nCol * sizeof(sqlite3_value*) * 2 + nCopy; + p->tblhdr.nBuf = 0; + sessionBufferGrow(&p->tblhdr, nByte, &rc); + } + + if( rc==SQLITE_OK ){ + int iPK = sizeof(sqlite3_value*)*p->nCol*2; + memset(p->tblhdr.aBuf, 0, iPK); + memcpy(&p->tblhdr.aBuf[iPK], &p->in.aData[p->in.iNext], nCopy); + p->in.iNext += nCopy; + } + + p->apValue = (sqlite3_value**)p->tblhdr.aBuf; + p->abPK = (u8*)&p->apValue[p->nCol*2]; + p->zTab = (char*)&p->abPK[p->nCol]; + return (p->rc = rc); +} + +/* +** Advance the changeset iterator to the next change. +** +** If both paRec and pnRec are NULL, then this function works like the public +** API sqlite3changeset_next(). If SQLITE_ROW is returned, then the +** sqlite3changeset_new() and old() APIs may be used to query for values. +** +** Otherwise, if paRec and pnRec are not NULL, then a pointer to the change +** record is written to *paRec before returning and the number of bytes in +** the record to *pnRec. +** +** Either way, this function returns SQLITE_ROW if the iterator is +** successfully advanced to the next change in the changeset, an SQLite +** error code if an error occurs, or SQLITE_DONE if there are no further +** changes in the changeset. +*/ +static int sessionChangesetNext( + sqlite3_changeset_iter *p, /* Changeset iterator */ + u8 **paRec, /* If non-NULL, store record pointer here */ + int *pnRec /* If non-NULL, store size of record here */ +){ + int i; + u8 op; + + assert( (paRec==0 && pnRec==0) || (paRec && pnRec) ); + + /* If the iterator is in the error-state, return immediately. */ + if( p->rc!=SQLITE_OK ) return p->rc; + + /* Free the current contents of p->apValue[], if any. */ + if( p->apValue ){ + for(i=0; inCol*2; i++){ + sqlite3ValueFree(p->apValue[i]); + } + memset(p->apValue, 0, sizeof(sqlite3_value*)*p->nCol*2); + } + + /* Make sure the buffer contains at least 10 bytes of input data, or all + ** remaining data if there are less than 10 bytes available. This is + ** sufficient either for the 'T' or 'P' byte and the varint that follows + ** it, or for the two single byte values otherwise. */ + p->rc = sessionInputBuffer(&p->in, 2); + if( p->rc!=SQLITE_OK ) return p->rc; + + /* If the iterator is already at the end of the changeset, return DONE. */ + if( p->in.iNext>=p->in.nData ){ + return SQLITE_DONE; + } + + sessionDiscardData(&p->in); + p->in.iCurrent = p->in.iNext; + + op = p->in.aData[p->in.iNext++]; + if( op=='T' || op=='P' ){ + p->bPatchset = (op=='P'); + if( sessionChangesetReadTblhdr(p) ) return p->rc; + if( (p->rc = sessionInputBuffer(&p->in, 2)) ) return p->rc; + p->in.iCurrent = p->in.iNext; + op = p->in.aData[p->in.iNext++]; + } + + p->op = op; + p->bIndirect = p->in.aData[p->in.iNext++]; + if( p->op!=SQLITE_UPDATE && p->op!=SQLITE_DELETE && p->op!=SQLITE_INSERT ){ + return (p->rc = SQLITE_CORRUPT_BKPT); + } + + if( paRec ){ + int nVal; /* Number of values to buffer */ + if( p->bPatchset==0 && op==SQLITE_UPDATE ){ + nVal = p->nCol * 2; + }else if( p->bPatchset && op==SQLITE_DELETE ){ + nVal = 0; + for(i=0; inCol; i++) if( p->abPK[i] ) nVal++; + }else{ + nVal = p->nCol; + } + p->rc = sessionChangesetBufferRecord(&p->in, nVal, pnRec); + if( p->rc!=SQLITE_OK ) return p->rc; + *paRec = &p->in.aData[p->in.iNext]; + p->in.iNext += *pnRec; + }else{ + + /* If this is an UPDATE or DELETE, read the old.* record. */ + if( p->op!=SQLITE_INSERT && (p->bPatchset==0 || p->op==SQLITE_DELETE) ){ + u8 *abPK = p->bPatchset ? p->abPK : 0; + p->rc = sessionReadRecord(&p->in, p->nCol, abPK, p->apValue); + if( p->rc!=SQLITE_OK ) return p->rc; + } + + /* If this is an INSERT or UPDATE, read the new.* record. */ + if( p->op!=SQLITE_DELETE ){ + p->rc = sessionReadRecord(&p->in, p->nCol, 0, &p->apValue[p->nCol]); + if( p->rc!=SQLITE_OK ) return p->rc; + } + + if( p->bPatchset && p->op==SQLITE_UPDATE ){ + /* If this is an UPDATE that is part of a patchset, then all PK and + ** modified fields are present in the new.* record. The old.* record + ** is currently completely empty. This block shifts the PK fields from + ** new.* to old.*, to accommodate the code that reads these arrays. */ + for(i=0; inCol; i++){ + assert( p->apValue[i]==0 ); + assert( p->abPK[i]==0 || p->apValue[i+p->nCol] ); + if( p->abPK[i] ){ + p->apValue[i] = p->apValue[i+p->nCol]; + p->apValue[i+p->nCol] = 0; + } + } + } + } + + return SQLITE_ROW; +} + +/* +** Advance an iterator created by sqlite3changeset_start() to the next +** change in the changeset. This function may return SQLITE_ROW, SQLITE_DONE +** or SQLITE_CORRUPT. +** +** This function may not be called on iterators passed to a conflict handler +** callback by changeset_apply(). +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_next(sqlite3_changeset_iter *p){ + return sessionChangesetNext(p, 0, 0); +} + +/* +** The following function extracts information on the current change +** from a changeset iterator. It may only be called after changeset_next() +** has returned SQLITE_ROW. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_op( + sqlite3_changeset_iter *pIter, /* Iterator handle */ + const char **pzTab, /* OUT: Pointer to table name */ + int *pnCol, /* OUT: Number of columns in table */ + int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + int *pbIndirect /* OUT: True if change is indirect */ +){ + *pOp = pIter->op; + *pnCol = pIter->nCol; + *pzTab = pIter->zTab; + if( pbIndirect ) *pbIndirect = pIter->bIndirect; + return SQLITE_OK; +} + +/* +** Return information regarding the PRIMARY KEY and number of columns in +** the database table affected by the change that pIter currently points +** to. This function may only be called after changeset_next() returns +** SQLITE_ROW. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_pk( + sqlite3_changeset_iter *pIter, /* Iterator object */ + unsigned char **pabPK, /* OUT: Array of boolean - true for PK cols */ + int *pnCol /* OUT: Number of entries in output array */ +){ + *pabPK = pIter->abPK; + if( pnCol ) *pnCol = pIter->nCol; + return SQLITE_OK; +} + +/* +** This function may only be called while the iterator is pointing to an +** SQLITE_UPDATE or SQLITE_DELETE change (see sqlite3changeset_op()). +** Otherwise, SQLITE_MISUSE is returned. +** +** It sets *ppValue to point to an sqlite3_value structure containing the +** iVal'th value in the old.* record. Or, if that particular value is not +** included in the record (because the change is an UPDATE and the field +** was not modified and is not a PK column), set *ppValue to NULL. +** +** If value iVal is out-of-range, SQLITE_RANGE is returned and *ppValue is +** not modified. Otherwise, SQLITE_OK. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_old( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Index of old.* value to retrieve */ + sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */ +){ + if( pIter->op!=SQLITE_UPDATE && pIter->op!=SQLITE_DELETE ){ + return SQLITE_MISUSE; + } + if( iVal<0 || iVal>=pIter->nCol ){ + return SQLITE_RANGE; + } + *ppValue = pIter->apValue[iVal]; + return SQLITE_OK; +} + +/* +** This function may only be called while the iterator is pointing to an +** SQLITE_UPDATE or SQLITE_INSERT change (see sqlite3changeset_op()). +** Otherwise, SQLITE_MISUSE is returned. +** +** It sets *ppValue to point to an sqlite3_value structure containing the +** iVal'th value in the new.* record. Or, if that particular value is not +** included in the record (because the change is an UPDATE and the field +** was not modified), set *ppValue to NULL. +** +** If value iVal is out-of-range, SQLITE_RANGE is returned and *ppValue is +** not modified. Otherwise, SQLITE_OK. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_new( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Index of new.* value to retrieve */ + sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */ +){ + if( pIter->op!=SQLITE_UPDATE && pIter->op!=SQLITE_INSERT ){ + return SQLITE_MISUSE; + } + if( iVal<0 || iVal>=pIter->nCol ){ + return SQLITE_RANGE; + } + *ppValue = pIter->apValue[pIter->nCol+iVal]; + return SQLITE_OK; +} + +/* +** The following two macros are used internally. They are similar to the +** sqlite3changeset_new() and sqlite3changeset_old() functions, except that +** they omit all error checking and return a pointer to the requested value. +*/ +#define sessionChangesetNew(pIter, iVal) (pIter)->apValue[(pIter)->nCol+(iVal)] +#define sessionChangesetOld(pIter, iVal) (pIter)->apValue[(iVal)] + +/* +** This function may only be called with a changeset iterator that has been +** passed to an SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT +** conflict-handler function. Otherwise, SQLITE_MISUSE is returned. +** +** If successful, *ppValue is set to point to an sqlite3_value structure +** containing the iVal'th value of the conflicting record. +** +** If value iVal is out-of-range or some other error occurs, an SQLite error +** code is returned. Otherwise, SQLITE_OK. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_conflict( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Index of conflict record value to fetch */ + sqlite3_value **ppValue /* OUT: Value from conflicting row */ +){ + if( !pIter->pConflict ){ + return SQLITE_MISUSE; + } + if( iVal<0 || iVal>=sqlite3_column_count(pIter->pConflict) ){ + return SQLITE_RANGE; + } + *ppValue = sqlite3_column_value(pIter->pConflict, iVal); + return SQLITE_OK; +} + +/* +** This function may only be called with an iterator passed to an +** SQLITE_CHANGESET_FOREIGN_KEY conflict handler callback. In this case +** it sets the output variable to the total number of known foreign key +** violations in the destination database and returns SQLITE_OK. +** +** In all other cases this function returns SQLITE_MISUSE. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_fk_conflicts( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int *pnOut /* OUT: Number of FK violations */ +){ + if( pIter->pConflict || pIter->apValue ){ + return SQLITE_MISUSE; + } + *pnOut = pIter->nCol; + return SQLITE_OK; +} + + +/* +** Finalize an iterator allocated with sqlite3changeset_start(). +** +** This function may not be called on iterators passed to a conflict handler +** callback by changeset_apply(). +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_finalize(sqlite3_changeset_iter *p){ + int rc = SQLITE_OK; + if( p ){ + int i; /* Used to iterate through p->apValue[] */ + rc = p->rc; + if( p->apValue ){ + for(i=0; inCol*2; i++) sqlite3ValueFree(p->apValue[i]); + } + sqlite3_free(p->tblhdr.aBuf); + sqlite3_free(p->in.buf.aBuf); + sqlite3_free(p); + } + return rc; +} + +static int sessionChangesetInvert( + SessionInput *pInput, /* Input changeset */ + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut, + int *pnInverted, /* OUT: Number of bytes in output changeset */ + void **ppInverted /* OUT: Inverse of pChangeset */ +){ + int rc = SQLITE_OK; /* Return value */ + SessionBuffer sOut; /* Output buffer */ + int nCol = 0; /* Number of cols in current table */ + u8 *abPK = 0; /* PK array for current table */ + sqlite3_value **apVal = 0; /* Space for values for UPDATE inversion */ + SessionBuffer sPK = {0, 0, 0}; /* PK array for current table */ + + /* Initialize the output buffer */ + memset(&sOut, 0, sizeof(SessionBuffer)); + + /* Zero the output variables in case an error occurs. */ + if( ppInverted ){ + *ppInverted = 0; + *pnInverted = 0; + } + + while( 1 ){ + u8 eType; + + /* Test for EOF. */ + if( (rc = sessionInputBuffer(pInput, 2)) ) goto finished_invert; + if( pInput->iNext>=pInput->nData ) break; + eType = pInput->aData[pInput->iNext]; + + switch( eType ){ + case 'T': { + /* A 'table' record consists of: + ** + ** * A constant 'T' character, + ** * Number of columns in said table (a varint), + ** * An array of nCol bytes (sPK), + ** * A nul-terminated table name. + */ + int nByte; + int nVar; + pInput->iNext++; + if( (rc = sessionChangesetBufferTblhdr(pInput, &nByte)) ){ + goto finished_invert; + } + nVar = sessionVarintGet(&pInput->aData[pInput->iNext], &nCol); + sPK.nBuf = 0; + sessionAppendBlob(&sPK, &pInput->aData[pInput->iNext+nVar], nCol, &rc); + sessionAppendByte(&sOut, eType, &rc); + sessionAppendBlob(&sOut, &pInput->aData[pInput->iNext], nByte, &rc); + if( rc ) goto finished_invert; + + pInput->iNext += nByte; + sqlite3_free(apVal); + apVal = 0; + abPK = sPK.aBuf; + break; + } + + case SQLITE_INSERT: + case SQLITE_DELETE: { + int nByte; + int bIndirect = pInput->aData[pInput->iNext+1]; + int eType2 = (eType==SQLITE_DELETE ? SQLITE_INSERT : SQLITE_DELETE); + pInput->iNext += 2; + assert( rc==SQLITE_OK ); + rc = sessionChangesetBufferRecord(pInput, nCol, &nByte); + sessionAppendByte(&sOut, eType2, &rc); + sessionAppendByte(&sOut, bIndirect, &rc); + sessionAppendBlob(&sOut, &pInput->aData[pInput->iNext], nByte, &rc); + pInput->iNext += nByte; + if( rc ) goto finished_invert; + break; + } + + case SQLITE_UPDATE: { + int iCol; + + if( 0==apVal ){ + apVal = (sqlite3_value **)sqlite3_malloc(sizeof(apVal[0])*nCol*2); + if( 0==apVal ){ + rc = SQLITE_NOMEM; + goto finished_invert; + } + memset(apVal, 0, sizeof(apVal[0])*nCol*2); + } + + /* Write the header for the new UPDATE change. Same as the original. */ + sessionAppendByte(&sOut, eType, &rc); + sessionAppendByte(&sOut, pInput->aData[pInput->iNext+1], &rc); + + /* Read the old.* and new.* records for the update change. */ + pInput->iNext += 2; + rc = sessionReadRecord(pInput, nCol, 0, &apVal[0]); + if( rc==SQLITE_OK ){ + rc = sessionReadRecord(pInput, nCol, 0, &apVal[nCol]); + } + + /* Write the new old.* record. Consists of the PK columns from the + ** original old.* record, and the other values from the original + ** new.* record. */ + for(iCol=0; iCol=SESSIONS_STRM_CHUNK_SIZE ){ + rc = xOutput(pOut, sOut.aBuf, sOut.nBuf); + sOut.nBuf = 0; + if( rc!=SQLITE_OK ) goto finished_invert; + } + } + + assert( rc==SQLITE_OK ); + if( pnInverted ){ + *pnInverted = sOut.nBuf; + *ppInverted = sOut.aBuf; + sOut.aBuf = 0; + }else if( sOut.nBuf>0 ){ + rc = xOutput(pOut, sOut.aBuf, sOut.nBuf); + } + + finished_invert: + sqlite3_free(sOut.aBuf); + sqlite3_free(apVal); + sqlite3_free(sPK.aBuf); + return rc; +} + + +/* +** Invert a changeset object. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_invert( + int nChangeset, /* Number of bytes in input */ + const void *pChangeset, /* Input changeset */ + int *pnInverted, /* OUT: Number of bytes in output changeset */ + void **ppInverted /* OUT: Inverse of pChangeset */ +){ + SessionInput sInput; + + /* Set up the input stream */ + memset(&sInput, 0, sizeof(SessionInput)); + sInput.nData = nChangeset; + sInput.aData = (u8*)pChangeset; + + return sessionChangesetInvert(&sInput, 0, 0, pnInverted, ppInverted); +} + +/* +** Streaming version of sqlite3changeset_invert(). +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_invert_strm( + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +){ + SessionInput sInput; + int rc; + + /* Set up the input stream */ + memset(&sInput, 0, sizeof(SessionInput)); + sInput.xInput = xInput; + sInput.pIn = pIn; + + rc = sessionChangesetInvert(&sInput, xOutput, pOut, 0, 0); + sqlite3_free(sInput.buf.aBuf); + return rc; +} + +typedef struct SessionApplyCtx SessionApplyCtx; +struct SessionApplyCtx { + sqlite3 *db; + sqlite3_stmt *pDelete; /* DELETE statement */ + sqlite3_stmt *pUpdate; /* UPDATE statement */ + sqlite3_stmt *pInsert; /* INSERT statement */ + sqlite3_stmt *pSelect; /* SELECT statement */ + int nCol; /* Size of azCol[] and abPK[] arrays */ + const char **azCol; /* Array of column names */ + u8 *abPK; /* Boolean array - true if column is in PK */ + + int bDeferConstraints; /* True to defer constraints */ + SessionBuffer constraints; /* Deferred constraints are stored here */ +}; + +/* +** Formulate a statement to DELETE a row from database db. Assuming a table +** structure like this: +** +** CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c)); +** +** The DELETE statement looks like this: +** +** DELETE FROM x WHERE a = :1 AND c = :3 AND (:5 OR b IS :2 AND d IS :4) +** +** Variable :5 (nCol+1) is a boolean. It should be set to 0 if we require +** matching b and d values, or 1 otherwise. The second case comes up if the +** conflict handler is invoked with NOTFOUND and returns CHANGESET_REPLACE. +** +** If successful, SQLITE_OK is returned and SessionApplyCtx.pDelete is left +** pointing to the prepared version of the SQL statement. +*/ +static int sessionDeleteRow( + sqlite3 *db, /* Database handle */ + const char *zTab, /* Table name */ + SessionApplyCtx *p /* Session changeset-apply context */ +){ + int i; + const char *zSep = ""; + int rc = SQLITE_OK; + SessionBuffer buf = {0, 0, 0}; + int nPk = 0; + + sessionAppendStr(&buf, "DELETE FROM ", &rc); + sessionAppendIdent(&buf, zTab, &rc); + sessionAppendStr(&buf, " WHERE ", &rc); + + for(i=0; inCol; i++){ + if( p->abPK[i] ){ + nPk++; + sessionAppendStr(&buf, zSep, &rc); + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " = ?", &rc); + sessionAppendInteger(&buf, i+1, &rc); + zSep = " AND "; + } + } + + if( nPknCol ){ + sessionAppendStr(&buf, " AND (?", &rc); + sessionAppendInteger(&buf, p->nCol+1, &rc); + sessionAppendStr(&buf, " OR ", &rc); + + zSep = ""; + for(i=0; inCol; i++){ + if( !p->abPK[i] ){ + sessionAppendStr(&buf, zSep, &rc); + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " IS ?", &rc); + sessionAppendInteger(&buf, i+1, &rc); + zSep = "AND "; + } + } + sessionAppendStr(&buf, ")", &rc); + } + + if( rc==SQLITE_OK ){ + rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, &p->pDelete, 0); + } + sqlite3_free(buf.aBuf); + + return rc; +} + +/* +** Formulate and prepare a statement to UPDATE a row from database db. +** Assuming a table structure like this: +** +** CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c)); +** +** The UPDATE statement looks like this: +** +** UPDATE x SET +** a = CASE WHEN ?2 THEN ?3 ELSE a END, +** b = CASE WHEN ?5 THEN ?6 ELSE b END, +** c = CASE WHEN ?8 THEN ?9 ELSE c END, +** d = CASE WHEN ?11 THEN ?12 ELSE d END +** WHERE a = ?1 AND c = ?7 AND (?13 OR +** (?5==0 OR b IS ?4) AND (?11==0 OR d IS ?10) AND +** ) +** +** For each column in the table, there are three variables to bind: +** +** ?(i*3+1) The old.* value of the column, if any. +** ?(i*3+2) A boolean flag indicating that the value is being modified. +** ?(i*3+3) The new.* value of the column, if any. +** +** Also, a boolean flag that, if set to true, causes the statement to update +** a row even if the non-PK values do not match. This is required if the +** conflict-handler is invoked with CHANGESET_DATA and returns +** CHANGESET_REPLACE. This is variable "?(nCol*3+1)". +** +** If successful, SQLITE_OK is returned and SessionApplyCtx.pUpdate is left +** pointing to the prepared version of the SQL statement. +*/ +static int sessionUpdateRow( + sqlite3 *db, /* Database handle */ + const char *zTab, /* Table name */ + SessionApplyCtx *p /* Session changeset-apply context */ +){ + int rc = SQLITE_OK; + int i; + const char *zSep = ""; + SessionBuffer buf = {0, 0, 0}; + + /* Append "UPDATE tbl SET " */ + sessionAppendStr(&buf, "UPDATE ", &rc); + sessionAppendIdent(&buf, zTab, &rc); + sessionAppendStr(&buf, " SET ", &rc); + + /* Append the assignments */ + for(i=0; inCol; i++){ + sessionAppendStr(&buf, zSep, &rc); + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " = CASE WHEN ?", &rc); + sessionAppendInteger(&buf, i*3+2, &rc); + sessionAppendStr(&buf, " THEN ?", &rc); + sessionAppendInteger(&buf, i*3+3, &rc); + sessionAppendStr(&buf, " ELSE ", &rc); + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " END", &rc); + zSep = ", "; + } + + /* Append the PK part of the WHERE clause */ + sessionAppendStr(&buf, " WHERE ", &rc); + for(i=0; inCol; i++){ + if( p->abPK[i] ){ + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " = ?", &rc); + sessionAppendInteger(&buf, i*3+1, &rc); + sessionAppendStr(&buf, " AND ", &rc); + } + } + + /* Append the non-PK part of the WHERE clause */ + sessionAppendStr(&buf, " (?", &rc); + sessionAppendInteger(&buf, p->nCol*3+1, &rc); + sessionAppendStr(&buf, " OR 1", &rc); + for(i=0; inCol; i++){ + if( !p->abPK[i] ){ + sessionAppendStr(&buf, " AND (?", &rc); + sessionAppendInteger(&buf, i*3+2, &rc); + sessionAppendStr(&buf, "=0 OR ", &rc); + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " IS ?", &rc); + sessionAppendInteger(&buf, i*3+1, &rc); + sessionAppendStr(&buf, ")", &rc); + } + } + sessionAppendStr(&buf, ")", &rc); + + if( rc==SQLITE_OK ){ + rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, &p->pUpdate, 0); + } + sqlite3_free(buf.aBuf); + + return rc; +} + +/* +** Formulate and prepare an SQL statement to query table zTab by primary +** key. Assuming the following table structure: +** +** CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c)); +** +** The SELECT statement looks like this: +** +** SELECT * FROM x WHERE a = ?1 AND c = ?3 +** +** If successful, SQLITE_OK is returned and SessionApplyCtx.pSelect is left +** pointing to the prepared version of the SQL statement. +*/ +static int sessionSelectRow( + sqlite3 *db, /* Database handle */ + const char *zTab, /* Table name */ + SessionApplyCtx *p /* Session changeset-apply context */ +){ + return sessionSelectStmt( + db, "main", zTab, p->nCol, p->azCol, p->abPK, &p->pSelect); +} + +/* +** Formulate and prepare an INSERT statement to add a record to table zTab. +** For example: +** +** INSERT INTO main."zTab" VALUES(?1, ?2, ?3 ...); +** +** If successful, SQLITE_OK is returned and SessionApplyCtx.pInsert is left +** pointing to the prepared version of the SQL statement. +*/ +static int sessionInsertRow( + sqlite3 *db, /* Database handle */ + const char *zTab, /* Table name */ + SessionApplyCtx *p /* Session changeset-apply context */ +){ + int rc = SQLITE_OK; + int i; + SessionBuffer buf = {0, 0, 0}; + + sessionAppendStr(&buf, "INSERT INTO main.", &rc); + sessionAppendIdent(&buf, zTab, &rc); + sessionAppendStr(&buf, " VALUES(?", &rc); + for(i=1; inCol; i++){ + sessionAppendStr(&buf, ", ?", &rc); + } + sessionAppendStr(&buf, ")", &rc); + + if( rc==SQLITE_OK ){ + rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, &p->pInsert, 0); + } + sqlite3_free(buf.aBuf); + return rc; +} + +/* +** A wrapper around sqlite3_bind_value() that detects an extra problem. +** See comments in the body of this function for details. +*/ +static int sessionBindValue( + sqlite3_stmt *pStmt, /* Statement to bind value to */ + int i, /* Parameter number to bind to */ + sqlite3_value *pVal /* Value to bind */ +){ + int eType = sqlite3_value_type(pVal); + /* COVERAGE: The (pVal->z==0) branch is never true using current versions + ** of SQLite. If a malloc fails in an sqlite3_value_xxx() function, either + ** the (pVal->z) variable remains as it was or the type of the value is + ** set to SQLITE_NULL. */ + if( (eType==SQLITE_TEXT || eType==SQLITE_BLOB) && pVal->z==0 ){ + /* This condition occurs when an earlier OOM in a call to + ** sqlite3_value_text() or sqlite3_value_blob() (perhaps from within + ** a conflict-handler) has zeroed the pVal->z pointer. Return NOMEM. */ + return SQLITE_NOMEM; + } + return sqlite3_bind_value(pStmt, i, pVal); +} + +/* +** Iterator pIter must point to an SQLITE_INSERT entry. This function +** transfers new.* values from the current iterator entry to statement +** pStmt. The table being inserted into has nCol columns. +** +** New.* value $i from the iterator is bound to variable ($i+1) of +** statement pStmt. If parameter abPK is NULL, all values from 0 to (nCol-1) +** are transfered to the statement. Otherwise, if abPK is not NULL, it points +** to an array nCol elements in size. In this case only those values for +** which abPK[$i] is true are read from the iterator and bound to the +** statement. +** +** An SQLite error code is returned if an error occurs. Otherwise, SQLITE_OK. +*/ +static int sessionBindRow( + sqlite3_changeset_iter *pIter, /* Iterator to read values from */ + int(*xValue)(sqlite3_changeset_iter *, int, sqlite3_value **), + int nCol, /* Number of columns */ + u8 *abPK, /* If not NULL, bind only if true */ + sqlite3_stmt *pStmt /* Bind values to this statement */ +){ + int i; + int rc = SQLITE_OK; + + /* Neither sqlite3changeset_old or sqlite3changeset_new can fail if the + ** argument iterator points to a suitable entry. Make sure that xValue + ** is one of these to guarantee that it is safe to ignore the return + ** in the code below. */ + assert( xValue==sqlite3changeset_old || xValue==sqlite3changeset_new ); + + for(i=0; rc==SQLITE_OK && idb, pIter, p->abPK, p->pSelect); + }else{ + rc = SQLITE_OK; + } + + if( rc==SQLITE_ROW ){ + /* There exists another row with the new.* primary key. */ + pIter->pConflict = p->pSelect; + res = xConflict(pCtx, eType, pIter); + pIter->pConflict = 0; + rc = sqlite3_reset(p->pSelect); + }else if( rc==SQLITE_OK ){ + if( p->bDeferConstraints && eType==SQLITE_CHANGESET_CONFLICT ){ + /* Instead of invoking the conflict handler, append the change blob + ** to the SessionApplyCtx.constraints buffer. */ + u8 *aBlob = &pIter->in.aData[pIter->in.iCurrent]; + int nBlob = pIter->in.iNext - pIter->in.iCurrent; + sessionAppendBlob(&p->constraints, aBlob, nBlob, &rc); + res = SQLITE_CHANGESET_OMIT; + }else{ + /* No other row with the new.* primary key. */ + res = xConflict(pCtx, eType+1, pIter); + if( res==SQLITE_CHANGESET_REPLACE ) rc = SQLITE_MISUSE; + } + } + + if( rc==SQLITE_OK ){ + switch( res ){ + case SQLITE_CHANGESET_REPLACE: + assert( pbReplace ); + *pbReplace = 1; + break; + + case SQLITE_CHANGESET_OMIT: + break; + + case SQLITE_CHANGESET_ABORT: + rc = SQLITE_ABORT; + break; + + default: + rc = SQLITE_MISUSE; + break; + } + } + + return rc; +} + +/* +** Attempt to apply the change that the iterator passed as the first argument +** currently points to to the database. If a conflict is encountered, invoke +** the conflict handler callback. +** +** If argument pbRetry is NULL, then ignore any CHANGESET_DATA conflict. If +** one is encountered, update or delete the row with the matching primary key +** instead. Or, if pbRetry is not NULL and a CHANGESET_DATA conflict occurs, +** invoke the conflict handler. If it returns CHANGESET_REPLACE, set *pbRetry +** to true before returning. In this case the caller will invoke this function +** again, this time with pbRetry set to NULL. +** +** If argument pbReplace is NULL and a CHANGESET_CONFLICT conflict is +** encountered invoke the conflict handler with CHANGESET_CONSTRAINT instead. +** Or, if pbReplace is not NULL, invoke it with CHANGESET_CONFLICT. If such +** an invocation returns SQLITE_CHANGESET_REPLACE, set *pbReplace to true +** before retrying. In this case the caller attempts to remove the conflicting +** row before invoking this function again, this time with pbReplace set +** to NULL. +** +** If any conflict handler returns SQLITE_CHANGESET_ABORT, this function +** returns SQLITE_ABORT. Otherwise, if no error occurs, SQLITE_OK is +** returned. +*/ +static int sessionApplyOneOp( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + SessionApplyCtx *p, /* changeset_apply() context */ + int(*xConflict)(void *, int, sqlite3_changeset_iter *), + void *pCtx, /* First argument for the conflict handler */ + int *pbReplace, /* OUT: True to remove PK row and retry */ + int *pbRetry /* OUT: True to retry. */ +){ + const char *zDummy; + int op; + int nCol; + int rc = SQLITE_OK; + + assert( p->pDelete && p->pUpdate && p->pInsert && p->pSelect ); + assert( p->azCol && p->abPK ); + assert( !pbReplace || *pbReplace==0 ); + + sqlite3changeset_op(pIter, &zDummy, &nCol, &op, 0); + + if( op==SQLITE_DELETE ){ + + /* Bind values to the DELETE statement. If conflict handling is required, + ** bind values for all columns and set bound variable (nCol+1) to true. + ** Or, if conflict handling is not required, bind just the PK column + ** values and, if it exists, set (nCol+1) to false. Conflict handling + ** is not required if: + ** + ** * this is a patchset, or + ** * (pbRetry==0), or + ** * all columns of the table are PK columns (in this case there is + ** no (nCol+1) variable to bind to). + */ + u8 *abPK = (pIter->bPatchset ? p->abPK : 0); + rc = sessionBindRow(pIter, sqlite3changeset_old, nCol, abPK, p->pDelete); + if( rc==SQLITE_OK && sqlite3_bind_parameter_count(p->pDelete)>nCol ){ + rc = sqlite3_bind_int(p->pDelete, nCol+1, (pbRetry==0 || abPK)); + } + if( rc!=SQLITE_OK ) return rc; + + sqlite3_step(p->pDelete); + rc = sqlite3_reset(p->pDelete); + if( rc==SQLITE_OK && sqlite3_changes(p->db)==0 ){ + rc = sessionConflictHandler( + SQLITE_CHANGESET_DATA, p, pIter, xConflict, pCtx, pbRetry + ); + }else if( (rc&0xff)==SQLITE_CONSTRAINT ){ + rc = sessionConflictHandler( + SQLITE_CHANGESET_CONFLICT, p, pIter, xConflict, pCtx, 0 + ); + } + + }else if( op==SQLITE_UPDATE ){ + int i; + + /* Bind values to the UPDATE statement. */ + for(i=0; rc==SQLITE_OK && ipUpdate, i*3+2, !!pNew); + if( pOld ){ + rc = sessionBindValue(p->pUpdate, i*3+1, pOld); + } + if( rc==SQLITE_OK && pNew ){ + rc = sessionBindValue(p->pUpdate, i*3+3, pNew); + } + } + if( rc==SQLITE_OK ){ + sqlite3_bind_int(p->pUpdate, nCol*3+1, pbRetry==0 || pIter->bPatchset); + } + if( rc!=SQLITE_OK ) return rc; + + /* Attempt the UPDATE. In the case of a NOTFOUND or DATA conflict, + ** the result will be SQLITE_OK with 0 rows modified. */ + sqlite3_step(p->pUpdate); + rc = sqlite3_reset(p->pUpdate); + + if( rc==SQLITE_OK && sqlite3_changes(p->db)==0 ){ + /* A NOTFOUND or DATA error. Search the table to see if it contains + ** a row with a matching primary key. If so, this is a DATA conflict. + ** Otherwise, if there is no primary key match, it is a NOTFOUND. */ + + rc = sessionConflictHandler( + SQLITE_CHANGESET_DATA, p, pIter, xConflict, pCtx, pbRetry + ); + + }else if( (rc&0xff)==SQLITE_CONSTRAINT ){ + /* This is always a CONSTRAINT conflict. */ + rc = sessionConflictHandler( + SQLITE_CHANGESET_CONFLICT, p, pIter, xConflict, pCtx, 0 + ); + } + + }else{ + assert( op==SQLITE_INSERT ); + rc = sessionBindRow(pIter, sqlite3changeset_new, nCol, 0, p->pInsert); + if( rc!=SQLITE_OK ) return rc; + + sqlite3_step(p->pInsert); + rc = sqlite3_reset(p->pInsert); + if( (rc&0xff)==SQLITE_CONSTRAINT ){ + rc = sessionConflictHandler( + SQLITE_CHANGESET_CONFLICT, p, pIter, xConflict, pCtx, pbReplace + ); + } + } + + return rc; +} + +/* +** Attempt to apply the change that the iterator passed as the first argument +** currently points to to the database. If a conflict is encountered, invoke +** the conflict handler callback. +** +** The difference between this function and sessionApplyOne() is that this +** function handles the case where the conflict-handler is invoked and +** returns SQLITE_CHANGESET_REPLACE - indicating that the change should be +** retried in some manner. +*/ +static int sessionApplyOneWithRetry( + sqlite3 *db, /* Apply change to "main" db of this handle */ + sqlite3_changeset_iter *pIter, /* Changeset iterator to read change from */ + SessionApplyCtx *pApply, /* Apply context */ + int(*xConflict)(void*, int, sqlite3_changeset_iter*), + void *pCtx /* First argument passed to xConflict */ +){ + int bReplace = 0; + int bRetry = 0; + int rc; + + rc = sessionApplyOneOp(pIter, pApply, xConflict, pCtx, &bReplace, &bRetry); + assert( rc==SQLITE_OK || (bRetry==0 && bReplace==0) ); + + /* If the bRetry flag is set, the change has not been applied due to an + ** SQLITE_CHANGESET_DATA problem (i.e. this is an UPDATE or DELETE and + ** a row with the correct PK is present in the db, but one or more other + ** fields do not contain the expected values) and the conflict handler + ** returned SQLITE_CHANGESET_REPLACE. In this case retry the operation, + ** but pass NULL as the final argument so that sessionApplyOneOp() ignores + ** the SQLITE_CHANGESET_DATA problem. */ + if( bRetry ){ + assert( pIter->op==SQLITE_UPDATE || pIter->op==SQLITE_DELETE ); + rc = sessionApplyOneOp(pIter, pApply, xConflict, pCtx, 0, 0); + } + + /* If the bReplace flag is set, the change is an INSERT that has not + ** been performed because the database already contains a row with the + ** specified primary key and the conflict handler returned + ** SQLITE_CHANGESET_REPLACE. In this case remove the conflicting row + ** before reattempting the INSERT. */ + else if( bReplace ){ + assert( pIter->op==SQLITE_INSERT ); + rc = sqlite3_exec(db, "SAVEPOINT replace_op", 0, 0, 0); + if( rc==SQLITE_OK ){ + rc = sessionBindRow(pIter, + sqlite3changeset_new, pApply->nCol, pApply->abPK, pApply->pDelete); + sqlite3_bind_int(pApply->pDelete, pApply->nCol+1, 1); + } + if( rc==SQLITE_OK ){ + sqlite3_step(pApply->pDelete); + rc = sqlite3_reset(pApply->pDelete); + } + if( rc==SQLITE_OK ){ + rc = sessionApplyOneOp(pIter, pApply, xConflict, pCtx, 0, 0); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(db, "RELEASE replace_op", 0, 0, 0); + } + } + + return rc; +} + +/* +** Retry the changes accumulated in the pApply->constraints buffer. +*/ +static int sessionRetryConstraints( + sqlite3 *db, + int bPatchset, + const char *zTab, + SessionApplyCtx *pApply, + int(*xConflict)(void*, int, sqlite3_changeset_iter*), + void *pCtx /* First argument passed to xConflict */ +){ + int rc = SQLITE_OK; + + while( pApply->constraints.nBuf ){ + sqlite3_changeset_iter *pIter2 = 0; + SessionBuffer cons = pApply->constraints; + memset(&pApply->constraints, 0, sizeof(SessionBuffer)); + + rc = sessionChangesetStart(&pIter2, 0, 0, cons.nBuf, cons.aBuf); + if( rc==SQLITE_OK ){ + int nByte = 2*pApply->nCol*sizeof(sqlite3_value*); + int rc2; + pIter2->bPatchset = bPatchset; + pIter2->zTab = (char*)zTab; + pIter2->nCol = pApply->nCol; + pIter2->abPK = pApply->abPK; + sessionBufferGrow(&pIter2->tblhdr, nByte, &rc); + pIter2->apValue = (sqlite3_value**)pIter2->tblhdr.aBuf; + if( rc==SQLITE_OK ) memset(pIter2->apValue, 0, nByte); + + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3changeset_next(pIter2) ){ + rc = sessionApplyOneWithRetry(db, pIter2, pApply, xConflict, pCtx); + } + + rc2 = sqlite3changeset_finalize(pIter2); + if( rc==SQLITE_OK ) rc = rc2; + } + assert( pApply->bDeferConstraints || pApply->constraints.nBuf==0 ); + + sqlite3_free(cons.aBuf); + if( rc!=SQLITE_OK ) break; + if( pApply->constraints.nBuf>=cons.nBuf ){ + /* No progress was made on the last round. */ + pApply->bDeferConstraints = 0; + } + } + + return rc; +} + +/* +** Argument pIter is a changeset iterator that has been initialized, but +** not yet passed to sqlite3changeset_next(). This function applies the +** changeset to the main database attached to handle "db". The supplied +** conflict handler callback is invoked to resolve any conflicts encountered +** while applying the change. +*/ +static int sessionChangesetApply( + sqlite3 *db, /* Apply change to "main" db of this handle */ + sqlite3_changeset_iter *pIter, /* Changeset to apply */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of fifth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +){ + int schemaMismatch = 0; + int rc; /* Return code */ + const char *zTab = 0; /* Name of current table */ + int nTab = 0; /* Result of sqlite3Strlen30(zTab) */ + SessionApplyCtx sApply; /* changeset_apply() context object */ + int bPatchset; + + assert( xConflict!=0 ); + + pIter->in.bNoDiscard = 1; + memset(&sApply, 0, sizeof(sApply)); + sqlite3_mutex_enter(sqlite3_db_mutex(db)); + rc = sqlite3_exec(db, "SAVEPOINT changeset_apply", 0, 0, 0); + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(db, "PRAGMA defer_foreign_keys = 1", 0, 0, 0); + } + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3changeset_next(pIter) ){ + int nCol; + int op; + const char *zNew; + + sqlite3changeset_op(pIter, &zNew, &nCol, &op, 0); + + if( zTab==0 || sqlite3_strnicmp(zNew, zTab, nTab+1) ){ + u8 *abPK; + + rc = sessionRetryConstraints( + db, pIter->bPatchset, zTab, &sApply, xConflict, pCtx + ); + if( rc!=SQLITE_OK ) break; + + sqlite3_free((char*)sApply.azCol); /* cast works around VC++ bug */ + sqlite3_finalize(sApply.pDelete); + sqlite3_finalize(sApply.pUpdate); + sqlite3_finalize(sApply.pInsert); + sqlite3_finalize(sApply.pSelect); + memset(&sApply, 0, sizeof(sApply)); + sApply.db = db; + sApply.bDeferConstraints = 1; + + /* If an xFilter() callback was specified, invoke it now. If the + ** xFilter callback returns zero, skip this table. If it returns + ** non-zero, proceed. */ + schemaMismatch = (xFilter && (0==xFilter(pCtx, zNew))); + if( schemaMismatch ){ + zTab = sqlite3_mprintf("%s", zNew); + if( zTab==0 ){ + rc = SQLITE_NOMEM; + break; + } + nTab = (int)strlen(zTab); + sApply.azCol = (const char **)zTab; + }else{ + sqlite3changeset_pk(pIter, &abPK, 0); + rc = sessionTableInfo( + db, "main", zNew, &sApply.nCol, &zTab, &sApply.azCol, &sApply.abPK + ); + if( rc!=SQLITE_OK ) break; + + if( sApply.nCol==0 ){ + schemaMismatch = 1; + sqlite3_log(SQLITE_SCHEMA, + "sqlite3changeset_apply(): no such table: %s", zTab + ); + } + else if( sApply.nCol!=nCol ){ + schemaMismatch = 1; + sqlite3_log(SQLITE_SCHEMA, + "sqlite3changeset_apply(): table %s has %d columns, expected %d", + zTab, sApply.nCol, nCol + ); + } + else if( memcmp(sApply.abPK, abPK, nCol)!=0 ){ + schemaMismatch = 1; + sqlite3_log(SQLITE_SCHEMA, "sqlite3changeset_apply(): " + "primary key mismatch for table %s", zTab + ); + } + else if( + (rc = sessionSelectRow(db, zTab, &sApply)) + || (rc = sessionUpdateRow(db, zTab, &sApply)) + || (rc = sessionDeleteRow(db, zTab, &sApply)) + || (rc = sessionInsertRow(db, zTab, &sApply)) + ){ + break; + } + nTab = sqlite3Strlen30(zTab); + } + } + + /* If there is a schema mismatch on the current table, proceed to the + ** next change. A log message has already been issued. */ + if( schemaMismatch ) continue; + + rc = sessionApplyOneWithRetry(db, pIter, &sApply, xConflict, pCtx); + } + + bPatchset = pIter->bPatchset; + if( rc==SQLITE_OK ){ + rc = sqlite3changeset_finalize(pIter); + }else{ + sqlite3changeset_finalize(pIter); + } + + if( rc==SQLITE_OK ){ + rc = sessionRetryConstraints(db, bPatchset, zTab, &sApply, xConflict, pCtx); + } + + if( rc==SQLITE_OK ){ + int nFk, notUsed; + sqlite3_db_status(db, SQLITE_DBSTATUS_DEFERRED_FKS, &nFk, ¬Used, 0); + if( nFk!=0 ){ + int res = SQLITE_CHANGESET_ABORT; + sqlite3_changeset_iter sIter; + memset(&sIter, 0, sizeof(sIter)); + sIter.nCol = nFk; + res = xConflict(pCtx, SQLITE_CHANGESET_FOREIGN_KEY, &sIter); + if( res!=SQLITE_CHANGESET_OMIT ){ + rc = SQLITE_CONSTRAINT; + } + } + } + sqlite3_exec(db, "PRAGMA defer_foreign_keys = 0", 0, 0, 0); + + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(db, "RELEASE changeset_apply", 0, 0, 0); + }else{ + sqlite3_exec(db, "ROLLBACK TO changeset_apply", 0, 0, 0); + sqlite3_exec(db, "RELEASE changeset_apply", 0, 0, 0); + } + + sqlite3_finalize(sApply.pInsert); + sqlite3_finalize(sApply.pDelete); + sqlite3_finalize(sApply.pUpdate); + sqlite3_finalize(sApply.pSelect); + sqlite3_free((char*)sApply.azCol); /* cast works around VC++ bug */ + sqlite3_free((char*)sApply.constraints.aBuf); + sqlite3_mutex_leave(sqlite3_db_mutex(db)); + return rc; +} + +/* +** Apply the changeset passed via pChangeset/nChangeset to the main database +** attached to handle "db". Invoke the supplied conflict handler callback +** to resolve any conflicts encountered while applying the change. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_apply( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int nChangeset, /* Size of changeset in bytes */ + void *pChangeset, /* Changeset blob */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of fifth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +){ + sqlite3_changeset_iter *pIter; /* Iterator to skip through changeset */ + int rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset); + if( rc==SQLITE_OK ){ + rc = sessionChangesetApply(db, pIter, xFilter, xConflict, pCtx); + } + return rc; +} + +/* +** Apply the changeset passed via xInput/pIn to the main database +** attached to handle "db". Invoke the supplied conflict handler callback +** to resolve any conflicts encountered while applying the change. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_apply_strm( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int (*xInput)(void *pIn, void *pData, int *pnData), /* Input function */ + void *pIn, /* First arg for xInput */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +){ + sqlite3_changeset_iter *pIter; /* Iterator to skip through changeset */ + int rc = sqlite3changeset_start_strm(&pIter, xInput, pIn); + if( rc==SQLITE_OK ){ + rc = sessionChangesetApply(db, pIter, xFilter, xConflict, pCtx); + } + return rc; +} + +/* +** sqlite3_changegroup handle. +*/ +struct sqlite3_changegroup { + int rc; /* Error code */ + int bPatch; /* True to accumulate patchsets */ + SessionTable *pList; /* List of tables in current patch */ +}; + +/* +** This function is called to merge two changes to the same row together as +** part of an sqlite3changeset_concat() operation. A new change object is +** allocated and a pointer to it stored in *ppNew. +*/ +static int sessionChangeMerge( + SessionTable *pTab, /* Table structure */ + int bPatchset, /* True for patchsets */ + SessionChange *pExist, /* Existing change */ + int op2, /* Second change operation */ + int bIndirect, /* True if second change is indirect */ + u8 *aRec, /* Second change record */ + int nRec, /* Number of bytes in aRec */ + SessionChange **ppNew /* OUT: Merged change */ +){ + SessionChange *pNew = 0; + + if( !pExist ){ + pNew = (SessionChange *)sqlite3_malloc(sizeof(SessionChange) + nRec); + if( !pNew ){ + return SQLITE_NOMEM; + } + memset(pNew, 0, sizeof(SessionChange)); + pNew->op = op2; + pNew->bIndirect = bIndirect; + pNew->nRecord = nRec; + pNew->aRecord = (u8*)&pNew[1]; + memcpy(pNew->aRecord, aRec, nRec); + }else{ + int op1 = pExist->op; + + /* + ** op1=INSERT, op2=INSERT -> Unsupported. Discard op2. + ** op1=INSERT, op2=UPDATE -> INSERT. + ** op1=INSERT, op2=DELETE -> (none) + ** + ** op1=UPDATE, op2=INSERT -> Unsupported. Discard op2. + ** op1=UPDATE, op2=UPDATE -> UPDATE. + ** op1=UPDATE, op2=DELETE -> DELETE. + ** + ** op1=DELETE, op2=INSERT -> UPDATE. + ** op1=DELETE, op2=UPDATE -> Unsupported. Discard op2. + ** op1=DELETE, op2=DELETE -> Unsupported. Discard op2. + */ + if( (op1==SQLITE_INSERT && op2==SQLITE_INSERT) + || (op1==SQLITE_UPDATE && op2==SQLITE_INSERT) + || (op1==SQLITE_DELETE && op2==SQLITE_UPDATE) + || (op1==SQLITE_DELETE && op2==SQLITE_DELETE) + ){ + pNew = pExist; + }else if( op1==SQLITE_INSERT && op2==SQLITE_DELETE ){ + sqlite3_free(pExist); + assert( pNew==0 ); + }else{ + u8 *aExist = pExist->aRecord; + int nByte; + u8 *aCsr; + + /* Allocate a new SessionChange object. Ensure that the aRecord[] + ** buffer of the new object is large enough to hold any record that + ** may be generated by combining the input records. */ + nByte = sizeof(SessionChange) + pExist->nRecord + nRec; + pNew = (SessionChange *)sqlite3_malloc(nByte); + if( !pNew ){ + sqlite3_free(pExist); + return SQLITE_NOMEM; + } + memset(pNew, 0, sizeof(SessionChange)); + pNew->bIndirect = (bIndirect && pExist->bIndirect); + aCsr = pNew->aRecord = (u8 *)&pNew[1]; + + if( op1==SQLITE_INSERT ){ /* INSERT + UPDATE */ + u8 *a1 = aRec; + assert( op2==SQLITE_UPDATE ); + pNew->op = SQLITE_INSERT; + if( bPatchset==0 ) sessionSkipRecord(&a1, pTab->nCol); + sessionMergeRecord(&aCsr, pTab->nCol, aExist, a1); + }else if( op1==SQLITE_DELETE ){ /* DELETE + INSERT */ + assert( op2==SQLITE_INSERT ); + pNew->op = SQLITE_UPDATE; + if( bPatchset ){ + memcpy(aCsr, aRec, nRec); + aCsr += nRec; + }else{ + if( 0==sessionMergeUpdate(&aCsr, pTab, bPatchset, aExist, 0,aRec,0) ){ + sqlite3_free(pNew); + pNew = 0; + } + } + }else if( op2==SQLITE_UPDATE ){ /* UPDATE + UPDATE */ + u8 *a1 = aExist; + u8 *a2 = aRec; + assert( op1==SQLITE_UPDATE ); + if( bPatchset==0 ){ + sessionSkipRecord(&a1, pTab->nCol); + sessionSkipRecord(&a2, pTab->nCol); + } + pNew->op = SQLITE_UPDATE; + if( 0==sessionMergeUpdate(&aCsr, pTab, bPatchset, aRec, aExist,a1,a2) ){ + sqlite3_free(pNew); + pNew = 0; + } + }else{ /* UPDATE + DELETE */ + assert( op1==SQLITE_UPDATE && op2==SQLITE_DELETE ); + pNew->op = SQLITE_DELETE; + if( bPatchset ){ + memcpy(aCsr, aRec, nRec); + aCsr += nRec; + }else{ + sessionMergeRecord(&aCsr, pTab->nCol, aRec, aExist); + } + } + + if( pNew ){ + pNew->nRecord = (int)(aCsr - pNew->aRecord); + } + sqlite3_free(pExist); + } + } + + *ppNew = pNew; + return SQLITE_OK; +} + +/* +** Add all changes in the changeset traversed by the iterator passed as +** the first argument to the changegroup hash tables. +*/ +static int sessionChangesetToHash( + sqlite3_changeset_iter *pIter, /* Iterator to read from */ + sqlite3_changegroup *pGrp /* Changegroup object to add changeset to */ +){ + u8 *aRec; + int nRec; + int rc = SQLITE_OK; + SessionTable *pTab = 0; + + + while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec) ){ + const char *zNew; + int nCol; + int op; + int iHash; + int bIndirect; + SessionChange *pChange; + SessionChange *pExist = 0; + SessionChange **pp; + + if( pGrp->pList==0 ){ + pGrp->bPatch = pIter->bPatchset; + }else if( pIter->bPatchset!=pGrp->bPatch ){ + rc = SQLITE_ERROR; + break; + } + + sqlite3changeset_op(pIter, &zNew, &nCol, &op, &bIndirect); + if( !pTab || sqlite3_stricmp(zNew, pTab->zName) ){ + /* Search the list for a matching table */ + int nNew = (int)strlen(zNew); + u8 *abPK; + + sqlite3changeset_pk(pIter, &abPK, 0); + for(pTab = pGrp->pList; pTab; pTab=pTab->pNext){ + if( 0==sqlite3_strnicmp(pTab->zName, zNew, nNew+1) ) break; + } + if( !pTab ){ + SessionTable **ppTab; + + pTab = sqlite3_malloc(sizeof(SessionTable) + nCol + nNew+1); + if( !pTab ){ + rc = SQLITE_NOMEM; + break; + } + memset(pTab, 0, sizeof(SessionTable)); + pTab->nCol = nCol; + pTab->abPK = (u8*)&pTab[1]; + memcpy(pTab->abPK, abPK, nCol); + pTab->zName = (char*)&pTab->abPK[nCol]; + memcpy(pTab->zName, zNew, nNew+1); + + /* The new object must be linked on to the end of the list, not + ** simply added to the start of it. This is to ensure that the + ** tables within the output of sqlite3changegroup_output() are in + ** the right order. */ + for(ppTab=&pGrp->pList; *ppTab; ppTab=&(*ppTab)->pNext); + *ppTab = pTab; + }else if( pTab->nCol!=nCol || memcmp(pTab->abPK, abPK, nCol) ){ + rc = SQLITE_SCHEMA; + break; + } + } + + if( sessionGrowHash(pIter->bPatchset, pTab) ){ + rc = SQLITE_NOMEM; + break; + } + iHash = sessionChangeHash( + pTab, (pIter->bPatchset && op==SQLITE_DELETE), aRec, pTab->nChange + ); + + /* Search for existing entry. If found, remove it from the hash table. + ** Code below may link it back in. + */ + for(pp=&pTab->apChange[iHash]; *pp; pp=&(*pp)->pNext){ + int bPkOnly1 = 0; + int bPkOnly2 = 0; + if( pIter->bPatchset ){ + bPkOnly1 = (*pp)->op==SQLITE_DELETE; + bPkOnly2 = op==SQLITE_DELETE; + } + if( sessionChangeEqual(pTab, bPkOnly1, (*pp)->aRecord, bPkOnly2, aRec) ){ + pExist = *pp; + *pp = (*pp)->pNext; + pTab->nEntry--; + break; + } + } + + rc = sessionChangeMerge(pTab, + pIter->bPatchset, pExist, op, bIndirect, aRec, nRec, &pChange + ); + if( rc ) break; + if( pChange ){ + pChange->pNext = pTab->apChange[iHash]; + pTab->apChange[iHash] = pChange; + pTab->nEntry++; + } + } + + if( rc==SQLITE_OK ) rc = pIter->rc; + return rc; +} + +/* +** Serialize a changeset (or patchset) based on all changesets (or patchsets) +** added to the changegroup object passed as the first argument. +** +** If xOutput is not NULL, then the changeset/patchset is returned to the +** user via one or more calls to xOutput, as with the other streaming +** interfaces. +** +** Or, if xOutput is NULL, then (*ppOut) is populated with a pointer to a +** buffer containing the output changeset before this function returns. In +** this case (*pnOut) is set to the size of the output buffer in bytes. It +** is the responsibility of the caller to free the output buffer using +** sqlite3_free() when it is no longer required. +** +** If successful, SQLITE_OK is returned. Or, if an error occurs, an SQLite +** error code. If an error occurs and xOutput is NULL, (*ppOut) and (*pnOut) +** are both set to 0 before returning. +*/ +static int sessionChangegroupOutput( + sqlite3_changegroup *pGrp, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut, + int *pnOut, + void **ppOut +){ + int rc = SQLITE_OK; + SessionBuffer buf = {0, 0, 0}; + SessionTable *pTab; + assert( xOutput==0 || (ppOut==0 && pnOut==0) ); + + /* Create the serialized output changeset based on the contents of the + ** hash tables attached to the SessionTable objects in list p->pList. + */ + for(pTab=pGrp->pList; rc==SQLITE_OK && pTab; pTab=pTab->pNext){ + int i; + if( pTab->nEntry==0 ) continue; + + sessionAppendTableHdr(&buf, pGrp->bPatch, pTab, &rc); + for(i=0; inChange; i++){ + SessionChange *p; + for(p=pTab->apChange[i]; p; p=p->pNext){ + sessionAppendByte(&buf, p->op, &rc); + sessionAppendByte(&buf, p->bIndirect, &rc); + sessionAppendBlob(&buf, p->aRecord, p->nRecord, &rc); + } + } + + if( rc==SQLITE_OK && xOutput && buf.nBuf>=SESSIONS_STRM_CHUNK_SIZE ){ + rc = xOutput(pOut, buf.aBuf, buf.nBuf); + buf.nBuf = 0; + } + } + + if( rc==SQLITE_OK ){ + if( xOutput ){ + if( buf.nBuf>0 ) rc = xOutput(pOut, buf.aBuf, buf.nBuf); + }else{ + *ppOut = buf.aBuf; + *pnOut = buf.nBuf; + buf.aBuf = 0; + } + } + sqlite3_free(buf.aBuf); + + return rc; +} + +/* +** Allocate a new, empty, sqlite3_changegroup. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changegroup_new(sqlite3_changegroup **pp){ + int rc = SQLITE_OK; /* Return code */ + sqlite3_changegroup *p; /* New object */ + p = (sqlite3_changegroup*)sqlite3_malloc(sizeof(sqlite3_changegroup)); + if( p==0 ){ + rc = SQLITE_NOMEM; + }else{ + memset(p, 0, sizeof(sqlite3_changegroup)); + } + *pp = p; + return rc; +} + +/* +** Add the changeset currently stored in buffer pData, size nData bytes, +** to changeset-group p. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changegroup_add(sqlite3_changegroup *pGrp, int nData, void *pData){ + sqlite3_changeset_iter *pIter; /* Iterator opened on pData/nData */ + int rc; /* Return code */ + + rc = sqlite3changeset_start(&pIter, nData, pData); + if( rc==SQLITE_OK ){ + rc = sessionChangesetToHash(pIter, pGrp); + } + sqlite3changeset_finalize(pIter); + return rc; +} + +/* +** Obtain a buffer containing a changeset representing the concatenation +** of all changesets added to the group so far. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changegroup_output( + sqlite3_changegroup *pGrp, + int *pnData, + void **ppData +){ + return sessionChangegroupOutput(pGrp, 0, 0, pnData, ppData); +} + +/* +** Streaming versions of changegroup_add(). +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changegroup_add_strm( + sqlite3_changegroup *pGrp, + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn +){ + sqlite3_changeset_iter *pIter; /* Iterator opened on pData/nData */ + int rc; /* Return code */ + + rc = sqlite3changeset_start_strm(&pIter, xInput, pIn); + if( rc==SQLITE_OK ){ + rc = sessionChangesetToHash(pIter, pGrp); + } + sqlite3changeset_finalize(pIter); + return rc; +} + +/* +** Streaming versions of changegroup_output(). +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changegroup_output_strm( + sqlite3_changegroup *pGrp, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +){ + return sessionChangegroupOutput(pGrp, xOutput, pOut, 0, 0); +} + +/* +** Delete a changegroup object. +*/ +SQLITE_API void SQLITE_STDCALL sqlite3changegroup_delete(sqlite3_changegroup *pGrp){ + if( pGrp ){ + sessionDeleteTable(pGrp->pList); + sqlite3_free(pGrp); + } +} + +/* +** Combine two changesets together. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_concat( + int nLeft, /* Number of bytes in lhs input */ + void *pLeft, /* Lhs input changeset */ + int nRight /* Number of bytes in rhs input */, + void *pRight, /* Rhs input changeset */ + int *pnOut, /* OUT: Number of bytes in output changeset */ + void **ppOut /* OUT: changeset (left right) */ +){ + sqlite3_changegroup *pGrp; + int rc; + + rc = sqlite3changegroup_new(&pGrp); + if( rc==SQLITE_OK ){ + rc = sqlite3changegroup_add(pGrp, nLeft, pLeft); + } + if( rc==SQLITE_OK ){ + rc = sqlite3changegroup_add(pGrp, nRight, pRight); + } + if( rc==SQLITE_OK ){ + rc = sqlite3changegroup_output(pGrp, pnOut, ppOut); + } + sqlite3changegroup_delete(pGrp); + + return rc; +} + +/* +** Streaming version of sqlite3changeset_concat(). +*/ +SQLITE_API int SQLITE_STDCALL sqlite3changeset_concat_strm( + int (*xInputA)(void *pIn, void *pData, int *pnData), + void *pInA, + int (*xInputB)(void *pIn, void *pData, int *pnData), + void *pInB, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +){ + sqlite3_changegroup *pGrp; + int rc; + + rc = sqlite3changegroup_new(&pGrp); + if( rc==SQLITE_OK ){ + rc = sqlite3changegroup_add_strm(pGrp, xInputA, pInA); + } + if( rc==SQLITE_OK ){ + rc = sqlite3changegroup_add_strm(pGrp, xInputB, pInB); + } + if( rc==SQLITE_OK ){ + rc = sqlite3changegroup_output_strm(pGrp, xOutput, pOut); + } + sqlite3changegroup_delete(pGrp); + + return rc; +} + +#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */ + +/************** End of sqlite3session.c **************************************/ /************** Begin file json1.c *******************************************/ /* ** 2015-08-12 @@ -163585,7 +174018,11 @@ SQLITE_EXTENSION_INIT1 /* #include */ /* #include */ -#define UNUSED_PARAM(X) (void)(X) +/* Mark a function parameter as unused, to suppress nuisance compiler +** warnings. */ +#ifndef UNUSED_PARAM +# define UNUSED_PARAM(X) (void)(X) +#endif #ifndef LARGEST_INT64 # define LARGEST_INT64 (0xffffffff|(((sqlite3_int64)0x7fffffff)<<32)) @@ -163830,10 +174267,33 @@ static void jsonAppendString(JsonString *p, const char *zIn, u32 N){ if( (N+p->nUsed+2 >= p->nAlloc) && jsonGrow(p,N+2)!=0 ) return; p->zBuf[p->nUsed++] = '"'; for(i=0; inUsed+N+3-i > p->nAlloc) && jsonGrow(p,N+3-i)!=0 ) return; p->zBuf[p->nUsed++] = '\\'; + }else if( c<=0x1f ){ + static const char aSpecial[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 'b', 't', 'n', 0, 'f', 'r', 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + assert( sizeof(aSpecial)==32 ); + assert( aSpecial['\b']=='b' ); + assert( aSpecial['\f']=='f' ); + assert( aSpecial['\n']=='n' ); + assert( aSpecial['\r']=='r' ); + assert( aSpecial['\t']=='t' ); + if( aSpecial[c] ){ + c = aSpecial[c]; + goto json_simple_escape; + } + if( (p->nUsed+N+7+i > p->nAlloc) && jsonGrow(p,N+7-i)!=0 ) return; + p->zBuf[p->nUsed++] = '\\'; + p->zBuf[p->nUsed++] = 'u'; + p->zBuf[p->nUsed++] = '0'; + p->zBuf[p->nUsed++] = '0'; + p->zBuf[p->nUsed++] = '0' + (c>>4); + c = "0123456789abcdef"[c&0xf]; } p->zBuf[p->nUsed++] = c; } @@ -163874,7 +174334,7 @@ static void jsonAppendValue( default: { if( p->bErr==0 ){ sqlite3_result_error(p->pCtx, "JSON cannot hold BLOB values", -1); - p->bErr = 1; + p->bErr = 2; jsonReset(p); } break; @@ -164735,7 +175195,7 @@ static void jsonTest1Func( #endif /* SQLITE_DEBUG */ /**************************************************************************** -** SQL function implementations +** Scalar SQL function implementations ****************************************************************************/ /* @@ -165068,6 +175528,104 @@ static void jsonValidFunc( sqlite3_result_int(ctx, rc); } + +/**************************************************************************** +** Aggregate SQL function implementations +****************************************************************************/ +/* +** json_group_array(VALUE) +** +** Return a JSON array composed of all values in the aggregate. +*/ +static void jsonArrayStep( + sqlite3_context *ctx, + int argc, + sqlite3_value **argv +){ + JsonString *pStr; + UNUSED_PARAM(argc); + pStr = (JsonString*)sqlite3_aggregate_context(ctx, sizeof(*pStr)); + if( pStr ){ + if( pStr->zBuf==0 ){ + jsonInit(pStr, ctx); + jsonAppendChar(pStr, '['); + }else{ + jsonAppendChar(pStr, ','); + pStr->pCtx = ctx; + } + jsonAppendValue(pStr, argv[0]); + } +} +static void jsonArrayFinal(sqlite3_context *ctx){ + JsonString *pStr; + pStr = (JsonString*)sqlite3_aggregate_context(ctx, 0); + if( pStr ){ + pStr->pCtx = ctx; + jsonAppendChar(pStr, ']'); + if( pStr->bErr ){ + if( pStr->bErr==1 ) sqlite3_result_error_nomem(ctx); + assert( pStr->bStatic ); + }else{ + sqlite3_result_text(ctx, pStr->zBuf, pStr->nUsed, + pStr->bStatic ? SQLITE_TRANSIENT : sqlite3_free); + pStr->bStatic = 1; + } + }else{ + sqlite3_result_text(ctx, "[]", 2, SQLITE_STATIC); + } + sqlite3_result_subtype(ctx, JSON_SUBTYPE); +} + +/* +** json_group_obj(NAME,VALUE) +** +** Return a JSON object composed of all names and values in the aggregate. +*/ +static void jsonObjectStep( + sqlite3_context *ctx, + int argc, + sqlite3_value **argv +){ + JsonString *pStr; + const char *z; + u32 n; + UNUSED_PARAM(argc); + pStr = (JsonString*)sqlite3_aggregate_context(ctx, sizeof(*pStr)); + if( pStr ){ + if( pStr->zBuf==0 ){ + jsonInit(pStr, ctx); + jsonAppendChar(pStr, '{'); + }else{ + jsonAppendChar(pStr, ','); + pStr->pCtx = ctx; + } + z = (const char*)sqlite3_value_text(argv[0]); + n = (u32)sqlite3_value_bytes(argv[0]); + jsonAppendString(pStr, z, n); + jsonAppendChar(pStr, ':'); + jsonAppendValue(pStr, argv[1]); + } +} +static void jsonObjectFinal(sqlite3_context *ctx){ + JsonString *pStr; + pStr = (JsonString*)sqlite3_aggregate_context(ctx, 0); + if( pStr ){ + jsonAppendChar(pStr, '}'); + if( pStr->bErr ){ + if( pStr->bErr==0 ) sqlite3_result_error_nomem(ctx); + assert( pStr->bStatic ); + }else{ + sqlite3_result_text(ctx, pStr->zBuf, pStr->nUsed, + pStr->bStatic ? SQLITE_TRANSIENT : sqlite3_free); + pStr->bStatic = 1; + } + }else{ + sqlite3_result_text(ctx, "{}", 2, SQLITE_STATIC); + } + sqlite3_result_subtype(ctx, JSON_SUBTYPE); +} + + #ifndef SQLITE_OMIT_VIRTUALTABLE /**************************************************************************** ** The json_each virtual table @@ -165566,6 +176124,15 @@ SQLITE_PRIVATE int sqlite3Json1Init(sqlite3 *db){ { "json_test1", 1, 0, jsonTest1Func }, #endif }; + static const struct { + const char *zName; + int nArg; + void (*xStep)(sqlite3_context*,int,sqlite3_value**); + void (*xFinal)(sqlite3_context*); + } aAgg[] = { + { "json_group_array", 1, jsonArrayStep, jsonArrayFinal }, + { "json_group_object", 2, jsonObjectStep, jsonObjectFinal }, + }; #ifndef SQLITE_OMIT_VIRTUALTABLE static const struct { const char *zName; @@ -165581,6 +176148,11 @@ SQLITE_PRIVATE int sqlite3Json1Init(sqlite3 *db){ (void*)&aFunc[i].flag, aFunc[i].xFunc, 0, 0); } + for(i=0; ixPhraseFirst(pFts, iPhrase, &iter, &iCol, &iOff); -** iOff>=0; +** iCol>=0; ** pApi->xPhraseNext(pFts, &iter, &iCol, &iOff) ** ){ ** // An instance of phrase iPhrase at offset iOff of column iCol @@ -165825,13 +176416,51 @@ struct Fts5PhraseIter { ** ** The Fts5PhraseIter structure is defined above. Applications should not ** modify this structure directly - it should only be used as shown above -** with the xPhraseFirst() and xPhraseNext() API methods. +** with the xPhraseFirst() and xPhraseNext() API methods (and by +** xPhraseFirstColumn() and xPhraseNextColumn() as illustrated below). +** +** This API can be quite slow if used with an FTS5 table created with the +** "detail=none" or "detail=column" option. If the FTS5 table is created +** with either "detail=none" or "detail=column" and "content=" option +** (i.e. if it is a contentless table), then this API always iterates +** through an empty set (all calls to xPhraseFirst() set iCol to -1). ** ** xPhraseNext() ** See xPhraseFirst above. +** +** xPhraseFirstColumn() +** This function and xPhraseNextColumn() are similar to the xPhraseFirst() +** and xPhraseNext() APIs described above. The difference is that instead +** of iterating through all instances of a phrase in the current row, these +** APIs are used to iterate through the set of columns in the current row +** that contain one or more instances of a specified phrase. For example: +** +** Fts5PhraseIter iter; +** int iCol; +** for(pApi->xPhraseFirstColumn(pFts, iPhrase, &iter, &iCol); +** iCol>=0; +** pApi->xPhraseNextColumn(pFts, &iter, &iCol) +** ){ +** // Column iCol contains at least one instance of phrase iPhrase +** } +** +** This API can be quite slow if used with an FTS5 table created with the +** "detail=none" option. If the FTS5 table is created with either +** "detail=none" "content=" option (i.e. if it is a contentless table), +** then this API always iterates through an empty set (all calls to +** xPhraseFirstColumn() set iCol to -1). +** +** The information accessed using this API and its companion +** xPhraseFirstColumn() may also be obtained using xPhraseFirst/xPhraseNext +** (or xInst/xInstCount). The chief advantage of this API is that it is +** significantly more efficient than those alternatives when used with +** "detail=column" tables. +** +** xPhraseNextColumn() +** See xPhraseFirstColumn above. */ struct Fts5ExtensionApi { - int iVersion; /* Currently always set to 1 */ + int iVersion; /* Currently always set to 3 */ void *(*xUserData)(Fts5Context*); @@ -165861,8 +176490,11 @@ struct Fts5ExtensionApi { int (*xSetAuxdata)(Fts5Context*, void *pAux, void(*xDelete)(void*)); void *(*xGetAuxdata)(Fts5Context*, int bClear); - void (*xPhraseFirst)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*, int*); + int (*xPhraseFirst)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*, int*); void (*xPhraseNext)(Fts5Context*, Fts5PhraseIter*, int *piCol, int *piOff); + + int (*xPhraseFirstColumn)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*); + void (*xPhraseNextColumn)(Fts5Context*, Fts5PhraseIter*, int *piCol); }; /* @@ -166156,6 +176788,7 @@ struct fts5_api { #ifndef _FTS5INT_H #define _FTS5INT_H +/* #include "fts5.h" */ /* #include "sqlite3ext.h" */ SQLITE_EXTENSION_INIT1 @@ -166167,10 +176800,11 @@ SQLITE_EXTENSION_INIT1 typedef unsigned char u8; typedef unsigned int u32; typedef unsigned short u16; +typedef short i16; typedef sqlite3_int64 i64; typedef sqlite3_uint64 u64; -#define ArraySize(x) (sizeof(x) / sizeof(x[0])) +#define ArraySize(x) ((int)(sizeof(x) / sizeof(x[0]))) #define testcase(x) #define ALWAYS(x) 1 @@ -166187,6 +176821,10 @@ typedef sqlite3_uint64 u64; #endif +/* Truncate very long tokens to this many bytes. Hard limit is +** (65536-1-1-4-9)==65521 bytes. The limiting factor is the 16-bit offset +** field that occurs at the start of each leaf page (see fts5_index.c). */ +#define FTS5_MAX_TOKEN_SIZE 32768 /* ** Maximum number of prefix indexes on single FTS5 table. This must be @@ -166221,6 +176859,16 @@ SQLITE_API extern int sqlite3_fts5_may_be_corrupt; # define assert_nc(x) assert(x) #endif +/* Mark a function parameter as unused, to suppress nuisance compiler +** warnings. */ +#ifndef UNUSED_PARAM +# define UNUSED_PARAM(X) (void)(X) +#endif + +#ifndef UNUSED_PARAM2 +# define UNUSED_PARAM2(X, Y) (void)(X), (void)(Y) +#endif + typedef struct Fts5Global Fts5Global; typedef struct Fts5Colset Fts5Colset; @@ -166292,6 +176940,7 @@ struct Fts5Config { char *zContent; /* content table */ char *zContentRowid; /* "content_rowid=" option value */ int bColumnsize; /* "columnsize=" option value (dflt==1) */ + int eDetail; /* FTS5_DETAIL_XXX value */ char *zContentExprlist; Fts5Tokenizer *pTok; fts5_tokenizer *pTokApi; @@ -166301,6 +176950,8 @@ struct Fts5Config { int pgsz; /* Approximate page size used in %_data */ int nAutomerge; /* 'automerge' setting */ int nCrisisMerge; /* Maximum allowed segments per level */ + int nUsermerge; /* 'usermerge' setting */ + int nHashSize; /* Bytes of memory for in-memory hash */ char *zRank; /* Name of rank function */ char *zRankArgs; /* Arguments to rank function */ @@ -166319,6 +176970,9 @@ struct Fts5Config { #define FTS5_CONTENT_NONE 1 #define FTS5_CONTENT_EXTERNAL 2 +#define FTS5_DETAIL_FULL 0 +#define FTS5_DETAIL_NONE 1 +#define FTS5_DETAIL_COLUMNS 2 @@ -166365,25 +177019,27 @@ struct Fts5Buffer { int nSpace; }; -static int sqlite3Fts5BufferGrow(int*, Fts5Buffer*, int); +static int sqlite3Fts5BufferSize(int*, Fts5Buffer*, u32); static void sqlite3Fts5BufferAppendVarint(int*, Fts5Buffer*, i64); -static void sqlite3Fts5BufferAppendBlob(int*, Fts5Buffer*, int, const u8*); +static void sqlite3Fts5BufferAppendBlob(int*, Fts5Buffer*, u32, const u8*); static void sqlite3Fts5BufferAppendString(int *, Fts5Buffer*, const char*); static void sqlite3Fts5BufferFree(Fts5Buffer*); static void sqlite3Fts5BufferZero(Fts5Buffer*); static void sqlite3Fts5BufferSet(int*, Fts5Buffer*, int, const u8*); static void sqlite3Fts5BufferAppendPrintf(int *, Fts5Buffer*, char *zFmt, ...); -static void sqlite3Fts5BufferAppend32(int*, Fts5Buffer*, int); static char *sqlite3Fts5Mprintf(int *pRc, const char *zFmt, ...); #define fts5BufferZero(x) sqlite3Fts5BufferZero(x) -#define fts5BufferGrow(a,b,c) sqlite3Fts5BufferGrow(a,b,c) #define fts5BufferAppendVarint(a,b,c) sqlite3Fts5BufferAppendVarint(a,b,c) #define fts5BufferFree(a) sqlite3Fts5BufferFree(a) #define fts5BufferAppendBlob(a,b,c,d) sqlite3Fts5BufferAppendBlob(a,b,c,d) #define fts5BufferSet(a,b,c,d) sqlite3Fts5BufferSet(a,b,c,d) -#define fts5BufferAppend32(a,b,c) sqlite3Fts5BufferAppend32(a,b,c) + +#define fts5BufferGrow(pRc,pBuf,nn) ( \ + (u32)((pBuf)->n) + (u32)(nn) <= (u32)((pBuf)->nSpace) ? 0 : \ + sqlite3Fts5BufferSize((pRc),(pBuf),(nn)+(pBuf)->n) \ +) /* Write and decode big-endian 32-bit integer values */ static void sqlite3Fts5Put32(u8*, int); @@ -166416,6 +177072,7 @@ struct Fts5PoslistWriter { i64 iPrev; }; static int sqlite3Fts5PoslistWriterAppend(Fts5Buffer*, Fts5PoslistWriter*, i64); +static void sqlite3Fts5PoslistSafeAppend(Fts5Buffer*, i64*, i64); static int sqlite3Fts5PoslistNext64( const u8 *a, int n, /* Buffer containing poslist */ @@ -166430,6 +177087,13 @@ static char *sqlite3Fts5Strndup(int *pRc, const char *pIn, int nIn); /* Character set tests (like isspace(), isalpha() etc.) */ static int sqlite3Fts5IsBareword(char t); + +/* Bucket of terms object used by the integrity-check in offsets=0 mode. */ +typedef struct Fts5Termset Fts5Termset; +static int sqlite3Fts5TermsetNew(Fts5Termset**); +static int sqlite3Fts5TermsetAdd(Fts5Termset*, int, const char*, int, int *pbPresent); +static void sqlite3Fts5TermsetFree(Fts5Termset*); + /* ** End of interface to code in fts5_buffer.c. **************************************************************************/ @@ -166442,6 +177106,15 @@ static int sqlite3Fts5IsBareword(char t); typedef struct Fts5Index Fts5Index; typedef struct Fts5IndexIter Fts5IndexIter; +struct Fts5IndexIter { + i64 iRowid; + const u8 *pData; + int nData; + u8 bEof; +}; + +#define sqlite3Fts5IterEof(x) ((x)->bEof) + /* ** Values used as part of the flags argument passed to IndexQuery(). */ @@ -166450,6 +177123,12 @@ typedef struct Fts5IndexIter Fts5IndexIter; #define FTS5INDEX_QUERY_TEST_NOIDX 0x0004 /* Do not use prefix index */ #define FTS5INDEX_QUERY_SCAN 0x0008 /* Scan query (fts5vocab) */ +/* The following are used internally by the fts5_index.c module. They are +** defined here only to make it easier to avoid clashes with the flags +** above. */ +#define FTS5INDEX_QUERY_SKIPEMPTY 0x0010 +#define FTS5INDEX_QUERY_NOOUTPUT 0x0020 + /* ** Create/destroy an Fts5Index object. */ @@ -166457,14 +177136,27 @@ static int sqlite3Fts5IndexOpen(Fts5Config *pConfig, int bCreate, Fts5Index**, c static int sqlite3Fts5IndexClose(Fts5Index *p); /* -** for( -** sqlite3Fts5IndexQuery(p, "token", 5, 0, 0, &pIter); -** 0==sqlite3Fts5IterEof(pIter); -** sqlite3Fts5IterNext(pIter) -** ){ -** i64 iRowid = sqlite3Fts5IterRowid(pIter); -** } +** Return a simple checksum value based on the arguments. */ +static u64 sqlite3Fts5IndexEntryCksum( + i64 iRowid, + int iCol, + int iPos, + int iIdx, + const char *pTerm, + int nTerm +); + +/* +** Argument p points to a buffer containing utf-8 text that is n bytes in +** size. Return the number of bytes in the nChar character prefix of the +** buffer, or 0 if there are less than nChar characters in total. +*/ +static int sqlite3Fts5IndexCharlenToBytelen( + const char *p, + int nByte, + int nChar +); /* ** Open a new iterator to iterate though all rowids that match the @@ -166482,12 +177174,8 @@ static int sqlite3Fts5IndexQuery( ** The various operations on open token or token prefix iterators opened ** using sqlite3Fts5IndexQuery(). */ -static int sqlite3Fts5IterEof(Fts5IndexIter*); static int sqlite3Fts5IterNext(Fts5IndexIter*); static int sqlite3Fts5IterNextFrom(Fts5IndexIter*, i64 iMatch); -static i64 sqlite3Fts5IterRowid(Fts5IndexIter*); -static int sqlite3Fts5IterPoslist(Fts5IndexIter*,Fts5Colset*, const u8**, int*, i64*); -static int sqlite3Fts5IterPoslistBuffer(Fts5IndexIter *pIter, Fts5Buffer *pBuf); /* ** Close an iterator opened by sqlite3Fts5IndexQuery(). @@ -166551,7 +177239,6 @@ static int sqlite3Fts5IndexSetAverages(Fts5Index *p, const u8*, int); /* ** Functions called by the storage module as part of integrity-check. */ -static u64 sqlite3Fts5IndexCksum(Fts5Config*,i64,int,int,const char*,int); static int sqlite3Fts5IndexIntegrityCheck(Fts5Index*, u64 cksum); /* @@ -166571,6 +177258,7 @@ static int sqlite3Fts5IndexReads(Fts5Index *p); static int sqlite3Fts5IndexReinit(Fts5Index *p); static int sqlite3Fts5IndexOptimize(Fts5Index *p); static int sqlite3Fts5IndexMerge(Fts5Index *p, int nMerge); +static int sqlite3Fts5IndexReset(Fts5Index *p); static int sqlite3Fts5IndexLoadConfig(Fts5Index *p); @@ -166616,7 +177304,7 @@ static int sqlite3Fts5GetTokenizer( char **pzErr ); -static Fts5Index *sqlite3Fts5IndexFromCsrid(Fts5Global*, i64, int*); +static Fts5Index *sqlite3Fts5IndexFromCsrid(Fts5Global*, i64, Fts5Config **); /* ** End of interface to code in fts5.c. @@ -166630,7 +177318,7 @@ typedef struct Fts5Hash Fts5Hash; /* ** Create a hash table, free a hash table. */ -static int sqlite3Fts5HashNew(Fts5Hash**, int *pnSize); +static int sqlite3Fts5HashNew(Fts5Config*, Fts5Hash**, int *pnSize); static void sqlite3Fts5HashFree(Fts5Hash*); static int sqlite3Fts5HashWrite( @@ -166689,7 +177377,7 @@ static int sqlite3Fts5StorageRename(Fts5Storage*, const char *zName); static int sqlite3Fts5DropAll(Fts5Config*); static int sqlite3Fts5CreateTable(Fts5Config*, const char*, const char*, int, char **); -static int sqlite3Fts5StorageDelete(Fts5Storage *p, i64); +static int sqlite3Fts5StorageDelete(Fts5Storage *p, i64, sqlite3_value**); static int sqlite3Fts5StorageContentInsert(Fts5Storage *p, sqlite3_value**, i64*); static int sqlite3Fts5StorageIndexInsert(Fts5Storage *p, sqlite3_value**, i64); @@ -166709,12 +177397,11 @@ static int sqlite3Fts5StorageConfigValue( Fts5Storage *p, const char*, sqlite3_value*, int ); -static int sqlite3Fts5StorageSpecialDelete(Fts5Storage *p, i64 iDel, sqlite3_value**); - static int sqlite3Fts5StorageDeleteAll(Fts5Storage *p); static int sqlite3Fts5StorageRebuild(Fts5Storage *p); static int sqlite3Fts5StorageOptimize(Fts5Storage *p); static int sqlite3Fts5StorageMerge(Fts5Storage *p, int nMerge); +static int sqlite3Fts5StorageReset(Fts5Storage *p); /* ** End of interface to code in fts5_storage.c. @@ -166767,7 +177454,17 @@ static int sqlite3Fts5ExprPhraseCount(Fts5Expr*); static int sqlite3Fts5ExprPhraseSize(Fts5Expr*, int iPhrase); static int sqlite3Fts5ExprPoslist(Fts5Expr*, int, const u8 **); -static int sqlite3Fts5ExprClonePhrase(Fts5Config*, Fts5Expr*, int, Fts5Expr**); +typedef struct Fts5PoslistPopulator Fts5PoslistPopulator; +static Fts5PoslistPopulator *sqlite3Fts5ExprClearPoslists(Fts5Expr*, int); +static int sqlite3Fts5ExprPopulatePoslists( + Fts5Config*, Fts5Expr*, Fts5PoslistPopulator*, int, const char*, int +); +static void sqlite3Fts5ExprCheckPoslists(Fts5Expr*, i64); +static void sqlite3Fts5ExprClearEof(Fts5Expr*); + +static int sqlite3Fts5ExprClonePhrase(Fts5Expr*, int, Fts5Expr**); + +static int sqlite3Fts5ExprPhraseCollist(Fts5Expr *, int, const u8 **, int *); /******************************************* ** The fts5_expr.c API above this point is used by the other hand-written @@ -166784,6 +177481,12 @@ static Fts5ExprNode *sqlite3Fts5ParseNode( Fts5ExprNearset *pNear ); +static Fts5ExprNode *sqlite3Fts5ParseImplicitAnd( + Fts5Parse *pParse, + Fts5ExprNode *pLeft, + Fts5ExprNode *pRight +); + static Fts5ExprPhrase *sqlite3Fts5ParseTerm( Fts5Parse *pParse, Fts5ExprPhrase *pPhrase, @@ -166873,19 +177576,35 @@ static int sqlite3Fts5UnicodeFold(int c, int bRemoveDiacritic); #define FTS5_PLUS 12 #define FTS5_STAR 13 -/* Driver template for the LEMON parser generator. -** The author disclaims copyright to this source code. +/* +** 2000-05-29 ** -** This version of "lempar.c" is modified, slightly, for use by SQLite. -** The only modifications are the addition of a couple of NEVER() -** macros to disable tests that are needed in the case of a general -** LALR(1) grammar but which are always false in the -** specific grammar used by SQLite. +** The author disclaims copyright to this source code. In place of +** a legal notice, here is a blessing: +** +** May you do good and not evil. +** May you find forgiveness for yourself and forgive others. +** May you share freely, never taking more than you give. +** +************************************************************************* +** Driver template for the LEMON parser generator. +** +** The "lemon" program processes an LALR(1) input grammar file, then uses +** this template to construct a parser. The "lemon" program inserts text +** at each "%%" line. Also, any "P-a-r-s-e" identifer prefix (without the +** interstitial "-" characters) contained in this template is changed into +** the value of the %name directive from the grammar. Otherwise, the content +** of this template is copied straight through into the generate parser +** source file. +** +** The following is the concatenation of all %include directives from the +** input grammar file: */ -/* First off, code is included that follows the "include" declaration -** in the input grammar file. */ /* #include */ +/************ Begin %include sections from the grammar ************************/ +/* #include "fts5Int.h" */ +/* #include "fts5parse.h" */ /* ** Disable all error recovery processing in the parser push-down @@ -166898,44 +177617,54 @@ static int sqlite3Fts5UnicodeFold(int c, int bRemoveDiacritic); */ #define fts5yytestcase(X) testcase(X) -/* Next is all token values, in a form suitable for use by makeheaders. -** This section will be null unless lemon is run with the -m switch. +/* +** Indicate that sqlite3ParserFree() will never be called with a null +** pointer. */ -/* -** These constants (all generated automatically by the parser generator) -** specify the various kinds of tokens (terminals) that the parser -** understands. -** -** Each symbol here is a terminal symbol in the grammar. +#define fts5YYPARSEFREENOTNULL 1 + +/* +** Alternative datatype for the argument to the malloc() routine passed +** into sqlite3ParserAlloc(). The default is size_t. */ -/* Make sure the INTERFACE macro is defined. -*/ -#ifndef INTERFACE -# define INTERFACE 1 -#endif -/* The next thing included is series of defines which control +#define fts5YYMALLOCARGTYPE u64 + +/**************** End of %include directives **********************************/ +/* These constants specify the various numeric values for terminal symbols +** in a format understandable to "makeheaders". This section is blank unless +** "lemon" is run with the "-m" command-line option. +***************** Begin makeheaders token definitions *************************/ +/**************** End makeheaders token definitions ***************************/ + +/* The next sections is a series of control #defines. ** various aspects of the generated parser. -** fts5YYCODETYPE is the data type used for storing terminal -** and nonterminal numbers. "unsigned char" is -** used if there are fewer than 250 terminals -** and nonterminals. "int" is used otherwise. -** fts5YYNOCODE is a number of type fts5YYCODETYPE which corresponds -** to no legal terminal or nonterminal number. This -** number is used to fill in empty slots of the hash -** table. +** fts5YYCODETYPE is the data type used to store the integer codes +** that represent terminal and non-terminal symbols. +** "unsigned char" is used if there are fewer than +** 256 symbols. Larger types otherwise. +** fts5YYNOCODE is a number of type fts5YYCODETYPE that is not used for +** any terminal or nonterminal symbol. ** fts5YYFALLBACK If defined, this indicates that one or more tokens -** have fall-back values which should be used if the -** original value of the token will not parse. -** fts5YYACTIONTYPE is the data type used for storing terminal -** and nonterminal numbers. "unsigned char" is -** used if there are fewer than 250 rules and -** states combined. "int" is used otherwise. -** sqlite3Fts5ParserFTS5TOKENTYPE is the data type used for minor tokens given -** directly to the parser from the tokenizer. -** fts5YYMINORTYPE is the data type used for all minor tokens. +** (also known as: "terminal symbols") have fall-back +** values which should be used if the original symbol +** would not parse. This permits keywords to sometimes +** be used as identifiers, for example. +** fts5YYACTIONTYPE is the data type used for "action codes" - numbers +** that indicate what to do in response to the next +** token. +** sqlite3Fts5ParserFTS5TOKENTYPE is the data type used for minor type for terminal +** symbols. Background: A "minor type" is a semantic +** value associated with a terminal or non-terminal +** symbols. For example, for an "ID" terminal symbol, +** the minor type might be the name of the identifier. +** Each non-terminal can have a different minor type. +** Terminal symbols all have the same minor type, though. +** This macros defines the minor type for terminal +** symbols. +** fts5YYMINORTYPE is the data type used for all minor types. ** This is typically a union of many types, one of ** which is sqlite3Fts5ParserFTS5TOKENTYPE. The entry in the union -** for base tokens is called "fts5yy0". +** for terminal symbols is called "fts5yy0". ** fts5YYSTACKDEPTH is the maximum depth of the parser's stack. If ** zero the stack is dynamically sized using realloc() ** sqlite3Fts5ParserARG_SDECL A static variable declaration for the %extra_argument @@ -166954,6 +177683,10 @@ static int sqlite3Fts5UnicodeFold(int c, int bRemoveDiacritic); ** fts5YY_ACCEPT_ACTION The fts5yy_action[] code for accept ** fts5YY_NO_ACTION The fts5yy_action[] code for no-op */ +#ifndef INTERFACE +# define INTERFACE 1 +#endif +/************* Begin control #defines *****************************************/ #define fts5YYCODETYPE unsigned char #define fts5YYNOCODE 27 #define fts5YYACTIONTYPE unsigned char @@ -166984,10 +177717,7 @@ typedef union { #define fts5YY_ERROR_ACTION 88 #define fts5YY_ACCEPT_ACTION 89 #define fts5YY_NO_ACTION 90 - -/* The fts5yyzerominor constant is used to initialize instances of -** fts5YYMINORTYPE objects to zero. */ -static const fts5YYMINORTYPE fts5yyzerominor = { 0 }; +/************* End control #defines *******************************************/ /* Define the fts5yytestcase() macro to be a no-op if is not already defined ** otherwise. @@ -167052,7 +177782,8 @@ static const fts5YYMINORTYPE fts5yyzerominor = { 0 }; ** fts5yy_reduce_ofst[] For each state, the offset into fts5yy_action for ** shifting non-terminals after a reduce. ** fts5yy_default[] Default action for each state. -*/ +** +*********** Begin parsing tables **********************************************/ #define fts5YY_ACTTAB_COUNT (78) static const fts5YYACTIONTYPE fts5yy_action[] = { /* 0 */ 89, 15, 46, 5, 48, 24, 12, 19, 23, 14, @@ -167096,9 +177827,10 @@ static const fts5YYACTIONTYPE fts5yy_default[] = { /* 10 */ 87, 88, 87, 87, 88, 88, 88, 66, 80, 88, /* 20 */ 81, 88, 88, 78, 88, 65, }; +/********** End of lemon-generated parsing tables *****************************/ -/* The next table maps tokens into fallback tokens. If a construct -** like the following: +/* The next table maps tokens (terminal symbols) into fallback tokens. +** If a construct like the following: ** ** %fallback ID X Y Z. ** @@ -167106,6 +177838,10 @@ static const fts5YYACTIONTYPE fts5yy_default[] = { ** and Z. Whenever one of the tokens X, Y, or Z is input to the parser ** but it does not parse, the type of the token is changed to ID and ** the parse is retried before an error is thrown. +** +** This feature can be used, for example, to cause some keywords in a language +** to revert to identifiers if they keyword does not apply in the context where +** it appears. */ #ifdef fts5YYFALLBACK static const fts5YYCODETYPE fts5yyFallback[] = { @@ -167144,7 +177880,9 @@ struct fts5yyParser { #ifdef fts5YYTRACKMAXSTACKDEPTH int fts5yyidxMax; /* Maximum value of fts5yyidx */ #endif +#ifndef fts5YYNOERRORRECOVERY int fts5yyerrcnt; /* Shifts left before out of the error */ +#endif sqlite3Fts5ParserARG_SDECL /* A place to hold %extra_argument */ #if fts5YYSTACKDEPTH<=0 int fts5yystksz; /* Current side of the stack */ @@ -167256,6 +177994,15 @@ static void fts5yyGrowStack(fts5yyParser *p){ } #endif +/* Datatype of the argument to the memory allocated passed as the +** second argument to sqlite3Fts5ParserAlloc() below. This can be changed by +** putting an appropriate #define in the %include section of the input +** grammar. +*/ +#ifndef fts5YYMALLOCARGTYPE +# define fts5YYMALLOCARGTYPE size_t +#endif + /* ** This function allocates a new parser. ** The only argument is a pointer to a function which works like @@ -167268,9 +178015,9 @@ static void fts5yyGrowStack(fts5yyParser *p){ ** A pointer to a parser. This pointer is used in subsequent calls ** to sqlite3Fts5Parser and sqlite3Fts5ParserFree. */ -static void *sqlite3Fts5ParserAlloc(void *(*mallocProc)(u64)){ +static void *sqlite3Fts5ParserAlloc(void *(*mallocProc)(fts5YYMALLOCARGTYPE)){ fts5yyParser *pParser; - pParser = (fts5yyParser*)(*mallocProc)( (u64)sizeof(fts5yyParser) ); + pParser = (fts5yyParser*)(*mallocProc)( (fts5YYMALLOCARGTYPE)sizeof(fts5yyParser) ); if( pParser ){ pParser->fts5yyidx = -1; #ifdef fts5YYTRACKMAXSTACKDEPTH @@ -167285,10 +178032,12 @@ static void *sqlite3Fts5ParserAlloc(void *(*mallocProc)(u64)){ return pParser; } -/* The following function deletes the value associated with a -** symbol. The symbol can be either a terminal or nonterminal. -** "fts5yymajor" is the symbol code, and "fts5yypminor" is a pointer to -** the value. +/* The following function deletes the "minor type" or semantic value +** associated with a symbol. The symbol can be either a terminal +** or nonterminal. "fts5yymajor" is the symbol code, and "fts5yypminor" is +** a pointer to the value to be deleted. The code used to do the +** deletions is derived from the %destructor and/or %token_destructor +** directives of the input grammar. */ static void fts5yy_destructor( fts5yyParser *fts5yypParser, /* The parser */ @@ -167304,9 +178053,10 @@ static void fts5yy_destructor( ** being destroyed before it is finished parsing. ** ** Note: during a reduce, the only symbols destroyed are those - ** which appear on the RHS of the rule, but which are not used + ** which appear on the RHS of the rule, but which are *not* used ** inside the C code. */ +/********* Begin destructor definitions ***************************************/ case 15: /* input */ { (void)pParse; @@ -167336,6 +178086,7 @@ static void fts5yy_destructor( sqlite3Fts5ParsePhraseFree((fts5yypminor->fts5yy11)); } break; +/********* End destructor definitions *****************************************/ default: break; /* If no destructor action specified: do nothing */ } } @@ -167345,49 +178096,37 @@ static void fts5yy_destructor( ** ** If there is a destructor routine associated with the token which ** is popped from the stack, then call it. -** -** Return the major token number for the symbol popped. */ -static int fts5yy_pop_parser_stack(fts5yyParser *pParser){ - fts5YYCODETYPE fts5yymajor; - fts5yyStackEntry *fts5yytos = &pParser->fts5yystack[pParser->fts5yyidx]; - - /* There is no mechanism by which the parser stack can be popped below - ** empty in SQLite. */ +static void fts5yy_pop_parser_stack(fts5yyParser *pParser){ + fts5yyStackEntry *fts5yytos; assert( pParser->fts5yyidx>=0 ); + fts5yytos = &pParser->fts5yystack[pParser->fts5yyidx--]; #ifndef NDEBUG - if( fts5yyTraceFILE && pParser->fts5yyidx>=0 ){ + if( fts5yyTraceFILE ){ fprintf(fts5yyTraceFILE,"%sPopping %s\n", fts5yyTracePrompt, fts5yyTokenName[fts5yytos->major]); } #endif - fts5yymajor = fts5yytos->major; - fts5yy_destructor(pParser, fts5yymajor, &fts5yytos->minor); - pParser->fts5yyidx--; - return fts5yymajor; + fts5yy_destructor(pParser, fts5yytos->major, &fts5yytos->minor); } /* -** Deallocate and destroy a parser. Destructors are all called for +** Deallocate and destroy a parser. Destructors are called for ** all stack elements before shutting the parser down. ** -** Inputs: -**
      -**
    • A pointer to the parser. This should be a pointer -** obtained from sqlite3Fts5ParserAlloc. -**
    • A pointer to a function used to reclaim memory obtained -** from malloc. -**
    +** If the fts5YYPARSEFREENEVERNULL macro exists (for example because it +** is defined in a %include section of the input grammar) then it is +** assumed that the input pointer is never NULL. */ static void sqlite3Fts5ParserFree( void *p, /* The parser to be deleted */ void (*freeProc)(void*) /* Function used to reclaim memory */ ){ fts5yyParser *pParser = (fts5yyParser*)p; - /* In SQLite, we never try to destroy a parser that was not successfully - ** created in the first place. */ - if( NEVER(pParser==0) ) return; +#ifndef fts5YYPARSEFREENEVERNULL + if( pParser==0 ) return; +#endif while( pParser->fts5yyidx>=0 ) fts5yy_pop_parser_stack(pParser); #if fts5YYSTACKDEPTH<=0 free(pParser->fts5yystack); @@ -167408,12 +178147,8 @@ static int sqlite3Fts5ParserStackPeak(void *p){ /* ** Find the appropriate action for a parser given the terminal ** look-ahead token iLookAhead. -** -** If the look-ahead token is fts5YYNOCODE, then check to see if the action is -** independent of the look-ahead. If it is, return the action, otherwise -** return fts5YY_NO_ACTION. */ -static int fts5yy_find_shift_action( +static unsigned int fts5yy_find_shift_action( fts5yyParser *pParser, /* The parser */ fts5YYCODETYPE iLookAhead /* The look-ahead token */ ){ @@ -167422,61 +178157,62 @@ static int fts5yy_find_shift_action( if( stateno>=fts5YY_MIN_REDUCE ) return stateno; assert( stateno <= fts5YY_SHIFT_COUNT ); - i = fts5yy_shift_ofst[stateno]; - if( i==fts5YY_SHIFT_USE_DFLT ) return fts5yy_default[stateno]; - assert( iLookAhead!=fts5YYNOCODE ); - i += iLookAhead; - if( i<0 || i>=fts5YY_ACTTAB_COUNT || fts5yy_lookahead[i]!=iLookAhead ){ - if( iLookAhead>0 ){ + do{ + i = fts5yy_shift_ofst[stateno]; + if( i==fts5YY_SHIFT_USE_DFLT ) return fts5yy_default[stateno]; + assert( iLookAhead!=fts5YYNOCODE ); + i += iLookAhead; + if( i<0 || i>=fts5YY_ACTTAB_COUNT || fts5yy_lookahead[i]!=iLookAhead ){ + if( iLookAhead>0 ){ #ifdef fts5YYFALLBACK - fts5YYCODETYPE iFallback; /* Fallback token */ - if( iLookAhead %s\n", - fts5yyTracePrompt, fts5yyTokenName[iLookAhead], fts5yyTokenName[iFallback]); - } -#endif - return fts5yy_find_shift_action(pParser, iFallback); - } -#endif -#ifdef fts5YYWILDCARD - { - int j = i - iLookAhead + fts5YYWILDCARD; - if( -#if fts5YY_SHIFT_MIN+fts5YYWILDCARD<0 - j>=0 && -#endif -#if fts5YY_SHIFT_MAX+fts5YYWILDCARD>=fts5YY_ACTTAB_COUNT - j %s\n", - fts5yyTracePrompt, fts5yyTokenName[iLookAhead], fts5yyTokenName[fts5YYWILDCARD]); + fprintf(fts5yyTraceFILE, "%sFALLBACK %s => %s\n", + fts5yyTracePrompt, fts5yyTokenName[iLookAhead], fts5yyTokenName[iFallback]); + } +#endif + assert( fts5yyFallback[iFallback]==0 ); /* Fallback loop must terminate */ + iLookAhead = iFallback; + continue; + } +#endif +#ifdef fts5YYWILDCARD + { + int j = i - iLookAhead + fts5YYWILDCARD; + if( +#if fts5YY_SHIFT_MIN+fts5YYWILDCARD<0 + j>=0 && +#endif +#if fts5YY_SHIFT_MAX+fts5YYWILDCARD>=fts5YY_ACTTAB_COUNT + j %s\n", + fts5yyTracePrompt, fts5yyTokenName[iLookAhead], + fts5yyTokenName[fts5YYWILDCARD]); + } +#endif /* NDEBUG */ + return fts5yy_action[j]; } -#endif /* NDEBUG */ - return fts5yy_action[j]; } - } #endif /* fts5YYWILDCARD */ + } + return fts5yy_default[stateno]; + }else{ + return fts5yy_action[i]; } - return fts5yy_default[stateno]; - }else{ - return fts5yy_action[i]; - } + }while(1); } /* ** Find the appropriate action for a parser given the non-terminal ** look-ahead token iLookAhead. -** -** If the look-ahead token is fts5YYNOCODE, then check to see if the action is -** independent of the look-ahead. If it is, return the action, otherwise -** return fts5YY_NO_ACTION. */ static int fts5yy_find_reduce_action( int stateno, /* Current state number */ @@ -167508,7 +178244,7 @@ static int fts5yy_find_reduce_action( /* ** The following routine is called if the stack overflows. */ -static void fts5yyStackOverflow(fts5yyParser *fts5yypParser, fts5YYMINORTYPE *fts5yypMinor){ +static void fts5yyStackOverflow(fts5yyParser *fts5yypParser){ sqlite3Fts5ParserARG_FETCH; fts5yypParser->fts5yyidx--; #ifndef NDEBUG @@ -167519,8 +178255,10 @@ static void fts5yyStackOverflow(fts5yyParser *fts5yypParser, fts5YYMINORTYPE *ft while( fts5yypParser->fts5yyidx>=0 ) fts5yy_pop_parser_stack(fts5yypParser); /* Here code is inserted which will execute if the parser ** stack every overflows */ +/******** Begin %stack_overflow code ******************************************/ - assert( 0 ); + sqlite3Fts5ParseError(pParse, "fts5: parser stack overflow"); +/******** End %stack_overflow code ********************************************/ sqlite3Fts5ParserARG_STORE; /* Suppress warning about unused %extra_argument var */ } @@ -167530,15 +178268,13 @@ static void fts5yyStackOverflow(fts5yyParser *fts5yypParser, fts5YYMINORTYPE *ft #ifndef NDEBUG static void fts5yyTraceShift(fts5yyParser *fts5yypParser, int fts5yyNewState){ if( fts5yyTraceFILE ){ - int i; if( fts5yyNewStatefts5yyidx; i++) - fprintf(fts5yyTraceFILE," %s",fts5yyTokenName[fts5yypParser->fts5yystack[i].major]); - fprintf(fts5yyTraceFILE,"\n"); + fprintf(fts5yyTraceFILE,"%sShift '%s', go to state %d\n", + fts5yyTracePrompt,fts5yyTokenName[fts5yypParser->fts5yystack[fts5yypParser->fts5yyidx].major], + fts5yyNewState); }else{ - fprintf(fts5yyTraceFILE,"%sShift *\n",fts5yyTracePrompt); + fprintf(fts5yyTraceFILE,"%sShift '%s'\n", + fts5yyTracePrompt,fts5yyTokenName[fts5yypParser->fts5yystack[fts5yypParser->fts5yyidx].major]); } } } @@ -167547,13 +178283,13 @@ static void fts5yyTraceShift(fts5yyParser *fts5yypParser, int fts5yyNewState){ #endif /* -** Perform a shift action. Return the number of errors. +** Perform a shift action. */ static void fts5yy_shift( fts5yyParser *fts5yypParser, /* The parser to be shifted */ int fts5yyNewState, /* The new state to shift in */ int fts5yyMajor, /* The major token to shift in */ - fts5YYMINORTYPE *fts5yypMinor /* Pointer to the minor token to shift in */ + sqlite3Fts5ParserFTS5TOKENTYPE fts5yyMinor /* The minor token to shift in */ ){ fts5yyStackEntry *fts5yytos; fts5yypParser->fts5yyidx++; @@ -167564,14 +178300,14 @@ static void fts5yy_shift( #endif #if fts5YYSTACKDEPTH>0 if( fts5yypParser->fts5yyidx>=fts5YYSTACKDEPTH ){ - fts5yyStackOverflow(fts5yypParser, fts5yypMinor); + fts5yyStackOverflow(fts5yypParser); return; } #else if( fts5yypParser->fts5yyidx>=fts5yypParser->fts5yystksz ){ fts5yyGrowStack(fts5yypParser); if( fts5yypParser->fts5yyidx>=fts5yypParser->fts5yystksz ){ - fts5yyStackOverflow(fts5yypParser, fts5yypMinor); + fts5yyStackOverflow(fts5yypParser); return; } } @@ -167579,7 +178315,7 @@ static void fts5yy_shift( fts5yytos = &fts5yypParser->fts5yystack[fts5yypParser->fts5yyidx]; fts5yytos->stateno = (fts5YYACTIONTYPE)fts5yyNewState; fts5yytos->major = (fts5YYCODETYPE)fts5yyMajor; - fts5yytos->minor = *fts5yypMinor; + fts5yytos->minor.fts5yy0 = fts5yyMinor; fts5yyTraceShift(fts5yypParser, fts5yyNewState); } @@ -167624,41 +178360,46 @@ static void fts5yy_accept(fts5yyParser*); /* Forward Declaration */ */ static void fts5yy_reduce( fts5yyParser *fts5yypParser, /* The parser */ - int fts5yyruleno /* Number of the rule by which to reduce */ + unsigned int fts5yyruleno /* Number of the rule by which to reduce */ ){ int fts5yygoto; /* The next state */ int fts5yyact; /* The next action */ - fts5YYMINORTYPE fts5yygotominor; /* The LHS of the rule reduced */ fts5yyStackEntry *fts5yymsp; /* The top of the parser's stack */ int fts5yysize; /* Amount to pop the stack */ sqlite3Fts5ParserARG_FETCH; fts5yymsp = &fts5yypParser->fts5yystack[fts5yypParser->fts5yyidx]; #ifndef NDEBUG - if( fts5yyTraceFILE && fts5yyruleno>=0 - && fts5yyruleno<(int)(sizeof(fts5yyRuleName)/sizeof(fts5yyRuleName[0])) ){ + if( fts5yyTraceFILE && fts5yyruleno<(int)(sizeof(fts5yyRuleName)/sizeof(fts5yyRuleName[0])) ){ fts5yysize = fts5yyRuleInfo[fts5yyruleno].nrhs; - fprintf(fts5yyTraceFILE, "%sReduce [%s] -> state %d.\n", fts5yyTracePrompt, + fprintf(fts5yyTraceFILE, "%sReduce [%s], go to state %d.\n", fts5yyTracePrompt, fts5yyRuleName[fts5yyruleno], fts5yymsp[-fts5yysize].stateno); } #endif /* NDEBUG */ - /* Silence complaints from purify about fts5yygotominor being uninitialized - ** in some cases when it is copied into the stack after the following - ** switch. fts5yygotominor is uninitialized when a rule reduces that does - ** not set the value of its left-hand side nonterminal. Leaving the - ** value of the nonterminal uninitialized is utterly harmless as long - ** as the value is never used. So really the only thing this code - ** accomplishes is to quieten purify. - ** - ** 2007-01-16: The wireshark project (www.wireshark.org) reports that - ** without this code, their parser segfaults. I'm not sure what there - ** parser is doing to make this happen. This is the second bug report - ** from wireshark this week. Clearly they are stressing Lemon in ways - ** that it has not been previously stressed... (SQLite ticket #2172) - */ - /*memset(&fts5yygotominor, 0, sizeof(fts5yygotominor));*/ - fts5yygotominor = fts5yyzerominor; - + /* Check that the stack is large enough to grow by a single entry + ** if the RHS of the rule is empty. This ensures that there is room + ** enough on the stack to push the LHS value */ + if( fts5yyRuleInfo[fts5yyruleno].nrhs==0 ){ +#ifdef fts5YYTRACKMAXSTACKDEPTH + if( fts5yypParser->fts5yyidx>fts5yypParser->fts5yyidxMax ){ + fts5yypParser->fts5yyidxMax = fts5yypParser->fts5yyidx; + } +#endif +#if fts5YYSTACKDEPTH>0 + if( fts5yypParser->fts5yyidx>=fts5YYSTACKDEPTH-1 ){ + fts5yyStackOverflow(fts5yypParser); + return; + } +#else + if( fts5yypParser->fts5yyidx>=fts5yypParser->fts5yystksz-1 ){ + fts5yyGrowStack(fts5yypParser); + if( fts5yypParser->fts5yyidx>=fts5yypParser->fts5yystksz-1 ){ + fts5yyStackOverflow(fts5yypParser); + return; + } + } +#endif + } switch( fts5yyruleno ){ /* Beginning here are the reduction cases. A typical example @@ -167669,132 +178410,142 @@ static void fts5yy_reduce( ** #line ** break; */ +/********** Begin reduce actions **********************************************/ + fts5YYMINORTYPE fts5yylhsminor; case 0: /* input ::= expr */ { sqlite3Fts5ParseFinished(pParse, fts5yymsp[0].minor.fts5yy18); } break; case 1: /* expr ::= expr AND expr */ { - fts5yygotominor.fts5yy18 = sqlite3Fts5ParseNode(pParse, FTS5_AND, fts5yymsp[-2].minor.fts5yy18, fts5yymsp[0].minor.fts5yy18, 0); + fts5yylhsminor.fts5yy18 = sqlite3Fts5ParseNode(pParse, FTS5_AND, fts5yymsp[-2].minor.fts5yy18, fts5yymsp[0].minor.fts5yy18, 0); } + fts5yymsp[-2].minor.fts5yy18 = fts5yylhsminor.fts5yy18; break; case 2: /* expr ::= expr OR expr */ { - fts5yygotominor.fts5yy18 = sqlite3Fts5ParseNode(pParse, FTS5_OR, fts5yymsp[-2].minor.fts5yy18, fts5yymsp[0].minor.fts5yy18, 0); + fts5yylhsminor.fts5yy18 = sqlite3Fts5ParseNode(pParse, FTS5_OR, fts5yymsp[-2].minor.fts5yy18, fts5yymsp[0].minor.fts5yy18, 0); } + fts5yymsp[-2].minor.fts5yy18 = fts5yylhsminor.fts5yy18; break; case 3: /* expr ::= expr NOT expr */ { - fts5yygotominor.fts5yy18 = sqlite3Fts5ParseNode(pParse, FTS5_NOT, fts5yymsp[-2].minor.fts5yy18, fts5yymsp[0].minor.fts5yy18, 0); + fts5yylhsminor.fts5yy18 = sqlite3Fts5ParseNode(pParse, FTS5_NOT, fts5yymsp[-2].minor.fts5yy18, fts5yymsp[0].minor.fts5yy18, 0); } + fts5yymsp[-2].minor.fts5yy18 = fts5yylhsminor.fts5yy18; break; case 4: /* expr ::= LP expr RP */ -{fts5yygotominor.fts5yy18 = fts5yymsp[-1].minor.fts5yy18;} +{fts5yymsp[-2].minor.fts5yy18 = fts5yymsp[-1].minor.fts5yy18;} break; case 5: /* expr ::= exprlist */ case 6: /* exprlist ::= cnearset */ fts5yytestcase(fts5yyruleno==6); -{fts5yygotominor.fts5yy18 = fts5yymsp[0].minor.fts5yy18;} +{fts5yylhsminor.fts5yy18 = fts5yymsp[0].minor.fts5yy18;} + fts5yymsp[0].minor.fts5yy18 = fts5yylhsminor.fts5yy18; break; case 7: /* exprlist ::= exprlist cnearset */ { - fts5yygotominor.fts5yy18 = sqlite3Fts5ParseNode(pParse, FTS5_AND, fts5yymsp[-1].minor.fts5yy18, fts5yymsp[0].minor.fts5yy18, 0); + fts5yylhsminor.fts5yy18 = sqlite3Fts5ParseImplicitAnd(pParse, fts5yymsp[-1].minor.fts5yy18, fts5yymsp[0].minor.fts5yy18); } + fts5yymsp[-1].minor.fts5yy18 = fts5yylhsminor.fts5yy18; break; case 8: /* cnearset ::= nearset */ { - fts5yygotominor.fts5yy18 = sqlite3Fts5ParseNode(pParse, FTS5_STRING, 0, 0, fts5yymsp[0].minor.fts5yy26); + fts5yylhsminor.fts5yy18 = sqlite3Fts5ParseNode(pParse, FTS5_STRING, 0, 0, fts5yymsp[0].minor.fts5yy26); } + fts5yymsp[0].minor.fts5yy18 = fts5yylhsminor.fts5yy18; break; case 9: /* cnearset ::= colset COLON nearset */ { sqlite3Fts5ParseSetColset(pParse, fts5yymsp[0].minor.fts5yy26, fts5yymsp[-2].minor.fts5yy3); - fts5yygotominor.fts5yy18 = sqlite3Fts5ParseNode(pParse, FTS5_STRING, 0, 0, fts5yymsp[0].minor.fts5yy26); + fts5yylhsminor.fts5yy18 = sqlite3Fts5ParseNode(pParse, FTS5_STRING, 0, 0, fts5yymsp[0].minor.fts5yy26); } + fts5yymsp[-2].minor.fts5yy18 = fts5yylhsminor.fts5yy18; break; case 10: /* colset ::= LCP colsetlist RCP */ -{ fts5yygotominor.fts5yy3 = fts5yymsp[-1].minor.fts5yy3; } +{ fts5yymsp[-2].minor.fts5yy3 = fts5yymsp[-1].minor.fts5yy3; } break; case 11: /* colset ::= STRING */ { - fts5yygotominor.fts5yy3 = sqlite3Fts5ParseColset(pParse, 0, &fts5yymsp[0].minor.fts5yy0); + fts5yylhsminor.fts5yy3 = sqlite3Fts5ParseColset(pParse, 0, &fts5yymsp[0].minor.fts5yy0); } + fts5yymsp[0].minor.fts5yy3 = fts5yylhsminor.fts5yy3; break; case 12: /* colsetlist ::= colsetlist STRING */ { - fts5yygotominor.fts5yy3 = sqlite3Fts5ParseColset(pParse, fts5yymsp[-1].minor.fts5yy3, &fts5yymsp[0].minor.fts5yy0); } + fts5yylhsminor.fts5yy3 = sqlite3Fts5ParseColset(pParse, fts5yymsp[-1].minor.fts5yy3, &fts5yymsp[0].minor.fts5yy0); } + fts5yymsp[-1].minor.fts5yy3 = fts5yylhsminor.fts5yy3; break; case 13: /* colsetlist ::= STRING */ { - fts5yygotominor.fts5yy3 = sqlite3Fts5ParseColset(pParse, 0, &fts5yymsp[0].minor.fts5yy0); + fts5yylhsminor.fts5yy3 = sqlite3Fts5ParseColset(pParse, 0, &fts5yymsp[0].minor.fts5yy0); } + fts5yymsp[0].minor.fts5yy3 = fts5yylhsminor.fts5yy3; break; case 14: /* nearset ::= phrase */ -{ fts5yygotominor.fts5yy26 = sqlite3Fts5ParseNearset(pParse, 0, fts5yymsp[0].minor.fts5yy11); } +{ fts5yylhsminor.fts5yy26 = sqlite3Fts5ParseNearset(pParse, 0, fts5yymsp[0].minor.fts5yy11); } + fts5yymsp[0].minor.fts5yy26 = fts5yylhsminor.fts5yy26; break; case 15: /* nearset ::= STRING LP nearphrases neardist_opt RP */ { sqlite3Fts5ParseNear(pParse, &fts5yymsp[-4].minor.fts5yy0); sqlite3Fts5ParseSetDistance(pParse, fts5yymsp[-2].minor.fts5yy26, &fts5yymsp[-1].minor.fts5yy0); - fts5yygotominor.fts5yy26 = fts5yymsp[-2].minor.fts5yy26; + fts5yylhsminor.fts5yy26 = fts5yymsp[-2].minor.fts5yy26; } + fts5yymsp[-4].minor.fts5yy26 = fts5yylhsminor.fts5yy26; break; case 16: /* nearphrases ::= phrase */ { - fts5yygotominor.fts5yy26 = sqlite3Fts5ParseNearset(pParse, 0, fts5yymsp[0].minor.fts5yy11); + fts5yylhsminor.fts5yy26 = sqlite3Fts5ParseNearset(pParse, 0, fts5yymsp[0].minor.fts5yy11); } + fts5yymsp[0].minor.fts5yy26 = fts5yylhsminor.fts5yy26; break; case 17: /* nearphrases ::= nearphrases phrase */ { - fts5yygotominor.fts5yy26 = sqlite3Fts5ParseNearset(pParse, fts5yymsp[-1].minor.fts5yy26, fts5yymsp[0].minor.fts5yy11); + fts5yylhsminor.fts5yy26 = sqlite3Fts5ParseNearset(pParse, fts5yymsp[-1].minor.fts5yy26, fts5yymsp[0].minor.fts5yy11); } + fts5yymsp[-1].minor.fts5yy26 = fts5yylhsminor.fts5yy26; break; case 18: /* neardist_opt ::= */ -{ fts5yygotominor.fts5yy0.p = 0; fts5yygotominor.fts5yy0.n = 0; } +{ fts5yymsp[1].minor.fts5yy0.p = 0; fts5yymsp[1].minor.fts5yy0.n = 0; } break; case 19: /* neardist_opt ::= COMMA STRING */ -{ fts5yygotominor.fts5yy0 = fts5yymsp[0].minor.fts5yy0; } +{ fts5yymsp[-1].minor.fts5yy0 = fts5yymsp[0].minor.fts5yy0; } break; case 20: /* phrase ::= phrase PLUS STRING star_opt */ { - fts5yygotominor.fts5yy11 = sqlite3Fts5ParseTerm(pParse, fts5yymsp[-3].minor.fts5yy11, &fts5yymsp[-1].minor.fts5yy0, fts5yymsp[0].minor.fts5yy20); + fts5yylhsminor.fts5yy11 = sqlite3Fts5ParseTerm(pParse, fts5yymsp[-3].minor.fts5yy11, &fts5yymsp[-1].minor.fts5yy0, fts5yymsp[0].minor.fts5yy20); } + fts5yymsp[-3].minor.fts5yy11 = fts5yylhsminor.fts5yy11; break; case 21: /* phrase ::= STRING star_opt */ { - fts5yygotominor.fts5yy11 = sqlite3Fts5ParseTerm(pParse, 0, &fts5yymsp[-1].minor.fts5yy0, fts5yymsp[0].minor.fts5yy20); + fts5yylhsminor.fts5yy11 = sqlite3Fts5ParseTerm(pParse, 0, &fts5yymsp[-1].minor.fts5yy0, fts5yymsp[0].minor.fts5yy20); } + fts5yymsp[-1].minor.fts5yy11 = fts5yylhsminor.fts5yy11; break; case 22: /* star_opt ::= STAR */ -{ fts5yygotominor.fts5yy20 = 1; } +{ fts5yymsp[0].minor.fts5yy20 = 1; } break; case 23: /* star_opt ::= */ -{ fts5yygotominor.fts5yy20 = 0; } +{ fts5yymsp[1].minor.fts5yy20 = 0; } break; default: break; +/********** End reduce actions ************************************************/ }; - assert( fts5yyruleno>=0 && fts5yyrulenofts5yyidx -= fts5yysize; fts5yyact = fts5yy_find_reduce_action(fts5yymsp[-fts5yysize].stateno,(fts5YYCODETYPE)fts5yygoto); if( fts5yyact <= fts5YY_MAX_SHIFTREDUCE ){ if( fts5yyact>fts5YY_MAX_SHIFT ) fts5yyact += fts5YY_MIN_REDUCE - fts5YY_MIN_SHIFTREDUCE; - /* If the reduce action popped at least - ** one element off the stack, then we can push the new element back - ** onto the stack here, and skip the stack overflow test in fts5yy_shift(). - ** That gives a significant speed improvement. */ - if( fts5yysize ){ - fts5yypParser->fts5yyidx++; - fts5yymsp -= fts5yysize-1; - fts5yymsp->stateno = (fts5YYACTIONTYPE)fts5yyact; - fts5yymsp->major = (fts5YYCODETYPE)fts5yygoto; - fts5yymsp->minor = fts5yygotominor; - fts5yyTraceShift(fts5yypParser, fts5yyact); - }else{ - fts5yy_shift(fts5yypParser,fts5yyact,fts5yygoto,&fts5yygotominor); - } + fts5yypParser->fts5yyidx -= fts5yysize - 1; + fts5yymsp -= fts5yysize-1; + fts5yymsp->stateno = (fts5YYACTIONTYPE)fts5yyact; + fts5yymsp->major = (fts5YYCODETYPE)fts5yygoto; + fts5yyTraceShift(fts5yypParser, fts5yyact); }else{ assert( fts5yyact == fts5YY_ACCEPT_ACTION ); + fts5yypParser->fts5yyidx -= fts5yysize; fts5yy_accept(fts5yypParser); } } @@ -167815,6 +178566,8 @@ static void fts5yy_parse_failed( while( fts5yypParser->fts5yyidx>=0 ) fts5yy_pop_parser_stack(fts5yypParser); /* Here code is inserted which will be executed whenever the ** parser fails */ +/************ Begin %parse_failure code ***************************************/ +/************ End %parse_failure code *****************************************/ sqlite3Fts5ParserARG_STORE; /* Suppress warning about unused %extra_argument variable */ } #endif /* fts5YYNOERRORRECOVERY */ @@ -167825,14 +178578,17 @@ static void fts5yy_parse_failed( static void fts5yy_syntax_error( fts5yyParser *fts5yypParser, /* The parser */ int fts5yymajor, /* The major type of the error token */ - fts5YYMINORTYPE fts5yyminor /* The minor type of the error token */ + sqlite3Fts5ParserFTS5TOKENTYPE fts5yyminor /* The minor type of the error token */ ){ sqlite3Fts5ParserARG_FETCH; -#define FTS5TOKEN (fts5yyminor.fts5yy0) +#define FTS5TOKEN fts5yyminor +/************ Begin %syntax_error code ****************************************/ + UNUSED_PARAM(fts5yymajor); /* Silence a compiler warning */ sqlite3Fts5ParseError( pParse, "fts5: syntax error near \"%.*s\"",FTS5TOKEN.n,FTS5TOKEN.p ); +/************ End %syntax_error code ******************************************/ sqlite3Fts5ParserARG_STORE; /* Suppress warning about unused %extra_argument variable */ } @@ -167851,6 +178607,8 @@ static void fts5yy_accept( while( fts5yypParser->fts5yyidx>=0 ) fts5yy_pop_parser_stack(fts5yypParser); /* Here code is inserted which will be executed whenever the ** parser accepts */ +/*********** Begin %parse_accept code *****************************************/ +/*********** End %parse_accept code *******************************************/ sqlite3Fts5ParserARG_STORE; /* Suppress warning about unused %extra_argument variable */ } @@ -167880,7 +178638,7 @@ static void sqlite3Fts5Parser( sqlite3Fts5ParserARG_PDECL /* Optional %extra_argument parameter */ ){ fts5YYMINORTYPE fts5yyminorunion; - int fts5yyact; /* The parser action. */ + unsigned int fts5yyact; /* The parser action. */ #if !defined(fts5YYERRORSYMBOL) && !defined(fts5YYNOERRORRECOVERY) int fts5yyendofinput; /* True if we are at the end of input */ #endif @@ -167894,18 +178652,23 @@ static void sqlite3Fts5Parser( if( fts5yypParser->fts5yyidx<0 ){ #if fts5YYSTACKDEPTH<=0 if( fts5yypParser->fts5yystksz <=0 ){ - /*memset(&fts5yyminorunion, 0, sizeof(fts5yyminorunion));*/ - fts5yyminorunion = fts5yyzerominor; - fts5yyStackOverflow(fts5yypParser, &fts5yyminorunion); + fts5yyStackOverflow(fts5yypParser); return; } #endif fts5yypParser->fts5yyidx = 0; +#ifndef fts5YYNOERRORRECOVERY fts5yypParser->fts5yyerrcnt = -1; +#endif fts5yypParser->fts5yystack[0].stateno = 0; fts5yypParser->fts5yystack[0].major = 0; +#ifndef NDEBUG + if( fts5yyTraceFILE ){ + fprintf(fts5yyTraceFILE,"%sInitialize. Empty stack. State 0\n", + fts5yyTracePrompt); + } +#endif } - fts5yyminorunion.fts5yy0 = fts5yyminor; #if !defined(fts5YYERRORSYMBOL) && !defined(fts5YYNOERRORRECOVERY) fts5yyendofinput = (fts5yymajor==0); #endif @@ -167913,7 +178676,7 @@ static void sqlite3Fts5Parser( #ifndef NDEBUG if( fts5yyTraceFILE ){ - fprintf(fts5yyTraceFILE,"%sInput %s\n",fts5yyTracePrompt,fts5yyTokenName[fts5yymajor]); + fprintf(fts5yyTraceFILE,"%sInput '%s'\n",fts5yyTracePrompt,fts5yyTokenName[fts5yymajor]); } #endif @@ -167921,13 +178684,16 @@ static void sqlite3Fts5Parser( fts5yyact = fts5yy_find_shift_action(fts5yypParser,(fts5YYCODETYPE)fts5yymajor); if( fts5yyact <= fts5YY_MAX_SHIFTREDUCE ){ if( fts5yyact > fts5YY_MAX_SHIFT ) fts5yyact += fts5YY_MIN_REDUCE - fts5YY_MIN_SHIFTREDUCE; - fts5yy_shift(fts5yypParser,fts5yyact,fts5yymajor,&fts5yyminorunion); + fts5yy_shift(fts5yypParser,fts5yyact,fts5yymajor,fts5yyminor); +#ifndef fts5YYNOERRORRECOVERY fts5yypParser->fts5yyerrcnt--; +#endif fts5yymajor = fts5YYNOCODE; }else if( fts5yyact <= fts5YY_MAX_REDUCE ){ fts5yy_reduce(fts5yypParser,fts5yyact-fts5YY_MIN_REDUCE); }else{ assert( fts5yyact == fts5YY_ERROR_ACTION ); + fts5yyminorunion.fts5yy0 = fts5yyminor; #ifdef fts5YYERRORSYMBOL int fts5yymx; #endif @@ -167957,7 +178723,7 @@ static void sqlite3Fts5Parser( ** */ if( fts5yypParser->fts5yyerrcnt<0 ){ - fts5yy_syntax_error(fts5yypParser,fts5yymajor,fts5yyminorunion); + fts5yy_syntax_error(fts5yypParser,fts5yymajor,fts5yyminor); } fts5yymx = fts5yypParser->fts5yystack[fts5yypParser->fts5yyidx].major; if( fts5yymx==fts5YYERRORSYMBOL || fts5yyerrorhit ){ @@ -167967,10 +178733,10 @@ static void sqlite3Fts5Parser( fts5yyTracePrompt,fts5yyTokenName[fts5yymajor]); } #endif - fts5yy_destructor(fts5yypParser, (fts5YYCODETYPE)fts5yymajor,&fts5yyminorunion); + fts5yy_destructor(fts5yypParser, (fts5YYCODETYPE)fts5yymajor, &fts5yyminorunion); fts5yymajor = fts5YYNOCODE; }else{ - while( + while( fts5yypParser->fts5yyidx >= 0 && fts5yymx != fts5YYERRORSYMBOL && (fts5yyact = fts5yy_find_reduce_action( @@ -167984,9 +178750,7 @@ static void sqlite3Fts5Parser( fts5yy_parse_failed(fts5yypParser); fts5yymajor = fts5YYNOCODE; }else if( fts5yymx!=fts5YYERRORSYMBOL ){ - fts5YYMINORTYPE u2; - u2.fts5YYERRSYMDT = 0; - fts5yy_shift(fts5yypParser,fts5yyact,fts5YYERRORSYMBOL,&u2); + fts5yy_shift(fts5yypParser,fts5yyact,fts5YYERRORSYMBOL,fts5yyminor); } } fts5yypParser->fts5yyerrcnt = 3; @@ -167999,7 +178763,7 @@ static void sqlite3Fts5Parser( ** Applications can set this macro (for example inside %include) if ** they intend to abandon the parse upon the first syntax error seen. */ - fts5yy_syntax_error(fts5yypParser,fts5yymajor,fts5yyminorunion); + fts5yy_syntax_error(fts5yypParser,fts5yymajor, fts5yyminor); fts5yy_destructor(fts5yypParser,(fts5YYCODETYPE)fts5yymajor,&fts5yyminorunion); fts5yymajor = fts5YYNOCODE; @@ -168014,7 +178778,7 @@ static void sqlite3Fts5Parser( ** three input tokens have been successfully shifted. */ if( fts5yypParser->fts5yyerrcnt<=0 ){ - fts5yy_syntax_error(fts5yypParser,fts5yymajor,fts5yyminorunion); + fts5yy_syntax_error(fts5yypParser,fts5yymajor, fts5yyminor); } fts5yypParser->fts5yyerrcnt = 3; fts5yy_destructor(fts5yypParser,(fts5YYCODETYPE)fts5yymajor,&fts5yyminorunion); @@ -168027,7 +178791,12 @@ static void sqlite3Fts5Parser( }while( fts5yymajor!=fts5YYNOCODE && fts5yypParser->fts5yyidx>=0 ); #ifndef NDEBUG if( fts5yyTraceFILE ){ - fprintf(fts5yyTraceFILE,"%sReturn\n",fts5yyTracePrompt); + int i; + fprintf(fts5yyTraceFILE,"%sReturn. Stack=",fts5yyTracePrompt); + for(i=1; i<=fts5yypParser->fts5yyidx; i++) + fprintf(fts5yyTraceFILE,"%c%s", i==1 ? '[' : ' ', + fts5yyTokenName[fts5yypParser->fts5yystack[i].major]); + fprintf(fts5yyTraceFILE,"]\n"); } #endif return; @@ -168047,6 +178816,7 @@ static void sqlite3Fts5Parser( */ +/* #include "fts5Int.h" */ #include /* amalgamator: keep */ /* @@ -168171,7 +178941,7 @@ static void fts5HighlightAppend( const char *z, int n ){ if( *pRc==SQLITE_OK ){ - if( n<0 ) n = strlen(z); + if( n<0 ) n = (int)strlen(z); p->zOut = sqlite3_mprintf("%z%.*s", p->zOut, n, z); if( p->zOut==0 ) *pRc = SQLITE_NOMEM; } @@ -168192,6 +178962,8 @@ static int fts5HighlightCb( int rc = SQLITE_OK; int iPos; + UNUSED_PARAM2(pToken, nToken); + if( tflags & FTS5_TOKEN_COLOCATED ) return SQLITE_OK; iPos = p->iPos++; @@ -168425,6 +179197,7 @@ static int fts5CountCb( void *pUserData /* Pointer to sqlite3_int64 variable */ ){ sqlite3_int64 *pn = (sqlite3_int64*)pUserData; + UNUSED_PARAM2(pApi, pFts); (*pn)++; return SQLITE_OK; } @@ -168578,7 +179351,7 @@ static int sqlite3Fts5AuxInit(fts5_api *pApi){ int rc = SQLITE_OK; /* Return code */ int i; /* To iterate through builtin functions */ - for(i=0; rc==SQLITE_OK && ixCreateFunction(pApi, aBuiltin[i].zFunc, aBuiltin[i].pUserData, @@ -168607,17 +179380,13 @@ static int sqlite3Fts5AuxInit(fts5_api *pApi){ +/* #include "fts5Int.h" */ -static int sqlite3Fts5BufferGrow(int *pRc, Fts5Buffer *pBuf, int nByte){ - - if( (pBuf->n + nByte) > pBuf->nSpace ){ +static int sqlite3Fts5BufferSize(int *pRc, Fts5Buffer *pBuf, u32 nByte){ + if( (u32)pBuf->nSpacenSpace ? pBuf->nSpace : 64; u8 *pNew; - int nNew = pBuf->nSpace ? pBuf->nSpace*2 : 64; - - /* A no-op if an error has already occurred */ - if( *pRc ) return 1; - - while( nNew<(pBuf->n + nByte) ){ + while( nNewp, nNew); @@ -168632,12 +179401,13 @@ static int sqlite3Fts5BufferGrow(int *pRc, Fts5Buffer *pBuf, int nByte){ return 0; } + /* ** Encode value iVal as an SQLite varint and append it to the buffer object ** pBuf. If an OOM error occurs, set the error code in p. */ static void sqlite3Fts5BufferAppendVarint(int *pRc, Fts5Buffer *pBuf, i64 iVal){ - if( sqlite3Fts5BufferGrow(pRc, pBuf, 9) ) return; + if( fts5BufferGrow(pRc, pBuf, 9) ) return; pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], iVal); } @@ -168652,12 +179422,6 @@ static int sqlite3Fts5Get32(const u8 *aBuf){ return (aBuf[0] << 24) + (aBuf[1] << 16) + (aBuf[2] << 8) + aBuf[3]; } -static void sqlite3Fts5BufferAppend32(int *pRc, Fts5Buffer *pBuf, int iVal){ - if( sqlite3Fts5BufferGrow(pRc, pBuf, 4) ) return; - sqlite3Fts5Put32(&pBuf->p[pBuf->n], iVal); - pBuf->n += 4; -} - /* ** Append buffer nData/pData to buffer pBuf. If an OOM error occurs, set ** the error code in p. If an error has already occurred when this function @@ -168666,11 +179430,11 @@ static void sqlite3Fts5BufferAppend32(int *pRc, Fts5Buffer *pBuf, int iVal){ static void sqlite3Fts5BufferAppendBlob( int *pRc, Fts5Buffer *pBuf, - int nData, + u32 nData, const u8 *pData ){ - assert( *pRc || nData>=0 ); - if( sqlite3Fts5BufferGrow(pRc, pBuf, nData) ) return; + assert_nc( *pRc || nData>=0 ); + if( fts5BufferGrow(pRc, pBuf, nData) ) return; memcpy(&pBuf->p[pBuf->n], pData, nData); pBuf->n += nData; } @@ -168685,7 +179449,7 @@ static void sqlite3Fts5BufferAppendString( Fts5Buffer *pBuf, const char *zStr ){ - int nStr = strlen(zStr); + int nStr = (int)strlen(zStr); sqlite3Fts5BufferAppendBlob(pRc, pBuf, nStr+1, (const u8*)zStr); pBuf->n--; } @@ -168813,23 +179577,36 @@ static int sqlite3Fts5PoslistReaderInit( return pIter->bEof; } +/* +** Append position iPos to the position list being accumulated in buffer +** pBuf, which must be already be large enough to hold the new data. +** The previous position written to this list is *piPrev. *piPrev is set +** to iPos before returning. +*/ +static void sqlite3Fts5PoslistSafeAppend( + Fts5Buffer *pBuf, + i64 *piPrev, + i64 iPos +){ + static const i64 colmask = ((i64)(0x7FFFFFFF)) << 32; + if( (iPos & colmask) != (*piPrev & colmask) ){ + pBuf->p[pBuf->n++] = 1; + pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], (iPos>>32)); + *piPrev = (iPos & colmask); + } + pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], (iPos-*piPrev)+2); + *piPrev = iPos; +} + static int sqlite3Fts5PoslistWriterAppend( Fts5Buffer *pBuf, Fts5PoslistWriter *pWriter, i64 iPos ){ - static const i64 colmask = ((i64)(0x7FFFFFFF)) << 32; - int rc = SQLITE_OK; - if( 0==sqlite3Fts5BufferGrow(&rc, pBuf, 5+5+5) ){ - if( (iPos & colmask) != (pWriter->iPrev & colmask) ){ - pBuf->p[pBuf->n++] = 1; - pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], (iPos>>32)); - pWriter->iPrev = (iPos & colmask); - } - pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], (iPos-pWriter->iPrev)+2); - pWriter->iPrev = iPos; - } - return rc; + int rc = 0; /* Initialized only to suppress erroneous warning from Clang */ + if( fts5BufferGrow(&rc, pBuf, 5+5+5) ) return rc; + sqlite3Fts5PoslistSafeAppend(pBuf, &pWriter->iPrev, iPos); + return SQLITE_OK; } static void *sqlite3Fts5MallocZero(int *pRc, int nByte){ @@ -168857,7 +179634,7 @@ static char *sqlite3Fts5Strndup(int *pRc, const char *pIn, int nIn){ char *zRet = 0; if( *pRc==SQLITE_OK ){ if( nIn<0 ){ - nIn = strlen(pIn); + nIn = (int)strlen(pIn); } zRet = (char*)sqlite3_malloc(nIn+1); if( zRet ){ @@ -168897,6 +179674,89 @@ static int sqlite3Fts5IsBareword(char t){ } +/************************************************************************* +*/ +typedef struct Fts5TermsetEntry Fts5TermsetEntry; +struct Fts5TermsetEntry { + char *pTerm; + int nTerm; + int iIdx; /* Index (main or aPrefix[] entry) */ + Fts5TermsetEntry *pNext; +}; + +struct Fts5Termset { + Fts5TermsetEntry *apHash[512]; +}; + +static int sqlite3Fts5TermsetNew(Fts5Termset **pp){ + int rc = SQLITE_OK; + *pp = sqlite3Fts5MallocZero(&rc, sizeof(Fts5Termset)); + return rc; +} + +static int sqlite3Fts5TermsetAdd( + Fts5Termset *p, + int iIdx, + const char *pTerm, int nTerm, + int *pbPresent +){ + int rc = SQLITE_OK; + *pbPresent = 0; + if( p ){ + int i; + u32 hash = 13; + Fts5TermsetEntry *pEntry; + + /* Calculate a hash value for this term. This is the same hash checksum + ** used by the fts5_hash.c module. This is not important for correct + ** operation of the module, but is necessary to ensure that some tests + ** designed to produce hash table collisions really do work. */ + for(i=nTerm-1; i>=0; i--){ + hash = (hash << 3) ^ hash ^ pTerm[i]; + } + hash = (hash << 3) ^ hash ^ iIdx; + hash = hash % ArraySize(p->apHash); + + for(pEntry=p->apHash[hash]; pEntry; pEntry=pEntry->pNext){ + if( pEntry->iIdx==iIdx + && pEntry->nTerm==nTerm + && memcmp(pEntry->pTerm, pTerm, nTerm)==0 + ){ + *pbPresent = 1; + break; + } + } + + if( pEntry==0 ){ + pEntry = sqlite3Fts5MallocZero(&rc, sizeof(Fts5TermsetEntry) + nTerm); + if( pEntry ){ + pEntry->pTerm = (char*)&pEntry[1]; + pEntry->nTerm = nTerm; + pEntry->iIdx = iIdx; + memcpy(pEntry->pTerm, pTerm, nTerm); + pEntry->pNext = p->apHash[hash]; + p->apHash[hash] = pEntry; + } + } + } + + return rc; +} + +static void sqlite3Fts5TermsetFree(Fts5Termset *p){ + if( p ){ + u32 i; + for(i=0; iapHash); i++){ + Fts5TermsetEntry *pEntry = p->apHash[i]; + while( pEntry ){ + Fts5TermsetEntry *pDel = pEntry; + pEntry = pEntry->pNext; + sqlite3_free(pDel); + } + } + sqlite3_free(p); + } +} /* ** 2014 Jun 09 @@ -168914,11 +179774,13 @@ static int sqlite3Fts5IsBareword(char t){ */ - +/* #include "fts5Int.h" */ #define FTS5_DEFAULT_PAGE_SIZE 4050 #define FTS5_DEFAULT_AUTOMERGE 4 +#define FTS5_DEFAULT_USERMERGE 4 #define FTS5_DEFAULT_CRISISMERGE 16 +#define FTS5_DEFAULT_HASHSIZE (1024*1024) /* Maximum allowed page size */ #define FTS5_MAX_PAGE_SIZE (128*1024) @@ -169093,6 +179955,33 @@ static void sqlite3Fts5Dequote(char *z){ } } + +struct Fts5Enum { + const char *zName; + int eVal; +}; +typedef struct Fts5Enum Fts5Enum; + +static int fts5ConfigSetEnum( + const Fts5Enum *aEnum, + const char *zEnum, + int *peVal +){ + int nEnum = (int)strlen(zEnum); + int i; + int iVal = -1; + + for(i=0; aEnum[i].zName; i++){ + if( sqlite3_strnicmp(aEnum[i].zName, zEnum, nEnum)==0 ){ + if( iVal>=0 ) return SQLITE_ERROR; + iVal = aEnum[i].eVal; + } + } + + *peVal = iVal; + return iVal<0 ? SQLITE_ERROR : SQLITE_OK; +} + /* ** Parse a "special" CREATE VIRTUAL TABLE directive and update ** configuration object pConfig as appropriate. @@ -169110,44 +179999,63 @@ static int fts5ConfigParseSpecial( char **pzErr /* OUT: Error message */ ){ int rc = SQLITE_OK; - int nCmd = strlen(zCmd); + int nCmd = (int)strlen(zCmd); if( sqlite3_strnicmp("prefix", zCmd, nCmd)==0 ){ const int nByte = sizeof(int) * FTS5_MAX_PREFIX_INDEXES; const char *p; - if( pConfig->aPrefix ){ - *pzErr = sqlite3_mprintf("multiple prefix=... directives"); - rc = SQLITE_ERROR; - }else{ + int bFirst = 1; + if( pConfig->aPrefix==0 ){ pConfig->aPrefix = sqlite3Fts5MallocZero(&rc, nByte); + if( rc ) return rc; } + p = zArg; - while( rc==SQLITE_OK && p[0] ){ + while( 1 ){ int nPre = 0; + while( p[0]==' ' ) p++; + if( bFirst==0 && p[0]==',' ){ + p++; + while( p[0]==' ' ) p++; + }else if( p[0]=='\0' ){ + break; + } + if( p[0]<'0' || p[0]>'9' ){ + *pzErr = sqlite3_mprintf("malformed prefix=... directive"); + rc = SQLITE_ERROR; + break; + } + + if( pConfig->nPrefix==FTS5_MAX_PREFIX_INDEXES ){ + *pzErr = sqlite3_mprintf( + "too many prefix indexes (max %d)", FTS5_MAX_PREFIX_INDEXES + ); + rc = SQLITE_ERROR; + break; + } + while( p[0]>='0' && p[0]<='9' && nPre<1000 ){ nPre = nPre*10 + (p[0] - '0'); p++; } - while( p[0]==' ' ) p++; - if( p[0]==',' ){ - p++; - }else if( p[0] ){ - *pzErr = sqlite3_mprintf("malformed prefix=... directive"); - rc = SQLITE_ERROR; - } - if( rc==SQLITE_OK && (nPre==0 || nPre>=1000) ){ - *pzErr = sqlite3_mprintf("prefix length out of range: %d", nPre); + + if( nPre<=0 || nPre>=1000 ){ + *pzErr = sqlite3_mprintf("prefix length out of range (max 999)"); rc = SQLITE_ERROR; + break; } + pConfig->aPrefix[pConfig->nPrefix] = nPre; pConfig->nPrefix++; + bFirst = 0; } + assert( pConfig->nPrefix<=FTS5_MAX_PREFIX_INDEXES ); return rc; } if( sqlite3_strnicmp("tokenize", zCmd, nCmd)==0 ){ const char *p = (const char*)zArg; - int nArg = strlen(zArg) + 1; + int nArg = (int)strlen(zArg) + 1; char **azArg = sqlite3Fts5MallocZero(&rc, sizeof(char*) * nArg); char *pDel = sqlite3Fts5MallocZero(&rc, nArg * 2); char *pSpace = pDel; @@ -169224,6 +180132,20 @@ static int fts5ConfigParseSpecial( return rc; } + if( sqlite3_strnicmp("detail", zCmd, nCmd)==0 ){ + const Fts5Enum aDetail[] = { + { "none", FTS5_DETAIL_NONE }, + { "full", FTS5_DETAIL_FULL }, + { "columns", FTS5_DETAIL_COLUMNS }, + { 0, 0 } + }; + + if( (rc = fts5ConfigSetEnum(aDetail, zArg, &pConfig->eDetail)) ){ + *pzErr = sqlite3_mprintf("malformed detail=... directive"); + } + return rc; + } + *pzErr = sqlite3_mprintf("unrecognized option: \"%.*s\"", nCmd, zCmd); return SQLITE_ERROR; } @@ -169263,7 +180185,7 @@ static const char *fts5ConfigGobbleWord( ){ const char *zRet = 0; - int nIn = strlen(zIn); + int nIn = (int)strlen(zIn); char *zOut = sqlite3_malloc(nIn+1); assert( *pRc==SQLITE_OK ); @@ -169280,7 +180202,9 @@ static const char *fts5ConfigGobbleWord( *pbQuoted = 1; }else{ zRet = fts5ConfigSkipBareword(zIn); - zOut[zRet-zIn] = '\0'; + if( zRet ){ + zOut[zRet-zIn] = '\0'; + } } } @@ -169379,6 +180303,7 @@ static int sqlite3Fts5ConfigParse( pRet->zDb = sqlite3Fts5Strndup(&rc, azArg[1], -1); pRet->zName = sqlite3Fts5Strndup(&rc, azArg[2], -1); pRet->bColumnsize = 1; + pRet->eDetail = FTS5_DETAIL_FULL; #ifdef SQLITE_DEBUG pRet->bPrefixIndex = 1; #endif @@ -169605,33 +180530,37 @@ static int sqlite3Fts5ConfigParseRank( *pzRank = 0; *pzRankArgs = 0; - p = fts5ConfigSkipWhitespace(p); - pRank = p; - p = fts5ConfigSkipBareword(p); - - if( p ){ - zRank = sqlite3Fts5MallocZero(&rc, 1 + p - pRank); - if( zRank ) memcpy(zRank, pRank, p-pRank); - }else{ + if( p==0 ){ rc = SQLITE_ERROR; - } + }else{ + p = fts5ConfigSkipWhitespace(p); + pRank = p; + p = fts5ConfigSkipBareword(p); - if( rc==SQLITE_OK ){ - p = fts5ConfigSkipWhitespace(p); - if( *p!='(' ) rc = SQLITE_ERROR; - p++; - } - if( rc==SQLITE_OK ){ - const char *pArgs; - p = fts5ConfigSkipWhitespace(p); - pArgs = p; - if( *p!=')' ){ - p = fts5ConfigSkipArgs(p); - if( p==0 ){ - rc = SQLITE_ERROR; - }else{ - zRankArgs = sqlite3Fts5MallocZero(&rc, 1 + p - pArgs); - if( zRankArgs ) memcpy(zRankArgs, pArgs, p-pArgs); + if( p ){ + zRank = sqlite3Fts5MallocZero(&rc, 1 + p - pRank); + if( zRank ) memcpy(zRank, pRank, p-pRank); + }else{ + rc = SQLITE_ERROR; + } + + if( rc==SQLITE_OK ){ + p = fts5ConfigSkipWhitespace(p); + if( *p!='(' ) rc = SQLITE_ERROR; + p++; + } + if( rc==SQLITE_OK ){ + const char *pArgs; + p = fts5ConfigSkipWhitespace(p); + pArgs = p; + if( *p!=')' ){ + p = fts5ConfigSkipArgs(p); + if( p==0 ){ + rc = SQLITE_ERROR; + }else{ + zRankArgs = sqlite3Fts5MallocZero(&rc, 1 + p - pArgs); + if( zRankArgs ) memcpy(zRankArgs, pArgs, p-pArgs); + } } } } @@ -169666,6 +180595,18 @@ static int sqlite3Fts5ConfigSetValue( } } + else if( 0==sqlite3_stricmp(zKey, "hashsize") ){ + int nHashSize = -1; + if( SQLITE_INTEGER==sqlite3_value_numeric_type(pVal) ){ + nHashSize = sqlite3_value_int(pVal); + } + if( nHashSize<=0 ){ + *pbBadkey = 1; + }else{ + pConfig->nHashSize = nHashSize; + } + } + else if( 0==sqlite3_stricmp(zKey, "automerge") ){ int nAutomerge = -1; if( SQLITE_INTEGER==sqlite3_value_numeric_type(pVal) ){ @@ -169679,6 +180620,18 @@ static int sqlite3Fts5ConfigSetValue( } } + else if( 0==sqlite3_stricmp(zKey, "usermerge") ){ + int nUsermerge = -1; + if( SQLITE_INTEGER==sqlite3_value_numeric_type(pVal) ){ + nUsermerge = sqlite3_value_int(pVal); + } + if( nUsermerge<2 || nUsermerge>16 ){ + *pbBadkey = 1; + }else{ + pConfig->nUsermerge = nUsermerge; + } + } + else if( 0==sqlite3_stricmp(zKey, "crisismerge") ){ int nCrisisMerge = -1; if( SQLITE_INTEGER==sqlite3_value_numeric_type(pVal) ){ @@ -169725,7 +180678,9 @@ static int sqlite3Fts5ConfigLoad(Fts5Config *pConfig, int iCookie){ /* Set default values */ pConfig->pgsz = FTS5_DEFAULT_PAGE_SIZE; pConfig->nAutomerge = FTS5_DEFAULT_AUTOMERGE; + pConfig->nUsermerge = FTS5_DEFAULT_USERMERGE; pConfig->nCrisisMerge = FTS5_DEFAULT_CRISISMERGE; + pConfig->nHashSize = FTS5_DEFAULT_HASHSIZE; zSql = sqlite3Fts5Mprintf(&rc, zSelect, pConfig->zDb, pConfig->zName); if( zSql ){ @@ -169765,7 +180720,6 @@ static int sqlite3Fts5ConfigLoad(Fts5Config *pConfig, int iCookie){ return rc; } - /* ** 2014 May 31 ** @@ -169782,6 +180736,8 @@ static int sqlite3Fts5ConfigLoad(Fts5Config *pConfig, int iCookie){ +/* #include "fts5Int.h" */ +/* #include "fts5parse.h" */ /* ** All token types in the generated fts5parse.h file are greater than 0. @@ -169806,6 +180762,7 @@ static void sqlite3Fts5ParserTrace(FILE*, char*); struct Fts5Expr { Fts5Index *pIndex; + Fts5Config *pConfig; Fts5ExprNode *pRoot; int bDesc; /* Iterate in descending rowid order */ int nPhrase; /* Number of phrases in expression */ @@ -169827,6 +180784,9 @@ struct Fts5ExprNode { int bEof; /* True at EOF */ int bNomatch; /* True if entry is not a match */ + /* Next method for this node. */ + int (*xNext)(Fts5Expr*, Fts5ExprNode*, int, i64); + i64 iRowid; /* Current rowid */ Fts5ExprNearset *pNear; /* For FTS5_STRING - cluster of phrases */ @@ -169838,6 +180798,12 @@ struct Fts5ExprNode { #define Fts5NodeIsString(p) ((p)->eType==FTS5_TERM || (p)->eType==FTS5_STRING) +/* +** Invoke the xNext method of an Fts5ExprNode object. This macro should be +** used as if it has the same signature as the xNext() methods themselves. +*/ +#define fts5ExprNodeNext(a,b,c,d) (b)->xNext((a), (b), (c), (d)) + /* ** An instance of the following structure represents a single search term ** or term prefix. @@ -169999,12 +180965,23 @@ static int sqlite3Fts5ExprNew( sParse.rc = SQLITE_NOMEM; sqlite3Fts5ParseNodeFree(sParse.pExpr); }else{ - pNew->pRoot = sParse.pExpr; + if( !sParse.pExpr ){ + const int nByte = sizeof(Fts5ExprNode); + pNew->pRoot = (Fts5ExprNode*)sqlite3Fts5MallocZero(&sParse.rc, nByte); + if( pNew->pRoot ){ + pNew->pRoot->bEof = 1; + } + }else{ + pNew->pRoot = sParse.pExpr; + } pNew->pIndex = 0; + pNew->pConfig = pConfig; pNew->apExprPhrase = sParse.apPhrase; pNew->nPhrase = sParse.nPhrase; sParse.apPhrase = 0; } + }else{ + sqlite3Fts5ParseNodeFree(sParse.pExpr); } sqlite3_free(sParse.apPhrase); @@ -170050,7 +181027,7 @@ static i64 fts5ExprSynonymRowid(Fts5ExprTerm *pTerm, int bDesc, int *pbEof){ assert( bDesc==0 || bDesc==1 ); for(p=pTerm; p; p=p->pSynonym){ if( 0==sqlite3Fts5IterEof(p->pIter) ){ - i64 iRowid = sqlite3Fts5IterRowid(p->pIter); + i64 iRowid = p->pIter->iRowid; if( bRetValid==0 || (bDesc!=(iRowidpSynonym ); for(p=pTerm; p; p=p->pSynonym){ Fts5IndexIter *pIter = p->pIter; - if( sqlite3Fts5IterEof(pIter)==0 && sqlite3Fts5IterRowid(pIter)==iRowid ){ - const u8 *a; - int n; - i64 dummy; - rc = sqlite3Fts5IterPoslist(pIter, pColset, &a, &n, &dummy); - if( rc!=SQLITE_OK ) goto synonym_poslist_out; + if( sqlite3Fts5IterEof(pIter)==0 && pIter->iRowid==iRowid ){ + if( pIter->nData==0 ) continue; if( nIter==nAlloc ){ int nByte = sizeof(Fts5PoslistReader) * nAlloc * 2; Fts5PoslistReader *aNew = (Fts5PoslistReader*)sqlite3_malloc(nByte); @@ -170100,20 +181072,19 @@ static int fts5ExprSynonymPoslist( if( aIter!=aStatic ) sqlite3_free(aIter); aIter = aNew; } - sqlite3Fts5PoslistReaderInit(a, n, &aIter[nIter]); + sqlite3Fts5PoslistReaderInit(pIter->pData, pIter->nData, &aIter[nIter]); assert( aIter[nIter].bEof==0 ); nIter++; } } - assert( *pbDel==0 ); if( nIter==1 ){ *pa = (u8*)aIter[0].a; *pn = aIter[0].n; }else{ Fts5PoslistWriter writer = {0}; - Fts5Buffer buf = {0,0,0}; i64 iPrev = -1; + fts5BufferZero(pBuf); while( 1 ){ int i; i64 iMin = FTS5_LARGEST_INT64; @@ -170128,15 +181099,12 @@ static int fts5ExprSynonymPoslist( } } if( iMin==FTS5_LARGEST_INT64 || rc!=SQLITE_OK ) break; - rc = sqlite3Fts5PoslistWriterAppend(&buf, &writer, iMin); + rc = sqlite3Fts5PoslistWriterAppend(pBuf, &writer, iMin); iPrev = iMin; } - if( rc ){ - sqlite3_free(buf.p); - }else{ - *pa = buf.p; - *pn = buf.n; - *pbDel = 1; + if( rc==SQLITE_OK ){ + *pa = pBuf->p; + *pn = pBuf->n; } } @@ -170159,7 +181127,6 @@ static int fts5ExprSynonymPoslist( */ static int fts5ExprPhraseIsMatch( Fts5ExprNode *pNode, /* Node pPhrase belongs to */ - Fts5Colset *pColset, /* Restrict matches to these columns */ Fts5ExprPhrase *pPhrase, /* Phrase object to initialize */ int *pbMatch /* OUT: Set to true if really a match */ ){ @@ -170173,7 +181140,7 @@ static int fts5ExprPhraseIsMatch( /* If the aStatic[] array is not large enough, allocate a large array ** using sqlite3_malloc(). This approach could be improved upon. */ - if( pPhrase->nTerm>(sizeof(aStatic) / sizeof(aStatic[0])) ){ + if( pPhrase->nTerm>ArraySize(aStatic) ){ int nByte = sizeof(Fts5PoslistReader) * pPhrase->nTerm; aIter = (Fts5PoslistReader*)sqlite3_malloc(nByte); if( !aIter ) return SQLITE_NOMEM; @@ -170183,20 +181150,23 @@ static int fts5ExprPhraseIsMatch( /* Initialize a term iterator for each term in the phrase */ for(i=0; inTerm; i++){ Fts5ExprTerm *pTerm = &pPhrase->aTerm[i]; - i64 dummy; int n = 0; int bFlag = 0; - const u8 *a = 0; + u8 *a = 0; if( pTerm->pSynonym ){ - rc = fts5ExprSynonymPoslist( - pTerm, pColset, pNode->iRowid, &bFlag, (u8**)&a, &n - ); + Fts5Buffer buf = {0, 0, 0}; + rc = fts5ExprSynonymList(pTerm, pNode->iRowid, &buf, &a, &n); + if( rc ){ + sqlite3_free(a); + goto ismatch_out; + } + if( a==buf.p ) bFlag = 1; }else{ - rc = sqlite3Fts5IterPoslist(pTerm->pIter, pColset, &a, &n, &dummy); + a = (u8*)pTerm->pIter->pData; + n = pTerm->pIter->nData; } - if( rc!=SQLITE_OK ) goto ismatch_out; sqlite3Fts5PoslistReaderInit(a, n, &aIter[i]); - aIter[i].bFlag = bFlag; + aIter[i].bFlag = (u8)bFlag; if( aIter[i].bEof ) goto ismatch_out; } @@ -170266,12 +181236,6 @@ static int fts5LookaheadReaderInit( return fts5LookaheadReaderNext(p); } -#if 0 -static int fts5LookaheadReaderEof(Fts5LookaheadReader *p){ - return (p->iPos==FTS5_LOOKAHEAD_EOF); -} -#endif - typedef struct Fts5NearTrimmer Fts5NearTrimmer; struct Fts5NearTrimmer { Fts5LookaheadReader reader; /* Input iterator */ @@ -170309,7 +181273,7 @@ static int fts5ExprNearIsMatch(int *pRc, Fts5ExprNearset *pNear){ /* If the aStatic[] array is not large enough, allocate a large array ** using sqlite3_malloc(). This approach could be improved upon. */ - if( pNear->nPhrase>(sizeof(aStatic) / sizeof(aStatic[0])) ){ + if( pNear->nPhrase>ArraySize(aStatic) ){ int nByte = sizeof(Fts5NearTrimmer) * pNear->nPhrase; a = (Fts5NearTrimmer*)sqlite3Fts5MallocZero(&rc, nByte); }else{ @@ -170386,6 +181350,315 @@ static int fts5ExprNearIsMatch(int *pRc, Fts5ExprNearset *pNear){ } } +/* +** Advance iterator pIter until it points to a value equal to or laster +** than the initial value of *piLast. If this means the iterator points +** to a value laster than *piLast, update *piLast to the new lastest value. +** +** If the iterator reaches EOF, set *pbEof to true before returning. If +** an error occurs, set *pRc to an error code. If either *pbEof or *pRc +** are set, return a non-zero value. Otherwise, return zero. +*/ +static int fts5ExprAdvanceto( + Fts5IndexIter *pIter, /* Iterator to advance */ + int bDesc, /* True if iterator is "rowid DESC" */ + i64 *piLast, /* IN/OUT: Lastest rowid seen so far */ + int *pRc, /* OUT: Error code */ + int *pbEof /* OUT: Set to true if EOF */ +){ + i64 iLast = *piLast; + i64 iRowid; + + iRowid = pIter->iRowid; + if( (bDesc==0 && iLast>iRowid) || (bDesc && iLastiRowid; + assert( (bDesc==0 && iRowid>=iLast) || (bDesc==1 && iRowid<=iLast) ); + } + *piLast = iRowid; + + return 0; +} + +static int fts5ExprSynonymAdvanceto( + Fts5ExprTerm *pTerm, /* Term iterator to advance */ + int bDesc, /* True if iterator is "rowid DESC" */ + i64 *piLast, /* IN/OUT: Lastest rowid seen so far */ + int *pRc /* OUT: Error code */ +){ + int rc = SQLITE_OK; + i64 iLast = *piLast; + Fts5ExprTerm *p; + int bEof = 0; + + for(p=pTerm; rc==SQLITE_OK && p; p=p->pSynonym){ + if( sqlite3Fts5IterEof(p->pIter)==0 ){ + i64 iRowid = p->pIter->iRowid; + if( (bDesc==0 && iLast>iRowid) || (bDesc && iLastpIter, iLast); + } + } + } + + if( rc!=SQLITE_OK ){ + *pRc = rc; + bEof = 1; + }else{ + *piLast = fts5ExprSynonymRowid(pTerm, bDesc, &bEof); + } + return bEof; +} + + +static int fts5ExprNearTest( + int *pRc, + Fts5Expr *pExpr, /* Expression that pNear is a part of */ + Fts5ExprNode *pNode /* The "NEAR" node (FTS5_STRING) */ +){ + Fts5ExprNearset *pNear = pNode->pNear; + int rc = *pRc; + + if( pExpr->pConfig->eDetail!=FTS5_DETAIL_FULL ){ + Fts5ExprTerm *pTerm; + Fts5ExprPhrase *pPhrase = pNear->apPhrase[0]; + pPhrase->poslist.n = 0; + for(pTerm=&pPhrase->aTerm[0]; pTerm; pTerm=pTerm->pSynonym){ + Fts5IndexIter *pIter = pTerm->pIter; + if( sqlite3Fts5IterEof(pIter)==0 ){ + if( pIter->iRowid==pNode->iRowid && pIter->nData>0 ){ + pPhrase->poslist.n = 1; + } + } + } + return pPhrase->poslist.n; + }else{ + int i; + + /* Check that each phrase in the nearset matches the current row. + ** Populate the pPhrase->poslist buffers at the same time. If any + ** phrase is not a match, break out of the loop early. */ + for(i=0; rc==SQLITE_OK && inPhrase; i++){ + Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; + if( pPhrase->nTerm>1 || pPhrase->aTerm[0].pSynonym || pNear->pColset ){ + int bMatch = 0; + rc = fts5ExprPhraseIsMatch(pNode, pPhrase, &bMatch); + if( bMatch==0 ) break; + }else{ + Fts5IndexIter *pIter = pPhrase->aTerm[0].pIter; + fts5BufferSet(&rc, &pPhrase->poslist, pIter->nData, pIter->pData); + } + } + + *pRc = rc; + if( i==pNear->nPhrase && (i==1 || fts5ExprNearIsMatch(pRc, pNear)) ){ + return 1; + } + return 0; + } +} + + +/* +** Initialize all term iterators in the pNear object. If any term is found +** to match no documents at all, return immediately without initializing any +** further iterators. +*/ +static int fts5ExprNearInitAll( + Fts5Expr *pExpr, + Fts5ExprNode *pNode +){ + Fts5ExprNearset *pNear = pNode->pNear; + int i, j; + int rc = SQLITE_OK; + + assert( pNode->bNomatch==0 ); + for(i=0; rc==SQLITE_OK && inPhrase; i++){ + Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; + for(j=0; jnTerm; j++){ + Fts5ExprTerm *pTerm = &pPhrase->aTerm[j]; + Fts5ExprTerm *p; + int bEof = 1; + + for(p=pTerm; p && rc==SQLITE_OK; p=p->pSynonym){ + if( p->pIter ){ + sqlite3Fts5IterClose(p->pIter); + p->pIter = 0; + } + rc = sqlite3Fts5IndexQuery( + pExpr->pIndex, p->zTerm, (int)strlen(p->zTerm), + (pTerm->bPrefix ? FTS5INDEX_QUERY_PREFIX : 0) | + (pExpr->bDesc ? FTS5INDEX_QUERY_DESC : 0), + pNear->pColset, + &p->pIter + ); + assert( rc==SQLITE_OK || p->pIter==0 ); + if( p->pIter && 0==sqlite3Fts5IterEof(p->pIter) ){ + bEof = 0; + } + } + + if( bEof ){ + pNode->bEof = 1; + return rc; + } + } + } + + return rc; +} + +/* +** If pExpr is an ASC iterator, this function returns a value with the +** same sign as: +** +** (iLhs - iRhs) +** +** Otherwise, if this is a DESC iterator, the opposite is returned: +** +** (iRhs - iLhs) +*/ +static int fts5RowidCmp( + Fts5Expr *pExpr, + i64 iLhs, + i64 iRhs +){ + assert( pExpr->bDesc==0 || pExpr->bDesc==1 ); + if( pExpr->bDesc==0 ){ + if( iLhs iRhs); + }else{ + if( iLhs>iRhs ) return -1; + return (iLhs < iRhs); + } +} + +static void fts5ExprSetEof(Fts5ExprNode *pNode){ + int i; + pNode->bEof = 1; + pNode->bNomatch = 0; + for(i=0; inChild; i++){ + fts5ExprSetEof(pNode->apChild[i]); + } +} + +static void fts5ExprNodeZeroPoslist(Fts5ExprNode *pNode){ + if( pNode->eType==FTS5_STRING || pNode->eType==FTS5_TERM ){ + Fts5ExprNearset *pNear = pNode->pNear; + int i; + for(i=0; inPhrase; i++){ + Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; + pPhrase->poslist.n = 0; + } + }else{ + int i; + for(i=0; inChild; i++){ + fts5ExprNodeZeroPoslist(pNode->apChild[i]); + } + } +} + + + +/* +** Compare the values currently indicated by the two nodes as follows: +** +** res = (*p1) - (*p2) +** +** Nodes that point to values that come later in the iteration order are +** considered to be larger. Nodes at EOF are the largest of all. +** +** This means that if the iteration order is ASC, then numerically larger +** rowids are considered larger. Or if it is the default DESC, numerically +** smaller rowids are larger. +*/ +static int fts5NodeCompare( + Fts5Expr *pExpr, + Fts5ExprNode *p1, + Fts5ExprNode *p2 +){ + if( p2->bEof ) return -1; + if( p1->bEof ) return +1; + return fts5RowidCmp(pExpr, p1->iRowid, p2->iRowid); +} + +/* +** All individual term iterators in pNear are guaranteed to be valid when +** this function is called. This function checks if all term iterators +** point to the same rowid, and if not, advances them until they do. +** If an EOF is reached before this happens, *pbEof is set to true before +** returning. +** +** SQLITE_OK is returned if an error occurs, or an SQLite error code +** otherwise. It is not considered an error code if an iterator reaches +** EOF. +*/ +static int fts5ExprNodeTest_STRING( + Fts5Expr *pExpr, /* Expression pPhrase belongs to */ + Fts5ExprNode *pNode +){ + Fts5ExprNearset *pNear = pNode->pNear; + Fts5ExprPhrase *pLeft = pNear->apPhrase[0]; + int rc = SQLITE_OK; + i64 iLast; /* Lastest rowid any iterator points to */ + int i, j; /* Phrase and token index, respectively */ + int bMatch; /* True if all terms are at the same rowid */ + const int bDesc = pExpr->bDesc; + + /* Check that this node should not be FTS5_TERM */ + assert( pNear->nPhrase>1 + || pNear->apPhrase[0]->nTerm>1 + || pNear->apPhrase[0]->aTerm[0].pSynonym + ); + + /* Initialize iLast, the "lastest" rowid any iterator points to. If the + ** iterator skips through rowids in the default ascending order, this means + ** the maximum rowid. Or, if the iterator is "ORDER BY rowid DESC", then it + ** means the minimum rowid. */ + if( pLeft->aTerm[0].pSynonym ){ + iLast = fts5ExprSynonymRowid(&pLeft->aTerm[0], bDesc, 0); + }else{ + iLast = pLeft->aTerm[0].pIter->iRowid; + } + + do { + bMatch = 1; + for(i=0; inPhrase; i++){ + Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; + for(j=0; jnTerm; j++){ + Fts5ExprTerm *pTerm = &pPhrase->aTerm[j]; + if( pTerm->pSynonym ){ + i64 iRowid = fts5ExprSynonymRowid(pTerm, bDesc, 0); + if( iRowid==iLast ) continue; + bMatch = 0; + if( fts5ExprSynonymAdvanceto(pTerm, bDesc, &iLast, &rc) ){ + pNode->bNomatch = 0; + pNode->bEof = 1; + return rc; + } + }else{ + Fts5IndexIter *pIter = pPhrase->aTerm[j].pIter; + if( pIter->iRowid==iLast ) continue; + bMatch = 0; + if( fts5ExprAdvanceto(pIter, bDesc, &iLast, &rc, &pNode->bEof) ){ + return rc; + } + } + } + } + }while( bMatch==0 ); + + pNode->iRowid = iLast; + pNode->bNomatch = ((0==fts5ExprNearTest(&rc, pExpr, pNode)) && rc==SQLITE_OK); + assert( pNode->bEof==0 || pNode->bNomatch==0 ); + + return rc; +} + /* ** Advance the first term iterator in the first phrase of pNear. Set output ** variable *pbEof to true if it reaches EOF or if an error occurs. @@ -170393,7 +181666,7 @@ static int fts5ExprNearIsMatch(int *pRc, Fts5ExprNearset *pNear){ ** Return SQLITE_OK if successful, or an SQLite error code if an error ** occurs. */ -static int fts5ExprNearAdvanceFirst( +static int fts5ExprNodeNext_STRING( Fts5Expr *pExpr, /* Expression pPhrase belongs to */ Fts5ExprNode *pNode, /* FTS5_STRING or FTS5_TERM node */ int bFromValid, @@ -170402,6 +181675,7 @@ static int fts5ExprNearAdvanceFirst( Fts5ExprTerm *pTerm = &pNode->pNear->apPhrase[0]->aTerm[0]; int rc = SQLITE_OK; + pNode->bNomatch = 0; if( pTerm->pSynonym ){ int bEof = 1; Fts5ExprTerm *p; @@ -170413,7 +181687,7 @@ static int fts5ExprNearAdvanceFirst( ** is valid - each iterator that points to a rowid before iFrom. */ for(p=pTerm; p; p=p->pSynonym){ if( sqlite3Fts5IterEof(p->pIter)==0 ){ - i64 ii = sqlite3Fts5IterRowid(p->pIter); + i64 ii = p->pIter->iRowid; if( ii==iRowid || (bFromValid && ii!=iFrom && (ii>iFrom)==pExpr->bDesc) ){ @@ -170448,108 +181722,16 @@ static int fts5ExprNearAdvanceFirst( pNode->bEof = (rc || sqlite3Fts5IterEof(pIter)); } + if( pNode->bEof==0 ){ + assert( rc==SQLITE_OK ); + rc = fts5ExprNodeTest_STRING(pExpr, pNode); + } + return rc; } -/* -** Advance iterator pIter until it points to a value equal to or laster -** than the initial value of *piLast. If this means the iterator points -** to a value laster than *piLast, update *piLast to the new lastest value. -** -** If the iterator reaches EOF, set *pbEof to true before returning. If -** an error occurs, set *pRc to an error code. If either *pbEof or *pRc -** are set, return a non-zero value. Otherwise, return zero. -*/ -static int fts5ExprAdvanceto( - Fts5IndexIter *pIter, /* Iterator to advance */ - int bDesc, /* True if iterator is "rowid DESC" */ - i64 *piLast, /* IN/OUT: Lastest rowid seen so far */ - int *pRc, /* OUT: Error code */ - int *pbEof /* OUT: Set to true if EOF */ -){ - i64 iLast = *piLast; - i64 iRowid; - iRowid = sqlite3Fts5IterRowid(pIter); - if( (bDesc==0 && iLast>iRowid) || (bDesc && iLast=iLast) || (bDesc==1 && iRowid<=iLast) ); - } - *piLast = iRowid; - - return 0; -} - -static int fts5ExprSynonymAdvanceto( - Fts5ExprTerm *pTerm, /* Term iterator to advance */ - int bDesc, /* True if iterator is "rowid DESC" */ - i64 *piLast, /* IN/OUT: Lastest rowid seen so far */ - int *pRc /* OUT: Error code */ -){ - int rc = SQLITE_OK; - i64 iLast = *piLast; - Fts5ExprTerm *p; - int bEof = 0; - - for(p=pTerm; rc==SQLITE_OK && p; p=p->pSynonym){ - if( sqlite3Fts5IterEof(p->pIter)==0 ){ - i64 iRowid = sqlite3Fts5IterRowid(p->pIter); - if( (bDesc==0 && iLast>iRowid) || (bDesc && iLastpIter, iLast); - } - } - } - - if( rc!=SQLITE_OK ){ - *pRc = rc; - bEof = 1; - }else{ - *piLast = fts5ExprSynonymRowid(pTerm, bDesc, &bEof); - } - return bEof; -} - - -static int fts5ExprNearTest( - int *pRc, - Fts5Expr *pExpr, /* Expression that pNear is a part of */ - Fts5ExprNode *pNode /* The "NEAR" node (FTS5_STRING) */ -){ - Fts5ExprNearset *pNear = pNode->pNear; - int rc = *pRc; - int i; - - /* Check that each phrase in the nearset matches the current row. - ** Populate the pPhrase->poslist buffers at the same time. If any - ** phrase is not a match, break out of the loop early. */ - for(i=0; rc==SQLITE_OK && inPhrase; i++){ - Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; - if( pPhrase->nTerm>1 || pPhrase->aTerm[0].pSynonym || pNear->pColset ){ - int bMatch = 0; - rc = fts5ExprPhraseIsMatch(pNode, pNear->pColset, pPhrase, &bMatch); - if( bMatch==0 ) break; - }else{ - rc = sqlite3Fts5IterPoslistBuffer( - pPhrase->aTerm[0].pIter, &pPhrase->poslist - ); - } - } - - *pRc = rc; - if( i==pNear->nPhrase && (i==1 || fts5ExprNearIsMatch(pRc, pNear)) ){ - return 1; - } - - return 0; -} - -static int fts5ExprTokenTest( +static int fts5ExprNodeTest_TERM( Fts5Expr *pExpr, /* Expression that pNear is a part of */ Fts5ExprNode *pNode /* The "NEAR" node (FTS5_TERM) */ ){ @@ -170558,203 +181740,98 @@ static int fts5ExprTokenTest( ** fts5_index.c iterator object. This is much faster than synthesizing ** a new poslist the way we have to for more complicated phrase or NEAR ** expressions. */ - Fts5ExprNearset *pNear = pNode->pNear; - Fts5ExprPhrase *pPhrase = pNear->apPhrase[0]; + Fts5ExprPhrase *pPhrase = pNode->pNear->apPhrase[0]; Fts5IndexIter *pIter = pPhrase->aTerm[0].pIter; - Fts5Colset *pColset = pNear->pColset; - int rc; assert( pNode->eType==FTS5_TERM ); - assert( pNear->nPhrase==1 && pPhrase->nTerm==1 ); + assert( pNode->pNear->nPhrase==1 && pPhrase->nTerm==1 ); assert( pPhrase->aTerm[0].pSynonym==0 ); - rc = sqlite3Fts5IterPoslist(pIter, pColset, - (const u8**)&pPhrase->poslist.p, &pPhrase->poslist.n, &pNode->iRowid - ); + pPhrase->poslist.n = pIter->nData; + if( pExpr->pConfig->eDetail==FTS5_DETAIL_FULL ){ + pPhrase->poslist.p = (u8*)pIter->pData; + } + pNode->iRowid = pIter->iRowid; pNode->bNomatch = (pPhrase->poslist.n==0); - return rc; + return SQLITE_OK; } /* -** All individual term iterators in pNear are guaranteed to be valid when -** this function is called. This function checks if all term iterators -** point to the same rowid, and if not, advances them until they do. -** If an EOF is reached before this happens, *pbEof is set to true before -** returning. -** -** SQLITE_OK is returned if an error occurs, or an SQLite error code -** otherwise. It is not considered an error code if an iterator reaches -** EOF. +** xNext() method for a node of type FTS5_TERM. */ -static int fts5ExprNearNextMatch( - Fts5Expr *pExpr, /* Expression pPhrase belongs to */ - Fts5ExprNode *pNode +static int fts5ExprNodeNext_TERM( + Fts5Expr *pExpr, + Fts5ExprNode *pNode, + int bFromValid, + i64 iFrom ){ - Fts5ExprNearset *pNear = pNode->pNear; - Fts5ExprPhrase *pLeft = pNear->apPhrase[0]; - int rc = SQLITE_OK; - i64 iLast; /* Lastest rowid any iterator points to */ - int i, j; /* Phrase and token index, respectively */ - int bMatch; /* True if all terms are at the same rowid */ - const int bDesc = pExpr->bDesc; + int rc; + Fts5IndexIter *pIter = pNode->pNear->apPhrase[0]->aTerm[0].pIter; - /* Check that this node should not be FTS5_TERM */ - assert( pNear->nPhrase>1 - || pNear->apPhrase[0]->nTerm>1 - || pNear->apPhrase[0]->aTerm[0].pSynonym - ); - - /* Initialize iLast, the "lastest" rowid any iterator points to. If the - ** iterator skips through rowids in the default ascending order, this means - ** the maximum rowid. Or, if the iterator is "ORDER BY rowid DESC", then it - ** means the minimum rowid. */ - if( pLeft->aTerm[0].pSynonym ){ - iLast = fts5ExprSynonymRowid(&pLeft->aTerm[0], bDesc, 0); + assert( pNode->bEof==0 ); + if( bFromValid ){ + rc = sqlite3Fts5IterNextFrom(pIter, iFrom); }else{ - iLast = sqlite3Fts5IterRowid(pLeft->aTerm[0].pIter); + rc = sqlite3Fts5IterNext(pIter); } - - do { - bMatch = 1; - for(i=0; inPhrase; i++){ - Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; - for(j=0; jnTerm; j++){ - Fts5ExprTerm *pTerm = &pPhrase->aTerm[j]; - if( pTerm->pSynonym ){ - i64 iRowid = fts5ExprSynonymRowid(pTerm, bDesc, 0); - if( iRowid==iLast ) continue; - bMatch = 0; - if( fts5ExprSynonymAdvanceto(pTerm, bDesc, &iLast, &rc) ){ - pNode->bEof = 1; - return rc; - } - }else{ - Fts5IndexIter *pIter = pPhrase->aTerm[j].pIter; - i64 iRowid = sqlite3Fts5IterRowid(pIter); - if( iRowid==iLast ) continue; - bMatch = 0; - if( fts5ExprAdvanceto(pIter, bDesc, &iLast, &rc, &pNode->bEof) ){ - return rc; - } - } - } - } - }while( bMatch==0 ); - - pNode->iRowid = iLast; - pNode->bNomatch = (0==fts5ExprNearTest(&rc, pExpr, pNode)); - - return rc; -} - -/* -** Initialize all term iterators in the pNear object. If any term is found -** to match no documents at all, return immediately without initializing any -** further iterators. -*/ -static int fts5ExprNearInitAll( - Fts5Expr *pExpr, - Fts5ExprNode *pNode -){ - Fts5ExprNearset *pNear = pNode->pNear; - int i, j; - int rc = SQLITE_OK; - - for(i=0; rc==SQLITE_OK && inPhrase; i++){ - Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; - for(j=0; jnTerm; j++){ - Fts5ExprTerm *pTerm = &pPhrase->aTerm[j]; - Fts5ExprTerm *p; - int bEof = 1; - - for(p=pTerm; p && rc==SQLITE_OK; p=p->pSynonym){ - if( p->pIter ){ - sqlite3Fts5IterClose(p->pIter); - p->pIter = 0; - } - rc = sqlite3Fts5IndexQuery( - pExpr->pIndex, p->zTerm, strlen(p->zTerm), - (pTerm->bPrefix ? FTS5INDEX_QUERY_PREFIX : 0) | - (pExpr->bDesc ? FTS5INDEX_QUERY_DESC : 0), - pNear->pColset, - &p->pIter - ); - assert( rc==SQLITE_OK || p->pIter==0 ); - if( p->pIter && 0==sqlite3Fts5IterEof(p->pIter) ){ - bEof = 0; - } - } - - if( bEof ){ - pNode->bEof = 1; - return rc; - } - } - } - - return rc; -} - -/* fts5ExprNodeNext() calls fts5ExprNodeNextMatch(). And vice-versa. */ -static int fts5ExprNodeNextMatch(Fts5Expr*, Fts5ExprNode*); - - -/* -** If pExpr is an ASC iterator, this function returns a value with the -** same sign as: -** -** (iLhs - iRhs) -** -** Otherwise, if this is a DESC iterator, the opposite is returned: -** -** (iRhs - iLhs) -*/ -static int fts5RowidCmp( - Fts5Expr *pExpr, - i64 iLhs, - i64 iRhs -){ - assert( pExpr->bDesc==0 || pExpr->bDesc==1 ); - if( pExpr->bDesc==0 ){ - if( iLhs iRhs); + if( rc==SQLITE_OK && sqlite3Fts5IterEof(pIter)==0 ){ + rc = fts5ExprNodeTest_TERM(pExpr, pNode); }else{ - if( iLhs>iRhs ) return -1; - return (iLhs < iRhs); + pNode->bEof = 1; + pNode->bNomatch = 0; } + return rc; } -static void fts5ExprSetEof(Fts5ExprNode *pNode){ +static void fts5ExprNodeTest_OR( + Fts5Expr *pExpr, /* Expression of which pNode is a part */ + Fts5ExprNode *pNode /* Expression node to test */ +){ + Fts5ExprNode *pNext = pNode->apChild[0]; int i; - pNode->bEof = 1; + + for(i=1; inChild; i++){ + Fts5ExprNode *pChild = pNode->apChild[i]; + int cmp = fts5NodeCompare(pExpr, pNext, pChild); + if( cmp>0 || (cmp==0 && pChild->bNomatch==0) ){ + pNext = pChild; + } + } + pNode->iRowid = pNext->iRowid; + pNode->bEof = pNext->bEof; + pNode->bNomatch = pNext->bNomatch; +} + +static int fts5ExprNodeNext_OR( + Fts5Expr *pExpr, + Fts5ExprNode *pNode, + int bFromValid, + i64 iFrom +){ + int i; + i64 iLast = pNode->iRowid; + for(i=0; inChild; i++){ - fts5ExprSetEof(pNode->apChild[i]); - } -} - -static void fts5ExprNodeZeroPoslist(Fts5ExprNode *pNode){ - if( pNode->eType==FTS5_STRING || pNode->eType==FTS5_TERM ){ - Fts5ExprNearset *pNear = pNode->pNear; - int i; - for(i=0; inPhrase; i++){ - Fts5ExprPhrase *pPhrase = pNear->apPhrase[i]; - pPhrase->poslist.n = 0; - } - }else{ - int i; - for(i=0; inChild; i++){ - fts5ExprNodeZeroPoslist(pNode->apChild[i]); + Fts5ExprNode *p1 = pNode->apChild[i]; + assert( p1->bEof || fts5RowidCmp(pExpr, p1->iRowid, iLast)>=0 ); + if( p1->bEof==0 ){ + if( (p1->iRowid==iLast) + || (bFromValid && fts5RowidCmp(pExpr, p1->iRowid, iFrom)<0) + ){ + int rc = fts5ExprNodeNext(pExpr, p1, bFromValid, iFrom); + if( rc!=SQLITE_OK ) return rc; + } } } + + fts5ExprNodeTest_OR(pExpr, pNode); + return SQLITE_OK; } - -static int fts5ExprNodeNext(Fts5Expr*, Fts5ExprNode*, int, i64); - /* ** Argument pNode is an FTS5_AND node. */ -static int fts5ExprAndNextRowid( +static int fts5ExprNodeTest_AND( Fts5Expr *pExpr, /* Expression pPhrase belongs to */ Fts5ExprNode *pAnd /* FTS5_AND node to advance */ ){ @@ -170769,15 +181846,11 @@ static int fts5ExprAndNextRowid( bMatch = 1; for(iChild=0; iChildnChild; iChild++){ Fts5ExprNode *pChild = pAnd->apChild[iChild]; - if( 0 && pChild->eType==FTS5_STRING ){ - /* TODO */ - }else{ - int cmp = fts5RowidCmp(pExpr, iLast, pChild->iRowid); - if( cmp>0 ){ - /* Advance pChild until it points to iLast or laster */ - rc = fts5ExprNodeNext(pExpr, pChild, 1, iLast); - if( rc!=SQLITE_OK ) return rc; - } + int cmp = fts5RowidCmp(pExpr, iLast, pChild->iRowid); + if( cmp>0 ){ + /* Advance pChild until it points to iLast or laster */ + rc = fts5ExprNodeNext(pExpr, pChild, 1, iLast); + if( rc!=SQLITE_OK ) return rc; } /* If the child node is now at EOF, so is the parent AND node. Otherwise, @@ -170807,126 +181880,66 @@ static int fts5ExprAndNextRowid( return SQLITE_OK; } - -/* -** Compare the values currently indicated by the two nodes as follows: -** -** res = (*p1) - (*p2) -** -** Nodes that point to values that come later in the iteration order are -** considered to be larger. Nodes at EOF are the largest of all. -** -** This means that if the iteration order is ASC, then numerically larger -** rowids are considered larger. Or if it is the default DESC, numerically -** smaller rowids are larger. -*/ -static int fts5NodeCompare( - Fts5Expr *pExpr, - Fts5ExprNode *p1, - Fts5ExprNode *p2 -){ - if( p2->bEof ) return -1; - if( p1->bEof ) return +1; - return fts5RowidCmp(pExpr, p1->iRowid, p2->iRowid); -} - -/* -** Advance node iterator pNode, part of expression pExpr. If argument -** bFromValid is zero, then pNode is advanced exactly once. Or, if argument -** bFromValid is non-zero, then pNode is advanced until it is at or past -** rowid value iFrom. Whether "past" means "less than" or "greater than" -** depends on whether this is an ASC or DESC iterator. -*/ -static int fts5ExprNodeNext( +static int fts5ExprNodeNext_AND( Fts5Expr *pExpr, Fts5ExprNode *pNode, int bFromValid, i64 iFrom ){ - int rc = SQLITE_OK; - - if( pNode->bEof==0 ){ - switch( pNode->eType ){ - case FTS5_STRING: { - rc = fts5ExprNearAdvanceFirst(pExpr, pNode, bFromValid, iFrom); - break; - }; - - case FTS5_TERM: { - Fts5IndexIter *pIter = pNode->pNear->apPhrase[0]->aTerm[0].pIter; - if( bFromValid ){ - rc = sqlite3Fts5IterNextFrom(pIter, iFrom); - }else{ - rc = sqlite3Fts5IterNext(pIter); - } - if( rc==SQLITE_OK && sqlite3Fts5IterEof(pIter)==0 ){ - assert( rc==SQLITE_OK ); - rc = fts5ExprTokenTest(pExpr, pNode); - }else{ - pNode->bEof = 1; - } - return rc; - }; - - case FTS5_AND: { - Fts5ExprNode *pLeft = pNode->apChild[0]; - rc = fts5ExprNodeNext(pExpr, pLeft, bFromValid, iFrom); - break; - } - - case FTS5_OR: { - int i; - i64 iLast = pNode->iRowid; - - for(i=0; rc==SQLITE_OK && inChild; i++){ - Fts5ExprNode *p1 = pNode->apChild[i]; - assert( p1->bEof || fts5RowidCmp(pExpr, p1->iRowid, iLast)>=0 ); - if( p1->bEof==0 ){ - if( (p1->iRowid==iLast) - || (bFromValid && fts5RowidCmp(pExpr, p1->iRowid, iFrom)<0) - ){ - rc = fts5ExprNodeNext(pExpr, p1, bFromValid, iFrom); - } - } - } - - break; - } - - default: assert( pNode->eType==FTS5_NOT ); { - assert( pNode->nChild==2 ); - rc = fts5ExprNodeNext(pExpr, pNode->apChild[0], bFromValid, iFrom); - break; - } - } - - if( rc==SQLITE_OK ){ - rc = fts5ExprNodeNextMatch(pExpr, pNode); - } + int rc = fts5ExprNodeNext(pExpr, pNode->apChild[0], bFromValid, iFrom); + if( rc==SQLITE_OK ){ + rc = fts5ExprNodeTest_AND(pExpr, pNode); } - - /* Assert that if bFromValid was true, either: - ** - ** a) an error occurred, or - ** b) the node is now at EOF, or - ** c) the node is now at or past rowid iFrom. - */ - assert( bFromValid==0 - || rc!=SQLITE_OK /* a */ - || pNode->bEof /* b */ - || pNode->iRowid==iFrom || pExpr->bDesc==(pNode->iRowidapChild[0]; + Fts5ExprNode *p2 = pNode->apChild[1]; + assert( pNode->nChild==2 ); + + while( rc==SQLITE_OK && p1->bEof==0 ){ + int cmp = fts5NodeCompare(pExpr, p1, p2); + if( cmp>0 ){ + rc = fts5ExprNodeNext(pExpr, p2, 1, p1->iRowid); + cmp = fts5NodeCompare(pExpr, p1, p2); + } + assert( rc!=SQLITE_OK || cmp<=0 ); + if( cmp || p2->bNomatch ) break; + rc = fts5ExprNodeNext(pExpr, p1, 0, 0); + } + pNode->bEof = p1->bEof; + pNode->bNomatch = p1->bNomatch; + pNode->iRowid = p1->iRowid; + if( p1->bEof ){ + fts5ExprNodeZeroPoslist(p2); + } + return rc; +} + +static int fts5ExprNodeNext_NOT( + Fts5Expr *pExpr, + Fts5ExprNode *pNode, + int bFromValid, + i64 iFrom +){ + int rc = fts5ExprNodeNext(pExpr, pNode->apChild[0], bFromValid, iFrom); + if( rc==SQLITE_OK ){ + rc = fts5ExprNodeTest_NOT(pExpr, pNode); + } + return rc; +} /* ** If pNode currently points to a match, this function returns SQLITE_OK ** without modifying it. Otherwise, pNode is advanced until it does point ** to a match or EOF is reached. */ -static int fts5ExprNodeNextMatch( +static int fts5ExprNodeTest( Fts5Expr *pExpr, /* Expression of which pNode is a part */ Fts5ExprNode *pNode /* Expression node to test */ ){ @@ -170935,55 +181948,27 @@ static int fts5ExprNodeNextMatch( switch( pNode->eType ){ case FTS5_STRING: { - /* Advance the iterators until they all point to the same rowid */ - rc = fts5ExprNearNextMatch(pExpr, pNode); + rc = fts5ExprNodeTest_STRING(pExpr, pNode); break; } case FTS5_TERM: { - rc = fts5ExprTokenTest(pExpr, pNode); + rc = fts5ExprNodeTest_TERM(pExpr, pNode); break; } case FTS5_AND: { - rc = fts5ExprAndNextRowid(pExpr, pNode); + rc = fts5ExprNodeTest_AND(pExpr, pNode); break; } case FTS5_OR: { - Fts5ExprNode *pNext = pNode->apChild[0]; - int i; - - for(i=1; inChild; i++){ - Fts5ExprNode *pChild = pNode->apChild[i]; - int cmp = fts5NodeCompare(pExpr, pNext, pChild); - if( cmp>0 || (cmp==0 && pChild->bNomatch==0) ){ - pNext = pChild; - } - } - pNode->iRowid = pNext->iRowid; - pNode->bEof = pNext->bEof; - pNode->bNomatch = pNext->bNomatch; + fts5ExprNodeTest_OR(pExpr, pNode); break; } default: assert( pNode->eType==FTS5_NOT ); { - Fts5ExprNode *p1 = pNode->apChild[0]; - Fts5ExprNode *p2 = pNode->apChild[1]; - assert( pNode->nChild==2 ); - - while( rc==SQLITE_OK && p1->bEof==0 ){ - int cmp = fts5NodeCompare(pExpr, p1, p2); - if( cmp>0 ){ - rc = fts5ExprNodeNext(pExpr, p2, 1, p1->iRowid); - cmp = fts5NodeCompare(pExpr, p1, p2); - } - assert( rc!=SQLITE_OK || cmp<=0 ); - if( cmp || p2->bNomatch ) break; - rc = fts5ExprNodeNext(pExpr, p1, 0, 0); - } - pNode->bEof = p1->bEof; - pNode->iRowid = p1->iRowid; + rc = fts5ExprNodeTest_NOT(pExpr, pNode); break; } } @@ -171002,20 +181987,42 @@ static int fts5ExprNodeNextMatch( static int fts5ExprNodeFirst(Fts5Expr *pExpr, Fts5ExprNode *pNode){ int rc = SQLITE_OK; pNode->bEof = 0; + pNode->bNomatch = 0; if( Fts5NodeIsString(pNode) ){ /* Initialize all term iterators in the NEAR object. */ rc = fts5ExprNearInitAll(pExpr, pNode); + }else if( pNode->xNext==0 ){ + pNode->bEof = 1; }else{ int i; + int nEof = 0; for(i=0; inChild && rc==SQLITE_OK; i++){ + Fts5ExprNode *pChild = pNode->apChild[i]; rc = fts5ExprNodeFirst(pExpr, pNode->apChild[i]); + assert( pChild->bEof==0 || pChild->bEof==1 ); + nEof += pChild->bEof; } pNode->iRowid = pNode->apChild[0]->iRowid; + + switch( pNode->eType ){ + case FTS5_AND: + if( nEof>0 ) fts5ExprSetEof(pNode); + break; + + case FTS5_OR: + if( pNode->nChild==nEof ) fts5ExprSetEof(pNode); + break; + + default: + assert( pNode->eType==FTS5_NOT ); + pNode->bEof = pNode->apChild[0]->bEof; + break; + } } if( rc==SQLITE_OK ){ - rc = fts5ExprNodeNextMatch(pExpr, pNode); + rc = fts5ExprNodeTest(pExpr, pNode); } return rc; } @@ -171038,22 +182045,22 @@ static int fts5ExprNodeFirst(Fts5Expr *pExpr, Fts5ExprNode *pNode){ */ static int sqlite3Fts5ExprFirst(Fts5Expr *p, Fts5Index *pIdx, i64 iFirst, int bDesc){ Fts5ExprNode *pRoot = p->pRoot; - int rc = SQLITE_OK; - if( pRoot ){ - p->pIndex = pIdx; - p->bDesc = bDesc; - rc = fts5ExprNodeFirst(p, pRoot); + int rc; /* Return code */ - /* If not at EOF but the current rowid occurs earlier than iFirst in - ** the iteration order, move to document iFirst or later. */ - if( pRoot->bEof==0 && fts5RowidCmp(p, pRoot->iRowid, iFirst)<0 ){ - rc = fts5ExprNodeNext(p, pRoot, 1, iFirst); - } + p->pIndex = pIdx; + p->bDesc = bDesc; + rc = fts5ExprNodeFirst(p, pRoot); - /* If the iterator is not at a real match, skip forward until it is. */ - while( pRoot->bNomatch && rc==SQLITE_OK && pRoot->bEof==0 ){ - rc = fts5ExprNodeNext(p, pRoot, 0, 0); - } + /* If not at EOF but the current rowid occurs earlier than iFirst in + ** the iteration order, move to document iFirst or later. */ + if( pRoot->bEof==0 && fts5RowidCmp(p, pRoot->iRowid, iFirst)<0 ){ + rc = fts5ExprNodeNext(p, pRoot, 1, iFirst); + } + + /* If the iterator is not at a real match, skip forward until it is. */ + while( pRoot->bNomatch ){ + assert( pRoot->bEof==0 && rc==SQLITE_OK ); + rc = fts5ExprNodeNext(p, pRoot, 0, 0); } return rc; } @@ -171067,9 +182074,11 @@ static int sqlite3Fts5ExprFirst(Fts5Expr *p, Fts5Index *pIdx, i64 iFirst, int bD static int sqlite3Fts5ExprNext(Fts5Expr *p, i64 iLast){ int rc; Fts5ExprNode *pRoot = p->pRoot; + assert( pRoot->bEof==0 && pRoot->bNomatch==0 ); do { rc = fts5ExprNodeNext(p, pRoot, 0, 0); - }while( pRoot->bNomatch && pRoot->bEof==0 && rc==SQLITE_OK ); + assert( pRoot->bNomatch==0 || (rc==SQLITE_OK && pRoot->bEof==0) ); + }while( pRoot->bNomatch ); if( fts5RowidCmp(p, pRoot->iRowid, iLast)>0 ){ pRoot->bEof = 1; } @@ -171077,7 +182086,7 @@ static int sqlite3Fts5ExprNext(Fts5Expr *p, i64 iLast){ } static int sqlite3Fts5ExprEof(Fts5Expr *p){ - return (p->pRoot==0 || p->pRoot->bEof); + return p->pRoot->bEof; } static i64 sqlite3Fts5ExprRowid(Fts5Expr *p){ @@ -171102,10 +182111,10 @@ static void fts5ExprPhraseFree(Fts5ExprPhrase *pPhrase){ Fts5ExprTerm *pTerm = &pPhrase->aTerm[i]; sqlite3_free(pTerm->zTerm); sqlite3Fts5IterClose(pTerm->pIter); - for(pSyn=pTerm->pSynonym; pSyn; pSyn=pNext){ pNext = pSyn->pSynonym; sqlite3Fts5IterClose(pSyn->pIter); + fts5BufferFree((Fts5Buffer*)&pSyn[1]); sqlite3_free(pSyn); } } @@ -171160,6 +182169,21 @@ static Fts5ExprNearset *sqlite3Fts5ParseNearset( sqlite3Fts5ParseNearsetFree(pNear); sqlite3Fts5ParsePhraseFree(pPhrase); }else{ + if( pRet->nPhrase>0 ){ + Fts5ExprPhrase *pLast = pRet->apPhrase[pRet->nPhrase-1]; + assert( pLast==pParse->apPhrase[pParse->nPhrase-2] ); + if( pPhrase->nTerm==0 ){ + fts5ExprPhraseFree(pPhrase); + pRet->nPhrase--; + pParse->nPhrase--; + pPhrase = pLast; + }else if( pLast->nTerm==0 ){ + fts5ExprPhraseFree(pLast); + pParse->apPhrase[pParse->nPhrase-2] = pPhrase; + pParse->nPhrase--; + pRet->nPhrase--; + } + } pRet->apPhrase[pRet->nPhrase++] = pPhrase; } return pRet; @@ -171187,19 +182211,21 @@ static int fts5ParseTokenize( TokenCtx *pCtx = (TokenCtx*)pContext; Fts5ExprPhrase *pPhrase = pCtx->pPhrase; + UNUSED_PARAM2(iUnused1, iUnused2); + /* If an error has already occurred, this is a no-op */ if( pCtx->rc!=SQLITE_OK ) return pCtx->rc; + if( nToken>FTS5_MAX_TOKEN_SIZE ) nToken = FTS5_MAX_TOKEN_SIZE; - assert( pPhrase==0 || pPhrase->nTerm>0 ); - if( pPhrase && (tflags & FTS5_TOKEN_COLOCATED) ){ + if( pPhrase && pPhrase->nTerm>0 && (tflags & FTS5_TOKEN_COLOCATED) ){ Fts5ExprTerm *pSyn; - int nByte = sizeof(Fts5ExprTerm) + nToken+1; + int nByte = sizeof(Fts5ExprTerm) + sizeof(Fts5Buffer) + nToken+1; pSyn = (Fts5ExprTerm*)sqlite3_malloc(nByte); if( pSyn==0 ){ rc = SQLITE_NOMEM; }else{ memset(pSyn, 0, nByte); - pSyn->zTerm = (char*)&pSyn[1]; + pSyn->zTerm = ((char*)pSyn) + sizeof(Fts5ExprTerm) + sizeof(Fts5Buffer); memcpy(pSyn->zTerm, pToken, nToken); pSyn->pSynonym = pPhrase->aTerm[pPhrase->nTerm-1].pSynonym; pPhrase->aTerm[pPhrase->nTerm-1].pSynonym = pSyn; @@ -171284,7 +182310,7 @@ static Fts5ExprPhrase *sqlite3Fts5ParseTerm( int flags = FTS5_TOKENIZE_QUERY | (bPrefix ? FTS5_TOKENIZE_QUERY : 0); int n; sqlite3Fts5Dequote(z); - n = strlen(z); + n = (int)strlen(z); rc = sqlite3Fts5Tokenize(pConfig, flags, z, n, &sCtx, fts5ParseTokenize); } sqlite3_free(z); @@ -171292,7 +182318,7 @@ static Fts5ExprPhrase *sqlite3Fts5ParseTerm( pParse->rc = rc; fts5ExprPhraseFree(sCtx.pPhrase); sCtx.pPhrase = 0; - }else if( sCtx.pPhrase ){ + }else{ if( pAppend==0 ){ if( (pParse->nPhrase % 8)==0 ){ @@ -171309,9 +182335,14 @@ static Fts5ExprPhrase *sqlite3Fts5ParseTerm( pParse->nPhrase++; } + if( sCtx.pPhrase==0 ){ + /* This happens when parsing a token or quoted phrase that contains + ** no token characters at all. (e.g ... MATCH '""'). */ + sCtx.pPhrase = sqlite3Fts5MallocZero(&pParse->rc, sizeof(Fts5ExprPhrase)); + }else if( sCtx.pPhrase->nTerm ){ + sCtx.pPhrase->aTerm[sCtx.pPhrase->nTerm-1].bPrefix = bPrefix; + } pParse->apPhrase[pParse->nPhrase-1] = sCtx.pPhrase; - assert( sCtx.pPhrase->nTerm>0 ); - sCtx.pPhrase->aTerm[sCtx.pPhrase->nTerm-1].bPrefix = bPrefix; } return sCtx.pPhrase; @@ -171322,7 +182353,6 @@ static Fts5ExprPhrase *sqlite3Fts5ParseTerm( ** expression passed as the second argument. */ static int sqlite3Fts5ExprClonePhrase( - Fts5Config *pConfig, Fts5Expr *pExpr, int iPhrase, Fts5Expr **ppNew @@ -171330,14 +182360,10 @@ static int sqlite3Fts5ExprClonePhrase( int rc = SQLITE_OK; /* Return code */ Fts5ExprPhrase *pOrig; /* The phrase extracted from pExpr */ int i; /* Used to iterate through phrase terms */ - Fts5Expr *pNew = 0; /* Expression to return via *ppNew */ - TokenCtx sCtx = {0,0}; /* Context object for fts5ParseTokenize */ - pOrig = pExpr->apExprPhrase[iPhrase]; - pNew = (Fts5Expr*)sqlite3Fts5MallocZero(&rc, sizeof(Fts5Expr)); if( rc==SQLITE_OK ){ pNew->apExprPhrase = (Fts5ExprPhrase**)sqlite3Fts5MallocZero(&rc, @@ -171351,13 +182377,25 @@ static int sqlite3Fts5ExprClonePhrase( pNew->pRoot->pNear = (Fts5ExprNearset*)sqlite3Fts5MallocZero(&rc, sizeof(Fts5ExprNearset) + sizeof(Fts5ExprPhrase*)); } + if( rc==SQLITE_OK ){ + Fts5Colset *pColsetOrig = pOrig->pNode->pNear->pColset; + if( pColsetOrig ){ + int nByte = sizeof(Fts5Colset) + pColsetOrig->nCol * sizeof(int); + Fts5Colset *pColset = (Fts5Colset*)sqlite3Fts5MallocZero(&rc, nByte); + if( pColset ){ + memcpy(pColset, pColsetOrig, nByte); + } + pNew->pRoot->pNear->pColset = pColset; + } + } for(i=0; rc==SQLITE_OK && inTerm; i++){ int tflags = 0; Fts5ExprTerm *p; for(p=&pOrig->aTerm[i]; p && rc==SQLITE_OK; p=p->pSynonym){ const char *zTerm = p->zTerm; - rc = fts5ParseTokenize((void*)&sCtx, tflags, zTerm, strlen(zTerm), 0, 0); + rc = fts5ParseTokenize((void*)&sCtx, tflags, zTerm, (int)strlen(zTerm), + 0, 0); tflags = FTS5_TOKEN_COLOCATED; } if( rc==SQLITE_OK ){ @@ -171368,6 +182406,7 @@ static int sqlite3Fts5ExprClonePhrase( if( rc==SQLITE_OK ){ /* All the allocations succeeded. Put the expression object together. */ pNew->pIndex = pExpr->pIndex; + pNew->pConfig = pExpr->pConfig; pNew->nPhrase = 1; pNew->apExprPhrase[0] = sCtx.pPhrase; pNew->pRoot->pNear->apPhrase[0] = sCtx.pPhrase; @@ -171376,8 +182415,10 @@ static int sqlite3Fts5ExprClonePhrase( if( pOrig->nTerm==1 && pOrig->aTerm[0].pSynonym==0 ){ pNew->pRoot->eType = FTS5_TERM; + pNew->pRoot->xNext = fts5ExprNodeNext_TERM; }else{ pNew->pRoot->eType = FTS5_STRING; + pNew->pRoot->xNext = fts5ExprNodeNext_STRING; } }else{ sqlite3Fts5ExprFree(pNew); @@ -171408,23 +182449,25 @@ static void sqlite3Fts5ParseSetDistance( Fts5ExprNearset *pNear, Fts5Token *p ){ - int nNear = 0; - int i; - if( p->n ){ - for(i=0; in; i++){ - char c = (char)p->p[i]; - if( c<'0' || c>'9' ){ - sqlite3Fts5ParseError( - pParse, "expected integer, got \"%.*s\"", p->n, p->p - ); - return; + if( pNear ){ + int nNear = 0; + int i; + if( p->n ){ + for(i=0; in; i++){ + char c = (char)p->p[i]; + if( c<'0' || c>'9' ){ + sqlite3Fts5ParseError( + pParse, "expected integer, got \"%.*s\"", p->n, p->p + ); + return; + } + nNear = nNear * 10 + (p->p[i] - '0'); } - nNear = nNear * 10 + (p->p[i] - '0'); + }else{ + nNear = FTS5_DEFAULT_NEARDIST; } - }else{ - nNear = FTS5_DEFAULT_NEARDIST; + pNear->nNear = nNear; } - pNear->nNear = nNear; } /* @@ -171509,6 +182552,15 @@ static void sqlite3Fts5ParseSetColset( Fts5ExprNearset *pNear, Fts5Colset *pColset ){ + if( pParse->pConfig->eDetail==FTS5_DETAIL_NONE ){ + pParse->rc = SQLITE_ERROR; + pParse->zErr = sqlite3_mprintf( + "fts5: column queries are not supported (detail=none)" + ); + sqlite3_free(pColset); + return; + } + if( pNear ){ pNear->pColset = pColset; }else{ @@ -171516,6 +182568,38 @@ static void sqlite3Fts5ParseSetColset( } } +static void fts5ExprAssignXNext(Fts5ExprNode *pNode){ + switch( pNode->eType ){ + case FTS5_STRING: { + Fts5ExprNearset *pNear = pNode->pNear; + if( pNear->nPhrase==1 && pNear->apPhrase[0]->nTerm==1 + && pNear->apPhrase[0]->aTerm[0].pSynonym==0 + ){ + pNode->eType = FTS5_TERM; + pNode->xNext = fts5ExprNodeNext_TERM; + }else{ + pNode->xNext = fts5ExprNodeNext_STRING; + } + break; + }; + + case FTS5_OR: { + pNode->xNext = fts5ExprNodeNext_OR; + break; + }; + + case FTS5_AND: { + pNode->xNext = fts5ExprNodeNext_AND; + break; + }; + + default: assert( pNode->eType==FTS5_NOT ); { + pNode->xNext = fts5ExprNodeNext_NOT; + break; + }; + } +} + static void fts5ExprAddChildren(Fts5ExprNode *p, Fts5ExprNode *pSub){ if( p->eType!=FTS5_NOT && pSub->eType==p->eType ){ int nByte = sizeof(Fts5ExprNode*) * pSub->nChild; @@ -171565,17 +182649,31 @@ static Fts5ExprNode *sqlite3Fts5ParseNode( if( pRet ){ pRet->eType = eType; pRet->pNear = pNear; + fts5ExprAssignXNext(pRet); if( eType==FTS5_STRING ){ int iPhrase; for(iPhrase=0; iPhrasenPhrase; iPhrase++){ pNear->apPhrase[iPhrase]->pNode = pRet; + if( pNear->apPhrase[iPhrase]->nTerm==0 ){ + pRet->xNext = 0; + pRet->eType = FTS5_EOF; + } } - if( pNear->nPhrase==1 - && pNear->apPhrase[0]->nTerm==1 - && pNear->apPhrase[0]->aTerm[0].pSynonym==0 + + if( pParse->pConfig->eDetail!=FTS5_DETAIL_FULL + && (pNear->nPhrase!=1 || pNear->apPhrase[0]->nTerm>1) ){ - pRet->eType = FTS5_TERM; + assert( pParse->rc==SQLITE_OK ); + pParse->rc = SQLITE_ERROR; + assert( pParse->zErr==0 ); + pParse->zErr = sqlite3_mprintf( + "fts5: %s queries are not supported (detail!=full)", + pNear->nPhrase==1 ? "phrase": "NEAR" + ); + sqlite3_free(pRet); + pRet = 0; } + }else{ fts5ExprAddChildren(pRet, pLeft); fts5ExprAddChildren(pRet, pRight); @@ -171592,6 +182690,70 @@ static Fts5ExprNode *sqlite3Fts5ParseNode( return pRet; } +static Fts5ExprNode *sqlite3Fts5ParseImplicitAnd( + Fts5Parse *pParse, /* Parse context */ + Fts5ExprNode *pLeft, /* Left hand child expression */ + Fts5ExprNode *pRight /* Right hand child expression */ +){ + Fts5ExprNode *pRet = 0; + Fts5ExprNode *pPrev; + + if( pParse->rc ){ + sqlite3Fts5ParseNodeFree(pLeft); + sqlite3Fts5ParseNodeFree(pRight); + }else{ + + assert( pLeft->eType==FTS5_STRING + || pLeft->eType==FTS5_TERM + || pLeft->eType==FTS5_EOF + || pLeft->eType==FTS5_AND + ); + assert( pRight->eType==FTS5_STRING + || pRight->eType==FTS5_TERM + || pRight->eType==FTS5_EOF + ); + + if( pLeft->eType==FTS5_AND ){ + pPrev = pLeft->apChild[pLeft->nChild-1]; + }else{ + pPrev = pLeft; + } + assert( pPrev->eType==FTS5_STRING + || pPrev->eType==FTS5_TERM + || pPrev->eType==FTS5_EOF + ); + + if( pRight->eType==FTS5_EOF ){ + assert( pParse->apPhrase[pParse->nPhrase-1]==pRight->pNear->apPhrase[0] ); + sqlite3Fts5ParseNodeFree(pRight); + pRet = pLeft; + pParse->nPhrase--; + } + else if( pPrev->eType==FTS5_EOF ){ + Fts5ExprPhrase **ap; + + if( pPrev==pLeft ){ + pRet = pRight; + }else{ + pLeft->apChild[pLeft->nChild-1] = pRight; + pRet = pLeft; + } + + ap = &pParse->apPhrase[pParse->nPhrase-1-pRight->pNear->nPhrase]; + assert( ap[0]==pPrev->pNear->apPhrase[0] ); + memmove(ap, &ap[1], sizeof(Fts5ExprPhrase*)*pRight->pNear->nPhrase); + pParse->nPhrase--; + + sqlite3Fts5ParseNodeFree(pPrev); + } + else{ + pRet = sqlite3Fts5ParseNode(pParse, FTS5_AND, pLeft, pRight, 0); + } + } + + return pRet; +} + static char *fts5ExprTermPrint(Fts5ExprTerm *pTerm){ int nByte = 0; Fts5ExprTerm *p; @@ -171599,7 +182761,7 @@ static char *fts5ExprTermPrint(Fts5ExprTerm *pTerm){ /* Determine the maximum amount of space required. */ for(p=pTerm; p; p=p->pSynonym){ - nByte += strlen(pTerm->zTerm) * 2 + 3 + 2; + nByte += (int)strlen(pTerm->zTerm) * 2 + 3 + 2; } zQuoted = sqlite3_malloc(nByte); @@ -171688,6 +182850,9 @@ static char *fts5ExprPrintTcl( for(iTerm=0; zRet && iTermnTerm; iTerm++){ char *zTerm = pPhrase->aTerm[iTerm].zTerm; zRet = fts5PrintfAppend(zRet, "%s%s", iTerm==0?"":" ", zTerm); + if( pPhrase->aTerm[iTerm].bPrefix ){ + zRet = fts5PrintfAppend(zRet, "*"); + } } if( zRet ) zRet = fts5PrintfAppend(zRet, "}"); @@ -171723,6 +182888,9 @@ static char *fts5ExprPrintTcl( static char *fts5ExprPrint(Fts5Config *pConfig, Fts5ExprNode *pExpr){ char *zRet = 0; + if( pExpr->eType==0 ){ + return sqlite3_mprintf("\"\""); + }else if( pExpr->eType==FTS5_STRING || pExpr->eType==FTS5_TERM ){ Fts5ExprNearset *pNear = pExpr->pNear; int i; @@ -171783,7 +182951,7 @@ static char *fts5ExprPrint(Fts5Config *pConfig, Fts5ExprNode *pExpr){ zRet = 0; }else{ int e = pExpr->apChild[i]->eType; - int b = (e!=FTS5_STRING && e!=FTS5_TERM); + int b = (e!=FTS5_STRING && e!=FTS5_TERM && e!=FTS5_EOF); zRet = fts5PrintfAppend(zRet, "%s%s%z%s", (i==0 ? "" : zOp), (b?"(":""), z, (b?")":"") @@ -171855,7 +183023,7 @@ static void fts5ExprFunction( } if( rc==SQLITE_OK ){ char *zText; - if( pExpr->pRoot==0 ){ + if( pExpr->pRoot->xNext==0 ){ zText = sqlite3_mprintf(""); }else if( bTcl ){ zText = fts5ExprPrintTcl(pConfig, zNearsetCmd, pExpr->pRoot); @@ -171955,7 +183123,7 @@ static int sqlite3Fts5ExprInit(Fts5Global *pGlobal, sqlite3 *db){ int rc = SQLITE_OK; void *pCtx = (void*)pGlobal; - for(i=0; rc==SQLITE_OK && i<(sizeof(aFunc) / sizeof(aFunc[0])); i++){ + for(i=0; rc==SQLITE_OK && iz, -1, SQLITE_UTF8, pCtx, p->x, 0, 0); } @@ -172001,6 +183169,226 @@ static int sqlite3Fts5ExprPoslist(Fts5Expr *pExpr, int iPhrase, const u8 **pa){ return nRet; } +struct Fts5PoslistPopulator { + Fts5PoslistWriter writer; + int bOk; /* True if ok to populate */ + int bMiss; +}; + +static Fts5PoslistPopulator *sqlite3Fts5ExprClearPoslists(Fts5Expr *pExpr, int bLive){ + Fts5PoslistPopulator *pRet; + pRet = sqlite3_malloc(sizeof(Fts5PoslistPopulator)*pExpr->nPhrase); + if( pRet ){ + int i; + memset(pRet, 0, sizeof(Fts5PoslistPopulator)*pExpr->nPhrase); + for(i=0; inPhrase; i++){ + Fts5Buffer *pBuf = &pExpr->apExprPhrase[i]->poslist; + Fts5ExprNode *pNode = pExpr->apExprPhrase[i]->pNode; + assert( pExpr->apExprPhrase[i]->nTerm==1 ); + if( bLive && + (pBuf->n==0 || pNode->iRowid!=pExpr->pRoot->iRowid || pNode->bEof) + ){ + pRet[i].bMiss = 1; + }else{ + pBuf->n = 0; + } + } + } + return pRet; +} + +struct Fts5ExprCtx { + Fts5Expr *pExpr; + Fts5PoslistPopulator *aPopulator; + i64 iOff; +}; +typedef struct Fts5ExprCtx Fts5ExprCtx; + +/* +** TODO: Make this more efficient! +*/ +static int fts5ExprColsetTest(Fts5Colset *pColset, int iCol){ + int i; + for(i=0; inCol; i++){ + if( pColset->aiCol[i]==iCol ) return 1; + } + return 0; +} + +static int fts5ExprPopulatePoslistsCb( + void *pCtx, /* Copy of 2nd argument to xTokenize() */ + int tflags, /* Mask of FTS5_TOKEN_* flags */ + const char *pToken, /* Pointer to buffer containing token */ + int nToken, /* Size of token in bytes */ + int iUnused1, /* Byte offset of token within input text */ + int iUnused2 /* Byte offset of end of token within input text */ +){ + Fts5ExprCtx *p = (Fts5ExprCtx*)pCtx; + Fts5Expr *pExpr = p->pExpr; + int i; + + UNUSED_PARAM2(iUnused1, iUnused2); + + if( nToken>FTS5_MAX_TOKEN_SIZE ) nToken = FTS5_MAX_TOKEN_SIZE; + if( (tflags & FTS5_TOKEN_COLOCATED)==0 ) p->iOff++; + for(i=0; inPhrase; i++){ + Fts5ExprTerm *pTerm; + if( p->aPopulator[i].bOk==0 ) continue; + for(pTerm=&pExpr->apExprPhrase[i]->aTerm[0]; pTerm; pTerm=pTerm->pSynonym){ + int nTerm = (int)strlen(pTerm->zTerm); + if( (nTerm==nToken || (nTermbPrefix)) + && memcmp(pTerm->zTerm, pToken, nTerm)==0 + ){ + int rc = sqlite3Fts5PoslistWriterAppend( + &pExpr->apExprPhrase[i]->poslist, &p->aPopulator[i].writer, p->iOff + ); + if( rc ) return rc; + break; + } + } + } + return SQLITE_OK; +} + +static int sqlite3Fts5ExprPopulatePoslists( + Fts5Config *pConfig, + Fts5Expr *pExpr, + Fts5PoslistPopulator *aPopulator, + int iCol, + const char *z, int n +){ + int i; + Fts5ExprCtx sCtx; + sCtx.pExpr = pExpr; + sCtx.aPopulator = aPopulator; + sCtx.iOff = (((i64)iCol) << 32) - 1; + + for(i=0; inPhrase; i++){ + Fts5ExprNode *pNode = pExpr->apExprPhrase[i]->pNode; + Fts5Colset *pColset = pNode->pNear->pColset; + if( (pColset && 0==fts5ExprColsetTest(pColset, iCol)) + || aPopulator[i].bMiss + ){ + aPopulator[i].bOk = 0; + }else{ + aPopulator[i].bOk = 1; + } + } + + return sqlite3Fts5Tokenize(pConfig, + FTS5_TOKENIZE_DOCUMENT, z, n, (void*)&sCtx, fts5ExprPopulatePoslistsCb + ); +} + +static void fts5ExprClearPoslists(Fts5ExprNode *pNode){ + if( pNode->eType==FTS5_TERM || pNode->eType==FTS5_STRING ){ + pNode->pNear->apPhrase[0]->poslist.n = 0; + }else{ + int i; + for(i=0; inChild; i++){ + fts5ExprClearPoslists(pNode->apChild[i]); + } + } +} + +static int fts5ExprCheckPoslists(Fts5ExprNode *pNode, i64 iRowid){ + pNode->iRowid = iRowid; + pNode->bEof = 0; + switch( pNode->eType ){ + case FTS5_TERM: + case FTS5_STRING: + return (pNode->pNear->apPhrase[0]->poslist.n>0); + + case FTS5_AND: { + int i; + for(i=0; inChild; i++){ + if( fts5ExprCheckPoslists(pNode->apChild[i], iRowid)==0 ){ + fts5ExprClearPoslists(pNode); + return 0; + } + } + break; + } + + case FTS5_OR: { + int i; + int bRet = 0; + for(i=0; inChild; i++){ + if( fts5ExprCheckPoslists(pNode->apChild[i], iRowid) ){ + bRet = 1; + } + } + return bRet; + } + + default: { + assert( pNode->eType==FTS5_NOT ); + if( 0==fts5ExprCheckPoslists(pNode->apChild[0], iRowid) + || 0!=fts5ExprCheckPoslists(pNode->apChild[1], iRowid) + ){ + fts5ExprClearPoslists(pNode); + return 0; + } + break; + } + } + return 1; +} + +static void sqlite3Fts5ExprCheckPoslists(Fts5Expr *pExpr, i64 iRowid){ + fts5ExprCheckPoslists(pExpr->pRoot, iRowid); +} + +static void fts5ExprClearEof(Fts5ExprNode *pNode){ + int i; + for(i=0; inChild; i++){ + fts5ExprClearEof(pNode->apChild[i]); + } + pNode->bEof = 0; +} +static void sqlite3Fts5ExprClearEof(Fts5Expr *pExpr){ + fts5ExprClearEof(pExpr->pRoot); +} + +/* +** This function is only called for detail=columns tables. +*/ +static int sqlite3Fts5ExprPhraseCollist( + Fts5Expr *pExpr, + int iPhrase, + const u8 **ppCollist, + int *pnCollist +){ + Fts5ExprPhrase *pPhrase = pExpr->apExprPhrase[iPhrase]; + Fts5ExprNode *pNode = pPhrase->pNode; + int rc = SQLITE_OK; + + assert( iPhrase>=0 && iPhrasenPhrase ); + assert( pExpr->pConfig->eDetail==FTS5_DETAIL_COLUMNS ); + + if( pNode->bEof==0 + && pNode->iRowid==pExpr->pRoot->iRowid + && pPhrase->poslist.n>0 + ){ + Fts5ExprTerm *pTerm = &pPhrase->aTerm[0]; + if( pTerm->pSynonym ){ + Fts5Buffer *pBuf = (Fts5Buffer*)&pTerm->pSynonym[1]; + rc = fts5ExprSynonymList( + pTerm, pNode->iRowid, pBuf, (u8**)ppCollist, pnCollist + ); + }else{ + *ppCollist = pPhrase->aTerm[0].pIter->pData; + *pnCollist = pPhrase->aTerm[0].pIter->nData; + } + }else{ + *ppCollist = 0; + *pnCollist = 0; + } + + return rc; +} + + /* ** 2014 August 11 ** @@ -172017,6 +183405,7 @@ static int sqlite3Fts5ExprPoslist(Fts5Expr *pExpr, int iPhrase, const u8 **pa){ +/* #include "fts5Int.h" */ typedef struct Fts5HashEntry Fts5HashEntry; @@ -172028,6 +183417,7 @@ typedef struct Fts5HashEntry Fts5HashEntry; struct Fts5Hash { + int eDetail; /* Copy of Fts5Config.eDetail */ int *pnByte; /* Pointer to bytes counter */ int nEntry; /* Number of entries currently in hash */ int nSlot; /* Size of aSlot[] array */ @@ -172063,9 +183453,10 @@ struct Fts5HashEntry { int nAlloc; /* Total size of allocation */ int iSzPoslist; /* Offset of space for 4-byte poslist size */ int nData; /* Total bytes of data (incl. structure) */ + int nKey; /* Length of zKey[] in bytes */ u8 bDel; /* Set delete-flag @ iSzPoslist */ - - int iCol; /* Column of last value written */ + u8 bContent; /* Set content-flag (detail=none mode) */ + i16 iCol; /* Column of last value written */ int iPos; /* Position of last value written */ i64 iRowid; /* Rowid of last value written */ char zKey[8]; /* Nul-terminated entry key */ @@ -172081,7 +183472,7 @@ struct Fts5HashEntry { /* ** Allocate a new hash table. */ -static int sqlite3Fts5HashNew(Fts5Hash **ppNew, int *pnByte){ +static int sqlite3Fts5HashNew(Fts5Config *pConfig, Fts5Hash **ppNew, int *pnByte){ int rc = SQLITE_OK; Fts5Hash *pNew; @@ -172092,6 +183483,7 @@ static int sqlite3Fts5HashNew(Fts5Hash **ppNew, int *pnByte){ int nByte; memset(pNew, 0, sizeof(Fts5Hash)); pNew->pnByte = pnByte; + pNew->eDetail = pConfig->eDetail; pNew->nSlot = 1024; nByte = sizeof(Fts5HashEntry*) * pNew->nSlot; @@ -172172,7 +183564,7 @@ static int fts5HashResize(Fts5Hash *pHash){ int iHash; Fts5HashEntry *p = apOld[i]; apOld[i] = p->pHashNext; - iHash = fts5HashKey(nNew, (u8*)p->zKey, strlen(p->zKey)); + iHash = fts5HashKey(nNew, (u8*)p->zKey, (int)strlen(p->zKey)); p->pHashNext = apNew[iHash]; apNew[iHash] = p; } @@ -172184,26 +183576,46 @@ static int fts5HashResize(Fts5Hash *pHash){ return SQLITE_OK; } -static void fts5HashAddPoslistSize(Fts5HashEntry *p){ +static void fts5HashAddPoslistSize(Fts5Hash *pHash, Fts5HashEntry *p){ if( p->iSzPoslist ){ u8 *pPtr = (u8*)p; - int nSz = (p->nData - p->iSzPoslist - 1); /* Size in bytes */ - int nPos = nSz*2 + p->bDel; /* Value of nPos field */ - - assert( p->bDel==0 || p->bDel==1 ); - if( nPos<=127 ){ - pPtr[p->iSzPoslist] = nPos; + if( pHash->eDetail==FTS5_DETAIL_NONE ){ + assert( p->nData==p->iSzPoslist ); + if( p->bDel ){ + pPtr[p->nData++] = 0x00; + if( p->bContent ){ + pPtr[p->nData++] = 0x00; + } + } }else{ - int nByte = sqlite3Fts5GetVarintLen((u32)nPos); - memmove(&pPtr[p->iSzPoslist + nByte], &pPtr[p->iSzPoslist + 1], nSz); - sqlite3Fts5PutVarint(&pPtr[p->iSzPoslist], nPos); - p->nData += (nByte-1); + int nSz = (p->nData - p->iSzPoslist - 1); /* Size in bytes */ + int nPos = nSz*2 + p->bDel; /* Value of nPos field */ + + assert( p->bDel==0 || p->bDel==1 ); + if( nPos<=127 ){ + pPtr[p->iSzPoslist] = (u8)nPos; + }else{ + int nByte = sqlite3Fts5GetVarintLen((u32)nPos); + memmove(&pPtr[p->iSzPoslist + nByte], &pPtr[p->iSzPoslist + 1], nSz); + sqlite3Fts5PutVarint(&pPtr[p->iSzPoslist], nPos); + p->nData += (nByte-1); + } } - p->bDel = 0; + p->iSzPoslist = 0; + p->bDel = 0; + p->bContent = 0; } } +/* +** Add an entry to the in-memory hash table. The key is the concatenation +** of bByte and (pToken/nToken). The value is (iRowid/iCol/iPos). +** +** (bByte || pToken) -> (iRowid,iCol,iPos) +** +** Or, if iCol is negative, then the value is a delete marker. +*/ static int sqlite3Fts5HashWrite( Fts5Hash *pHash, i64 iRowid, /* Rowid for this entry */ @@ -172216,13 +183628,16 @@ static int sqlite3Fts5HashWrite( Fts5HashEntry *p; u8 *pPtr; int nIncr = 0; /* Amount to increment (*pHash->pnByte) by */ + int bNew; /* If non-delete entry should be written */ + + bNew = (pHash->eDetail==FTS5_DETAIL_FULL); /* Attempt to locate an existing hash entry */ iHash = fts5HashKey2(pHash->nSlot, (u8)bByte, (const u8*)pToken, nToken); for(p=pHash->aSlot[iHash]; p; p=p->pHashNext){ if( p->zKey[0]==bByte + && p->nKey==nToken && memcmp(&p->zKey[1], pToken, nToken)==0 - && p->zKey[nToken+1]==0 ){ break; } @@ -172230,15 +183645,18 @@ static int sqlite3Fts5HashWrite( /* If an existing hash entry cannot be found, create a new one. */ if( p==0 ){ + /* Figure out how much space to allocate */ int nByte = FTS5_HASHENTRYSIZE + (nToken+1) + 1 + 64; if( nByte<128 ) nByte = 128; + /* Grow the Fts5Hash.aSlot[] array if necessary. */ if( (pHash->nEntry*2)>=pHash->nSlot ){ int rc = fts5HashResize(pHash); if( rc!=SQLITE_OK ) return rc; iHash = fts5HashKey2(pHash->nSlot, (u8)bByte, (const u8*)pToken, nToken); } + /* Allocate new Fts5HashEntry and add it to the hash table. */ p = (Fts5HashEntry*)sqlite3_malloc(nByte); if( !p ) return SQLITE_NOMEM; memset(p, 0, FTS5_HASHENTRYSIZE); @@ -172246,72 +183664,98 @@ static int sqlite3Fts5HashWrite( p->zKey[0] = bByte; memcpy(&p->zKey[1], pToken, nToken); assert( iHash==fts5HashKey(pHash->nSlot, (u8*)p->zKey, nToken+1) ); + p->nKey = nToken; p->zKey[nToken+1] = '\0'; p->nData = nToken+1 + 1 + FTS5_HASHENTRYSIZE; - p->nData += sqlite3Fts5PutVarint(&((u8*)p)[p->nData], iRowid); - p->iSzPoslist = p->nData; - p->nData += 1; - p->iRowid = iRowid; p->pHashNext = pHash->aSlot[iHash]; pHash->aSlot[iHash] = p; pHash->nEntry++; - nIncr += p->nData; - } - /* Check there is enough space to append a new entry. Worst case scenario - ** is: - ** - ** + 9 bytes for a new rowid, - ** + 4 byte reserved for the "poslist size" varint. - ** + 1 byte for a "new column" byte, - ** + 3 bytes for a new column number (16-bit max) as a varint, - ** + 5 bytes for the new position offset (32-bit max). - */ - if( (p->nAlloc - p->nData) < (9 + 4 + 1 + 3 + 5) ){ - int nNew = p->nAlloc * 2; - Fts5HashEntry *pNew; - Fts5HashEntry **pp; - pNew = (Fts5HashEntry*)sqlite3_realloc(p, nNew); - if( pNew==0 ) return SQLITE_NOMEM; - pNew->nAlloc = nNew; - for(pp=&pHash->aSlot[iHash]; *pp!=p; pp=&(*pp)->pHashNext); - *pp = pNew; - p = pNew; + /* Add the first rowid field to the hash-entry */ + p->nData += sqlite3Fts5PutVarint(&((u8*)p)[p->nData], iRowid); + p->iRowid = iRowid; + + p->iSzPoslist = p->nData; + if( pHash->eDetail!=FTS5_DETAIL_NONE ){ + p->nData += 1; + p->iCol = (pHash->eDetail==FTS5_DETAIL_FULL ? 0 : -1); + } + + nIncr += p->nData; + }else{ + + /* Appending to an existing hash-entry. Check that there is enough + ** space to append the largest possible new entry. Worst case scenario + ** is: + ** + ** + 9 bytes for a new rowid, + ** + 4 byte reserved for the "poslist size" varint. + ** + 1 byte for a "new column" byte, + ** + 3 bytes for a new column number (16-bit max) as a varint, + ** + 5 bytes for the new position offset (32-bit max). + */ + if( (p->nAlloc - p->nData) < (9 + 4 + 1 + 3 + 5) ){ + int nNew = p->nAlloc * 2; + Fts5HashEntry *pNew; + Fts5HashEntry **pp; + pNew = (Fts5HashEntry*)sqlite3_realloc(p, nNew); + if( pNew==0 ) return SQLITE_NOMEM; + pNew->nAlloc = nNew; + for(pp=&pHash->aSlot[iHash]; *pp!=p; pp=&(*pp)->pHashNext); + *pp = pNew; + p = pNew; + } + nIncr -= p->nData; } + assert( (p->nAlloc - p->nData) >= (9 + 4 + 1 + 3 + 5) ); + pPtr = (u8*)p; - nIncr -= p->nData; /* If this is a new rowid, append the 4-byte size field for the previous ** entry, and the new rowid for this entry. */ if( iRowid!=p->iRowid ){ - fts5HashAddPoslistSize(p); + fts5HashAddPoslistSize(pHash, p); p->nData += sqlite3Fts5PutVarint(&pPtr[p->nData], iRowid - p->iRowid); - p->iSzPoslist = p->nData; - p->nData += 1; - p->iCol = 0; - p->iPos = 0; p->iRowid = iRowid; + bNew = 1; + p->iSzPoslist = p->nData; + if( pHash->eDetail!=FTS5_DETAIL_NONE ){ + p->nData += 1; + p->iCol = (pHash->eDetail==FTS5_DETAIL_FULL ? 0 : -1); + p->iPos = 0; + } } if( iCol>=0 ){ - /* Append a new column value, if necessary */ - assert( iCol>=p->iCol ); - if( iCol!=p->iCol ){ - pPtr[p->nData++] = 0x01; - p->nData += sqlite3Fts5PutVarint(&pPtr[p->nData], iCol); - p->iCol = iCol; - p->iPos = 0; - } + if( pHash->eDetail==FTS5_DETAIL_NONE ){ + p->bContent = 1; + }else{ + /* Append a new column value, if necessary */ + assert( iCol>=p->iCol ); + if( iCol!=p->iCol ){ + if( pHash->eDetail==FTS5_DETAIL_FULL ){ + pPtr[p->nData++] = 0x01; + p->nData += sqlite3Fts5PutVarint(&pPtr[p->nData], iCol); + p->iCol = (i16)iCol; + p->iPos = 0; + }else{ + bNew = 1; + p->iCol = (i16)(iPos = iCol); + } + } - /* Append the new position offset */ - p->nData += sqlite3Fts5PutVarint(&pPtr[p->nData], iPos - p->iPos + 2); - p->iPos = iPos; + /* Append the new position offset, if necessary */ + if( bNew ){ + p->nData += sqlite3Fts5PutVarint(&pPtr[p->nData], iPos - p->iPos + 2); + p->iPos = iPos; + } + } }else{ /* This is a delete. Set the delete flag. */ p->bDel = 1; } - nIncr += p->nData; + nIncr += p->nData; *pHash->pnByte += nIncr; return SQLITE_OK; } @@ -172425,7 +183869,7 @@ static int sqlite3Fts5HashQuery( } if( p ){ - fts5HashAddPoslistSize(p); + fts5HashAddPoslistSize(pHash, p); *ppDoclist = (const u8*)&p->zKey[nTerm+1]; *pnDoclist = p->nData - (FTS5_HASHENTRYSIZE + nTerm + 1); }else{ @@ -172460,8 +183904,8 @@ static void sqlite3Fts5HashScanEntry( ){ Fts5HashEntry *p; if( (p = pHash->pScan) ){ - int nTerm = strlen(p->zKey); - fts5HashAddPoslistSize(p); + int nTerm = (int)strlen(p->zKey); + fts5HashAddPoslistSize(pHash, p); *pzTerm = p->zKey; *ppDoclist = (const u8*)&p->zKey[nTerm+1]; *pnDoclist = p->nData - (FTS5_HASHENTRYSIZE + nTerm + 1); @@ -172492,6 +183936,7 @@ static void sqlite3Fts5HashScanEntry( */ +/* #include "fts5Int.h" */ /* ** Overview: @@ -172735,6 +184180,7 @@ typedef struct Fts5Data Fts5Data; typedef struct Fts5DlidxIter Fts5DlidxIter; typedef struct Fts5DlidxLvl Fts5DlidxLvl; typedef struct Fts5DlidxWriter Fts5DlidxWriter; +typedef struct Fts5Iter Fts5Iter; typedef struct Fts5PageWriter Fts5PageWriter; typedef struct Fts5SegIter Fts5SegIter; typedef struct Fts5DoclistIter Fts5DoclistIter; @@ -172762,7 +184208,6 @@ struct Fts5Index { ** in-memory hash tables before they are flushed to disk. */ Fts5Hash *pHash; /* Hash table for in-memory data */ - int nMaxPendingData; /* Max pending data before flush to disk */ int nPendingData; /* Current bytes of pending data */ i64 iWriteRowid; /* Rowid for current doc being written */ int bDelete; /* Current write is a delete */ @@ -172778,6 +184223,10 @@ struct Fts5Index { sqlite3_stmt *pIdxDeleter; /* "DELETE FROM %_idx WHERE segid=? */ sqlite3_stmt *pIdxSelect; int nRead; /* Total number of blocks read */ + + sqlite3_stmt *pDataVersion; + i64 iStructVersion; /* data_version when pStruct read */ + Fts5Structure *pStruct; /* Current db structure (or NULL) */ }; struct Fts5DoclistIter { @@ -172848,26 +184297,6 @@ struct Fts5SegWriter { int iBtPage; /* Page number corresponding to btterm */ }; -/* -** Object for iterating through the merged results of one or more segments, -** visiting each term/rowid pair in the merged data. -** -** nSeg is always a power of two greater than or equal to the number of -** segments that this object is merging data from. Both the aSeg[] and -** aFirst[] arrays are sized at nSeg entries. The aSeg[] array is padded -** with zeroed objects - these are handled as if they were iterators opened -** on empty segments. -** -** The results of comparing segments aSeg[N] and aSeg[N+1], where N is an -** even number, is stored in aFirst[(nSeg+N)/2]. The "result" of the -** comparison in this context is the index of the iterator that currently -** points to the smaller term/rowid combination. Iterators at EOF are -** considered to be greater than all other iterators. -** -** aFirst[1] contains the index in aSeg[] of the iterator that points to -** the smallest key overall. aFirst[0] is unused. -*/ - typedef struct Fts5CResult Fts5CResult; struct Fts5CResult { u16 iFirst; /* aSeg[] index of firstest iterator */ @@ -172928,6 +184357,9 @@ struct Fts5SegIter { Fts5Data *pNextLeaf; /* Leaf page (iLeafPgno+1) */ int iLeafOffset; /* Byte offset within current leaf */ + /* Next method */ + void (*xNext)(Fts5Index*, Fts5SegIter*, int*); + /* The page and offset from which the current term was read. The offset ** is the offset of the first rowid in the current doclist. */ int iTermLeafPgno; @@ -172947,7 +184379,7 @@ struct Fts5SegIter { Fts5Buffer term; /* Current term */ i64 iRowid; /* Current rowid */ int nPos; /* Number of bytes in current position list */ - int bDel; /* True if the delete flag is set */ + u8 bDel; /* True if the delete flag is set */ }; /* @@ -172961,7 +184393,6 @@ struct Fts5SegIter { #define FTS5_SEGITER_ONETERM 0x01 #define FTS5_SEGITER_REVERSE 0x02 - /* ** Argument is a pointer to an Fts5Data structure that contains a leaf ** page. This macro evaluates to true if the leaf contains no terms, or @@ -172974,20 +184405,42 @@ struct Fts5SegIter { #define fts5LeafFirstRowidOff(x) (fts5GetU16((x)->p)) /* +** Object for iterating through the merged results of one or more segments, +** visiting each term/rowid pair in the merged data. +** +** nSeg is always a power of two greater than or equal to the number of +** segments that this object is merging data from. Both the aSeg[] and +** aFirst[] arrays are sized at nSeg entries. The aSeg[] array is padded +** with zeroed objects - these are handled as if they were iterators opened +** on empty segments. +** +** The results of comparing segments aSeg[N] and aSeg[N+1], where N is an +** even number, is stored in aFirst[(nSeg+N)/2]. The "result" of the +** comparison in this context is the index of the iterator that currently +** points to the smaller term/rowid combination. Iterators at EOF are +** considered to be greater than all other iterators. +** +** aFirst[1] contains the index in aSeg[] of the iterator that points to +** the smallest key overall. aFirst[0] is unused. +** ** poslist: ** Used by sqlite3Fts5IterPoslist() when the poslist needs to be buffered. ** There is no way to tell if this is populated or not. */ -struct Fts5IndexIter { +struct Fts5Iter { + Fts5IndexIter base; /* Base class containing output vars */ + Fts5Index *pIndex; /* Index that owns this iterator */ Fts5Structure *pStruct; /* Database structure for this iterator */ Fts5Buffer poslist; /* Buffer containing current poslist */ + Fts5Colset *pColset; /* Restrict matches to these columns */ + + /* Invoked to set output variables. */ + void (*xSetOutputs)(Fts5Iter*, Fts5SegIter*); int nSeg; /* Size of aSeg[] array */ int bRev; /* True to iterate in reverse order */ u8 bSkipEmpty; /* True to skip deleted entries */ - u8 bEof; /* True at EOF */ - u8 bFiltered; /* True if column-filter already applied */ i64 iSwitchRowid; /* Firstest rowid of other than aFirst[1] */ Fts5CResult *aFirst; /* Current merge state (see above) */ @@ -173077,17 +184530,6 @@ static int fts5BufferCompare(Fts5Buffer *pLeft, Fts5Buffer *pRight){ return (res==0 ? (pLeft->n - pRight->n) : res); } -#ifdef SQLITE_DEBUG -static int fts5BlobCompare( - const u8 *pLeft, int nLeft, - const u8 *pRight, int nRight -){ - int nCmp = MIN(nLeft, nRight); - int res = memcmp(pLeft, pRight, nCmp); - return (res==0 ? (nLeft - nRight) : res); -} -#endif - static int fts5LeafFirstTermOff(Fts5Data *pLeaf){ int ret; fts5GetVarint32(&pLeaf->p[pLeaf->szLeaf], ret); @@ -173179,6 +184621,7 @@ static Fts5Data *fts5DataRead(Fts5Index *p, i64 iRowid){ return pRet; } + /* ** Release a reference to data record returned by an earlier call to ** fts5DataRead(). @@ -173346,28 +184789,37 @@ static int fts5StructureDecode( for(iLvl=0; rc==SQLITE_OK && iLvlaLevel[iLvl]; - int nTotal; + int nTotal = 0; int iSeg; - i += fts5GetVarint32(&pData[i], pLvl->nMerge); - i += fts5GetVarint32(&pData[i], nTotal); - assert( nTotal>=pLvl->nMerge ); - pLvl->aSeg = (Fts5StructureSegment*)sqlite3Fts5MallocZero(&rc, - nTotal * sizeof(Fts5StructureSegment) - ); + if( i>=nData ){ + rc = FTS5_CORRUPT; + }else{ + i += fts5GetVarint32(&pData[i], pLvl->nMerge); + i += fts5GetVarint32(&pData[i], nTotal); + assert( nTotal>=pLvl->nMerge ); + pLvl->aSeg = (Fts5StructureSegment*)sqlite3Fts5MallocZero(&rc, + nTotal * sizeof(Fts5StructureSegment) + ); + } if( rc==SQLITE_OK ){ pLvl->nSeg = nTotal; for(iSeg=0; iSeg=nData ){ + rc = FTS5_CORRUPT; + break; + } i += fts5GetVarint32(&pData[i], pLvl->aSeg[iSeg].iSegid); i += fts5GetVarint32(&pData[i], pLvl->aSeg[iSeg].pgnoFirst); i += fts5GetVarint32(&pData[i], pLvl->aSeg[iSeg].pgnoLast); } - }else{ - fts5StructureRelease(pRet); - pRet = 0; } } + if( rc!=SQLITE_OK ){ + fts5StructureRelease(pRet); + pRet = 0; + } } *ppOut = pRet; @@ -173430,6 +184882,50 @@ static void fts5StructureExtendLevel( } } +static Fts5Structure *fts5StructureReadUncached(Fts5Index *p){ + Fts5Structure *pRet = 0; + Fts5Config *pConfig = p->pConfig; + int iCookie; /* Configuration cookie */ + Fts5Data *pData; + + pData = fts5DataRead(p, FTS5_STRUCTURE_ROWID); + if( p->rc==SQLITE_OK ){ + /* TODO: Do we need this if the leaf-index is appended? Probably... */ + memset(&pData->p[pData->nn], 0, FTS5_DATA_PADDING); + p->rc = fts5StructureDecode(pData->p, pData->nn, &iCookie, &pRet); + if( p->rc==SQLITE_OK && pConfig->iCookie!=iCookie ){ + p->rc = sqlite3Fts5ConfigLoad(pConfig, iCookie); + } + fts5DataRelease(pData); + if( p->rc!=SQLITE_OK ){ + fts5StructureRelease(pRet); + pRet = 0; + } + } + + return pRet; +} + +static i64 fts5IndexDataVersion(Fts5Index *p){ + i64 iVersion = 0; + + if( p->rc==SQLITE_OK ){ + if( p->pDataVersion==0 ){ + p->rc = fts5IndexPrepareStmt(p, &p->pDataVersion, + sqlite3_mprintf("PRAGMA %Q.data_version", p->pConfig->zDb) + ); + if( p->rc ) return 0; + } + + if( SQLITE_ROW==sqlite3_step(p->pDataVersion) ){ + iVersion = sqlite3_column_int64(p->pDataVersion, 0); + } + p->rc = sqlite3_reset(p->pDataVersion); + } + + return iVersion; +} + /* ** Read, deserialize and return the structure record. ** @@ -173442,26 +184938,49 @@ static void fts5StructureExtendLevel( ** is called, it is a no-op. */ static Fts5Structure *fts5StructureRead(Fts5Index *p){ - Fts5Config *pConfig = p->pConfig; - Fts5Structure *pRet = 0; /* Object to return */ - int iCookie; /* Configuration cookie */ - Fts5Data *pData; - pData = fts5DataRead(p, FTS5_STRUCTURE_ROWID); - if( p->rc ) return 0; - /* TODO: Do we need this if the leaf-index is appended? Probably... */ - memset(&pData->p[pData->nn], 0, FTS5_DATA_PADDING); - p->rc = fts5StructureDecode(pData->p, pData->nn, &iCookie, &pRet); - if( p->rc==SQLITE_OK && pConfig->iCookie!=iCookie ){ - p->rc = sqlite3Fts5ConfigLoad(pConfig, iCookie); + if( p->pStruct==0 ){ + p->iStructVersion = fts5IndexDataVersion(p); + if( p->rc==SQLITE_OK ){ + p->pStruct = fts5StructureReadUncached(p); + } } - fts5DataRelease(pData); - if( p->rc!=SQLITE_OK ){ - fts5StructureRelease(pRet); - pRet = 0; +#if 0 + else{ + Fts5Structure *pTest = fts5StructureReadUncached(p); + if( pTest ){ + int i, j; + assert_nc( p->pStruct->nSegment==pTest->nSegment ); + assert_nc( p->pStruct->nLevel==pTest->nLevel ); + for(i=0; inLevel; i++){ + assert_nc( p->pStruct->aLevel[i].nMerge==pTest->aLevel[i].nMerge ); + assert_nc( p->pStruct->aLevel[i].nSeg==pTest->aLevel[i].nSeg ); + for(j=0; jaLevel[i].nSeg; j++){ + Fts5StructureSegment *p1 = &pTest->aLevel[i].aSeg[j]; + Fts5StructureSegment *p2 = &p->pStruct->aLevel[i].aSeg[j]; + assert_nc( p1->iSegid==p2->iSegid ); + assert_nc( p1->pgnoFirst==p2->pgnoFirst ); + assert_nc( p1->pgnoLast==p2->pgnoLast ); + } + } + fts5StructureRelease(pTest); + } + } +#endif + + if( p->rc!=SQLITE_OK ) return 0; + assert( p->iStructVersion!=0 ); + assert( p->pStruct!=0 ); + fts5StructureRef(p->pStruct); + return p->pStruct; +} + +static void fts5StructureInvalidate(Fts5Index *p){ + if( p->pStruct ){ + fts5StructureRelease(p->pStruct); + p->pStruct = 0; } - return pRet; } /* @@ -173482,6 +185001,18 @@ static int fts5StructureCountSegments(Fts5Structure *pStruct){ } #endif +#define fts5BufferSafeAppendBlob(pBuf, pBlob, nBlob) { \ + assert( (pBuf)->nSpace>=((pBuf)->n+nBlob) ); \ + memcpy(&(pBuf)->p[(pBuf)->n], pBlob, nBlob); \ + (pBuf)->n += nBlob; \ +} + +#define fts5BufferSafeAppendVarint(pBuf, iVal) { \ + (pBuf)->n += sqlite3Fts5PutVarint(&(pBuf)->p[(pBuf)->n], (iVal)); \ + assert( (pBuf)->nSpace>=(pBuf)->n ); \ +} + + /* ** Serialize and store the "structure" record. ** @@ -173500,11 +185031,14 @@ static void fts5StructureWrite(Fts5Index *p, Fts5Structure *pStruct){ /* Append the current configuration cookie */ iCookie = p->pConfig->iCookie; if( iCookie<0 ) iCookie = 0; - fts5BufferAppend32(&p->rc, &buf, iCookie); - fts5BufferAppendVarint(&p->rc, &buf, pStruct->nLevel); - fts5BufferAppendVarint(&p->rc, &buf, pStruct->nSegment); - fts5BufferAppendVarint(&p->rc, &buf, (i64)pStruct->nWriteCounter); + if( 0==sqlite3Fts5BufferSize(&p->rc, &buf, 4+9+9+9) ){ + sqlite3Fts5Put32(buf.p, iCookie); + buf.n = 4; + fts5BufferSafeAppendVarint(&buf, pStruct->nLevel); + fts5BufferSafeAppendVarint(&buf, pStruct->nSegment); + fts5BufferSafeAppendVarint(&buf, (i64)pStruct->nWriteCounter); + } for(iLvl=0; iLvlnLevel; iLvl++){ int iSeg; /* Used to iterate through segments */ @@ -173954,11 +185488,28 @@ static int fts5GetPoslistSize(const u8 *p, int *pnSz, int *pbDel){ static void fts5SegIterLoadNPos(Fts5Index *p, Fts5SegIter *pIter){ if( p->rc==SQLITE_OK ){ int iOff = pIter->iLeafOffset; /* Offset to read at */ - int nSz; ASSERT_SZLEAF_OK(pIter->pLeaf); - fts5FastGetVarint32(pIter->pLeaf->p, iOff, nSz); - pIter->bDel = (nSz & 0x0001); - pIter->nPos = nSz>>1; + if( p->pConfig->eDetail==FTS5_DETAIL_NONE ){ + int iEod = MIN(pIter->iEndofDoclist, pIter->pLeaf->szLeaf); + pIter->bDel = 0; + pIter->nPos = 1; + if( iOffpLeaf->p[iOff]==0 ){ + pIter->bDel = 1; + iOff++; + if( iOffpLeaf->p[iOff]==0 ){ + pIter->nPos = 1; + iOff++; + }else{ + pIter->nPos = 0; + } + } + }else{ + int nSz; + fts5FastGetVarint32(pIter->pLeaf->p, iOff, nSz); + pIter->bDel = (nSz & 0x0001); + pIter->nPos = nSz>>1; + assert_nc( pIter->nPos>=0 ); + } pIter->iLeafOffset = iOff; } } @@ -174002,6 +185553,10 @@ static void fts5SegIterLoadTerm(Fts5Index *p, Fts5SegIter *pIter, int nKeep){ int nNew; /* Bytes of new data */ iOff += fts5GetVarint32(&a[iOff], nNew); + if( iOff+nNew>pIter->pLeaf->nn ){ + p->rc = FTS5_CORRUPT; + return; + } pIter->term.n = nKeep; fts5BufferAppendBlob(&p->rc, &pIter->term, nNew, &a[iOff]); iOff += nNew; @@ -174020,6 +185575,20 @@ static void fts5SegIterLoadTerm(Fts5Index *p, Fts5SegIter *pIter, int nKeep){ fts5SegIterLoadRowid(p, pIter); } +static void fts5SegIterNext(Fts5Index*, Fts5SegIter*, int*); +static void fts5SegIterNext_Reverse(Fts5Index*, Fts5SegIter*, int*); +static void fts5SegIterNext_None(Fts5Index*, Fts5SegIter*, int*); + +static void fts5SegIterSetNext(Fts5Index *p, Fts5SegIter *pIter){ + if( pIter->flags & FTS5_SEGITER_REVERSE ){ + pIter->xNext = fts5SegIterNext_Reverse; + }else if( p->pConfig->eDetail==FTS5_DETAIL_NONE ){ + pIter->xNext = fts5SegIterNext_None; + }else{ + pIter->xNext = fts5SegIterNext; + } +} + /* ** Initialize the iterator object pIter to iterate through the entries in ** segment pSeg. The iterator is left pointing to the first entry when @@ -174045,6 +185614,7 @@ static void fts5SegIterInit( if( p->rc==SQLITE_OK ){ memset(pIter, 0, sizeof(*pIter)); + fts5SegIterSetNext(p, pIter); pIter->pSeg = pSeg; pIter->iLeafPgno = pSeg->pgnoFirst-1; fts5SegIterNextPage(p, pIter); @@ -174076,6 +185646,7 @@ static void fts5SegIterInit( ** byte of the position list content associated with said rowid. */ static void fts5SegIterReverseInitPage(Fts5Index *p, Fts5SegIter *pIter){ + int eDetail = p->pConfig->eDetail; int n = pIter->pLeaf->szLeaf; int i = pIter->iLeafOffset; u8 *a = pIter->pLeaf->p; @@ -174088,15 +185659,24 @@ static void fts5SegIterReverseInitPage(Fts5Index *p, Fts5SegIter *pIter){ ASSERT_SZLEAF_OK(pIter->pLeaf); while( 1 ){ i64 iDelta = 0; - int nPos; - int bDummy; - i += fts5GetPoslistSize(&a[i], &nPos, &bDummy); - i += nPos; + if( eDetail==FTS5_DETAIL_NONE ){ + /* todo */ + if( i=n ) break; i += fts5GetVarint(&a[i], (u64*)&iDelta); pIter->iRowid += iDelta; + /* If necessary, grow the pIter->aRowidOffset[] array. */ if( iRowidOffset>=pIter->nRowidOffset ){ int nNew = pIter->nRowidOffset + 8; int *aNew = (int*)sqlite3_realloc(pIter->aRowidOffset, nNew*sizeof(int)); @@ -174133,12 +185713,13 @@ static void fts5SegIterReverseNewPage(Fts5Index *p, Fts5SegIter *pIter){ if( pNew ){ /* iTermLeafOffset may be equal to szLeaf if the term is the last ** thing on the page - i.e. the first rowid is on the following page. - ** In this case leaf pIter->pLeaf==0, this iterator is at EOF. */ - if( pIter->iLeafPgno==pIter->iTermLeafPgno - && pIter->iTermLeafOffsetszLeaf - ){ - pIter->pLeaf = pNew; - pIter->iLeafOffset = pIter->iTermLeafOffset; + ** In this case leave pIter->pLeaf==0, this iterator is at EOF. */ + if( pIter->iLeafPgno==pIter->iTermLeafPgno ){ + assert( pIter->pLeaf==0 ); + if( pIter->iTermLeafOffsetszLeaf ){ + pIter->pLeaf = pNew; + pIter->iLeafOffset = pIter->iTermLeafOffset; + } }else{ int iRowidOff; iRowidOff = fts5LeafFirstRowidOff(pNew); @@ -174169,11 +185750,115 @@ static void fts5SegIterReverseNewPage(Fts5Index *p, Fts5SegIter *pIter){ ** points to a delete marker. A delete marker is an entry with a 0 byte ** position-list. */ -static int fts5MultiIterIsEmpty(Fts5Index *p, Fts5IndexIter *pIter){ +static int fts5MultiIterIsEmpty(Fts5Index *p, Fts5Iter *pIter){ Fts5SegIter *pSeg = &pIter->aSeg[pIter->aFirst[1].iFirst]; return (p->rc==SQLITE_OK && pSeg->pLeaf && pSeg->nPos==0); } +/* +** Advance iterator pIter to the next entry. +** +** This version of fts5SegIterNext() is only used by reverse iterators. +*/ +static void fts5SegIterNext_Reverse( + Fts5Index *p, /* FTS5 backend object */ + Fts5SegIter *pIter, /* Iterator to advance */ + int *pbUnused /* Unused */ +){ + assert( pIter->flags & FTS5_SEGITER_REVERSE ); + assert( pIter->pNextLeaf==0 ); + UNUSED_PARAM(pbUnused); + + if( pIter->iRowidOffset>0 ){ + u8 *a = pIter->pLeaf->p; + int iOff; + i64 iDelta; + + pIter->iRowidOffset--; + pIter->iLeafOffset = pIter->aRowidOffset[pIter->iRowidOffset]; + fts5SegIterLoadNPos(p, pIter); + iOff = pIter->iLeafOffset; + if( p->pConfig->eDetail!=FTS5_DETAIL_NONE ){ + iOff += pIter->nPos; + } + fts5GetVarint(&a[iOff], (u64*)&iDelta); + pIter->iRowid -= iDelta; + }else{ + fts5SegIterReverseNewPage(p, pIter); + } +} + +/* +** Advance iterator pIter to the next entry. +** +** This version of fts5SegIterNext() is only used if detail=none and the +** iterator is not a reverse direction iterator. +*/ +static void fts5SegIterNext_None( + Fts5Index *p, /* FTS5 backend object */ + Fts5SegIter *pIter, /* Iterator to advance */ + int *pbNewTerm /* OUT: Set for new term */ +){ + int iOff; + + assert( p->rc==SQLITE_OK ); + assert( (pIter->flags & FTS5_SEGITER_REVERSE)==0 ); + assert( p->pConfig->eDetail==FTS5_DETAIL_NONE ); + + ASSERT_SZLEAF_OK(pIter->pLeaf); + iOff = pIter->iLeafOffset; + + /* Next entry is on the next page */ + if( pIter->pSeg && iOff>=pIter->pLeaf->szLeaf ){ + fts5SegIterNextPage(p, pIter); + if( p->rc || pIter->pLeaf==0 ) return; + pIter->iRowid = 0; + iOff = 4; + } + + if( iOffiEndofDoclist ){ + /* Next entry is on the current page */ + i64 iDelta; + iOff += sqlite3Fts5GetVarint(&pIter->pLeaf->p[iOff], (u64*)&iDelta); + pIter->iLeafOffset = iOff; + pIter->iRowid += iDelta; + }else if( (pIter->flags & FTS5_SEGITER_ONETERM)==0 ){ + if( pIter->pSeg ){ + int nKeep = 0; + if( iOff!=fts5LeafFirstTermOff(pIter->pLeaf) ){ + iOff += fts5GetVarint32(&pIter->pLeaf->p[iOff], nKeep); + } + pIter->iLeafOffset = iOff; + fts5SegIterLoadTerm(p, pIter, nKeep); + }else{ + const u8 *pList = 0; + const char *zTerm = 0; + int nList; + sqlite3Fts5HashScanNext(p->pHash); + sqlite3Fts5HashScanEntry(p->pHash, &zTerm, &pList, &nList); + if( pList==0 ) goto next_none_eof; + pIter->pLeaf->p = (u8*)pList; + pIter->pLeaf->nn = nList; + pIter->pLeaf->szLeaf = nList; + pIter->iEndofDoclist = nList; + sqlite3Fts5BufferSet(&p->rc,&pIter->term, (int)strlen(zTerm), (u8*)zTerm); + pIter->iLeafOffset = fts5GetVarint(pList, (u64*)&pIter->iRowid); + } + + if( pbNewTerm ) *pbNewTerm = 1; + }else{ + goto next_none_eof; + } + + fts5SegIterLoadNPos(p, pIter); + + return; + next_none_eof: + fts5DataRelease(pIter->pLeaf); + pIter->pLeaf = 0; +} + + /* ** Advance iterator pIter to the next entry. ** @@ -174186,131 +185871,132 @@ static void fts5SegIterNext( Fts5SegIter *pIter, /* Iterator to advance */ int *pbNewTerm /* OUT: Set for new term */ ){ - assert( pbNewTerm==0 || *pbNewTerm==0 ); - if( p->rc==SQLITE_OK ){ - if( pIter->flags & FTS5_SEGITER_REVERSE ){ - assert( pIter->pNextLeaf==0 ); - if( pIter->iRowidOffset>0 ){ - u8 *a = pIter->pLeaf->p; - int iOff; - int nPos; - int bDummy; - i64 iDelta; + Fts5Data *pLeaf = pIter->pLeaf; + int iOff; + int bNewTerm = 0; + int nKeep = 0; + u8 *a; + int n; - pIter->iRowidOffset--; - pIter->iLeafOffset = iOff = pIter->aRowidOffset[pIter->iRowidOffset]; - iOff += fts5GetPoslistSize(&a[iOff], &nPos, &bDummy); - iOff += nPos; - fts5GetVarint(&a[iOff], (u64*)&iDelta); - pIter->iRowid -= iDelta; - fts5SegIterLoadNPos(p, pIter); - }else{ - fts5SegIterReverseNewPage(p, pIter); + assert( pbNewTerm==0 || *pbNewTerm==0 ); + assert( p->pConfig->eDetail!=FTS5_DETAIL_NONE ); + + /* Search for the end of the position list within the current page. */ + a = pLeaf->p; + n = pLeaf->szLeaf; + + ASSERT_SZLEAF_OK(pLeaf); + iOff = pIter->iLeafOffset + pIter->nPos; + + if( iOffiEndofDoclist ); + if( iOff>=pIter->iEndofDoclist ){ + bNewTerm = 1; + if( iOff!=fts5LeafFirstTermOff(pLeaf) ){ + iOff += fts5GetVarint32(&a[iOff], nKeep); } }else{ - Fts5Data *pLeaf = pIter->pLeaf; - int iOff; - int bNewTerm = 0; - int nKeep = 0; - - /* Search for the end of the position list within the current page. */ - u8 *a = pLeaf->p; - int n = pLeaf->szLeaf; + u64 iDelta; + iOff += sqlite3Fts5GetVarint(&a[iOff], &iDelta); + pIter->iRowid += iDelta; + assert_nc( iDelta>0 ); + } + pIter->iLeafOffset = iOff; + }else if( pIter->pSeg==0 ){ + const u8 *pList = 0; + const char *zTerm = 0; + int nList = 0; + assert( (pIter->flags & FTS5_SEGITER_ONETERM) || pbNewTerm ); + if( 0==(pIter->flags & FTS5_SEGITER_ONETERM) ){ + sqlite3Fts5HashScanNext(p->pHash); + sqlite3Fts5HashScanEntry(p->pHash, &zTerm, &pList, &nList); + } + if( pList==0 ){ + fts5DataRelease(pIter->pLeaf); + pIter->pLeaf = 0; + }else{ + pIter->pLeaf->p = (u8*)pList; + pIter->pLeaf->nn = nList; + pIter->pLeaf->szLeaf = nList; + pIter->iEndofDoclist = nList+1; + sqlite3Fts5BufferSet(&p->rc, &pIter->term, (int)strlen(zTerm), + (u8*)zTerm); + pIter->iLeafOffset = fts5GetVarint(pList, (u64*)&pIter->iRowid); + *pbNewTerm = 1; + } + }else{ + iOff = 0; + /* Next entry is not on the current page */ + while( iOff==0 ){ + fts5SegIterNextPage(p, pIter); + pLeaf = pIter->pLeaf; + if( pLeaf==0 ) break; ASSERT_SZLEAF_OK(pLeaf); - iOff = pIter->iLeafOffset + pIter->nPos; - - if( iOffiEndofDoclist ); - if( iOff>=pIter->iEndofDoclist ){ - bNewTerm = 1; - if( iOff!=fts5LeafFirstTermOff(pLeaf) ){ - iOff += fts5GetVarint32(&a[iOff], nKeep); - } - }else{ - u64 iDelta; - iOff += sqlite3Fts5GetVarint(&a[iOff], &iDelta); - pIter->iRowid += iDelta; - assert_nc( iDelta>0 ); - } + if( (iOff = fts5LeafFirstRowidOff(pLeaf)) && iOffszLeaf ){ + iOff += sqlite3Fts5GetVarint(&pLeaf->p[iOff], (u64*)&pIter->iRowid); pIter->iLeafOffset = iOff; - }else if( pIter->pSeg==0 ){ - const u8 *pList = 0; - const char *zTerm = 0; - int nList = 0; - if( 0==(pIter->flags & FTS5_SEGITER_ONETERM) ){ - sqlite3Fts5HashScanNext(p->pHash); - sqlite3Fts5HashScanEntry(p->pHash, &zTerm, &pList, &nList); - } - if( pList==0 ){ - fts5DataRelease(pIter->pLeaf); - pIter->pLeaf = 0; - }else{ - pIter->pLeaf->p = (u8*)pList; - pIter->pLeaf->nn = nList; - pIter->pLeaf->szLeaf = nList; - pIter->iEndofDoclist = nList+1; - sqlite3Fts5BufferSet(&p->rc, &pIter->term, strlen(zTerm), (u8*)zTerm); - pIter->iLeafOffset = fts5GetVarint(pList, (u64*)&pIter->iRowid); - if( pbNewTerm ) *pbNewTerm = 1; - } - }else{ - iOff = 0; - /* Next entry is not on the current page */ - while( iOff==0 ){ - fts5SegIterNextPage(p, pIter); - pLeaf = pIter->pLeaf; - if( pLeaf==0 ) break; - ASSERT_SZLEAF_OK(pLeaf); - if( (iOff = fts5LeafFirstRowidOff(pLeaf)) && iOffszLeaf ){ - iOff += sqlite3Fts5GetVarint(&pLeaf->p[iOff], (u64*)&pIter->iRowid); - pIter->iLeafOffset = iOff; - - if( pLeaf->nn>pLeaf->szLeaf ){ - pIter->iPgidxOff = pLeaf->szLeaf + fts5GetVarint32( - &pLeaf->p[pLeaf->szLeaf], pIter->iEndofDoclist + if( pLeaf->nn>pLeaf->szLeaf ){ + pIter->iPgidxOff = pLeaf->szLeaf + fts5GetVarint32( + &pLeaf->p[pLeaf->szLeaf], pIter->iEndofDoclist ); - } + } - } - else if( pLeaf->nn>pLeaf->szLeaf ){ - pIter->iPgidxOff = pLeaf->szLeaf + fts5GetVarint32( - &pLeaf->p[pLeaf->szLeaf], iOff + } + else if( pLeaf->nn>pLeaf->szLeaf ){ + pIter->iPgidxOff = pLeaf->szLeaf + fts5GetVarint32( + &pLeaf->p[pLeaf->szLeaf], iOff ); - pIter->iLeafOffset = iOff; - pIter->iEndofDoclist = iOff; - bNewTerm = 1; - } - if( iOff>=pLeaf->szLeaf ){ - p->rc = FTS5_CORRUPT; - return; - } - } + pIter->iLeafOffset = iOff; + pIter->iEndofDoclist = iOff; + bNewTerm = 1; } + assert_nc( iOffszLeaf ); + if( iOff>pLeaf->szLeaf ){ + p->rc = FTS5_CORRUPT; + return; + } + } + } - /* Check if the iterator is now at EOF. If so, return early. */ - if( pIter->pLeaf ){ - if( bNewTerm ){ - if( pIter->flags & FTS5_SEGITER_ONETERM ){ - fts5DataRelease(pIter->pLeaf); - pIter->pLeaf = 0; - }else{ - fts5SegIterLoadTerm(p, pIter, nKeep); - fts5SegIterLoadNPos(p, pIter); - if( pbNewTerm ) *pbNewTerm = 1; - } - }else{ - fts5SegIterLoadNPos(p, pIter); - } + /* Check if the iterator is now at EOF. If so, return early. */ + if( pIter->pLeaf ){ + if( bNewTerm ){ + if( pIter->flags & FTS5_SEGITER_ONETERM ){ + fts5DataRelease(pIter->pLeaf); + pIter->pLeaf = 0; + }else{ + fts5SegIterLoadTerm(p, pIter, nKeep); + fts5SegIterLoadNPos(p, pIter); + if( pbNewTerm ) *pbNewTerm = 1; } + }else{ + /* The following could be done by calling fts5SegIterLoadNPos(). But + ** this block is particularly performance critical, so equivalent + ** code is inlined. + ** + ** Later: Switched back to fts5SegIterLoadNPos() because it supports + ** detail=none mode. Not ideal. + */ + int nSz; + assert( p->rc==SQLITE_OK ); + fts5FastGetVarint32(pIter->pLeaf->p, pIter->iLeafOffset, nSz); + pIter->bDel = (nSz & 0x0001); + pIter->nPos = nSz>>1; + assert_nc( pIter->nPos>=0 ); } } } #define SWAPVAL(T, a, b) { T tmp; tmp=a; a=b; b=tmp; } +#define fts5IndexSkipVarint(a, iOff) { \ + int iEnd = iOff+9; \ + while( (a[iOff++] & 0x80) && iOffiLeafOffset -= sqlite3Fts5GetVarintLen(pIter->nPos*2+pIter->bDel); + int iPoslist; + if( pIter->iTermLeafPgno==pIter->iLeafPgno ){ + iPoslist = pIter->iTermLeafOffset; + }else{ + iPoslist = 4; + } + fts5IndexSkipVarint(pLeaf->p, iPoslist); + pIter->iLeafOffset = iPoslist; /* If this condition is true then the largest rowid for the current ** term may not be stored on the current page. So search forward to @@ -174415,11 +186108,6 @@ static void fts5SegIterLoadDlidx(Fts5Index *p, Fts5SegIter *pIter){ pIter->pDlidx = fts5DlidxIterInit(p, bRev, iSeg, pIter->iTermLeafPgno); } -#define fts5IndexSkipVarint(a, iOff) { \ - int iEnd = iOff+9; \ - while( (a[iOff++] & 0x80) && iOffn ){ + p->rc = FTS5_CORRUPT; + return; + } while( 1 ){ @@ -174511,11 +186203,14 @@ static void fts5LeafSeek( if( pIter->pLeaf==0 ) return; a = pIter->pLeaf->p; if( fts5LeafIsTermless(pIter->pLeaf)==0 ){ - fts5GetVarint32(&pIter->pLeaf->p[pIter->pLeaf->szLeaf], iOff); + iPgidx = pIter->pLeaf->szLeaf; + iPgidx += fts5GetVarint32(&pIter->pLeaf->p[iPgidx], iOff); if( iOff<4 || iOff>=pIter->pLeaf->szLeaf ){ p->rc = FTS5_CORRUPT; }else{ nKeep = 0; + iTermOff = iOff; + n = pIter->pLeaf->nn; iOff += fts5GetVarint32(&a[iOff], nNew); break; } @@ -174545,6 +186240,18 @@ static void fts5LeafSeek( fts5SegIterLoadNPos(p, pIter); } +static sqlite3_stmt *fts5IdxSelectStmt(Fts5Index *p){ + if( p->pIdxSelect==0 ){ + Fts5Config *pConfig = p->pConfig; + fts5IndexPrepareStmt(p, &p->pIdxSelect, sqlite3_mprintf( + "SELECT pgno FROM '%q'.'%q_idx' WHERE " + "segid=? AND term<=? ORDER BY term DESC LIMIT 1", + pConfig->zDb, pConfig->zName + )); + } + return p->pIdxSelect; +} + /* ** Initialize the object pIter to point to term pTerm/nTerm within segment ** pSeg. If there is no such term in the index, the iterator is set to EOF. @@ -174554,7 +186261,6 @@ static void fts5LeafSeek( */ static void fts5SegIterSeekInit( Fts5Index *p, /* FTS5 backend */ - Fts5Buffer *pBuf, /* Buffer to use for loading pages */ const u8 *pTerm, int nTerm, /* Term to seek to */ int flags, /* Mask of FTS5INDEX_XXX flags */ Fts5StructureSegment *pSeg, /* Description of segment */ @@ -174563,9 +186269,7 @@ static void fts5SegIterSeekInit( int iPg = 1; int bGe = (flags & FTS5INDEX_QUERY_SCAN); int bDlidx = 0; /* True if there is a doclist-index */ - - static int nCall = 0; - nCall++; + sqlite3_stmt *pIdxSelect = 0; assert( bGe==0 || (flags & FTS5INDEX_QUERY_DESC)==0 ); assert( pTerm && nTerm ); @@ -174574,23 +186278,16 @@ static void fts5SegIterSeekInit( /* This block sets stack variable iPg to the leaf page number that may ** contain term (pTerm/nTerm), if it is present in the segment. */ - if( p->pIdxSelect==0 ){ - Fts5Config *pConfig = p->pConfig; - fts5IndexPrepareStmt(p, &p->pIdxSelect, sqlite3_mprintf( - "SELECT pgno FROM '%q'.'%q_idx' WHERE " - "segid=? AND term<=? ORDER BY term DESC LIMIT 1", - pConfig->zDb, pConfig->zName - )); - } + pIdxSelect = fts5IdxSelectStmt(p); if( p->rc ) return; - sqlite3_bind_int(p->pIdxSelect, 1, pSeg->iSegid); - sqlite3_bind_blob(p->pIdxSelect, 2, pTerm, nTerm, SQLITE_STATIC); - if( SQLITE_ROW==sqlite3_step(p->pIdxSelect) ){ - i64 val = sqlite3_column_int(p->pIdxSelect, 0); + sqlite3_bind_int(pIdxSelect, 1, pSeg->iSegid); + sqlite3_bind_blob(pIdxSelect, 2, pTerm, nTerm, SQLITE_STATIC); + if( SQLITE_ROW==sqlite3_step(pIdxSelect) ){ + i64 val = sqlite3_column_int(pIdxSelect, 0); iPg = (int)(val>>1); bDlidx = (val & 0x0001); } - p->rc = sqlite3_reset(p->pIdxSelect); + p->rc = sqlite3_reset(pIdxSelect); if( iPgpgnoFirst ){ iPg = pSeg->pgnoFirst; @@ -174619,6 +186316,8 @@ static void fts5SegIterSeekInit( } } + fts5SegIterSetNext(p, pIter); + /* Either: ** ** 1) an error has occurred, or @@ -174659,7 +186358,7 @@ static void fts5SegIterHashInit( if( pTerm==0 || (flags & FTS5INDEX_QUERY_SCAN) ){ p->rc = sqlite3Fts5HashScanInit(p->pHash, (const char*)pTerm, nTerm); sqlite3Fts5HashScanEntry(p->pHash, (const char**)&z, &pList, &nList); - n = (z ? strlen((const char*)z) : 0); + n = (z ? (int)strlen((const char*)z) : 0); }else{ pIter->flags |= FTS5_SEGITER_ONETERM; sqlite3Fts5HashQuery(p->pHash, (const char*)pTerm, nTerm, &pList, &nList); @@ -174676,7 +186375,7 @@ static void fts5SegIterHashInit( pLeaf->nn = pLeaf->szLeaf = nList; pIter->pLeaf = pLeaf; pIter->iLeafOffset = fts5GetVarint(pLeaf->p, (u64*)&pIter->iRowid); - pIter->iEndofDoclist = pLeaf->nn+1; + pIter->iEndofDoclist = pLeaf->nn; if( flags & FTS5INDEX_QUERY_DESC ){ pIter->flags |= FTS5_SEGITER_REVERSE; @@ -174685,6 +186384,8 @@ static void fts5SegIterHashInit( fts5SegIterLoadNPos(p, pIter); } } + + fts5SegIterSetNext(p, pIter); } /* @@ -174708,7 +186409,7 @@ static void fts5SegIterClear(Fts5SegIter *pIter){ ** two iterators. */ static void fts5AssertComparisonResult( - Fts5IndexIter *pIter, + Fts5Iter *pIter, Fts5SegIter *p1, Fts5SegIter *p2, Fts5CResult *pRes @@ -174749,12 +186450,12 @@ static void fts5AssertComparisonResult( ** statement used to verify that the contents of the pIter->aFirst[] array ** are correct. */ -static void fts5AssertMultiIterSetup(Fts5Index *p, Fts5IndexIter *pIter){ +static void fts5AssertMultiIterSetup(Fts5Index *p, Fts5Iter *pIter){ if( p->rc==SQLITE_OK ){ Fts5SegIter *pFirst = &pIter->aSeg[ pIter->aFirst[1].iFirst ]; int i; - assert( (pFirst->pLeaf==0)==pIter->bEof ); + assert( (pFirst->pLeaf==0)==pIter->base.bEof ); /* Check that pIter->iSwitchRowid is set correctly. */ for(i=0; inSeg; i++){ @@ -174794,7 +186495,7 @@ static void fts5AssertMultiIterSetup(Fts5Index *p, Fts5IndexIter *pIter){ ** to a key that is a duplicate of another, higher priority, ** segment-iterator in the pSeg->aSeg[] array. */ -static int fts5MultiIterDoCompare(Fts5IndexIter *pIter, int iOut){ +static int fts5MultiIterDoCompare(Fts5Iter *pIter, int iOut){ int i1; /* Index of left-hand Fts5SegIter */ int i2; /* Index of right-hand Fts5SegIter */ int iRes; @@ -174840,7 +186541,7 @@ static int fts5MultiIterDoCompare(Fts5IndexIter *pIter, int iOut){ } } - pRes->iFirst = iRes; + pRes->iFirst = (u16)iRes; return 0; } @@ -174928,7 +186629,7 @@ static void fts5SegIterNextFrom( } do{ - if( bMove ) fts5SegIterNext(p, pIter, 0); + if( bMove && p->rc==SQLITE_OK ) pIter->xNext(p, pIter, 0); if( pIter->pLeaf==0 ) break; if( bRev==0 && pIter->iRowid>=iMatch ) break; if( bRev!=0 && pIter->iRowid<=iMatch ) break; @@ -174940,7 +186641,7 @@ static void fts5SegIterNextFrom( /* ** Free the iterator object passed as the second argument. */ -static void fts5MultiIterFree(Fts5Index *p, Fts5IndexIter *pIter){ +static void fts5MultiIterFree(Fts5Iter *pIter){ if( pIter ){ int i; for(i=0; inSeg; i++){ @@ -174954,7 +186655,7 @@ static void fts5MultiIterFree(Fts5Index *p, Fts5IndexIter *pIter){ static void fts5MultiIterAdvanced( Fts5Index *p, /* FTS5 backend to iterate within */ - Fts5IndexIter *pIter, /* Iterator to update aFirst[] array for */ + Fts5Iter *pIter, /* Iterator to update aFirst[] array for */ int iChanged, /* Index of sub-iterator just advanced */ int iMinset /* Minimum entry in aFirst[] to set */ ){ @@ -174962,7 +186663,9 @@ static void fts5MultiIterAdvanced( for(i=(pIter->nSeg+iChanged)/2; i>=iMinset && p->rc==SQLITE_OK; i=i/2){ int iEq; if( (iEq = fts5MultiIterDoCompare(pIter, i)) ){ - fts5SegIterNext(p, &pIter->aSeg[iEq], 0); + Fts5SegIter *pSeg = &pIter->aSeg[iEq]; + assert( p->rc==SQLITE_OK ); + pSeg->xNext(p, pSeg, 0); i = pIter->nSeg + iEq; } } @@ -174979,9 +186682,9 @@ static void fts5MultiIterAdvanced( ** that it deals with more complicated cases as well. */ static int fts5MultiIterAdvanceRowid( - Fts5Index *p, /* FTS5 backend to iterate within */ - Fts5IndexIter *pIter, /* Iterator to update aFirst[] array for */ - int iChanged /* Index of sub-iterator just advanced */ + Fts5Iter *pIter, /* Iterator to update aFirst[] array for */ + int iChanged, /* Index of sub-iterator just advanced */ + Fts5SegIter **ppFirst ){ Fts5SegIter *pNew = &pIter->aSeg[iChanged]; @@ -175007,22 +186710,23 @@ static int fts5MultiIterAdvanceRowid( pIter->iSwitchRowid = pOther->iRowid; } } - pRes->iFirst = (pNew - pIter->aSeg); + pRes->iFirst = (u16)(pNew - pIter->aSeg); if( i==1 ) break; pOther = &pIter->aSeg[ pIter->aFirst[i ^ 0x0001].iFirst ]; } } + *ppFirst = pNew; return 0; } /* ** Set the pIter->bEof variable based on the state of the sub-iterators. */ -static void fts5MultiIterSetEof(Fts5IndexIter *pIter){ +static void fts5MultiIterSetEof(Fts5Iter *pIter){ Fts5SegIter *pSeg = &pIter->aSeg[ pIter->aFirst[1].iFirst ]; - pIter->bEof = pSeg->pLeaf==0; + pIter->base.bEof = pSeg->pLeaf==0; pIter->iSwitchRowid = pSeg->iRowid; } @@ -175035,46 +186739,84 @@ static void fts5MultiIterSetEof(Fts5IndexIter *pIter){ */ static void fts5MultiIterNext( Fts5Index *p, - Fts5IndexIter *pIter, + Fts5Iter *pIter, int bFrom, /* True if argument iFrom is valid */ i64 iFrom /* Advance at least as far as this */ ){ - if( p->rc==SQLITE_OK ){ - int bUseFrom = bFrom; - do { - int iFirst = pIter->aFirst[1].iFirst; - int bNewTerm = 0; - Fts5SegIter *pSeg = &pIter->aSeg[iFirst]; - assert( p->rc==SQLITE_OK ); - if( bUseFrom && pSeg->pDlidx ){ - fts5SegIterNextFrom(p, pSeg, iFrom); - }else{ - fts5SegIterNext(p, pSeg, &bNewTerm); - } + int bUseFrom = bFrom; + while( p->rc==SQLITE_OK ){ + int iFirst = pIter->aFirst[1].iFirst; + int bNewTerm = 0; + Fts5SegIter *pSeg = &pIter->aSeg[iFirst]; + assert( p->rc==SQLITE_OK ); + if( bUseFrom && pSeg->pDlidx ){ + fts5SegIterNextFrom(p, pSeg, iFrom); + }else{ + pSeg->xNext(p, pSeg, &bNewTerm); + } - if( pSeg->pLeaf==0 || bNewTerm - || fts5MultiIterAdvanceRowid(p, pIter, iFirst) - ){ - fts5MultiIterAdvanced(p, pIter, iFirst, 1); - fts5MultiIterSetEof(pIter); - } - fts5AssertMultiIterSetup(p, pIter); + if( pSeg->pLeaf==0 || bNewTerm + || fts5MultiIterAdvanceRowid(pIter, iFirst, &pSeg) + ){ + fts5MultiIterAdvanced(p, pIter, iFirst, 1); + fts5MultiIterSetEof(pIter); + pSeg = &pIter->aSeg[pIter->aFirst[1].iFirst]; + if( pSeg->pLeaf==0 ) return; + } - bUseFrom = 0; - }while( pIter->bSkipEmpty && fts5MultiIterIsEmpty(p, pIter) ); + fts5AssertMultiIterSetup(p, pIter); + assert( pSeg==&pIter->aSeg[pIter->aFirst[1].iFirst] && pSeg->pLeaf ); + if( pIter->bSkipEmpty==0 || pSeg->nPos ){ + pIter->xSetOutputs(pIter, pSeg); + return; + } + bUseFrom = 0; } } -static Fts5IndexIter *fts5MultiIterAlloc( +static void fts5MultiIterNext2( + Fts5Index *p, + Fts5Iter *pIter, + int *pbNewTerm /* OUT: True if *might* be new term */ +){ + assert( pIter->bSkipEmpty ); + if( p->rc==SQLITE_OK ){ + do { + int iFirst = pIter->aFirst[1].iFirst; + Fts5SegIter *pSeg = &pIter->aSeg[iFirst]; + int bNewTerm = 0; + + assert( p->rc==SQLITE_OK ); + pSeg->xNext(p, pSeg, &bNewTerm); + if( pSeg->pLeaf==0 || bNewTerm + || fts5MultiIterAdvanceRowid(pIter, iFirst, &pSeg) + ){ + fts5MultiIterAdvanced(p, pIter, iFirst, 1); + fts5MultiIterSetEof(pIter); + *pbNewTerm = 1; + }else{ + *pbNewTerm = 0; + } + fts5AssertMultiIterSetup(p, pIter); + + }while( fts5MultiIterIsEmpty(p, pIter) ); + } +} + +static void fts5IterSetOutputs_Noop(Fts5Iter *pUnused1, Fts5SegIter *pUnused2){ + UNUSED_PARAM2(pUnused1, pUnused2); +} + +static Fts5Iter *fts5MultiIterAlloc( Fts5Index *p, /* FTS5 backend to iterate within */ int nSeg ){ - Fts5IndexIter *pNew; + Fts5Iter *pNew; int nSlot; /* Power of two >= nSeg */ for(nSlot=2; nSlotaSeg[] */ sizeof(Fts5CResult) * nSlot /* pNew->aFirst[] */ ); @@ -175082,196 +186824,122 @@ static Fts5IndexIter *fts5MultiIterAlloc( pNew->nSeg = nSlot; pNew->aFirst = (Fts5CResult*)&pNew->aSeg[nSlot]; pNew->pIndex = p; + pNew->xSetOutputs = fts5IterSetOutputs_Noop; } return pNew; } -/* -** Allocate a new Fts5IndexIter object. -** -** The new object will be used to iterate through data in structure pStruct. -** If iLevel is -ve, then all data in all segments is merged. Or, if iLevel -** is zero or greater, data from the first nSegment segments on level iLevel -** is merged. -** -** The iterator initially points to the first term/rowid entry in the -** iterated data. -*/ -static void fts5MultiIterNew( - Fts5Index *p, /* FTS5 backend to iterate within */ - Fts5Structure *pStruct, /* Structure of specific index */ - int bSkipEmpty, /* True to ignore delete-keys */ - int flags, /* FTS5INDEX_QUERY_XXX flags */ - const u8 *pTerm, int nTerm, /* Term to seek to (or NULL/0) */ - int iLevel, /* Level to iterate (-1 for all) */ - int nSegment, /* Number of segments to merge (iLevel>=0) */ - Fts5IndexIter **ppOut /* New object */ +static void fts5PoslistCallback( + Fts5Index *pUnused, + void *pContext, + const u8 *pChunk, int nChunk ){ - int nSeg = 0; /* Number of segment-iters in use */ - int iIter = 0; /* */ - int iSeg; /* Used to iterate through segments */ - Fts5Buffer buf = {0,0,0}; /* Buffer used by fts5SegIterSeekInit() */ - Fts5StructureLevel *pLvl; - Fts5IndexIter *pNew; + UNUSED_PARAM(pUnused); + assert_nc( nChunk>=0 ); + if( nChunk>0 ){ + fts5BufferSafeAppendBlob((Fts5Buffer*)pContext, pChunk, nChunk); + } +} - assert( (pTerm==0 && nTerm==0) || iLevel<0 ); +typedef struct PoslistCallbackCtx PoslistCallbackCtx; +struct PoslistCallbackCtx { + Fts5Buffer *pBuf; /* Append to this buffer */ + Fts5Colset *pColset; /* Restrict matches to this column */ + int eState; /* See above */ +}; - /* Allocate space for the new multi-seg-iterator. */ - if( p->rc==SQLITE_OK ){ - if( iLevel<0 ){ - assert( pStruct->nSegment==fts5StructureCountSegments(pStruct) ); - nSeg = pStruct->nSegment; - nSeg += (p->pHash ? 1 : 0); - }else{ - nSeg = MIN(pStruct->aLevel[iLevel].nSeg, nSegment); +typedef struct PoslistOffsetsCtx PoslistOffsetsCtx; +struct PoslistOffsetsCtx { + Fts5Buffer *pBuf; /* Append to this buffer */ + Fts5Colset *pColset; /* Restrict matches to this column */ + int iRead; + int iWrite; +}; + +/* +** TODO: Make this more efficient! +*/ +static int fts5IndexColsetTest(Fts5Colset *pColset, int iCol){ + int i; + for(i=0; inCol; i++){ + if( pColset->aiCol[i]==iCol ) return 1; + } + return 0; +} + +static void fts5PoslistOffsetsCallback( + Fts5Index *pUnused, + void *pContext, + const u8 *pChunk, int nChunk +){ + PoslistOffsetsCtx *pCtx = (PoslistOffsetsCtx*)pContext; + UNUSED_PARAM(pUnused); + assert_nc( nChunk>=0 ); + if( nChunk>0 ){ + int i = 0; + while( iiRead - 2; + pCtx->iRead = iVal; + if( fts5IndexColsetTest(pCtx->pColset, iVal) ){ + fts5BufferSafeAppendVarint(pCtx->pBuf, iVal + 2 - pCtx->iWrite); + pCtx->iWrite = iVal; + } } } - *ppOut = pNew = fts5MultiIterAlloc(p, nSeg); - if( pNew==0 ) return; - pNew->bRev = (0!=(flags & FTS5INDEX_QUERY_DESC)); - pNew->bSkipEmpty = bSkipEmpty; - pNew->pStruct = pStruct; - fts5StructureRef(pStruct); +} - /* Initialize each of the component segment iterators. */ - if( iLevel<0 ){ - Fts5StructureLevel *pEnd = &pStruct->aLevel[pStruct->nLevel]; - if( p->pHash ){ - /* Add a segment iterator for the current contents of the hash table. */ - Fts5SegIter *pIter = &pNew->aSeg[iIter++]; - fts5SegIterHashInit(p, pTerm, nTerm, flags, pIter); +static void fts5PoslistFilterCallback( + Fts5Index *pUnused, + void *pContext, + const u8 *pChunk, int nChunk +){ + PoslistCallbackCtx *pCtx = (PoslistCallbackCtx*)pContext; + UNUSED_PARAM(pUnused); + assert_nc( nChunk>=0 ); + if( nChunk>0 ){ + /* Search through to find the first varint with value 1. This is the + ** start of the next columns hits. */ + int i = 0; + int iStart = 0; + + if( pCtx->eState==2 ){ + int iCol; + fts5FastGetVarint32(pChunk, i, iCol); + if( fts5IndexColsetTest(pCtx->pColset, iCol) ){ + pCtx->eState = 1; + fts5BufferSafeAppendVarint(pCtx->pBuf, 1); + }else{ + pCtx->eState = 0; + } } - for(pLvl=&pStruct->aLevel[0]; pLvlnSeg-1; iSeg>=0; iSeg--){ - Fts5StructureSegment *pSeg = &pLvl->aSeg[iSeg]; - Fts5SegIter *pIter = &pNew->aSeg[iIter++]; - if( pTerm==0 ){ - fts5SegIterInit(p, pSeg, pIter); + + do { + while( ieState ){ + fts5BufferSafeAppendBlob(pCtx->pBuf, &pChunk[iStart], i-iStart); + } + if( i=nChunk ){ + pCtx->eState = 2; }else{ - fts5SegIterSeekInit(p, &buf, pTerm, nTerm, flags, pSeg, pIter); + fts5FastGetVarint32(pChunk, i, iCol); + pCtx->eState = fts5IndexColsetTest(pCtx->pColset, iCol); + if( pCtx->eState ){ + fts5BufferSafeAppendBlob(pCtx->pBuf, &pChunk[iStart], i-iStart); + iStart = i; + } } } - } - }else{ - pLvl = &pStruct->aLevel[iLevel]; - for(iSeg=nSeg-1; iSeg>=0; iSeg--){ - fts5SegIterInit(p, &pLvl->aSeg[iSeg], &pNew->aSeg[iIter++]); - } + }while( irc==SQLITE_OK ){ - for(iIter=pNew->nSeg-1; iIter>0; iIter--){ - int iEq; - if( (iEq = fts5MultiIterDoCompare(pNew, iIter)) ){ - fts5SegIterNext(p, &pNew->aSeg[iEq], 0); - fts5MultiIterAdvanced(p, pNew, iEq, iIter); - } - } - fts5MultiIterSetEof(pNew); - fts5AssertMultiIterSetup(p, pNew); - - if( pNew->bSkipEmpty && fts5MultiIterIsEmpty(p, pNew) ){ - fts5MultiIterNext(p, pNew, 0, 0); - } - }else{ - fts5MultiIterFree(p, pNew); - *ppOut = 0; - } - fts5BufferFree(&buf); -} - -/* -** Create an Fts5IndexIter that iterates through the doclist provided -** as the second argument. -*/ -static void fts5MultiIterNew2( - Fts5Index *p, /* FTS5 backend to iterate within */ - Fts5Data *pData, /* Doclist to iterate through */ - int bDesc, /* True for descending rowid order */ - Fts5IndexIter **ppOut /* New object */ -){ - Fts5IndexIter *pNew; - pNew = fts5MultiIterAlloc(p, 2); - if( pNew ){ - Fts5SegIter *pIter = &pNew->aSeg[1]; - - pNew->bFiltered = 1; - pIter->flags = FTS5_SEGITER_ONETERM; - if( pData->szLeaf>0 ){ - pIter->pLeaf = pData; - pIter->iLeafOffset = fts5GetVarint(pData->p, (u64*)&pIter->iRowid); - pIter->iEndofDoclist = pData->nn; - pNew->aFirst[1].iFirst = 1; - if( bDesc ){ - pNew->bRev = 1; - pIter->flags |= FTS5_SEGITER_REVERSE; - fts5SegIterReverseInitPage(p, pIter); - }else{ - fts5SegIterLoadNPos(p, pIter); - } - pData = 0; - }else{ - pNew->bEof = 1; - } - - *ppOut = pNew; - } - - fts5DataRelease(pData); -} - -/* -** Return true if the iterator is at EOF or if an error has occurred. -** False otherwise. -*/ -static int fts5MultiIterEof(Fts5Index *p, Fts5IndexIter *pIter){ - assert( p->rc - || (pIter->aSeg[ pIter->aFirst[1].iFirst ].pLeaf==0)==pIter->bEof - ); - return (p->rc || pIter->bEof); -} - -/* -** Return the rowid of the entry that the iterator currently points -** to. If the iterator points to EOF when this function is called the -** results are undefined. -*/ -static i64 fts5MultiIterRowid(Fts5IndexIter *pIter){ - assert( pIter->aSeg[ pIter->aFirst[1].iFirst ].pLeaf ); - return pIter->aSeg[ pIter->aFirst[1].iFirst ].iRowid; -} - -/* -** Move the iterator to the next entry at or following iMatch. -*/ -static void fts5MultiIterNextFrom( - Fts5Index *p, - Fts5IndexIter *pIter, - i64 iMatch -){ - while( 1 ){ - i64 iRowid; - fts5MultiIterNext(p, pIter, 1, iMatch); - if( fts5MultiIterEof(p, pIter) ) break; - iRowid = fts5MultiIterRowid(pIter); - if( pIter->bRev==0 && iRowid>=iMatch ) break; - if( pIter->bRev!=0 && iRowid<=iMatch ) break; - } -} - -/* -** Return a pointer to a buffer containing the term associated with the -** entry that the iterator currently points to. -*/ -static const u8 *fts5MultiIterTerm(Fts5IndexIter *pIter, int *pn){ - Fts5SegIter *p = &pIter->aSeg[ pIter->aFirst[1].iFirst ]; - *pn = p->term.n; - return p->term.p; } static void fts5ChunkIterate( @@ -175287,6 +186955,9 @@ static void fts5ChunkIterate( int pgno = pSeg->iLeafPgno; int pgnoSave = 0; + /* This function does notmwork with detail=none databases. */ + assert( p->pConfig->eDetail!=FTS5_DETAIL_NONE ); + if( (pSeg->flags & FTS5_SEGITER_REVERSE)==0 ){ pgnoSave = pgno+1; } @@ -175312,7 +186983,455 @@ static void fts5ChunkIterate( } } +/* +** Iterator pIter currently points to a valid entry (not EOF). This +** function appends the position list data for the current entry to +** buffer pBuf. It does not make a copy of the position-list size +** field. +*/ +static void fts5SegiterPoslist( + Fts5Index *p, + Fts5SegIter *pSeg, + Fts5Colset *pColset, + Fts5Buffer *pBuf +){ + if( 0==fts5BufferGrow(&p->rc, pBuf, pSeg->nPos) ){ + if( pColset==0 ){ + fts5ChunkIterate(p, pSeg, (void*)pBuf, fts5PoslistCallback); + }else{ + if( p->pConfig->eDetail==FTS5_DETAIL_FULL ){ + PoslistCallbackCtx sCtx; + sCtx.pBuf = pBuf; + sCtx.pColset = pColset; + sCtx.eState = fts5IndexColsetTest(pColset, 0); + assert( sCtx.eState==0 || sCtx.eState==1 ); + fts5ChunkIterate(p, pSeg, (void*)&sCtx, fts5PoslistFilterCallback); + }else{ + PoslistOffsetsCtx sCtx; + memset(&sCtx, 0, sizeof(sCtx)); + sCtx.pBuf = pBuf; + sCtx.pColset = pColset; + fts5ChunkIterate(p, pSeg, (void*)&sCtx, fts5PoslistOffsetsCallback); + } + } + } +} +/* +** IN/OUT parameter (*pa) points to a position list n bytes in size. If +** the position list contains entries for column iCol, then (*pa) is set +** to point to the sub-position-list for that column and the number of +** bytes in it returned. Or, if the argument position list does not +** contain any entries for column iCol, return 0. +*/ +static int fts5IndexExtractCol( + const u8 **pa, /* IN/OUT: Pointer to poslist */ + int n, /* IN: Size of poslist in bytes */ + int iCol /* Column to extract from poslist */ +){ + int iCurrent = 0; /* Anything before the first 0x01 is col 0 */ + const u8 *p = *pa; + const u8 *pEnd = &p[n]; /* One byte past end of position list */ + + while( iCol>iCurrent ){ + /* Advance pointer p until it points to pEnd or an 0x01 byte that is + ** not part of a varint. Note that it is not possible for a negative + ** or extremely large varint to occur within an uncorrupted position + ** list. So the last byte of each varint may be assumed to have a clear + ** 0x80 bit. */ + while( *p!=0x01 ){ + while( *p++ & 0x80 ); + if( p>=pEnd ) return 0; + } + *pa = p++; + iCurrent = *p++; + if( iCurrent & 0x80 ){ + p--; + p += fts5GetVarint32(p, iCurrent); + } + } + if( iCol!=iCurrent ) return 0; + + /* Advance pointer p until it points to pEnd or an 0x01 byte that is + ** not part of a varint */ + while( pnCol; i++){ + const u8 *pSub = pPos; + int nSub = fts5IndexExtractCol(&pSub, nPos, pColset->aiCol[i]); + if( nSub ){ + fts5BufferAppendBlob(&rc, pBuf, nSub, pSub); + } + } + return rc; +} + +/* +** xSetOutputs callback used by detail=none tables. +*/ +static void fts5IterSetOutputs_None(Fts5Iter *pIter, Fts5SegIter *pSeg){ + assert( pIter->pIndex->pConfig->eDetail==FTS5_DETAIL_NONE ); + pIter->base.iRowid = pSeg->iRowid; + pIter->base.nData = pSeg->nPos; +} + +/* +** xSetOutputs callback used by detail=full and detail=col tables when no +** column filters are specified. +*/ +static void fts5IterSetOutputs_Nocolset(Fts5Iter *pIter, Fts5SegIter *pSeg){ + pIter->base.iRowid = pSeg->iRowid; + pIter->base.nData = pSeg->nPos; + + assert( pIter->pIndex->pConfig->eDetail!=FTS5_DETAIL_NONE ); + assert( pIter->pColset==0 ); + + if( pSeg->iLeafOffset+pSeg->nPos<=pSeg->pLeaf->szLeaf ){ + /* All data is stored on the current page. Populate the output + ** variables to point into the body of the page object. */ + pIter->base.pData = &pSeg->pLeaf->p[pSeg->iLeafOffset]; + }else{ + /* The data is distributed over two or more pages. Copy it into the + ** Fts5Iter.poslist buffer and then set the output pointer to point + ** to this buffer. */ + fts5BufferZero(&pIter->poslist); + fts5SegiterPoslist(pIter->pIndex, pSeg, 0, &pIter->poslist); + pIter->base.pData = pIter->poslist.p; + } +} + +/* +** xSetOutputs callback used by detail=col when there is a column filter +** and there are 100 or more columns. Also called as a fallback from +** fts5IterSetOutputs_Col100 if the column-list spans more than one page. +*/ +static void fts5IterSetOutputs_Col(Fts5Iter *pIter, Fts5SegIter *pSeg){ + fts5BufferZero(&pIter->poslist); + fts5SegiterPoslist(pIter->pIndex, pSeg, pIter->pColset, &pIter->poslist); + pIter->base.iRowid = pSeg->iRowid; + pIter->base.pData = pIter->poslist.p; + pIter->base.nData = pIter->poslist.n; +} + +/* +** xSetOutputs callback used when: +** +** * detail=col, +** * there is a column filter, and +** * the table contains 100 or fewer columns. +** +** The last point is to ensure all column numbers are stored as +** single-byte varints. +*/ +static void fts5IterSetOutputs_Col100(Fts5Iter *pIter, Fts5SegIter *pSeg){ + + assert( pIter->pIndex->pConfig->eDetail==FTS5_DETAIL_COLUMNS ); + assert( pIter->pColset ); + + if( pSeg->iLeafOffset+pSeg->nPos>pSeg->pLeaf->szLeaf ){ + fts5IterSetOutputs_Col(pIter, pSeg); + }else{ + u8 *a = (u8*)&pSeg->pLeaf->p[pSeg->iLeafOffset]; + u8 *pEnd = (u8*)&a[pSeg->nPos]; + int iPrev = 0; + int *aiCol = pIter->pColset->aiCol; + int *aiColEnd = &aiCol[pIter->pColset->nCol]; + + u8 *aOut = pIter->poslist.p; + int iPrevOut = 0; + + pIter->base.iRowid = pSeg->iRowid; + + while( abase.pData = pIter->poslist.p; + pIter->base.nData = aOut - pIter->poslist.p; + } +} + +/* +** xSetOutputs callback used by detail=full when there is a column filter. +*/ +static void fts5IterSetOutputs_Full(Fts5Iter *pIter, Fts5SegIter *pSeg){ + Fts5Colset *pColset = pIter->pColset; + pIter->base.iRowid = pSeg->iRowid; + + assert( pIter->pIndex->pConfig->eDetail==FTS5_DETAIL_FULL ); + assert( pColset ); + + if( pSeg->iLeafOffset+pSeg->nPos<=pSeg->pLeaf->szLeaf ){ + /* All data is stored on the current page. Populate the output + ** variables to point into the body of the page object. */ + const u8 *a = &pSeg->pLeaf->p[pSeg->iLeafOffset]; + if( pColset->nCol==1 ){ + pIter->base.nData = fts5IndexExtractCol(&a, pSeg->nPos,pColset->aiCol[0]); + pIter->base.pData = a; + }else{ + fts5BufferZero(&pIter->poslist); + fts5IndexExtractColset(pColset, a, pSeg->nPos, &pIter->poslist); + pIter->base.pData = pIter->poslist.p; + pIter->base.nData = pIter->poslist.n; + } + }else{ + /* The data is distributed over two or more pages. Copy it into the + ** Fts5Iter.poslist buffer and then set the output pointer to point + ** to this buffer. */ + fts5BufferZero(&pIter->poslist); + fts5SegiterPoslist(pIter->pIndex, pSeg, pColset, &pIter->poslist); + pIter->base.pData = pIter->poslist.p; + pIter->base.nData = pIter->poslist.n; + } +} + +static void fts5IterSetOutputCb(int *pRc, Fts5Iter *pIter){ + if( *pRc==SQLITE_OK ){ + Fts5Config *pConfig = pIter->pIndex->pConfig; + if( pConfig->eDetail==FTS5_DETAIL_NONE ){ + pIter->xSetOutputs = fts5IterSetOutputs_None; + } + + else if( pIter->pColset==0 ){ + pIter->xSetOutputs = fts5IterSetOutputs_Nocolset; + } + + else if( pConfig->eDetail==FTS5_DETAIL_FULL ){ + pIter->xSetOutputs = fts5IterSetOutputs_Full; + } + + else{ + assert( pConfig->eDetail==FTS5_DETAIL_COLUMNS ); + if( pConfig->nCol<=100 ){ + pIter->xSetOutputs = fts5IterSetOutputs_Col100; + sqlite3Fts5BufferSize(pRc, &pIter->poslist, pConfig->nCol); + }else{ + pIter->xSetOutputs = fts5IterSetOutputs_Col; + } + } + } +} + + +/* +** Allocate a new Fts5Iter object. +** +** The new object will be used to iterate through data in structure pStruct. +** If iLevel is -ve, then all data in all segments is merged. Or, if iLevel +** is zero or greater, data from the first nSegment segments on level iLevel +** is merged. +** +** The iterator initially points to the first term/rowid entry in the +** iterated data. +*/ +static void fts5MultiIterNew( + Fts5Index *p, /* FTS5 backend to iterate within */ + Fts5Structure *pStruct, /* Structure of specific index */ + int flags, /* FTS5INDEX_QUERY_XXX flags */ + Fts5Colset *pColset, /* Colset to filter on (or NULL) */ + const u8 *pTerm, int nTerm, /* Term to seek to (or NULL/0) */ + int iLevel, /* Level to iterate (-1 for all) */ + int nSegment, /* Number of segments to merge (iLevel>=0) */ + Fts5Iter **ppOut /* New object */ +){ + int nSeg = 0; /* Number of segment-iters in use */ + int iIter = 0; /* */ + int iSeg; /* Used to iterate through segments */ + Fts5StructureLevel *pLvl; + Fts5Iter *pNew; + + assert( (pTerm==0 && nTerm==0) || iLevel<0 ); + + /* Allocate space for the new multi-seg-iterator. */ + if( p->rc==SQLITE_OK ){ + if( iLevel<0 ){ + assert( pStruct->nSegment==fts5StructureCountSegments(pStruct) ); + nSeg = pStruct->nSegment; + nSeg += (p->pHash ? 1 : 0); + }else{ + nSeg = MIN(pStruct->aLevel[iLevel].nSeg, nSegment); + } + } + *ppOut = pNew = fts5MultiIterAlloc(p, nSeg); + if( pNew==0 ) return; + pNew->bRev = (0!=(flags & FTS5INDEX_QUERY_DESC)); + pNew->bSkipEmpty = (0!=(flags & FTS5INDEX_QUERY_SKIPEMPTY)); + pNew->pStruct = pStruct; + pNew->pColset = pColset; + fts5StructureRef(pStruct); + if( (flags & FTS5INDEX_QUERY_NOOUTPUT)==0 ){ + fts5IterSetOutputCb(&p->rc, pNew); + } + + /* Initialize each of the component segment iterators. */ + if( p->rc==SQLITE_OK ){ + if( iLevel<0 ){ + Fts5StructureLevel *pEnd = &pStruct->aLevel[pStruct->nLevel]; + if( p->pHash ){ + /* Add a segment iterator for the current contents of the hash table. */ + Fts5SegIter *pIter = &pNew->aSeg[iIter++]; + fts5SegIterHashInit(p, pTerm, nTerm, flags, pIter); + } + for(pLvl=&pStruct->aLevel[0]; pLvlnSeg-1; iSeg>=0; iSeg--){ + Fts5StructureSegment *pSeg = &pLvl->aSeg[iSeg]; + Fts5SegIter *pIter = &pNew->aSeg[iIter++]; + if( pTerm==0 ){ + fts5SegIterInit(p, pSeg, pIter); + }else{ + fts5SegIterSeekInit(p, pTerm, nTerm, flags, pSeg, pIter); + } + } + } + }else{ + pLvl = &pStruct->aLevel[iLevel]; + for(iSeg=nSeg-1; iSeg>=0; iSeg--){ + fts5SegIterInit(p, &pLvl->aSeg[iSeg], &pNew->aSeg[iIter++]); + } + } + assert( iIter==nSeg ); + } + + /* If the above was successful, each component iterators now points + ** to the first entry in its segment. In this case initialize the + ** aFirst[] array. Or, if an error has occurred, free the iterator + ** object and set the output variable to NULL. */ + if( p->rc==SQLITE_OK ){ + for(iIter=pNew->nSeg-1; iIter>0; iIter--){ + int iEq; + if( (iEq = fts5MultiIterDoCompare(pNew, iIter)) ){ + Fts5SegIter *pSeg = &pNew->aSeg[iEq]; + if( p->rc==SQLITE_OK ) pSeg->xNext(p, pSeg, 0); + fts5MultiIterAdvanced(p, pNew, iEq, iIter); + } + } + fts5MultiIterSetEof(pNew); + fts5AssertMultiIterSetup(p, pNew); + + if( pNew->bSkipEmpty && fts5MultiIterIsEmpty(p, pNew) ){ + fts5MultiIterNext(p, pNew, 0, 0); + }else if( pNew->base.bEof==0 ){ + Fts5SegIter *pSeg = &pNew->aSeg[pNew->aFirst[1].iFirst]; + pNew->xSetOutputs(pNew, pSeg); + } + + }else{ + fts5MultiIterFree(pNew); + *ppOut = 0; + } +} + +/* +** Create an Fts5Iter that iterates through the doclist provided +** as the second argument. +*/ +static void fts5MultiIterNew2( + Fts5Index *p, /* FTS5 backend to iterate within */ + Fts5Data *pData, /* Doclist to iterate through */ + int bDesc, /* True for descending rowid order */ + Fts5Iter **ppOut /* New object */ +){ + Fts5Iter *pNew; + pNew = fts5MultiIterAlloc(p, 2); + if( pNew ){ + Fts5SegIter *pIter = &pNew->aSeg[1]; + + pIter->flags = FTS5_SEGITER_ONETERM; + if( pData->szLeaf>0 ){ + pIter->pLeaf = pData; + pIter->iLeafOffset = fts5GetVarint(pData->p, (u64*)&pIter->iRowid); + pIter->iEndofDoclist = pData->nn; + pNew->aFirst[1].iFirst = 1; + if( bDesc ){ + pNew->bRev = 1; + pIter->flags |= FTS5_SEGITER_REVERSE; + fts5SegIterReverseInitPage(p, pIter); + }else{ + fts5SegIterLoadNPos(p, pIter); + } + pData = 0; + }else{ + pNew->base.bEof = 1; + } + fts5SegIterSetNext(p, pIter); + + *ppOut = pNew; + } + + fts5DataRelease(pData); +} + +/* +** Return true if the iterator is at EOF or if an error has occurred. +** False otherwise. +*/ +static int fts5MultiIterEof(Fts5Index *p, Fts5Iter *pIter){ + assert( p->rc + || (pIter->aSeg[ pIter->aFirst[1].iFirst ].pLeaf==0)==pIter->base.bEof + ); + return (p->rc || pIter->base.bEof); +} + +/* +** Return the rowid of the entry that the iterator currently points +** to. If the iterator points to EOF when this function is called the +** results are undefined. +*/ +static i64 fts5MultiIterRowid(Fts5Iter *pIter){ + assert( pIter->aSeg[ pIter->aFirst[1].iFirst ].pLeaf ); + return pIter->aSeg[ pIter->aFirst[1].iFirst ].iRowid; +} + +/* +** Move the iterator to the next entry at or following iMatch. +*/ +static void fts5MultiIterNextFrom( + Fts5Index *p, + Fts5Iter *pIter, + i64 iMatch +){ + while( 1 ){ + i64 iRowid; + fts5MultiIterNext(p, pIter, 1, iMatch); + if( fts5MultiIterEof(p, pIter) ) break; + iRowid = fts5MultiIterRowid(pIter); + if( pIter->bRev==0 && iRowid>=iMatch ) break; + if( pIter->bRev!=0 && iRowid<=iMatch ) break; + } +} + +/* +** Return a pointer to a buffer containing the term associated with the +** entry that the iterator currently points to. +*/ +static const u8 *fts5MultiIterTerm(Fts5Iter *pIter, int *pn){ + Fts5SegIter *p = &pIter->aSeg[ pIter->aFirst[1].iFirst ]; + *pn = p->term.n; + return p->term.p; +} /* ** Allocate a new segment-id for the structure pStruct. The new segment @@ -175330,18 +187449,46 @@ static int fts5AllocateSegid(Fts5Index *p, Fts5Structure *pStruct){ if( pStruct->nSegment>=FTS5_MAX_SEGMENT ){ p->rc = SQLITE_FULL; }else{ - while( iSegid==0 ){ - int iLvl, iSeg; - sqlite3_randomness(sizeof(u32), (void*)&iSegid); - iSegid = iSegid & ((1 << FTS5_DATA_ID_B)-1); - for(iLvl=0; iLvlnLevel; iLvl++){ - for(iSeg=0; iSegaLevel[iLvl].nSeg; iSeg++){ - if( iSegid==pStruct->aLevel[iLvl].aSeg[iSeg].iSegid ){ - iSegid = 0; - } + /* FTS5_MAX_SEGMENT is currently defined as 2000. So the following + ** array is 63 elements, or 252 bytes, in size. */ + u32 aUsed[(FTS5_MAX_SEGMENT+31) / 32]; + int iLvl, iSeg; + int i; + u32 mask; + memset(aUsed, 0, sizeof(aUsed)); + for(iLvl=0; iLvlnLevel; iLvl++){ + for(iSeg=0; iSegaLevel[iLvl].nSeg; iSeg++){ + int iId = pStruct->aLevel[iLvl].aSeg[iSeg].iSegid; + if( iId<=FTS5_MAX_SEGMENT ){ + aUsed[(iId-1) / 32] |= 1 << ((iId-1) % 32); } } } + + for(i=0; aUsed[i]==0xFFFFFFFF; i++); + mask = aUsed[i]; + for(iSegid=0; mask & (1 << iSegid); iSegid++); + iSegid += 1 + i*32; + +#ifdef SQLITE_DEBUG + for(iLvl=0; iLvlnLevel; iLvl++){ + for(iSeg=0; iSegaLevel[iLvl].nSeg; iSeg++){ + assert( iSegid!=pStruct->aLevel[iLvl].aSeg[iSeg].iSegid ); + } + } + assert( iSegid>0 && iSegid<=FTS5_MAX_SEGMENT ); + + { + sqlite3_stmt *pIdxSelect = fts5IdxSelectStmt(p); + if( p->rc==SQLITE_OK ){ + u8 aBlob[2] = {0xff, 0xff}; + sqlite3_bind_int(pIdxSelect, 1, iSegid); + sqlite3_bind_blob(pIdxSelect, 2, aBlob, 2, SQLITE_STATIC); + assert( sqlite3_step(pIdxSelect)!=SQLITE_ROW ); + p->rc = sqlite3_reset(pIdxSelect); + } + } +#endif } } @@ -175360,15 +187507,14 @@ static void fts5IndexDiscardData(Fts5Index *p){ } /* -** Return the size of the prefix, in bytes, that buffer (nNew/pNew) shares -** with buffer (nOld/pOld). +** Return the size of the prefix, in bytes, that buffer +** (pNew/) shares with buffer (pOld/nOld). +** +** Buffer (pNew/) is guaranteed to be greater +** than buffer (pOld/nOld). */ -static int fts5PrefixCompress( - int nOld, const u8 *pOld, - int nNew, const u8 *pNew -){ +static int fts5PrefixCompress(int nOld, const u8 *pOld, const u8 *pNew){ int i; - assert( fts5BlobCompare(pOld, nOld, pNew, nNew)<0 ); for(i=0; iwriter; i64 iRowid; +static int nCall = 0; +nCall++; + assert( (pPage->pgidx.n==0)==(pWriter->bFirstTermInPage) ); /* Set the szLeaf header field. */ assert( 0==fts5GetU16(&pPage->buf.p[2]) ); - fts5PutU16(&pPage->buf.p[2], pPage->buf.n); + fts5PutU16(&pPage->buf.p[2], (u16)pPage->buf.n); if( pWriter->bFirstTermInPage ){ /* No term was written to this page. */ @@ -175678,13 +187827,13 @@ static void fts5WriteAppendTerm( ** inefficient, but still correct. */ int n = nTerm; if( pPage->term.n ){ - n = 1 + fts5PrefixCompress(pPage->term.n, pPage->term.p, nTerm, pTerm); + n = 1 + fts5PrefixCompress(pPage->term.n, pPage->term.p, pTerm); } fts5WriteBtreeTerm(p, pWriter, n, pTerm); pPage = &pWriter->writer; } }else{ - nPrefix = fts5PrefixCompress(pPage->term.n, pPage->term.p, nTerm, pTerm); + nPrefix = fts5PrefixCompress(pPage->term.n, pPage->term.p, pTerm); fts5BufferAppendVarint(&p->rc, &pPage->buf, nPrefix); } @@ -175710,8 +187859,7 @@ static void fts5WriteAppendTerm( static void fts5WriteAppendRowid( Fts5Index *p, Fts5SegWriter *pWriter, - i64 iRowid, - int nPos + i64 iRowid ){ if( p->rc==SQLITE_OK ){ Fts5PageWriter *pPage = &pWriter->writer; @@ -175724,7 +187872,7 @@ static void fts5WriteAppendRowid( ** rowid-pointer in the page-header. Also append a value to the dlidx ** buffer, in case a doclist-index is required. */ if( pWriter->bFirstRowidInPage ){ - fts5PutU16(pPage->buf.p, pPage->buf.n); + fts5PutU16(pPage->buf.p, (u16)pPage->buf.n); fts5WriteDlidxAppend(p, pWriter, iRowid); } @@ -175738,8 +187886,6 @@ static void fts5WriteAppendRowid( pWriter->iPrevRowid = iRowid; pWriter->bFirstRowidInDoclist = 0; pWriter->bFirstRowidInPage = 0; - - fts5BufferAppendVarint(&p->rc, &pPage->buf, nPos); } } @@ -175790,7 +187936,9 @@ static void fts5WriteFinish( fts5WriteFlushLeaf(p, pWriter); } *pnLeaf = pLeaf->pgno-1; - fts5WriteFlushBtree(p, pWriter); + if( pLeaf->pgno>1 ){ + fts5WriteFlushBtree(p, pWriter); + } } fts5BufferFree(&pLeaf->term); fts5BufferFree(&pLeaf->buf); @@ -175818,9 +187966,12 @@ static void fts5WriteInit( pWriter->bFirstTermInPage = 1; pWriter->iBtPage = 1; + assert( pWriter->writer.buf.n==0 ); + assert( pWriter->writer.pgidx.n==0 ); + /* Grow the two buffers to pgsz + padding bytes in size. */ - fts5BufferGrow(&p->rc, &pWriter->writer.pgidx, nBuffer); - fts5BufferGrow(&p->rc, &pWriter->writer.buf, nBuffer); + sqlite3Fts5BufferSize(&p->rc, &pWriter->writer.pgidx, nBuffer); + sqlite3Fts5BufferSize(&p->rc, &pWriter->writer.buf, nBuffer); if( p->pIdxWriter==0 ){ Fts5Config *pConfig = p->pConfig; @@ -175847,7 +187998,7 @@ static void fts5WriteInit( ** incremental merge operation. This function is called if the incremental ** merge step has finished but the input has not been completely exhausted. */ -static void fts5TrimSegments(Fts5Index *p, Fts5IndexIter *pIter){ +static void fts5TrimSegments(Fts5Index *p, Fts5Iter *pIter){ int i; Fts5Buffer buf; memset(&buf, 0, sizeof(Fts5Buffer)); @@ -175879,7 +188030,7 @@ static void fts5TrimSegments(Fts5Index *p, Fts5IndexIter *pIter){ fts5BufferAppendBlob(&p->rc, &buf, pData->szLeaf-iOff, &pData->p[iOff]); if( p->rc==SQLITE_OK ){ /* Set the szLeaf field */ - fts5PutU16(&buf.p[2], buf.n); + fts5PutU16(&buf.p[2], (u16)buf.n); } /* Set up the new page-index array */ @@ -175925,13 +188076,15 @@ static void fts5IndexMergeLevel( Fts5Structure *pStruct = *ppStruct; Fts5StructureLevel *pLvl = &pStruct->aLevel[iLvl]; Fts5StructureLevel *pLvlOut; - Fts5IndexIter *pIter = 0; /* Iterator to read input data */ + Fts5Iter *pIter = 0; /* Iterator to read input data */ int nRem = pnRem ? *pnRem : 0; /* Output leaf pages left to write */ int nInput; /* Number of input segments */ Fts5SegWriter writer; /* Writer object */ Fts5StructureSegment *pSeg; /* Output segment */ Fts5Buffer term; int bOldest; /* True if the output segment is the oldest */ + int eDetail = p->pConfig->eDetail; + const int flags = FTS5INDEX_QUERY_NOOUTPUT; assert( iLvlnLevel ); assert( pLvl->nMerge<=pLvl->nSeg ); @@ -175976,7 +188129,7 @@ static void fts5IndexMergeLevel( bOldest = (pLvlOut->nSeg==1 && pStruct->nLevel==iLvl+2); assert( iLvl>=0 ); - for(fts5MultiIterNew(p, pStruct, 0, 0, 0, 0, iLvl, nInput, &pIter); + for(fts5MultiIterNew(p, pStruct, flags, 0, 0, 0, iLvl, nInput, &pIter); fts5MultiIterEof(p, pIter)==0; fts5MultiIterNext(p, pIter, 0, 0) ){ @@ -176001,11 +188154,21 @@ static void fts5IndexMergeLevel( /* Append the rowid to the output */ /* WRITEPOSLISTSIZE */ - nPos = pSegIter->nPos*2 + pSegIter->bDel; - fts5WriteAppendRowid(p, &writer, fts5MultiIterRowid(pIter), nPos); + fts5WriteAppendRowid(p, &writer, fts5MultiIterRowid(pIter)); - /* Append the position-list data to the output */ - fts5ChunkIterate(p, pSegIter, (void*)&writer, fts5MergeChunkCallback); + if( eDetail==FTS5_DETAIL_NONE ){ + if( pSegIter->bDel ){ + fts5BufferAppendVarint(&p->rc, &writer.writer.buf, 0); + if( pSegIter->nPos>0 ){ + fts5BufferAppendVarint(&p->rc, &writer.writer.buf, 0); + } + } + }else{ + /* Append the position-list data to the output */ + nPos = pSegIter->nPos*2 + pSegIter->bDel; + fts5BufferAppendVarint(&p->rc, &writer.writer.buf, nPos); + fts5ChunkIterate(p, pSegIter, (void*)&writer, fts5MergeChunkCallback); + } } /* Flush the last leaf page to disk. Set the output segment b-tree height @@ -176038,20 +188201,24 @@ static void fts5IndexMergeLevel( pLvl->nMerge = nInput; } - fts5MultiIterFree(p, pIter); + fts5MultiIterFree(pIter); fts5BufferFree(&term); if( pnRem ) *pnRem -= writer.nLeafWritten; } /* ** Do up to nPg pages of automerge work on the index. +** +** Return true if any changes were actually made, or false otherwise. */ -static void fts5IndexMerge( +static int fts5IndexMerge( Fts5Index *p, /* FTS5 backend object */ Fts5Structure **ppStruct, /* IN/OUT: Current structure of index */ - int nPg /* Pages of work to do */ + int nPg, /* Pages of work to do */ + int nMin /* Minimum number of segments to merge */ ){ int nRem = nPg; + int bRet = 0; Fts5Structure *pStruct = *ppStruct; while( nRem>0 && p->rc==SQLITE_OK ){ int iLvl; /* To iterate through levels */ @@ -176082,17 +188249,17 @@ static void fts5IndexMerge( } #endif - if( nBestpConfig->nAutomerge - && pStruct->aLevel[iBestLvl].nMerge==0 - ){ + if( nBestaLevel[iBestLvl].nMerge==0 ){ break; } + bRet = 1; fts5IndexMergeLevel(p, &pStruct, iBestLvl, &nRem); if( p->rc==SQLITE_OK && pStruct->aLevel[iBestLvl].nMerge==0 ){ fts5StructurePromote(p, iBestLvl+1, pStruct); } } *ppStruct = pStruct; + return bRet; } /* @@ -176120,7 +188287,7 @@ static void fts5IndexAutomerge( pStruct->nWriteCounter += nLeaf; nRem = (int)(p->nWorkUnit * nWork * pStruct->nLevel); - fts5IndexMerge(p, ppStruct, nRem); + fts5IndexMerge(p, ppStruct, nRem, p->pConfig->nAutomerge); } } @@ -176173,17 +188340,6 @@ static int fts5PoslistPrefix(const u8 *aBuf, int nMax){ return ret; } -#define fts5BufferSafeAppendBlob(pBuf, pBlob, nBlob) { \ - assert( (pBuf)->nSpace>=((pBuf)->n+nBlob) ); \ - memcpy(&(pBuf)->p[(pBuf)->n], pBlob, nBlob); \ - (pBuf)->n += nBlob; \ -} - -#define fts5BufferSafeAppendVarint(pBuf, iVal) { \ - (pBuf)->n += sqlite3Fts5PutVarint(&(pBuf)->p[(pBuf)->n], (iVal)); \ - assert( (pBuf)->nSpace>=(pBuf)->n ); \ -} - /* ** Flush the contents of in-memory hash table iHash to a new level-0 ** segment on disk. Also update the corresponding structure record. @@ -176201,10 +188357,11 @@ static void fts5FlushOneHash(Fts5Index *p){ ** for the new level-0 segment. */ pStruct = fts5StructureRead(p); iSegid = fts5AllocateSegid(p, pStruct); + fts5StructureInvalidate(p); if( iSegid ){ const int pgsz = p->pConfig->pgsz; - + int eDetail = p->pConfig->eDetail; Fts5StructureSegment *pSeg; /* New segment within pStruct */ Fts5Buffer *pBuf; /* Buffer in which to assemble leaf page */ Fts5Buffer *pPgidx; /* Buffer in which to assemble pgidx */ @@ -176232,7 +188389,7 @@ static void fts5FlushOneHash(Fts5Index *p){ /* Write the term for this entry to disk. */ sqlite3Fts5HashScanEntry(pHash, &zTerm, &pDoclist, &nDoclist); - fts5WriteAppendTerm(p, &writer, strlen(zTerm), (const u8*)zTerm); + fts5WriteAppendTerm(p, &writer, (int)strlen(zTerm), (const u8*)zTerm); assert( writer.bFirstRowidInPage==0 ); if( pgsz>=(pBuf->n + pPgidx->n + nDoclist + 1) ){ @@ -176247,16 +188404,11 @@ static void fts5FlushOneHash(Fts5Index *p){ ** loop iterates through the poslists that make up the current ** doclist. */ while( p->rc==SQLITE_OK && iOffp[0], pBuf->n); /* first rowid on page */ + fts5PutU16(&pBuf->p[0], (u16)pBuf->n); /* first rowid on page */ pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], iRowid); writer.bFirstRowidInPage = 0; fts5WriteDlidxAppend(p, &writer, iRowid); @@ -176265,34 +188417,52 @@ static void fts5FlushOneHash(Fts5Index *p){ } assert( pBuf->n<=pBuf->nSpace ); - if( (pBuf->n + pPgidx->n + nCopy) <= pgsz ){ - /* The entire poslist will fit on the current leaf. So copy - ** it in one go. */ - fts5BufferSafeAppendBlob(pBuf, &pDoclist[iOff], nCopy); - }else{ - /* The entire poslist will not fit on this leaf. So it needs - ** to be broken into sections. The only qualification being - ** that each varint must be stored contiguously. */ - const u8 *pPoslist = &pDoclist[iOff]; - int iPos = 0; - while( p->rc==SQLITE_OK ){ - int nSpace = pgsz - pBuf->n - pPgidx->n; - int n = 0; - if( (nCopy - iPos)<=nSpace ){ - n = nCopy - iPos; - }else{ - n = fts5PoslistPrefix(&pPoslist[iPos], nSpace); + if( eDetail==FTS5_DETAIL_NONE ){ + if( iOffp[pBuf->n++] = 0; + iOff++; + if( iOffp[pBuf->n++] = 0; + iOff++; } - assert( n>0 ); - fts5BufferSafeAppendBlob(pBuf, &pPoslist[iPos], n); - iPos += n; - if( (pBuf->n + pPgidx->n)>=pgsz ){ - fts5WriteFlushLeaf(p, &writer); - } - if( iPos>=nCopy ) break; } + if( (pBuf->n + pPgidx->n)>=pgsz ){ + fts5WriteFlushLeaf(p, &writer); + } + }else{ + int bDummy; + int nPos; + int nCopy = fts5GetPoslistSize(&pDoclist[iOff], &nPos, &bDummy); + nCopy += nPos; + if( (pBuf->n + pPgidx->n + nCopy) <= pgsz ){ + /* The entire poslist will fit on the current leaf. So copy + ** it in one go. */ + fts5BufferSafeAppendBlob(pBuf, &pDoclist[iOff], nCopy); + }else{ + /* The entire poslist will not fit on this leaf. So it needs + ** to be broken into sections. The only qualification being + ** that each varint must be stored contiguously. */ + const u8 *pPoslist = &pDoclist[iOff]; + int iPos = 0; + while( p->rc==SQLITE_OK ){ + int nSpace = pgsz - pBuf->n - pPgidx->n; + int n = 0; + if( (nCopy - iPos)<=nSpace ){ + n = nCopy - iPos; + }else{ + n = fts5PoslistPrefix(&pPoslist[iPos], nSpace); + } + assert( n>0 ); + fts5BufferSafeAppendBlob(pBuf, &pPoslist[iPos], n); + iPos += n; + if( (pBuf->n + pPgidx->n)>=pgsz ){ + fts5WriteFlushLeaf(p, &writer); + } + if( iPos>=nCopy ) break; + } + } + iOff += nCopy; } - iOff += nCopy; } } @@ -176338,28 +188508,41 @@ static void fts5IndexFlush(Fts5Index *p){ } } - -static int sqlite3Fts5IndexOptimize(Fts5Index *p){ - Fts5Structure *pStruct; +static Fts5Structure *fts5IndexOptimizeStruct( + Fts5Index *p, + Fts5Structure *pStruct +){ Fts5Structure *pNew = 0; - int nSeg = 0; + int nByte = sizeof(Fts5Structure); + int nSeg = pStruct->nSegment; + int i; - assert( p->rc==SQLITE_OK ); - fts5IndexFlush(p); - pStruct = fts5StructureRead(p); - - if( pStruct ){ - assert( pStruct->nSegment==fts5StructureCountSegments(pStruct) ); - nSeg = pStruct->nSegment; - if( nSeg>1 ){ - int nByte = sizeof(Fts5Structure); - nByte += (pStruct->nLevel+1) * sizeof(Fts5StructureLevel); - pNew = (Fts5Structure*)sqlite3Fts5MallocZero(&p->rc, nByte); + /* Figure out if this structure requires optimization. A structure does + ** not require optimization if either: + ** + ** + it consists of fewer than two segments, or + ** + all segments are on the same level, or + ** + all segments except one are currently inputs to a merge operation. + ** + ** In the first case, return NULL. In the second, increment the ref-count + ** on *pStruct and return a copy of the pointer to it. + */ + if( nSeg<2 ) return 0; + for(i=0; inLevel; i++){ + int nThis = pStruct->aLevel[i].nSeg; + if( nThis==nSeg || (nThis==nSeg-1 && pStruct->aLevel[i].nMerge==nThis) ){ + fts5StructureRef(pStruct); + return pStruct; } + assert( pStruct->aLevel[i].nMerge<=nThis ); } + + nByte += (pStruct->nLevel+1) * sizeof(Fts5StructureLevel); + pNew = (Fts5Structure*)sqlite3Fts5MallocZero(&p->rc, nByte); + if( pNew ){ Fts5StructureLevel *pLvl; - int nByte = nSeg * sizeof(Fts5StructureSegment); + nByte = nSeg * sizeof(Fts5StructureSegment); pNew->nLevel = pStruct->nLevel+1; pNew->nRef = 1; pNew->nWriteCounter = pStruct->nWriteCounter; @@ -176368,7 +188551,10 @@ static int sqlite3Fts5IndexOptimize(Fts5Index *p){ if( pLvl->aSeg ){ int iLvl, iSeg; int iSegOut = 0; - for(iLvl=0; iLvlnLevel; iLvl++){ + /* Iterate through all segments, from oldest to newest. Add them to + ** the new Fts5Level object so that pLvl->aSeg[0] is the oldest + ** segment in the data structure. */ + for(iLvl=pStruct->nLevel-1; iLvl>=0; iLvl--){ for(iSeg=0; iSegaLevel[iLvl].nSeg; iSeg++){ pLvl->aSeg[iSegOut] = pStruct->aLevel[iLvl].aSeg[iSeg]; iSegOut++; @@ -176381,8 +188567,27 @@ static int sqlite3Fts5IndexOptimize(Fts5Index *p){ } } + return pNew; +} + +static int sqlite3Fts5IndexOptimize(Fts5Index *p){ + Fts5Structure *pStruct; + Fts5Structure *pNew = 0; + + assert( p->rc==SQLITE_OK ); + fts5IndexFlush(p); + pStruct = fts5StructureRead(p); + fts5StructureInvalidate(p); + + if( pStruct ){ + pNew = fts5IndexOptimizeStruct(p, pStruct); + } + fts5StructureRelease(pStruct); + + assert( pNew==0 || pNew->nSegment>0 ); if( pNew ){ - int iLvl = pNew->nLevel-1; + int iLvl; + for(iLvl=0; pNew->aLevel[iLvl].nSeg==0; iLvl++){} while( p->rc==SQLITE_OK && pNew->aLevel[iLvl].nSeg>0 ){ int nRem = FTS5_OPT_WORK_UNIT; fts5IndexMergeLevel(p, &pNew, iLvl, &nRem); @@ -176392,240 +188597,61 @@ static int sqlite3Fts5IndexOptimize(Fts5Index *p){ fts5StructureRelease(pNew); } - fts5StructureRelease(pStruct); return fts5IndexReturn(p); } +/* +** This is called to implement the special "VALUES('merge', $nMerge)" +** INSERT command. +*/ static int sqlite3Fts5IndexMerge(Fts5Index *p, int nMerge){ - Fts5Structure *pStruct; - - pStruct = fts5StructureRead(p); - if( pStruct && pStruct->nLevel ){ - fts5IndexMerge(p, &pStruct, nMerge); - fts5StructureWrite(p, pStruct); + Fts5Structure *pStruct = fts5StructureRead(p); + if( pStruct ){ + int nMin = p->pConfig->nUsermerge; + fts5StructureInvalidate(p); + if( nMerge<0 ){ + Fts5Structure *pNew = fts5IndexOptimizeStruct(p, pStruct); + fts5StructureRelease(pStruct); + pStruct = pNew; + nMin = 2; + nMerge = nMerge*-1; + } + if( pStruct && pStruct->nLevel ){ + if( fts5IndexMerge(p, &pStruct, nMerge, nMin) ){ + fts5StructureWrite(p, pStruct); + } + } + fts5StructureRelease(pStruct); } - fts5StructureRelease(pStruct); - return fts5IndexReturn(p); } -static void fts5PoslistCallback( - Fts5Index *p, - void *pContext, - const u8 *pChunk, int nChunk -){ - assert_nc( nChunk>=0 ); - if( nChunk>0 ){ - fts5BufferSafeAppendBlob((Fts5Buffer*)pContext, pChunk, nChunk); - } -} - -typedef struct PoslistCallbackCtx PoslistCallbackCtx; -struct PoslistCallbackCtx { - Fts5Buffer *pBuf; /* Append to this buffer */ - Fts5Colset *pColset; /* Restrict matches to this column */ - int eState; /* See above */ -}; - -/* -** TODO: Make this more efficient! -*/ -static int fts5IndexColsetTest(Fts5Colset *pColset, int iCol){ - int i; - for(i=0; inCol; i++){ - if( pColset->aiCol[i]==iCol ) return 1; - } - return 0; -} - -static void fts5PoslistFilterCallback( - Fts5Index *p, - void *pContext, - const u8 *pChunk, int nChunk -){ - PoslistCallbackCtx *pCtx = (PoslistCallbackCtx*)pContext; - assert_nc( nChunk>=0 ); - if( nChunk>0 ){ - /* Search through to find the first varint with value 1. This is the - ** start of the next columns hits. */ - int i = 0; - int iStart = 0; - - if( pCtx->eState==2 ){ - int iCol; - fts5FastGetVarint32(pChunk, i, iCol); - if( fts5IndexColsetTest(pCtx->pColset, iCol) ){ - pCtx->eState = 1; - fts5BufferSafeAppendVarint(pCtx->pBuf, 1); - }else{ - pCtx->eState = 0; - } - } - - do { - while( ieState ){ - fts5BufferSafeAppendBlob(pCtx->pBuf, &pChunk[iStart], i-iStart); - } - if( i=nChunk ){ - pCtx->eState = 2; - }else{ - fts5FastGetVarint32(pChunk, i, iCol); - pCtx->eState = fts5IndexColsetTest(pCtx->pColset, iCol); - if( pCtx->eState ){ - fts5BufferSafeAppendBlob(pCtx->pBuf, &pChunk[iStart], i-iStart); - iStart = i; - } - } - } - }while( irc, pBuf, pSeg->nPos) ){ - if( pColset==0 ){ - fts5ChunkIterate(p, pSeg, (void*)pBuf, fts5PoslistCallback); - }else{ - PoslistCallbackCtx sCtx; - sCtx.pBuf = pBuf; - sCtx.pColset = pColset; - sCtx.eState = pColset ? fts5IndexColsetTest(pColset, 0) : 1; - assert( sCtx.eState==0 || sCtx.eState==1 ); - fts5ChunkIterate(p, pSeg, (void*)&sCtx, fts5PoslistFilterCallback); - } - } -} - -/* -** IN/OUT parameter (*pa) points to a position list n bytes in size. If -** the position list contains entries for column iCol, then (*pa) is set -** to point to the sub-position-list for that column and the number of -** bytes in it returned. Or, if the argument position list does not -** contain any entries for column iCol, return 0. -*/ -static int fts5IndexExtractCol( - const u8 **pa, /* IN/OUT: Pointer to poslist */ - int n, /* IN: Size of poslist in bytes */ - int iCol /* Column to extract from poslist */ -){ - int iCurrent = 0; /* Anything before the first 0x01 is col 0 */ - const u8 *p = *pa; - const u8 *pEnd = &p[n]; /* One byte past end of position list */ - u8 prev = 0; - - while( iCol!=iCurrent ){ - /* Advance pointer p until it points to pEnd or an 0x01 byte that is - ** not part of a varint */ - while( (prev & 0x80) || *p!=0x01 ){ - prev = *p++; - if( p==pEnd ) return 0; - } - *pa = p++; - p += fts5GetVarint32(p, iCurrent); - } - - /* Advance pointer p until it points to pEnd or an 0x01 byte that is - ** not part of a varint */ - assert( (prev & 0x80)==0 ); - while( prc. -*/ -static int fts5AppendPoslist( +static void fts5AppendRowid( Fts5Index *p, i64 iDelta, - Fts5IndexIter *pMulti, - Fts5Colset *pColset, + Fts5Iter *pUnused, Fts5Buffer *pBuf ){ - if( p->rc==SQLITE_OK ){ - Fts5SegIter *pSeg = &pMulti->aSeg[ pMulti->aFirst[1].iFirst ]; - assert( fts5MultiIterEof(p, pMulti)==0 ); - assert( pSeg->nPos>0 ); - if( 0==fts5BufferGrow(&p->rc, pBuf, pSeg->nPos+9+9) ){ - int iSv1; - int iSv2; - int iData; - - /* Append iDelta */ - iSv1 = pBuf->n; - fts5BufferSafeAppendVarint(pBuf, iDelta); - - /* WRITEPOSLISTSIZE */ - iSv2 = pBuf->n; - fts5BufferSafeAppendVarint(pBuf, pSeg->nPos*2); - iData = pBuf->n; - - if( pSeg->iLeafOffset+pSeg->nPos<=pSeg->pLeaf->szLeaf - && (pColset==0 || pColset->nCol==1) - ){ - const u8 *pPos = &pSeg->pLeaf->p[pSeg->iLeafOffset]; - int nPos; - if( pColset ){ - nPos = fts5IndexExtractCol(&pPos, pSeg->nPos, pColset->aiCol[0]); - }else{ - nPos = pSeg->nPos; - } - fts5BufferSafeAppendBlob(pBuf, pPos, nPos); - }else{ - fts5SegiterPoslist(p, pSeg, pColset, pBuf); - } - - if( pColset ){ - int nActual = pBuf->n - iData; - if( nActual!=pSeg->nPos ){ - if( nActual==0 ){ - pBuf->n = iSv1; - return 1; - }else{ - int nReq = sqlite3Fts5GetVarintLen((u32)(nActual*2)); - while( iSv2<(iData-nReq) ){ pBuf->p[iSv2++] = 0x80; } - sqlite3Fts5PutVarint(&pBuf->p[iSv2], nActual*2); - } - } - } - } - } - - return 0; + UNUSED_PARAM(pUnused); + fts5BufferAppendVarint(&p->rc, pBuf, iDelta); } +static void fts5AppendPoslist( + Fts5Index *p, + i64 iDelta, + Fts5Iter *pMulti, + Fts5Buffer *pBuf +){ + int nData = pMulti->base.nData; + assert( nData>0 ); + if( p->rc==SQLITE_OK && 0==fts5BufferGrow(&p->rc, pBuf, nData+9+9) ){ + fts5BufferSafeAppendVarint(pBuf, iDelta); + fts5BufferSafeAppendVarint(pBuf, nData*2); + fts5BufferSafeAppendBlob(pBuf, pMulti->base.pData, nData); + } +} + + static void fts5DoclistIterNext(Fts5DoclistIter *pIter){ u8 *p = pIter->aPoslist + pIter->nSize + pIter->nPoslist; @@ -176686,6 +188712,69 @@ static void fts5MergeAppendDocid( (iLastRowid) = (iRowid); \ } +/* +** Swap the contents of buffer *p1 with that of *p2. +*/ +static void fts5BufferSwap(Fts5Buffer *p1, Fts5Buffer *p2){ + Fts5Buffer tmp = *p1; + *p1 = *p2; + *p2 = tmp; +} + +static void fts5NextRowid(Fts5Buffer *pBuf, int *piOff, i64 *piRowid){ + int i = *piOff; + if( i>=pBuf->n ){ + *piOff = -1; + }else{ + u64 iVal; + *piOff = i + sqlite3Fts5GetVarint(&pBuf->p[i], &iVal); + *piRowid += iVal; + } +} + +/* +** This is the equivalent of fts5MergePrefixLists() for detail=none mode. +** In this case the buffers consist of a delta-encoded list of rowids only. +*/ +static void fts5MergeRowidLists( + Fts5Index *p, /* FTS5 backend object */ + Fts5Buffer *p1, /* First list to merge */ + Fts5Buffer *p2 /* Second list to merge */ +){ + int i1 = 0; + int i2 = 0; + i64 iRowid1 = 0; + i64 iRowid2 = 0; + i64 iOut = 0; + + Fts5Buffer out; + memset(&out, 0, sizeof(out)); + sqlite3Fts5BufferSize(&p->rc, &out, p1->n + p2->n); + if( p->rc ) return; + + fts5NextRowid(p1, &i1, &iRowid1); + fts5NextRowid(p2, &i2, &iRowid2); + while( i1>=0 || i2>=0 ){ + if( i1>=0 && (i2<0 || iRowid1iOut ); + fts5BufferSafeAppendVarint(&out, iRowid1 - iOut); + iOut = iRowid1; + fts5NextRowid(p1, &i1, &iRowid1); + }else{ + assert( iOut==0 || iRowid2>iOut ); + fts5BufferSafeAppendVarint(&out, iRowid2 - iOut); + iOut = iRowid2; + if( i1>=0 && iRowid1==iRowid2 ){ + fts5NextRowid(p1, &i1, &iRowid1); + } + fts5NextRowid(p2, &i2, &iRowid2); + } + } + + fts5BufferSwap(&out, p1); + fts5BufferFree(&out); +} + /* ** Buffers p1 and p2 contain doclists. This function merges the content ** of the two doclists together and sets buffer p1 to the result before @@ -176703,28 +188792,30 @@ static void fts5MergePrefixLists( i64 iLastRowid = 0; Fts5DoclistIter i1; Fts5DoclistIter i2; - Fts5Buffer out; - Fts5Buffer tmp; - memset(&out, 0, sizeof(out)); - memset(&tmp, 0, sizeof(tmp)); + Fts5Buffer out = {0, 0, 0}; + Fts5Buffer tmp = {0, 0, 0}; - sqlite3Fts5BufferGrow(&p->rc, &out, p1->n + p2->n); + if( sqlite3Fts5BufferSize(&p->rc, &out, p1->n + p2->n) ) return; fts5DoclistIterInit(p1, &i1); fts5DoclistIterInit(p2, &i2); - while( p->rc==SQLITE_OK && (i1.aPoslist!=0 || i2.aPoslist!=0) ){ - if( i2.aPoslist==0 || (i1.aPoslist && i1.iRowidrc, &tmp, i1.nPoslist + i2.nPoslist); + if( p->rc ) break; sqlite3Fts5PoslistNext64(a1, i1.nPoslist, &iOff1, &iPos1); sqlite3Fts5PoslistNext64(a2, i2.nPoslist, &iOff2, &iPos2); + assert( iPos1>=0 && iPos2>=0 ); - while( p->rc==SQLITE_OK && (iPos1>=0 || iPos2>=0) ){ - i64 iNew; - if( iPos2<0 || (iPos1>=0 && iPos1=0 && iPos2>=0 ){ + while( 1 ){ + if( iPos1rc = sqlite3Fts5PoslistWriterAppend(&tmp, &writer, iNew); + } + + if( iPos1>=0 ){ + if( iPos1!=iPrev ){ + sqlite3Fts5PoslistSafeAppend(&tmp, &iPrev, iPos1); + } + fts5BufferSafeAppendBlob(&tmp, &a1[iOff1], i1.nPoslist-iOff1); + }else{ + assert( iPos2>=0 && iPos2!=iPrev ); + sqlite3Fts5PoslistSafeAppend(&tmp, &iPrev, iPos2); + fts5BufferSafeAppendBlob(&tmp, &a2[iOff2], i2.nPoslist-iOff2); } /* WRITEPOSLISTSIZE */ @@ -176762,81 +188877,105 @@ static void fts5MergePrefixLists( fts5BufferSafeAppendBlob(&out, tmp.p, tmp.n); fts5DoclistIterNext(&i1); fts5DoclistIterNext(&i2); + if( i1.aPoslist==0 || i2.aPoslist==0 ) break; } } + if( i1.aPoslist ){ + fts5MergeAppendDocid(&out, iLastRowid, i1.iRowid); + fts5BufferSafeAppendBlob(&out, i1.aPoslist, i1.aEof - i1.aPoslist); + } + else if( i2.aPoslist ){ + fts5MergeAppendDocid(&out, iLastRowid, i2.iRowid); + fts5BufferSafeAppendBlob(&out, i2.aPoslist, i2.aEof - i2.aPoslist); + } + fts5BufferSet(&p->rc, p1, out.n, out.p); fts5BufferFree(&tmp); fts5BufferFree(&out); } } -static void fts5BufferSwap(Fts5Buffer *p1, Fts5Buffer *p2){ - Fts5Buffer tmp = *p1; - *p1 = *p2; - *p2 = tmp; -} - static void fts5SetupPrefixIter( Fts5Index *p, /* Index to read from */ int bDesc, /* True for "ORDER BY rowid DESC" */ const u8 *pToken, /* Buffer containing prefix to match */ int nToken, /* Size of buffer pToken in bytes */ Fts5Colset *pColset, /* Restrict matches to these columns */ - Fts5IndexIter **ppIter /* OUT: New iterator */ + Fts5Iter **ppIter /* OUT: New iterator */ ){ Fts5Structure *pStruct; Fts5Buffer *aBuf; const int nBuf = 32; + void (*xMerge)(Fts5Index*, Fts5Buffer*, Fts5Buffer*); + void (*xAppend)(Fts5Index*, i64, Fts5Iter*, Fts5Buffer*); + if( p->pConfig->eDetail==FTS5_DETAIL_NONE ){ + xMerge = fts5MergeRowidLists; + xAppend = fts5AppendRowid; + }else{ + xMerge = fts5MergePrefixLists; + xAppend = fts5AppendPoslist; + } + aBuf = (Fts5Buffer*)fts5IdxMalloc(p, sizeof(Fts5Buffer)*nBuf); pStruct = fts5StructureRead(p); if( aBuf && pStruct ){ - const int flags = FTS5INDEX_QUERY_SCAN; + const int flags = FTS5INDEX_QUERY_SCAN + | FTS5INDEX_QUERY_SKIPEMPTY + | FTS5INDEX_QUERY_NOOUTPUT; int i; i64 iLastRowid = 0; - Fts5IndexIter *p1 = 0; /* Iterator used to gather data from index */ + Fts5Iter *p1 = 0; /* Iterator used to gather data from index */ Fts5Data *pData; Fts5Buffer doclist; + int bNewTerm = 1; memset(&doclist, 0, sizeof(doclist)); - for(fts5MultiIterNew(p, pStruct, 1, flags, pToken, nToken, -1, 0, &p1); + fts5MultiIterNew(p, pStruct, flags, pColset, pToken, nToken, -1, 0, &p1); + fts5IterSetOutputCb(&p->rc, p1); + for( /* no-op */ ; fts5MultiIterEof(p, p1)==0; - fts5MultiIterNext(p, p1, 0, 0) + fts5MultiIterNext2(p, p1, &bNewTerm) ){ - i64 iRowid = fts5MultiIterRowid(p1); - int nTerm; - const u8 *pTerm = fts5MultiIterTerm(p1, &nTerm); - assert_nc( memcmp(pToken, pTerm, MIN(nToken, nTerm))<=0 ); - if( nTermaSeg[ p1->aFirst[1].iFirst ]; + int nTerm = pSeg->term.n; + const u8 *pTerm = pSeg->term.p; + p1->xSetOutputs(p1, pSeg); - if( doclist.n>0 && iRowid<=iLastRowid ){ + assert_nc( memcmp(pToken, pTerm, MIN(nToken, nTerm))<=0 ); + if( bNewTerm ){ + if( nTermbase.nData==0 ) continue; + + if( p1->base.iRowid<=iLastRowid && doclist.n>0 ){ for(i=0; p->rc==SQLITE_OK && doclist.n; i++){ assert( ibase.iRowid-iLastRowid, p1, &doclist); + iLastRowid = p1->base.iRowid; } for(i=0; irc==SQLITE_OK ){ - fts5MergePrefixLists(p, &doclist, &aBuf[i]); + xMerge(p, &doclist, &aBuf[i]); } fts5BufferFree(&aBuf[i]); } - fts5MultiIterFree(p, p1); + fts5MultiIterFree(p1); pData = fts5IdxMalloc(p, sizeof(Fts5Data) + doclist.n); if( pData ){ @@ -176862,13 +189001,13 @@ static int sqlite3Fts5IndexBeginWrite(Fts5Index *p, int bDelete, i64 iRowid){ /* Allocate the hash table if it has not already been allocated */ if( p->pHash==0 ){ - p->rc = sqlite3Fts5HashNew(&p->pHash, &p->nPendingData); + p->rc = sqlite3Fts5HashNew(p->pConfig, &p->pHash, &p->nPendingData); } /* Flush the hash table to disk if required */ if( iRowidiWriteRowid || (iRowid==p->iWriteRowid && p->bDelete==0) - || (p->nPendingData > p->nMaxPendingData) + || (p->nPendingData > p->pConfig->nHashSize) ){ fts5IndexFlush(p); } @@ -176897,7 +189036,8 @@ static int sqlite3Fts5IndexSync(Fts5Index *p, int bCommit){ static int sqlite3Fts5IndexRollback(Fts5Index *p){ fts5CloseReader(p); fts5IndexDiscardData(p); - assert( p->rc==SQLITE_OK ); + fts5StructureInvalidate(p); + /* assert( p->rc==SQLITE_OK ); */ return SQLITE_OK; } @@ -176908,6 +189048,7 @@ static int sqlite3Fts5IndexRollback(Fts5Index *p){ */ static int sqlite3Fts5IndexReinit(Fts5Index *p){ Fts5Structure s; + fts5StructureInvalidate(p); memset(&s, 0, sizeof(Fts5Structure)); fts5DataWrite(p, FTS5_AVERAGES_ROWID, (const u8*)"", 0); fts5StructureWrite(p, &s); @@ -176934,7 +189075,6 @@ static int sqlite3Fts5IndexOpen( if( rc==SQLITE_OK ){ p->pConfig = pConfig; p->nWorkUnit = FTS5_WORK_UNIT; - p->nMaxPendingData = 1024*1024; p->zDataTbl = sqlite3Fts5Mprintf(&rc, "%s_data", pConfig->zName); if( p->zDataTbl && bCreate ){ rc = sqlite3Fts5CreateTable( @@ -176967,11 +189107,13 @@ static int sqlite3Fts5IndexClose(Fts5Index *p){ int rc = SQLITE_OK; if( p ){ assert( p->pReader==0 ); + fts5StructureInvalidate(p); sqlite3_finalize(p->pWriter); sqlite3_finalize(p->pDeleter); sqlite3_finalize(p->pIdxWriter); sqlite3_finalize(p->pIdxDeleter); sqlite3_finalize(p->pIdxSelect); + sqlite3_finalize(p->pDataVersion); sqlite3Fts5HashFree(p->pHash); sqlite3_free(p->zDataTbl); sqlite3_free(p); @@ -176984,7 +189126,11 @@ static int sqlite3Fts5IndexClose(Fts5Index *p){ ** size. Return the number of bytes in the nChar character prefix of the ** buffer, or 0 if there are less than nChar characters in total. */ -static int fts5IndexCharlenToBytelen(const char *p, int nByte, int nChar){ +static int sqlite3Fts5IndexCharlenToBytelen( + const char *p, + int nByte, + int nChar +){ int n = 0; int i; for(i=0; inPrefix && rc==SQLITE_OK; i++){ - int nByte = fts5IndexCharlenToBytelen(pToken, nToken, pConfig->aPrefix[i]); + const int nChar = pConfig->aPrefix[i]; + int nByte = sqlite3Fts5IndexCharlenToBytelen(pToken, nToken, nChar); if( nByte ){ rc = sqlite3Fts5HashWrite(p->pHash, - p->iWriteRowid, iCol, iPos, FTS5_MAIN_PREFIX+i+1, pToken, nByte + p->iWriteRowid, iCol, iPos, (char)(FTS5_MAIN_PREFIX+i+1), pToken, + nByte ); } } @@ -177064,24 +189212,27 @@ static int sqlite3Fts5IndexQuery( Fts5IndexIter **ppIter /* OUT: New iterator object */ ){ Fts5Config *pConfig = p->pConfig; - Fts5IndexIter *pRet = 0; - int iIdx = 0; + Fts5Iter *pRet = 0; Fts5Buffer buf = {0, 0, 0}; /* If the QUERY_SCAN flag is set, all other flags must be clear. */ - assert( (flags & FTS5INDEX_QUERY_SCAN)==0 - || (flags & FTS5INDEX_QUERY_SCAN)==FTS5INDEX_QUERY_SCAN - ); + assert( (flags & FTS5INDEX_QUERY_SCAN)==0 || flags==FTS5INDEX_QUERY_SCAN ); - if( sqlite3Fts5BufferGrow(&p->rc, &buf, nToken+1)==0 ){ + if( sqlite3Fts5BufferSize(&p->rc, &buf, nToken+1)==0 ){ + int iIdx = 0; /* Index to search */ memcpy(&buf.p[1], pToken, nToken); -#ifdef SQLITE_DEBUG - /* If the QUERY_TEST_NOIDX flag was specified, then this must be a + /* Figure out which index to search and set iIdx accordingly. If this + ** is a prefix query for which there is no prefix index, set iIdx to + ** greater than pConfig->nPrefix to indicate that the query will be + ** satisfied by scanning multiple terms in the main index. + ** + ** If the QUERY_TEST_NOIDX flag was specified, then this must be a ** prefix-query. Instead of using a prefix-index (if one exists), ** evaluate the prefix query using the main FTS index. This is used ** for internal sanity checking by the integrity-check in debug ** mode only. */ +#ifdef SQLITE_DEBUG if( pConfig->bPrefixIndex==0 || (flags & FTS5INDEX_QUERY_TEST_NOIDX) ){ assert( flags & FTS5INDEX_QUERY_PREFIX ); iIdx = 1+pConfig->nPrefix; @@ -177095,24 +189246,35 @@ static int sqlite3Fts5IndexQuery( } if( iIdx<=pConfig->nPrefix ){ + /* Straight index lookup */ Fts5Structure *pStruct = fts5StructureRead(p); - buf.p[0] = FTS5_MAIN_PREFIX + iIdx; + buf.p[0] = (u8)(FTS5_MAIN_PREFIX + iIdx); if( pStruct ){ - fts5MultiIterNew(p, pStruct, 1, flags, buf.p, nToken+1, -1, 0, &pRet); + fts5MultiIterNew(p, pStruct, flags | FTS5INDEX_QUERY_SKIPEMPTY, + pColset, buf.p, nToken+1, -1, 0, &pRet + ); fts5StructureRelease(pStruct); } }else{ + /* Scan multiple terms in the main index */ int bDesc = (flags & FTS5INDEX_QUERY_DESC)!=0; buf.p[0] = FTS5_MAIN_PREFIX; fts5SetupPrefixIter(p, bDesc, buf.p, nToken+1, pColset, &pRet); + assert( p->rc!=SQLITE_OK || pRet->pColset==0 ); + fts5IterSetOutputCb(&p->rc, pRet); + if( p->rc==SQLITE_OK ){ + Fts5SegIter *pSeg = &pRet->aSeg[pRet->aFirst[1].iFirst]; + if( pSeg->pLeaf ) pRet->xSetOutputs(pRet, pSeg); + } } if( p->rc ){ - sqlite3Fts5IterClose(pRet); + sqlite3Fts5IterClose(&pRet->base); pRet = 0; fts5CloseReader(p); } - *ppIter = pRet; + + *ppIter = &pRet->base; sqlite3Fts5BufferFree(&buf); } return fts5IndexReturn(p); @@ -177121,15 +189283,11 @@ static int sqlite3Fts5IndexQuery( /* ** Return true if the iterator passed as the only argument is at EOF. */ -static int sqlite3Fts5IterEof(Fts5IndexIter *pIter){ - assert( pIter->pIndex->rc==SQLITE_OK ); - return pIter->bEof; -} - /* ** Move to the next matching rowid. */ -static int sqlite3Fts5IterNext(Fts5IndexIter *pIter){ +static int sqlite3Fts5IterNext(Fts5IndexIter *pIndexIter){ + Fts5Iter *pIter = (Fts5Iter*)pIndexIter; assert( pIter->pIndex->rc==SQLITE_OK ); fts5MultiIterNext(pIter->pIndex, pIter, 0, 0); return fts5IndexReturn(pIter->pIndex); @@ -177138,7 +189296,8 @@ static int sqlite3Fts5IterNext(Fts5IndexIter *pIter){ /* ** Move to the next matching term/rowid. Used by the fts5vocab module. */ -static int sqlite3Fts5IterNextScan(Fts5IndexIter *pIter){ +static int sqlite3Fts5IterNextScan(Fts5IndexIter *pIndexIter){ + Fts5Iter *pIter = (Fts5Iter*)pIndexIter; Fts5Index *p = pIter->pIndex; assert( pIter->pIndex->rc==SQLITE_OK ); @@ -177149,7 +189308,7 @@ static int sqlite3Fts5IterNextScan(Fts5IndexIter *pIter){ if( pSeg->pLeaf && pSeg->term.p[0]!=FTS5_MAIN_PREFIX ){ fts5DataRelease(pSeg->pLeaf); pSeg->pLeaf = 0; - pIter->bEof = 1; + pIter->base.bEof = 1; } } @@ -177161,111 +189320,30 @@ static int sqlite3Fts5IterNextScan(Fts5IndexIter *pIter){ ** definition of "at or after" depends on whether this iterator iterates ** in ascending or descending rowid order. */ -static int sqlite3Fts5IterNextFrom(Fts5IndexIter *pIter, i64 iMatch){ +static int sqlite3Fts5IterNextFrom(Fts5IndexIter *pIndexIter, i64 iMatch){ + Fts5Iter *pIter = (Fts5Iter*)pIndexIter; fts5MultiIterNextFrom(pIter->pIndex, pIter, iMatch); return fts5IndexReturn(pIter->pIndex); } -/* -** Return the current rowid. -*/ -static i64 sqlite3Fts5IterRowid(Fts5IndexIter *pIter){ - return fts5MultiIterRowid(pIter); -} - /* ** Return the current term. */ -static const char *sqlite3Fts5IterTerm(Fts5IndexIter *pIter, int *pn){ +static const char *sqlite3Fts5IterTerm(Fts5IndexIter *pIndexIter, int *pn){ int n; - const char *z = (const char*)fts5MultiIterTerm(pIter, &n); + const char *z = (const char*)fts5MultiIterTerm((Fts5Iter*)pIndexIter, &n); *pn = n-1; return &z[1]; } - -static int fts5IndexExtractColset ( - Fts5Colset *pColset, /* Colset to filter on */ - const u8 *pPos, int nPos, /* Position list */ - Fts5Buffer *pBuf /* Output buffer */ -){ - int rc = SQLITE_OK; - int i; - - fts5BufferZero(pBuf); - for(i=0; inCol; i++){ - const u8 *pSub = pPos; - int nSub = fts5IndexExtractCol(&pSub, nPos, pColset->aiCol[i]); - if( nSub ){ - fts5BufferAppendBlob(&rc, pBuf, nSub, pSub); - } - } - return rc; -} - - -/* -** Return a pointer to a buffer containing a copy of the position list for -** the current entry. Output variable *pn is set to the size of the buffer -** in bytes before returning. -** -** The returned position list does not include the "number of bytes" varint -** field that starts the position list on disk. -*/ -static int sqlite3Fts5IterPoslist( - Fts5IndexIter *pIter, - Fts5Colset *pColset, /* Column filter (or NULL) */ - const u8 **pp, /* OUT: Pointer to position-list data */ - int *pn, /* OUT: Size of position-list in bytes */ - i64 *piRowid /* OUT: Current rowid */ -){ - Fts5SegIter *pSeg = &pIter->aSeg[ pIter->aFirst[1].iFirst ]; - assert( pIter->pIndex->rc==SQLITE_OK ); - *piRowid = pSeg->iRowid; - if( pSeg->iLeafOffset+pSeg->nPos<=pSeg->pLeaf->szLeaf ){ - u8 *pPos = &pSeg->pLeaf->p[pSeg->iLeafOffset]; - if( pColset==0 || pIter->bFiltered ){ - *pn = pSeg->nPos; - *pp = pPos; - }else if( pColset->nCol==1 ){ - *pp = pPos; - *pn = fts5IndexExtractCol(pp, pSeg->nPos, pColset->aiCol[0]); - }else{ - fts5BufferZero(&pIter->poslist); - fts5IndexExtractColset(pColset, pPos, pSeg->nPos, &pIter->poslist); - *pp = pIter->poslist.p; - *pn = pIter->poslist.n; - } - }else{ - fts5BufferZero(&pIter->poslist); - fts5SegiterPoslist(pIter->pIndex, pSeg, pColset, &pIter->poslist); - *pp = pIter->poslist.p; - *pn = pIter->poslist.n; - } - return fts5IndexReturn(pIter->pIndex); -} - -/* -** This function is similar to sqlite3Fts5IterPoslist(), except that it -** copies the position list into the buffer supplied as the second -** argument. -*/ -static int sqlite3Fts5IterPoslistBuffer(Fts5IndexIter *pIter, Fts5Buffer *pBuf){ - Fts5Index *p = pIter->pIndex; - Fts5SegIter *pSeg = &pIter->aSeg[ pIter->aFirst[1].iFirst ]; - assert( p->rc==SQLITE_OK ); - fts5BufferZero(pBuf); - fts5SegiterPoslist(p, pSeg, 0, pBuf); - return fts5IndexReturn(p); -} - /* ** Close an iterator opened by an earlier call to sqlite3Fts5IndexQuery(). */ -static void sqlite3Fts5IterClose(Fts5IndexIter *pIter){ - if( pIter ){ +static void sqlite3Fts5IterClose(Fts5IndexIter *pIndexIter){ + if( pIndexIter ){ + Fts5Iter *pIter = (Fts5Iter*)pIndexIter; Fts5Index *pIndex = pIter->pIndex; - fts5MultiIterFree(pIter->pIndex, pIter); + fts5MultiIterFree(pIter); fts5CloseReader(pIndex); } } @@ -177358,7 +189436,7 @@ static int sqlite3Fts5IndexLoadConfig(Fts5Index *p){ /* ** Return a simple checksum value based on the arguments. */ -static u64 fts5IndexEntryCksum( +static u64 sqlite3Fts5IndexEntryCksum( i64 iRowid, int iCol, int iPos, @@ -177428,30 +189506,32 @@ static int fts5QueryCksum( int flags, /* Flags for Fts5IndexQuery */ u64 *pCksum /* IN/OUT: Checksum value */ ){ + int eDetail = p->pConfig->eDetail; u64 cksum = *pCksum; - Fts5IndexIter *pIdxIter = 0; - int rc = sqlite3Fts5IndexQuery(p, z, n, flags, 0, &pIdxIter); + Fts5IndexIter *pIter = 0; + int rc = sqlite3Fts5IndexQuery(p, z, n, flags, 0, &pIter); - while( rc==SQLITE_OK && 0==sqlite3Fts5IterEof(pIdxIter) ){ - i64 dummy; - const u8 *pPos; - int nPos; - i64 rowid = sqlite3Fts5IterRowid(pIdxIter); - rc = sqlite3Fts5IterPoslist(pIdxIter, 0, &pPos, &nPos, &dummy); - if( rc==SQLITE_OK ){ + while( rc==SQLITE_OK && 0==sqlite3Fts5IterEof(pIter) ){ + i64 rowid = pIter->iRowid; + + if( eDetail==FTS5_DETAIL_NONE ){ + cksum ^= sqlite3Fts5IndexEntryCksum(rowid, 0, 0, iIdx, z, n); + }else{ Fts5PoslistReader sReader; - for(sqlite3Fts5PoslistReaderInit(pPos, nPos, &sReader); + for(sqlite3Fts5PoslistReaderInit(pIter->pData, pIter->nData, &sReader); sReader.bEof==0; sqlite3Fts5PoslistReaderNext(&sReader) ){ int iCol = FTS5_POS2COLUMN(sReader.iPos); int iOff = FTS5_POS2OFFSET(sReader.iPos); - cksum ^= fts5IndexEntryCksum(rowid, iCol, iOff, iIdx, z, n); + cksum ^= sqlite3Fts5IndexEntryCksum(rowid, iCol, iOff, iIdx, z, n); } - rc = sqlite3Fts5IterNext(pIdxIter); + } + if( rc==SQLITE_OK ){ + rc = sqlite3Fts5IterNext(pIter); } } - sqlite3Fts5IterClose(pIdxIter); + sqlite3Fts5IterClose(pIter); *pCksum = cksum; return rc; @@ -177670,7 +189750,6 @@ static void fts5IndexIntegrityCheckSegment( fts5DataRelease(pLeaf); if( p->rc ) break; - /* Now check that the iter.nEmpty leaves following the current leaf ** (a) exist and (b) contain no terms. */ fts5IndexIntegrityCheckEmpty( @@ -177746,7 +189825,7 @@ static void fts5IndexIntegrityCheckSegment( /* ** Run internal checks to ensure that the FTS index (a) is internally ** consistent and (b) contains entries for which the XOR of the checksums -** as calculated by fts5IndexEntryCksum() is cksum. +** as calculated by sqlite3Fts5IndexEntryCksum() is cksum. ** ** Return SQLITE_CORRUPT if any of the internal checks fail, or if the ** checksum does not match. Return SQLITE_OK if all checks pass without @@ -177754,14 +189833,18 @@ static void fts5IndexIntegrityCheckSegment( ** occurs. */ static int sqlite3Fts5IndexIntegrityCheck(Fts5Index *p, u64 cksum){ + int eDetail = p->pConfig->eDetail; u64 cksum2 = 0; /* Checksum based on contents of indexes */ Fts5Buffer poslist = {0,0,0}; /* Buffer used to hold a poslist */ - Fts5IndexIter *pIter; /* Used to iterate through entire index */ + Fts5Iter *pIter; /* Used to iterate through entire index */ Fts5Structure *pStruct; /* Index structure */ +#ifdef SQLITE_DEBUG /* Used by extra internal tests only run if NDEBUG is not defined */ u64 cksum3 = 0; /* Checksum based on contents of indexes */ Fts5Buffer term = {0,0,0}; /* Buffer used to hold most recent term */ +#endif + const int flags = FTS5INDEX_QUERY_NOOUTPUT; /* Load the FTS index structure */ pStruct = fts5StructureRead(p); @@ -177790,7 +189873,7 @@ static int sqlite3Fts5IndexIntegrityCheck(Fts5Index *p, u64 cksum){ ** same term is performed. cksum3 is calculated based on the entries ** extracted by these queries. */ - for(fts5MultiIterNew(p, pStruct, 0, 0, 0, 0, -1, 0, &pIter); + for(fts5MultiIterNew(p, pStruct, flags, 0, 0, 0, -1, 0, &pIter); fts5MultiIterEof(p, pIter)==0; fts5MultiIterNext(p, pIter, 0, 0) ){ @@ -177803,53 +189886,33 @@ static int sqlite3Fts5IndexIntegrityCheck(Fts5Index *p, u64 cksum){ /* If this is a new term, query for it. Update cksum3 with the results. */ fts5TestTerm(p, &term, z, n, cksum2, &cksum3); - poslist.n = 0; - fts5SegiterPoslist(p, &pIter->aSeg[pIter->aFirst[1].iFirst] , 0, &poslist); - while( 0==sqlite3Fts5PoslistNext64(poslist.p, poslist.n, &iOff, &iPos) ){ - int iCol = FTS5_POS2COLUMN(iPos); - int iTokOff = FTS5_POS2OFFSET(iPos); - cksum2 ^= fts5IndexEntryCksum(iRowid, iCol, iTokOff, -1, z, n); + if( eDetail==FTS5_DETAIL_NONE ){ + if( 0==fts5MultiIterIsEmpty(p, pIter) ){ + cksum2 ^= sqlite3Fts5IndexEntryCksum(iRowid, 0, 0, -1, z, n); + } + }else{ + poslist.n = 0; + fts5SegiterPoslist(p, &pIter->aSeg[pIter->aFirst[1].iFirst], 0, &poslist); + while( 0==sqlite3Fts5PoslistNext64(poslist.p, poslist.n, &iOff, &iPos) ){ + int iCol = FTS5_POS2COLUMN(iPos); + int iTokOff = FTS5_POS2OFFSET(iPos); + cksum2 ^= sqlite3Fts5IndexEntryCksum(iRowid, iCol, iTokOff, -1, z, n); + } } } fts5TestTerm(p, &term, 0, 0, cksum2, &cksum3); - fts5MultiIterFree(p, pIter); + fts5MultiIterFree(pIter); if( p->rc==SQLITE_OK && cksum!=cksum2 ) p->rc = FTS5_CORRUPT; fts5StructureRelease(pStruct); +#ifdef SQLITE_DEBUG fts5BufferFree(&term); +#endif fts5BufferFree(&poslist); return fts5IndexReturn(p); } - -/* -** Calculate and return a checksum that is the XOR of the index entry -** checksum of all entries that would be generated by the token specified -** by the final 5 arguments. -*/ -static u64 sqlite3Fts5IndexCksum( - Fts5Config *pConfig, /* Configuration object */ - i64 iRowid, /* Document term appears in */ - int iCol, /* Column term appears in */ - int iPos, /* Position term appears in */ - const char *pTerm, int nTerm /* Term at iPos */ -){ - u64 ret = 0; /* Return value */ - int iIdx; /* For iterating through indexes */ - - ret = fts5IndexEntryCksum(iRowid, iCol, iPos, 0, pTerm, nTerm); - - for(iIdx=0; iIdxnPrefix; iIdx++){ - int nByte = fts5IndexCharlenToBytelen(pTerm, nTerm, pConfig->aPrefix[iIdx]); - if( nByte ){ - ret ^= fts5IndexEntryCksum(iRowid, iCol, iPos, iIdx+1, pTerm, nByte); - } - } - - return ret; -} - /************************************************************************* ************************************************************************** ** Below this point is the implementation of the fts5_decode() scalar @@ -178002,8 +190065,9 @@ static int fts5DecodeDoclist(int *pRc, Fts5Buffer *pBuf, const u8 *a, int n){ } while( iOffpStruct==0 || p->iStructVersion!=0 ); + if( fts5IndexDataVersion(p)!=p->iStructVersion ){ + fts5StructureInvalidate(p); + } + return fts5IndexReturn(p); +} + /* ** 2014 Jun 09 ** @@ -178224,6 +190395,7 @@ static int sqlite3Fts5IndexInit(sqlite3 *db){ */ +/* #include "fts5Int.h" */ /* ** This variable is set to false when running tests for which the on disk @@ -178429,12 +190601,13 @@ struct Fts5Cursor { /* ** Values for Fts5Cursor.csrflags */ -#define FTS5CSR_REQUIRE_CONTENT 0x01 -#define FTS5CSR_REQUIRE_DOCSIZE 0x02 -#define FTS5CSR_REQUIRE_INST 0x04 -#define FTS5CSR_EOF 0x08 +#define FTS5CSR_EOF 0x01 +#define FTS5CSR_REQUIRE_CONTENT 0x02 +#define FTS5CSR_REQUIRE_DOCSIZE 0x04 +#define FTS5CSR_REQUIRE_INST 0x08 #define FTS5CSR_FREE_ZRANK 0x10 #define FTS5CSR_REQUIRE_RESEEK 0x20 +#define FTS5CSR_REQUIRE_POSLIST 0x40 #define BitFlagAllTest(x,y) (((x) & (y))==(y)) #define BitFlagTest(x,y) (((x) & (y))!=0) @@ -178604,6 +190777,15 @@ static int fts5InitVtab( rc = sqlite3Fts5ConfigDeclareVtab(pConfig); } + /* Load the initial configuration */ + if( rc==SQLITE_OK ){ + assert( pConfig->pzErrmsg==0 ); + pConfig->pzErrmsg = pzErr; + rc = sqlite3Fts5IndexLoadConfig(pTab->pIndex); + sqlite3Fts5IndexRollback(pTab->pIndex); + pConfig->pzErrmsg = 0; + } + if( rc!=SQLITE_OK ){ fts5FreeVtab(pTab); pTab = 0; @@ -178656,7 +190838,10 @@ static int fts5CreateMethod( */ static void fts5SetUniqueFlag(sqlite3_index_info *pIdxInfo){ #if SQLITE_VERSION_NUMBER>=3008012 - if( sqlite3_libversion_number()>=3008012 ){ +#ifndef SQLITE_CORE + if( sqlite3_libversion_number()>=3008012 ) +#endif + { pIdxInfo->idxFlags |= SQLITE_INDEX_SCAN_UNIQUE; } #endif @@ -178734,7 +190919,7 @@ static int fts5BestIndexMethod(sqlite3_vtab *pVTab, sqlite3_index_info *pInfo){ for(i=0; inConstraint; i++){ struct sqlite3_index_constraint *p = &pInfo->aConstraint[i]; int j; - for(j=0; jiColumn==aColMap[pC->iCol] && p->op & pC->op ){ if( p->usable ){ @@ -178781,11 +190966,11 @@ static int fts5BestIndexMethod(sqlite3_vtab *pVTab, sqlite3_index_info *pInfo){ /* Assign argvIndex values to each constraint in use. */ iNext = 1; - for(i=0; iiConsIndex>=0 ){ pInfo->aConstraintUsage[pC->iConsIndex].argvIndex = iNext++; - pInfo->aConstraintUsage[pC->iConsIndex].omit = pC->omit; + pInfo->aConstraintUsage[pC->iConsIndex].omit = (unsigned char)pC->omit; } } @@ -178793,27 +190978,38 @@ static int fts5BestIndexMethod(sqlite3_vtab *pVTab, sqlite3_index_info *pInfo){ return SQLITE_OK; } +static int fts5NewTransaction(Fts5Table *pTab){ + Fts5Cursor *pCsr; + for(pCsr=pTab->pGlobal->pCsr; pCsr; pCsr=pCsr->pNext){ + if( pCsr->base.pVtab==(sqlite3_vtab*)pTab ) return SQLITE_OK; + } + return sqlite3Fts5StorageReset(pTab->pStorage); +} + /* ** Implementation of xOpen method. */ static int fts5OpenMethod(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCsr){ Fts5Table *pTab = (Fts5Table*)pVTab; Fts5Config *pConfig = pTab->pConfig; - Fts5Cursor *pCsr; /* New cursor object */ + Fts5Cursor *pCsr = 0; /* New cursor object */ int nByte; /* Bytes of space to allocate */ - int rc = SQLITE_OK; /* Return code */ + int rc; /* Return code */ - nByte = sizeof(Fts5Cursor) + pConfig->nCol * sizeof(int); - pCsr = (Fts5Cursor*)sqlite3_malloc(nByte); - if( pCsr ){ - Fts5Global *pGlobal = pTab->pGlobal; - memset(pCsr, 0, nByte); - pCsr->aColumnSize = (int*)&pCsr[1]; - pCsr->pNext = pGlobal->pCsr; - pGlobal->pCsr = pCsr; - pCsr->iCsrId = ++pGlobal->iNextId; - }else{ - rc = SQLITE_NOMEM; + rc = fts5NewTransaction(pTab); + if( rc==SQLITE_OK ){ + nByte = sizeof(Fts5Cursor) + pConfig->nCol * sizeof(int); + pCsr = (Fts5Cursor*)sqlite3_malloc(nByte); + if( pCsr ){ + Fts5Global *pGlobal = pTab->pGlobal; + memset(pCsr, 0, nByte); + pCsr->aColumnSize = (int*)&pCsr[1]; + pCsr->pNext = pGlobal->pCsr; + pGlobal->pCsr = pCsr; + pCsr->iCsrId = ++pGlobal->iNextId; + }else{ + rc = SQLITE_NOMEM; + } } *ppCsr = (sqlite3_vtab_cursor*)pCsr; return rc; @@ -178836,6 +191032,7 @@ static void fts5CsrNewrow(Fts5Cursor *pCsr){ FTS5CSR_REQUIRE_CONTENT | FTS5CSR_REQUIRE_DOCSIZE | FTS5CSR_REQUIRE_INST + | FTS5CSR_REQUIRE_POSLIST ); } @@ -178918,15 +191115,18 @@ static int fts5SorterNext(Fts5Cursor *pCsr){ nBlob = sqlite3_column_bytes(pSorter->pStmt, 1); aBlob = a = sqlite3_column_blob(pSorter->pStmt, 1); - for(i=0; i<(pSorter->nIdx-1); i++){ - int iVal; - a += fts5GetVarint32(a, iVal); - iOff += iVal; - pSorter->aIdx[i] = iOff; + /* nBlob==0 in detail=none mode. */ + if( nBlob>0 ){ + for(i=0; i<(pSorter->nIdx-1); i++){ + int iVal; + a += fts5GetVarint32(a, iVal); + iOff += iVal; + pSorter->aIdx[i] = iOff; + } + pSorter->aIdx[i] = &aBlob[nBlob] - a; + pSorter->aPoslist = a; } - pSorter->aIdx[i] = &aBlob[nBlob] - a; - pSorter->aPoslist = a; fts5CsrNewrow(pCsr); } @@ -178970,7 +191170,7 @@ static int fts5CursorReseek(Fts5Cursor *pCsr, int *pbSkip){ i64 iRowid = sqlite3Fts5ExprRowid(pCsr->pExpr); rc = sqlite3Fts5ExprFirst(pCsr->pExpr, pTab->pIndex, iRowid, bDesc); - if( rc==SQLITE_OK && iRowid!=sqlite3Fts5ExprRowid(pCsr->pExpr) ){ + if( rc==SQLITE_OK && iRowid!=sqlite3Fts5ExprRowid(pCsr->pExpr) ){ *pbSkip = 1; } @@ -178978,6 +191178,7 @@ static int fts5CursorReseek(Fts5Cursor *pCsr, int *pbSkip){ fts5CsrNewrow(pCsr); if( sqlite3Fts5ExprEof(pCsr->pExpr) ){ CsrFlagSet(pCsr, FTS5CSR_EOF); + *pbSkip = 1; } } return rc; @@ -178994,24 +191195,24 @@ static int fts5CursorReseek(Fts5Cursor *pCsr, int *pbSkip){ */ static int fts5NextMethod(sqlite3_vtab_cursor *pCursor){ Fts5Cursor *pCsr = (Fts5Cursor*)pCursor; - int rc = SQLITE_OK; + int rc; assert( (pCsr->ePlan<3)== (pCsr->ePlan==FTS5_PLAN_MATCH || pCsr->ePlan==FTS5_PLAN_SOURCE) ); + assert( !CsrFlagTest(pCsr, FTS5CSR_EOF) ); if( pCsr->ePlan<3 ){ int bSkip = 0; if( (rc = fts5CursorReseek(pCsr, &bSkip)) || bSkip ) return rc; rc = sqlite3Fts5ExprNext(pCsr->pExpr, pCsr->iLastRowid); - if( sqlite3Fts5ExprEof(pCsr->pExpr) ){ - CsrFlagSet(pCsr, FTS5CSR_EOF); - } + CsrFlagSet(pCsr, sqlite3Fts5ExprEof(pCsr->pExpr)); fts5CsrNewrow(pCsr); }else{ switch( pCsr->ePlan ){ case FTS5_PLAN_SPECIAL: { CsrFlagSet(pCsr, FTS5CSR_EOF); + rc = SQLITE_OK; break; } @@ -179035,13 +191236,41 @@ static int fts5NextMethod(sqlite3_vtab_cursor *pCursor){ return rc; } + +static int fts5PrepareStatement( + sqlite3_stmt **ppStmt, + Fts5Config *pConfig, + const char *zFmt, + ... +){ + sqlite3_stmt *pRet = 0; + int rc; + char *zSql; + va_list ap; + + va_start(ap, zFmt); + zSql = sqlite3_vmprintf(zFmt, ap); + if( zSql==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = sqlite3_prepare_v2(pConfig->db, zSql, -1, &pRet, 0); + if( rc!=SQLITE_OK ){ + *pConfig->pzErrmsg = sqlite3_mprintf("%s", sqlite3_errmsg(pConfig->db)); + } + sqlite3_free(zSql); + } + + va_end(ap); + *ppStmt = pRet; + return rc; +} + static int fts5CursorFirstSorted(Fts5Table *pTab, Fts5Cursor *pCsr, int bDesc){ Fts5Config *pConfig = pTab->pConfig; Fts5Sorter *pSorter; int nPhrase; int nByte; - int rc = SQLITE_OK; - char *zSql; + int rc; const char *zRank = pCsr->zRank; const char *zRankArgs = pCsr->zRankArgs; @@ -179059,17 +191288,13 @@ static int fts5CursorFirstSorted(Fts5Table *pTab, Fts5Cursor *pCsr, int bDesc){ ** table, saving it creates a circular reference. ** ** If SQLite a built-in statement cache, this wouldn't be a problem. */ - zSql = sqlite3Fts5Mprintf(&rc, + rc = fts5PrepareStatement(&pSorter->pStmt, pConfig, "SELECT rowid, rank FROM %Q.%Q ORDER BY %s(%s%s%s) %s", pConfig->zDb, pConfig->zName, zRank, pConfig->zName, (zRankArgs ? ", " : ""), (zRankArgs ? zRankArgs : ""), bDesc ? "DESC" : "ASC" ); - if( zSql ){ - rc = sqlite3_prepare_v2(pConfig->db, zSql, -1, &pSorter->pStmt, 0); - sqlite3_free(zSql); - } pCsr->pSorter = pSorter; if( rc==SQLITE_OK ){ @@ -179263,7 +191488,7 @@ static i64 fts5GetRowidLimit(sqlite3_value *pVal, i64 iDefault){ static int fts5FilterMethod( sqlite3_vtab_cursor *pCursor, /* The cursor used for this query */ int idxNum, /* Strategy index */ - const char *idxStr, /* Unused */ + const char *zUnused, /* Unused */ int nVal, /* Number of elements in apVal */ sqlite3_value **apVal /* Arguments for the indexing scheme */ ){ @@ -179281,6 +191506,9 @@ static int fts5FilterMethod( sqlite3_value *pRowidGe = 0; /* rowid >= ? expression (or NULL) */ char **pzErrmsg = pConfig->pzErrmsg; + UNUSED_PARAM(zUnused); + UNUSED_PARAM(nVal); + if( pCsr->ePlan ){ fts5FreeCursorComponents(pCsr); memset(&pCsr->ePlan, 0, sizeof(Fts5Cursor) - ((u8*)&pCsr->ePlan-(u8*)pCsr)); @@ -179339,6 +191567,7 @@ static int fts5FilterMethod( pCsr->ePlan = FTS5_PLAN_SOURCE; pCsr->pExpr = pTab->pSortCsr->pExpr; rc = fts5CursorFirst(pTab, pCsr, bDesc); + sqlite3Fts5ExprClearEof(pCsr->pExpr); }else if( pMatch ){ const char *zExpr = (const char*)sqlite3_value_text(apVal[0]); if( zExpr==0 ) zExpr = ""; @@ -179564,14 +191793,13 @@ static int fts5SpecialInsert( static int fts5SpecialDelete( Fts5Table *pTab, - sqlite3_value **apVal, - sqlite3_int64 *piRowid + sqlite3_value **apVal ){ int rc = SQLITE_OK; int eType1 = sqlite3_value_type(apVal[1]); if( eType1==SQLITE_INTEGER ){ sqlite3_int64 iDel = sqlite3_value_int64(apVal[1]); - rc = sqlite3Fts5StorageSpecialDelete(pTab->pStorage, iDel, &apVal[2]); + rc = sqlite3Fts5StorageDelete(pTab->pStorage, iDel, &apVal[2]); } return rc; } @@ -179641,7 +191869,7 @@ static int fts5UpdateMethod( if( pConfig->eContent!=FTS5_CONTENT_NORMAL && 0==sqlite3_stricmp("delete", z) ){ - rc = fts5SpecialDelete(pTab, apVal, pRowid); + rc = fts5SpecialDelete(pTab, apVal); }else{ rc = fts5SpecialInsert(pTab, z, apVal[2 + pConfig->nCol + 1]); } @@ -179657,7 +191885,10 @@ static int fts5UpdateMethod( ** ** Cases 3 and 4 may violate the rowid constraint. */ - int eConflict = sqlite3_vtab_on_conflict(pConfig->db); + int eConflict = SQLITE_ABORT; + if( pConfig->eContent==FTS5_CONTENT_NORMAL ){ + eConflict = sqlite3_vtab_on_conflict(pConfig->db); + } assert( eType0==SQLITE_INTEGER || eType0==SQLITE_NULL ); assert( nArg!=1 || eType0==SQLITE_INTEGER ); @@ -179672,46 +191903,46 @@ static int fts5UpdateMethod( rc = SQLITE_ERROR; } - /* Case 1: DELETE */ + /* DELETE */ else if( nArg==1 ){ i64 iDel = sqlite3_value_int64(apVal[0]); /* Rowid to delete */ - rc = sqlite3Fts5StorageDelete(pTab->pStorage, iDel); + rc = sqlite3Fts5StorageDelete(pTab->pStorage, iDel, 0); } - /* Case 2: INSERT */ + /* INSERT */ else if( eType0!=SQLITE_INTEGER ){ /* If this is a REPLACE, first remove the current entry (if any) */ if( eConflict==SQLITE_REPLACE && sqlite3_value_type(apVal[1])==SQLITE_INTEGER ){ i64 iNew = sqlite3_value_int64(apVal[1]); /* Rowid to delete */ - rc = sqlite3Fts5StorageDelete(pTab->pStorage, iNew); + rc = sqlite3Fts5StorageDelete(pTab->pStorage, iNew, 0); } fts5StorageInsert(&rc, pTab, apVal, pRowid); } - /* Case 2: UPDATE */ + /* UPDATE */ else{ i64 iOld = sqlite3_value_int64(apVal[0]); /* Old rowid */ i64 iNew = sqlite3_value_int64(apVal[1]); /* New rowid */ if( iOld!=iNew ){ if( eConflict==SQLITE_REPLACE ){ - rc = sqlite3Fts5StorageDelete(pTab->pStorage, iOld); + rc = sqlite3Fts5StorageDelete(pTab->pStorage, iOld, 0); if( rc==SQLITE_OK ){ - rc = sqlite3Fts5StorageDelete(pTab->pStorage, iNew); + rc = sqlite3Fts5StorageDelete(pTab->pStorage, iNew, 0); } fts5StorageInsert(&rc, pTab, apVal, pRowid); }else{ rc = sqlite3Fts5StorageContentInsert(pTab->pStorage, apVal, pRowid); if( rc==SQLITE_OK ){ - rc = sqlite3Fts5StorageDelete(pTab->pStorage, iOld); + rc = sqlite3Fts5StorageDelete(pTab->pStorage, iOld, 0); } if( rc==SQLITE_OK ){ rc = sqlite3Fts5StorageIndexInsert(pTab->pStorage, apVal, *pRowid); } } }else{ - rc = sqlite3Fts5StorageDelete(pTab->pStorage, iOld); + rc = sqlite3Fts5StorageDelete(pTab->pStorage, iOld, 0); fts5StorageInsert(&rc, pTab, apVal, pRowid); } } @@ -179740,6 +191971,7 @@ static int fts5SyncMethod(sqlite3_vtab *pVtab){ */ static int fts5BeginMethod(sqlite3_vtab *pVtab){ fts5CheckTransactionState((Fts5Table*)pVtab, FTS5_BEGIN, 0); + fts5NewTransaction((Fts5Table*)pVtab); return SQLITE_OK; } @@ -179749,6 +191981,7 @@ static int fts5BeginMethod(sqlite3_vtab *pVtab){ ** by fts5SyncMethod(). */ static int fts5CommitMethod(sqlite3_vtab *pVtab){ + UNUSED_PARAM(pVtab); /* Call below is a no-op for NDEBUG builds */ fts5CheckTransactionState((Fts5Table*)pVtab, FTS5_COMMIT, 0); return SQLITE_OK; } @@ -179765,6 +191998,8 @@ static int fts5RollbackMethod(sqlite3_vtab *pVtab){ return rc; } +static int fts5CsrPoslist(Fts5Cursor*, int, const u8**, int*); + static void *fts5ApiUserData(Fts5Context *pCtx){ Fts5Cursor *pCsr = (Fts5Cursor*)pCtx; return pCsr->pAux->pUserData; @@ -179814,17 +192049,72 @@ static int fts5ApiPhraseSize(Fts5Context *pCtx, int iPhrase){ return sqlite3Fts5ExprPhraseSize(pCsr->pExpr, iPhrase); } -static int fts5CsrPoslist(Fts5Cursor *pCsr, int iPhrase, const u8 **pa){ - int n; - if( pCsr->pSorter ){ +static int fts5ApiColumnText( + Fts5Context *pCtx, + int iCol, + const char **pz, + int *pn +){ + int rc = SQLITE_OK; + Fts5Cursor *pCsr = (Fts5Cursor*)pCtx; + if( fts5IsContentless((Fts5Table*)(pCsr->base.pVtab)) ){ + *pz = 0; + *pn = 0; + }else{ + rc = fts5SeekCursor(pCsr, 0); + if( rc==SQLITE_OK ){ + *pz = (const char*)sqlite3_column_text(pCsr->pStmt, iCol+1); + *pn = sqlite3_column_bytes(pCsr->pStmt, iCol+1); + } + } + return rc; +} + +static int fts5CsrPoslist( + Fts5Cursor *pCsr, + int iPhrase, + const u8 **pa, + int *pn +){ + Fts5Config *pConfig = ((Fts5Table*)(pCsr->base.pVtab))->pConfig; + int rc = SQLITE_OK; + int bLive = (pCsr->pSorter==0); + + if( CsrFlagTest(pCsr, FTS5CSR_REQUIRE_POSLIST) ){ + + if( pConfig->eDetail!=FTS5_DETAIL_FULL ){ + Fts5PoslistPopulator *aPopulator; + int i; + aPopulator = sqlite3Fts5ExprClearPoslists(pCsr->pExpr, bLive); + if( aPopulator==0 ) rc = SQLITE_NOMEM; + for(i=0; inCol && rc==SQLITE_OK; i++){ + int n; const char *z; + rc = fts5ApiColumnText((Fts5Context*)pCsr, i, &z, &n); + if( rc==SQLITE_OK ){ + rc = sqlite3Fts5ExprPopulatePoslists( + pConfig, pCsr->pExpr, aPopulator, i, z, n + ); + } + } + sqlite3_free(aPopulator); + + if( pCsr->pSorter ){ + sqlite3Fts5ExprCheckPoslists(pCsr->pExpr, pCsr->pSorter->iRowid); + } + } + CsrFlagClear(pCsr, FTS5CSR_REQUIRE_POSLIST); + } + + if( pCsr->pSorter && pConfig->eDetail==FTS5_DETAIL_FULL ){ Fts5Sorter *pSorter = pCsr->pSorter; int i1 = (iPhrase==0 ? 0 : pSorter->aIdx[iPhrase-1]); - n = pSorter->aIdx[iPhrase] - i1; + *pn = pSorter->aIdx[iPhrase] - i1; *pa = &pSorter->aPoslist[i1]; }else{ - n = sqlite3Fts5ExprPoslist(pCsr->pExpr, iPhrase, pa); + *pn = sqlite3Fts5ExprPoslist(pCsr->pExpr, iPhrase, pa); } - return n; + + return rc; } /* @@ -179849,43 +192139,48 @@ static int fts5CacheInstArray(Fts5Cursor *pCsr){ int i; /* Initialize all iterators */ - for(i=0; i=pCsr->nInstAlloc ){ - pCsr->nInstAlloc = pCsr->nInstAlloc ? pCsr->nInstAlloc*2 : 32; - aInst = (int*)sqlite3_realloc( - pCsr->aInst, pCsr->nInstAlloc*sizeof(int)*3 - ); - if( aInst ){ - pCsr->aInst = aInst; - }else{ - rc = SQLITE_NOMEM; - break; + nInst++; + if( nInst>=pCsr->nInstAlloc ){ + pCsr->nInstAlloc = pCsr->nInstAlloc ? pCsr->nInstAlloc*2 : 32; + aInst = (int*)sqlite3_realloc( + pCsr->aInst, pCsr->nInstAlloc*sizeof(int)*3 + ); + if( aInst ){ + pCsr->aInst = aInst; + }else{ + rc = SQLITE_NOMEM; + break; + } } - } - aInst = &pCsr->aInst[3 * (nInst-1)]; - aInst[0] = iBest; - aInst[1] = FTS5_POS2COLUMN(aIter[iBest].iPos); - aInst[2] = FTS5_POS2OFFSET(aIter[iBest].iPos); - sqlite3Fts5PoslistReaderNext(&aIter[iBest]); + aInst = &pCsr->aInst[3 * (nInst-1)]; + aInst[0] = iBest; + aInst[1] = FTS5_POS2COLUMN(aIter[iBest].iPos); + aInst[2] = FTS5_POS2OFFSET(aIter[iBest].iPos); + sqlite3Fts5PoslistReaderNext(&aIter[iBest]); + } } pCsr->nInstCount = nInst; @@ -179918,6 +192213,12 @@ static int fts5ApiInst( ){ if( iIdx<0 || iIdx>=pCsr->nInstCount ){ rc = SQLITE_RANGE; +#if 0 + }else if( fts5IsOffsetless((Fts5Table*)pCsr->base.pVtab) ){ + *piPhrase = pCsr->aInst[iIdx*3]; + *piCol = pCsr->aInst[iIdx*3 + 2]; + *piOff = -1; +#endif }else{ *piPhrase = pCsr->aInst[iIdx*3]; *piCol = pCsr->aInst[iIdx*3 + 1]; @@ -179931,36 +192232,17 @@ static sqlite3_int64 fts5ApiRowid(Fts5Context *pCtx){ return fts5CursorRowid((Fts5Cursor*)pCtx); } -static int fts5ApiColumnText( - Fts5Context *pCtx, - int iCol, - const char **pz, - int *pn -){ - int rc = SQLITE_OK; - Fts5Cursor *pCsr = (Fts5Cursor*)pCtx; - if( fts5IsContentless((Fts5Table*)(pCsr->base.pVtab)) ){ - *pz = 0; - *pn = 0; - }else{ - rc = fts5SeekCursor(pCsr, 0); - if( rc==SQLITE_OK ){ - *pz = (const char*)sqlite3_column_text(pCsr->pStmt, iCol+1); - *pn = sqlite3_column_bytes(pCsr->pStmt, iCol+1); - } - } - return rc; -} - static int fts5ColumnSizeCb( void *pContext, /* Pointer to int */ int tflags, - const char *pToken, /* Buffer containing token */ - int nToken, /* Size of token in bytes */ - int iStart, /* Start offset of token */ - int iEnd /* End offset of token */ + const char *pUnused, /* Buffer containing token */ + int nUnused, /* Size of token in bytes */ + int iUnused1, /* Start offset of token */ + int iUnused2 /* End offset of token */ ){ int *pCnt = (int*)pContext; + UNUSED_PARAM2(pUnused, nUnused); + UNUSED_PARAM2(iUnused1, iUnused2); if( (tflags & FTS5_TOKEN_COLOCATED)==0 ){ (*pCnt)++; } @@ -180076,10 +192358,11 @@ static void *fts5ApiGetAuxdata(Fts5Context *pCtx, int bClear){ } static void fts5ApiPhraseNext( - Fts5Context *pCtx, + Fts5Context *pUnused, Fts5PhraseIter *pIter, int *piCol, int *piOff ){ + UNUSED_PARAM(pUnused); if( pIter->a>=pIter->b ){ *piCol = -1; *piOff = -1; @@ -180096,20 +192379,98 @@ static void fts5ApiPhraseNext( } } -static void fts5ApiPhraseFirst( +static int fts5ApiPhraseFirst( Fts5Context *pCtx, int iPhrase, Fts5PhraseIter *pIter, int *piCol, int *piOff ){ Fts5Cursor *pCsr = (Fts5Cursor*)pCtx; - int n = fts5CsrPoslist(pCsr, iPhrase, &pIter->a); - pIter->b = &pIter->a[n]; - *piCol = 0; - *piOff = 0; - fts5ApiPhraseNext(pCtx, pIter, piCol, piOff); + int n; + int rc = fts5CsrPoslist(pCsr, iPhrase, &pIter->a, &n); + if( rc==SQLITE_OK ){ + pIter->b = &pIter->a[n]; + *piCol = 0; + *piOff = 0; + fts5ApiPhraseNext(pCtx, pIter, piCol, piOff); + } + return rc; } +static void fts5ApiPhraseNextColumn( + Fts5Context *pCtx, + Fts5PhraseIter *pIter, + int *piCol +){ + Fts5Cursor *pCsr = (Fts5Cursor*)pCtx; + Fts5Config *pConfig = ((Fts5Table*)(pCsr->base.pVtab))->pConfig; + + if( pConfig->eDetail==FTS5_DETAIL_COLUMNS ){ + if( pIter->a>=pIter->b ){ + *piCol = -1; + }else{ + int iIncr; + pIter->a += fts5GetVarint32(&pIter->a[0], iIncr); + *piCol += (iIncr-2); + } + }else{ + while( 1 ){ + int dummy; + if( pIter->a>=pIter->b ){ + *piCol = -1; + return; + } + if( pIter->a[0]==0x01 ) break; + pIter->a += fts5GetVarint32(pIter->a, dummy); + } + pIter->a += 1 + fts5GetVarint32(&pIter->a[1], *piCol); + } +} + +static int fts5ApiPhraseFirstColumn( + Fts5Context *pCtx, + int iPhrase, + Fts5PhraseIter *pIter, + int *piCol +){ + int rc = SQLITE_OK; + Fts5Cursor *pCsr = (Fts5Cursor*)pCtx; + Fts5Config *pConfig = ((Fts5Table*)(pCsr->base.pVtab))->pConfig; + + if( pConfig->eDetail==FTS5_DETAIL_COLUMNS ){ + Fts5Sorter *pSorter = pCsr->pSorter; + int n; + if( pSorter ){ + int i1 = (iPhrase==0 ? 0 : pSorter->aIdx[iPhrase-1]); + n = pSorter->aIdx[iPhrase] - i1; + pIter->a = &pSorter->aPoslist[i1]; + }else{ + rc = sqlite3Fts5ExprPhraseCollist(pCsr->pExpr, iPhrase, &pIter->a, &n); + } + if( rc==SQLITE_OK ){ + pIter->b = &pIter->a[n]; + *piCol = 0; + fts5ApiPhraseNextColumn(pCtx, pIter, piCol); + } + }else{ + int n; + rc = fts5CsrPoslist(pCsr, iPhrase, &pIter->a, &n); + if( rc==SQLITE_OK ){ + pIter->b = &pIter->a[n]; + if( n<=0 ){ + *piCol = -1; + }else if( pIter->a[0]==0x01 ){ + pIter->a += 1 + fts5GetVarint32(&pIter->a[1], *piCol); + }else{ + *piCol = 0; + } + } + } + + return rc; +} + + static int fts5ApiQueryPhrase(Fts5Context*, int, void*, int(*)(const Fts5ExtensionApi*, Fts5Context*, void*) ); @@ -180133,9 +192494,10 @@ static const Fts5ExtensionApi sFts5Api = { fts5ApiGetAuxdata, fts5ApiPhraseFirst, fts5ApiPhraseNext, + fts5ApiPhraseFirstColumn, + fts5ApiPhraseNextColumn, }; - /* ** Implementation of API function xQueryPhrase(). */ @@ -180152,12 +192514,11 @@ static int fts5ApiQueryPhrase( rc = fts5OpenMethod(pCsr->base.pVtab, (sqlite3_vtab_cursor**)&pNew); if( rc==SQLITE_OK ){ - Fts5Config *pConf = pTab->pConfig; pNew->ePlan = FTS5_PLAN_MATCH; pNew->iFirstRowid = SMALLEST_INT64; pNew->iLastRowid = LARGEST_INT64; pNew->base.pVtab = (sqlite3_vtab*)pTab; - rc = sqlite3Fts5ExprClonePhrase(pConf, pCsr->pExpr, iPhrase, &pNew->pExpr); + rc = sqlite3Fts5ExprClonePhrase(pCsr->pExpr, iPhrase, &pNew->pExpr); } if( rc==SQLITE_OK ){ @@ -180227,20 +192588,20 @@ static void fts5ApiCallback( ** Given cursor id iId, return a pointer to the corresponding Fts5Index ** object. Or NULL If the cursor id does not exist. ** -** If successful, set *pnCol to the number of indexed columns in the -** table before returning. +** If successful, set *ppConfig to point to the associated config object +** before returning. */ static Fts5Index *sqlite3Fts5IndexFromCsrid( - Fts5Global *pGlobal, - i64 iCsrId, - int *pnCol + Fts5Global *pGlobal, /* FTS5 global context for db handle */ + i64 iCsrId, /* Id of cursor to find */ + Fts5Config **ppConfig /* OUT: Configuration object */ ){ Fts5Cursor *pCsr; Fts5Table *pTab; pCsr = fts5CursorFromCsrid(pGlobal, iCsrId); pTab = (Fts5Table*)pCsr->base.pVtab; - *pnCol = pTab->pConfig->nCol; + *ppConfig = pTab->pConfig; return pTab->pIndex; } @@ -180267,20 +192628,46 @@ static int fts5PoslistBlob(sqlite3_context *pCtx, Fts5Cursor *pCsr){ Fts5Buffer val; memset(&val, 0, sizeof(Fts5Buffer)); + switch( ((Fts5Table*)(pCsr->base.pVtab))->pConfig->eDetail ){ + case FTS5_DETAIL_FULL: - /* Append the varints */ - for(i=0; i<(nPhrase-1); i++){ - const u8 *dummy; - int nByte = sqlite3Fts5ExprPoslist(pCsr->pExpr, i, &dummy); - sqlite3Fts5BufferAppendVarint(&rc, &val, nByte); - } + /* Append the varints */ + for(i=0; i<(nPhrase-1); i++){ + const u8 *dummy; + int nByte = sqlite3Fts5ExprPoslist(pCsr->pExpr, i, &dummy); + sqlite3Fts5BufferAppendVarint(&rc, &val, nByte); + } - /* Append the position lists */ - for(i=0; ipExpr, i, &pPoslist); - sqlite3Fts5BufferAppendBlob(&rc, &val, nPoslist, pPoslist); + /* Append the position lists */ + for(i=0; ipExpr, i, &pPoslist); + sqlite3Fts5BufferAppendBlob(&rc, &val, nPoslist, pPoslist); + } + break; + + case FTS5_DETAIL_COLUMNS: + + /* Append the varints */ + for(i=0; rc==SQLITE_OK && i<(nPhrase-1); i++){ + const u8 *dummy; + int nByte; + rc = sqlite3Fts5ExprPhraseCollist(pCsr->pExpr, i, &dummy, &nByte); + sqlite3Fts5BufferAppendVarint(&rc, &val, nByte); + } + + /* Append the position lists */ + for(i=0; rc==SQLITE_OK && ipExpr, i, &pPoslist, &nPoslist); + sqlite3Fts5BufferAppendBlob(&rc, &val, nPoslist, pPoslist); + } + break; + + default: + break; } sqlite3_result_blob(pCtx, val.p, val.n, sqlite3_free); @@ -180344,7 +192731,7 @@ static int fts5ColumnMethod( */ static int fts5FindFunctionMethod( sqlite3_vtab *pVtab, /* Virtual table handle */ - int nArg, /* Number of SQL function arguments */ + int nUnused, /* Number of SQL function arguments */ const char *zName, /* Name of SQL function */ void (**pxFunc)(sqlite3_context*,int,sqlite3_value**), /* OUT: Result */ void **ppArg /* OUT: User data for *pxFunc */ @@ -180352,6 +192739,7 @@ static int fts5FindFunctionMethod( Fts5Table *pTab = (Fts5Table*)pVtab; Fts5Auxiliary *pAux; + UNUSED_PARAM(nUnused); pAux = fts5FindAuxiliary(pTab, zName); if( pAux ){ *pxFunc = fts5ApiCallback; @@ -180381,6 +192769,7 @@ static int fts5RenameMethod( */ static int fts5SavepointMethod(sqlite3_vtab *pVtab, int iSavepoint){ Fts5Table *pTab = (Fts5Table*)pVtab; + UNUSED_PARAM(iSavepoint); /* Call below is a no-op for NDEBUG builds */ fts5CheckTransactionState(pTab, FTS5_SAVEPOINT, iSavepoint); fts5TripCursors(pTab); return sqlite3Fts5StorageSync(pTab->pStorage, 0); @@ -180393,6 +192782,7 @@ static int fts5SavepointMethod(sqlite3_vtab *pVtab, int iSavepoint){ */ static int fts5ReleaseMethod(sqlite3_vtab *pVtab, int iSavepoint){ Fts5Table *pTab = (Fts5Table*)pVtab; + UNUSED_PARAM(iSavepoint); /* Call below is a no-op for NDEBUG builds */ fts5CheckTransactionState(pTab, FTS5_RELEASE, iSavepoint); fts5TripCursors(pTab); return sqlite3Fts5StorageSync(pTab->pStorage, 0); @@ -180405,6 +192795,7 @@ static int fts5ReleaseMethod(sqlite3_vtab *pVtab, int iSavepoint){ */ static int fts5RollbackToMethod(sqlite3_vtab *pVtab, int iSavepoint){ Fts5Table *pTab = (Fts5Table*)pVtab; + UNUSED_PARAM(iSavepoint); /* Call below is a no-op for NDEBUG builds */ fts5CheckTransactionState(pTab, FTS5_ROLLBACKTO, iSavepoint); fts5TripCursors(pTab); return sqlite3Fts5StorageRollback(pTab->pStorage); @@ -180584,10 +192975,11 @@ static void fts5ModuleDestroy(void *pCtx){ static void fts5Fts5Func( sqlite3_context *pCtx, /* Function call context */ int nArg, /* Number of args */ - sqlite3_value **apVal /* Function arguments */ + sqlite3_value **apUnused /* Function arguments */ ){ Fts5Global *pGlobal = (Fts5Global*)sqlite3_user_data(pCtx); char buf[8]; + UNUSED_PARAM2(nArg, apUnused); assert( nArg==0 ); assert( sizeof(buf)>=sizeof(pGlobal) ); memcpy(buf, (void*)&pGlobal, sizeof(pGlobal)); @@ -180600,10 +192992,11 @@ static void fts5Fts5Func( static void fts5SourceIdFunc( sqlite3_context *pCtx, /* Function call context */ int nArg, /* Number of args */ - sqlite3_value **apVal /* Function arguments */ + sqlite3_value **apUnused /* Function arguments */ ){ assert( nArg==0 ); - sqlite3_result_text(pCtx, "fts5: 2015-11-02 18:31:45 bda77dda9697c463c3d0704014d51627fceee328", -1, SQLITE_TRANSIENT); + UNUSED_PARAM2(nArg, apUnused); + sqlite3_result_text(pCtx, "fts5: 2016-05-18 10:57:30 fc49f556e48970561d7ab6a2f24fdd7d9eb81ff2", -1, SQLITE_TRANSIENT); } static int fts5Init(sqlite3 *db){ @@ -180664,6 +193057,17 @@ static int fts5Init(sqlite3 *db){ ); } } + + /* If SQLITE_FTS5_ENABLE_TEST_MI is defined, assume that the file + ** fts5_test_mi.c is compiled and linked into the executable. And call + ** its entry point to enable the matchinfo() demo. */ +#ifdef SQLITE_FTS5_ENABLE_TEST_MI + if( rc==SQLITE_OK ){ + extern int sqlite3Fts5TestRegisterMatchinfo(sqlite3*); + rc = sqlite3Fts5TestRegisterMatchinfo(db); + } +#endif + return rc; } @@ -180724,6 +193128,7 @@ SQLITE_PRIVATE int sqlite3Fts5Init(sqlite3 *db){ +/* #include "fts5Int.h" */ struct Fts5Storage { Fts5Config *pConfig; @@ -180854,6 +193259,7 @@ static int fts5StorageGetStmt( } *ppStmt = p->aStmt[eStmt]; + sqlite3_reset(*ppStmt); return rc; } @@ -181006,10 +193412,10 @@ static int sqlite3Fts5StorageOpen( int i; int iOff; sqlite3_snprintf(nDefn, zDefn, "id INTEGER PRIMARY KEY"); - iOff = strlen(zDefn); + iOff = (int)strlen(zDefn); for(i=0; inCol; i++){ sqlite3_snprintf(nDefn-iOff, &zDefn[iOff], ", c%d", i); - iOff += strlen(&zDefn[iOff]); + iOff += (int)strlen(&zDefn[iOff]); } rc = sqlite3Fts5CreateTable(pConfig, "content", zDefn, 0, pzErr); } @@ -181071,11 +193477,13 @@ static int fts5StorageInsertCallback( int tflags, const char *pToken, /* Buffer containing token */ int nToken, /* Size of token in bytes */ - int iStart, /* Start offset of token */ - int iEnd /* End offset of token */ + int iUnused1, /* Start offset of token */ + int iUnused2 /* End offset of token */ ){ Fts5InsertCtx *pCtx = (Fts5InsertCtx*)pContext; Fts5Index *pIdx = pCtx->pStorage->pIndex; + UNUSED_PARAM2(iUnused1, iUnused2); + if( nToken>FTS5_MAX_TOKEN_SIZE ) nToken = FTS5_MAX_TOKEN_SIZE; if( (tflags & FTS5_TOKEN_COLOCATED)==0 || pCtx->szCol==0 ){ pCtx->szCol++; } @@ -181087,39 +193495,52 @@ static int fts5StorageInsertCallback( ** delete-markers to the FTS index necessary to delete it. Do not actually ** remove the %_content row at this time though. */ -static int fts5StorageDeleteFromIndex(Fts5Storage *p, i64 iDel){ +static int fts5StorageDeleteFromIndex( + Fts5Storage *p, + i64 iDel, + sqlite3_value **apVal +){ Fts5Config *pConfig = p->pConfig; - sqlite3_stmt *pSeek; /* SELECT to read row iDel from %_data */ + sqlite3_stmt *pSeek = 0; /* SELECT to read row iDel from %_data */ int rc; /* Return code */ + int rc2; /* sqlite3_reset() return code */ + int iCol; + Fts5InsertCtx ctx; - rc = fts5StorageGetStmt(p, FTS5_STMT_LOOKUP, &pSeek, 0); - if( rc==SQLITE_OK ){ - int rc2; + if( apVal==0 ){ + rc = fts5StorageGetStmt(p, FTS5_STMT_LOOKUP, &pSeek, 0); + if( rc!=SQLITE_OK ) return rc; sqlite3_bind_int64(pSeek, 1, iDel); - if( sqlite3_step(pSeek)==SQLITE_ROW ){ - int iCol; - Fts5InsertCtx ctx; - ctx.pStorage = p; - ctx.iCol = -1; - rc = sqlite3Fts5IndexBeginWrite(p->pIndex, 1, iDel); - for(iCol=1; rc==SQLITE_OK && iCol<=pConfig->nCol; iCol++){ - if( pConfig->abUnindexed[iCol-1] ) continue; - ctx.szCol = 0; - rc = sqlite3Fts5Tokenize(pConfig, - FTS5_TOKENIZE_DOCUMENT, - (const char*)sqlite3_column_text(pSeek, iCol), - sqlite3_column_bytes(pSeek, iCol), - (void*)&ctx, - fts5StorageInsertCallback - ); - p->aTotalSize[iCol-1] -= (i64)ctx.szCol; - } - p->nTotalRow--; + if( sqlite3_step(pSeek)!=SQLITE_ROW ){ + return sqlite3_reset(pSeek); } - rc2 = sqlite3_reset(pSeek); - if( rc==SQLITE_OK ) rc = rc2; } + ctx.pStorage = p; + ctx.iCol = -1; + rc = sqlite3Fts5IndexBeginWrite(p->pIndex, 1, iDel); + for(iCol=1; rc==SQLITE_OK && iCol<=pConfig->nCol; iCol++){ + if( pConfig->abUnindexed[iCol-1]==0 ){ + const char *zText; + int nText; + if( pSeek ){ + zText = (const char*)sqlite3_column_text(pSeek, iCol); + nText = sqlite3_column_bytes(pSeek, iCol); + }else{ + zText = (const char*)sqlite3_value_text(apVal[iCol-1]); + nText = sqlite3_value_bytes(apVal[iCol-1]); + } + ctx.szCol = 0; + rc = sqlite3Fts5Tokenize(pConfig, FTS5_TOKENIZE_DOCUMENT, + zText, nText, (void*)&ctx, fts5StorageInsertCallback + ); + p->aTotalSize[iCol-1] -= (i64)ctx.szCol; + } + } + p->nTotalRow--; + + rc2 = sqlite3_reset(pSeek); + if( rc==SQLITE_OK ) rc = rc2; return rc; } @@ -181199,16 +193620,17 @@ static int fts5StorageSaveTotals(Fts5Storage *p){ /* ** Remove a row from the FTS table. */ -static int sqlite3Fts5StorageDelete(Fts5Storage *p, i64 iDel){ +static int sqlite3Fts5StorageDelete(Fts5Storage *p, i64 iDel, sqlite3_value **apVal){ Fts5Config *pConfig = p->pConfig; int rc; sqlite3_stmt *pDel = 0; + assert( pConfig->eContent!=FTS5_CONTENT_NORMAL || apVal==0 ); rc = fts5StorageLoadTotals(p, 1); /* Delete the index records */ if( rc==SQLITE_OK ){ - rc = fts5StorageDeleteFromIndex(p, iDel); + rc = fts5StorageDeleteFromIndex(p, iDel, apVal); } /* Delete the %_docsize record */ @@ -181222,62 +193644,9 @@ static int sqlite3Fts5StorageDelete(Fts5Storage *p, i64 iDel){ } /* Delete the %_content record */ - if( rc==SQLITE_OK ){ - rc = fts5StorageGetStmt(p, FTS5_STMT_DELETE_CONTENT, &pDel, 0); - } - if( rc==SQLITE_OK ){ - sqlite3_bind_int64(pDel, 1, iDel); - sqlite3_step(pDel); - rc = sqlite3_reset(pDel); - } - - /* Write the averages record */ - if( rc==SQLITE_OK ){ - rc = fts5StorageSaveTotals(p); - } - - return rc; -} - -static int sqlite3Fts5StorageSpecialDelete( - Fts5Storage *p, - i64 iDel, - sqlite3_value **apVal -){ - Fts5Config *pConfig = p->pConfig; - int rc; - sqlite3_stmt *pDel = 0; - - assert( pConfig->eContent!=FTS5_CONTENT_NORMAL ); - rc = fts5StorageLoadTotals(p, 1); - - /* Delete the index records */ - if( rc==SQLITE_OK ){ - int iCol; - Fts5InsertCtx ctx; - ctx.pStorage = p; - ctx.iCol = -1; - - rc = sqlite3Fts5IndexBeginWrite(p->pIndex, 1, iDel); - for(iCol=0; rc==SQLITE_OK && iColnCol; iCol++){ - if( pConfig->abUnindexed[iCol] ) continue; - ctx.szCol = 0; - rc = sqlite3Fts5Tokenize(pConfig, - FTS5_TOKENIZE_DOCUMENT, - (const char*)sqlite3_value_text(apVal[iCol]), - sqlite3_value_bytes(apVal[iCol]), - (void*)&ctx, - fts5StorageInsertCallback - ); - p->aTotalSize[iCol] -= (i64)ctx.szCol; - } - p->nTotalRow--; - } - - /* Delete the %_docsize record */ - if( pConfig->bColumnsize ){ + if( pConfig->eContent==FTS5_CONTENT_NORMAL ){ if( rc==SQLITE_OK ){ - rc = fts5StorageGetStmt(p, FTS5_STMT_DELETE_DOCSIZE, &pDel, 0); + rc = fts5StorageGetStmt(p, FTS5_STMT_DELETE_CONTENT, &pDel, 0); } if( rc==SQLITE_OK ){ sqlite3_bind_int64(pDel, 1, iDel); @@ -181386,6 +193755,10 @@ static int sqlite3Fts5StorageMerge(Fts5Storage *p, int nMerge){ return sqlite3Fts5IndexMerge(p->pIndex, nMerge); } +static int sqlite3Fts5StorageReset(Fts5Storage *p){ + return sqlite3Fts5IndexReset(p->pIndex); +} + /* ** Allocate a new rowid. This is used for "external content" tables when ** a NULL value is inserted into the rowid column. The new rowid is allocated @@ -181434,17 +193807,7 @@ static int sqlite3Fts5StorageContentInsert( }else{ sqlite3_stmt *pInsert = 0; /* Statement to write %_content table */ int i; /* Counter variable */ -#if 0 - if( eConflict==SQLITE_REPLACE ){ - eStmt = FTS5_STMT_REPLACE_CONTENT; - rc = fts5StorageDeleteFromIndex(p, sqlite3_value_int64(apVal[1])); - }else{ - eStmt = FTS5_STMT_INSERT_CONTENT; - } -#endif - if( rc==SQLITE_OK ){ - rc = fts5StorageGetStmt(p, FTS5_STMT_INSERT_CONTENT, &pInsert, 0); - } + rc = fts5StorageGetStmt(p, FTS5_STMT_INSERT_CONTENT, &pInsert, 0); for(i=1; rc==SQLITE_OK && i<=pConfig->nCol+1; i++){ rc = sqlite3_bind_value(pInsert, i, apVal[i]); } @@ -181542,28 +193905,76 @@ struct Fts5IntegrityCtx { int iCol; int szCol; u64 cksum; + Fts5Termset *pTermset; Fts5Config *pConfig; }; + /* ** Tokenization callback used by integrity check. */ static int fts5StorageIntegrityCallback( - void *pContext, /* Pointer to Fts5InsertCtx object */ + void *pContext, /* Pointer to Fts5IntegrityCtx object */ int tflags, const char *pToken, /* Buffer containing token */ int nToken, /* Size of token in bytes */ - int iStart, /* Start offset of token */ - int iEnd /* End offset of token */ + int iUnused1, /* Start offset of token */ + int iUnused2 /* End offset of token */ ){ Fts5IntegrityCtx *pCtx = (Fts5IntegrityCtx*)pContext; + Fts5Termset *pTermset = pCtx->pTermset; + int bPresent; + int ii; + int rc = SQLITE_OK; + int iPos; + int iCol; + + UNUSED_PARAM2(iUnused1, iUnused2); + if( nToken>FTS5_MAX_TOKEN_SIZE ) nToken = FTS5_MAX_TOKEN_SIZE; + if( (tflags & FTS5_TOKEN_COLOCATED)==0 || pCtx->szCol==0 ){ pCtx->szCol++; } - pCtx->cksum ^= sqlite3Fts5IndexCksum( - pCtx->pConfig, pCtx->iRowid, pCtx->iCol, pCtx->szCol-1, pToken, nToken - ); - return SQLITE_OK; + + switch( pCtx->pConfig->eDetail ){ + case FTS5_DETAIL_FULL: + iPos = pCtx->szCol-1; + iCol = pCtx->iCol; + break; + + case FTS5_DETAIL_COLUMNS: + iPos = pCtx->iCol; + iCol = 0; + break; + + default: + assert( pCtx->pConfig->eDetail==FTS5_DETAIL_NONE ); + iPos = 0; + iCol = 0; + break; + } + + rc = sqlite3Fts5TermsetAdd(pTermset, 0, pToken, nToken, &bPresent); + if( rc==SQLITE_OK && bPresent==0 ){ + pCtx->cksum ^= sqlite3Fts5IndexEntryCksum( + pCtx->iRowid, iCol, iPos, 0, pToken, nToken + ); + } + + for(ii=0; rc==SQLITE_OK && iipConfig->nPrefix; ii++){ + const int nChar = pCtx->pConfig->aPrefix[ii]; + int nByte = sqlite3Fts5IndexCharlenToBytelen(pToken, nToken, nChar); + if( nByte ){ + rc = sqlite3Fts5TermsetAdd(pTermset, ii+1, pToken, nByte, &bPresent); + if( bPresent==0 ){ + pCtx->cksum ^= sqlite3Fts5IndexEntryCksum( + pCtx->iRowid, iCol, iPos, ii+1, pToken, nByte + ); + } + } + } + + return rc; } /* @@ -181599,22 +194010,37 @@ static int sqlite3Fts5StorageIntegrity(Fts5Storage *p){ if( pConfig->bColumnsize ){ rc = sqlite3Fts5StorageDocsize(p, ctx.iRowid, aColSize); } + if( rc==SQLITE_OK && pConfig->eDetail==FTS5_DETAIL_NONE ){ + rc = sqlite3Fts5TermsetNew(&ctx.pTermset); + } for(i=0; rc==SQLITE_OK && inCol; i++){ if( pConfig->abUnindexed[i] ) continue; ctx.iCol = i; ctx.szCol = 0; - rc = sqlite3Fts5Tokenize(pConfig, - FTS5_TOKENIZE_DOCUMENT, - (const char*)sqlite3_column_text(pScan, i+1), - sqlite3_column_bytes(pScan, i+1), - (void*)&ctx, - fts5StorageIntegrityCallback - ); - if( pConfig->bColumnsize && ctx.szCol!=aColSize[i] ){ + if( pConfig->eDetail==FTS5_DETAIL_COLUMNS ){ + rc = sqlite3Fts5TermsetNew(&ctx.pTermset); + } + if( rc==SQLITE_OK ){ + rc = sqlite3Fts5Tokenize(pConfig, + FTS5_TOKENIZE_DOCUMENT, + (const char*)sqlite3_column_text(pScan, i+1), + sqlite3_column_bytes(pScan, i+1), + (void*)&ctx, + fts5StorageIntegrityCallback + ); + } + if( rc==SQLITE_OK && pConfig->bColumnsize && ctx.szCol!=aColSize[i] ){ rc = FTS5_CORRUPT; } aTotalSize[i] += ctx.szCol; + if( pConfig->eDetail==FTS5_DETAIL_COLUMNS ){ + sqlite3Fts5TermsetFree(ctx.pTermset); + ctx.pTermset = 0; + } } + sqlite3Fts5TermsetFree(ctx.pTermset); + ctx.pTermset = 0; + if( rc!=SQLITE_OK ) break; } rc2 = sqlite3_reset(pScan); @@ -181633,12 +194059,12 @@ static int sqlite3Fts5StorageIntegrity(Fts5Storage *p){ /* Check that the %_docsize and %_content tables contain the expected ** number of rows. */ if( rc==SQLITE_OK && pConfig->eContent==FTS5_CONTENT_NORMAL ){ - i64 nRow; + i64 nRow = 0; rc = fts5StorageCount(p, "content", &nRow); if( rc==SQLITE_OK && nRow!=p->nTotalRow ) rc = FTS5_CORRUPT; } if( rc==SQLITE_OK && pConfig->bColumnsize ){ - i64 nRow; + i64 nRow = 0; rc = fts5StorageCount(p, "docsize", &nRow); if( rc==SQLITE_OK && nRow!=p->nTotalRow ) rc = FTS5_CORRUPT; } @@ -181817,8 +194243,6 @@ static int sqlite3Fts5StorageConfigValue( return rc; } - - /* ** 2014 May 31 ** @@ -181833,6 +194257,7 @@ static int sqlite3Fts5StorageConfigValue( */ +/* #include "fts5Int.h" */ /************************************************************************** ** Start of ascii tokenizer implementation. @@ -181882,12 +194307,13 @@ static void fts5AsciiDelete(Fts5Tokenizer *p){ ** Create an "ascii" tokenizer. */ static int fts5AsciiCreate( - void *pCtx, + void *pUnused, const char **azArg, int nArg, Fts5Tokenizer **ppOut ){ int rc = SQLITE_OK; AsciiTokenizer *p = 0; + UNUSED_PARAM(pUnused); if( nArg%2 ){ rc = SQLITE_ERROR; }else{ @@ -181936,7 +194362,7 @@ static void asciiFold(char *aOut, const char *aIn, int nByte){ static int fts5AsciiTokenize( Fts5Tokenizer *pTokenizer, void *pCtx, - int flags, + int iUnused, const char *pText, int nText, int (*xToken)(void*, int, const char*, int nToken, int iStart, int iEnd) ){ @@ -181950,6 +194376,8 @@ static int fts5AsciiTokenize( char *pFold = aFold; unsigned char *a = p->aTokenChar; + UNUSED_PARAM(iUnused); + while( is0 ){ @@ -182076,7 +194504,7 @@ static int fts5UnicodeAddExceptions( int bToken; READ_UTF8(zCsr, zTerm, iCode); if( iCode<128 ){ - p->aTokenChar[iCode] = bTokenChars; + p->aTokenChar[iCode] = (unsigned char)bTokenChars; }else{ bToken = sqlite3Fts5UnicodeIsalnum(iCode); assert( (bToken==0 || bToken==1) ); @@ -182143,13 +194571,15 @@ static void fts5UnicodeDelete(Fts5Tokenizer *pTok){ ** Create a "unicode61" tokenizer. */ static int fts5UnicodeCreate( - void *pCtx, + void *pUnused, const char **azArg, int nArg, Fts5Tokenizer **ppOut ){ int rc = SQLITE_OK; /* Return code */ Unicode61Tokenizer *p = 0; /* New tokenizer object */ + UNUSED_PARAM(pUnused); + if( nArg%2 ){ rc = SQLITE_ERROR; }else{ @@ -182206,7 +194636,7 @@ static int fts5UnicodeIsAlnum(Unicode61Tokenizer *p, int iCode){ static int fts5UnicodeTokenize( Fts5Tokenizer *pTokenizer, void *pCtx, - int flags, + int iUnused, const char *pText, int nText, int (*xToken)(void*, int, const char*, int nToken, int iStart, int iEnd) ){ @@ -182222,6 +194652,8 @@ static int fts5UnicodeTokenize( int nFold = p->nFold; const char *pEnd = &aFold[nFold-6]; + UNUSED_PARAM(iUnused); + /* Each iteration of this loop gobbles up a contiguous run of separators, ** then the next token. */ while( rc==SQLITE_OK ){ @@ -183040,7 +195472,7 @@ static int sqlite3Fts5TokenizerInit(fts5_api *pApi){ int rc = SQLITE_OK; /* Return code */ int i; /* To iterate through builtin functions */ - for(i=0; rc==SQLITE_OK && ixCreateTokenizer(pApi, aBuiltin[i].zName, (void*)pApi, @@ -183181,9 +195613,9 @@ static int sqlite3Fts5UnicodeIsalnum(int c){ 0xFFFFFFFF, 0xFC00FFFF, 0xF8000001, 0xF8000001, }; - if( c<128 ){ + if( (unsigned int)c<128 ){ return ( (aAscii[c >> 5] & (1 << (c & 0x001F)))==0 ); - }else if( c<(1<<22) ){ + }else if( (unsigned int)c<(1<<22) ){ unsigned int key = (((unsigned int)c)<<10) | 0x000003FF; int iRes = 0; int iHi = sizeof(aEntry)/sizeof(aEntry[0]) - 1; @@ -183431,6 +195863,7 @@ static int sqlite3Fts5UnicodeFold(int c, int bRemoveDiacritic){ */ +/* #include "fts5Int.h" */ /* ** This is a copy of the sqlite3GetVarint32() routine from the SQLite core. @@ -183749,7 +196182,10 @@ static int sqlite3Fts5PutVarint(unsigned char *p, u64 v){ static int sqlite3Fts5GetVarintLen(u32 iVal){ +#if 0 if( iVal<(1 << 7 ) ) return 1; +#endif + assert( iVal>=(1 << 7) ); if( iVal<(1 << 14) ) return 2; if( iVal<(1 << 21) ) return 3; if( iVal<(1 << 28) ) return 4; @@ -183791,6 +196227,7 @@ static int sqlite3Fts5GetVarintLen(u32 iVal){ */ +/* #include "fts5Int.h" */ typedef struct Fts5VocabTable Fts5VocabTable; @@ -183813,16 +196250,18 @@ struct Fts5VocabCursor { int bEof; /* True if this cursor is at EOF */ Fts5IndexIter *pIter; /* Term/rowid iterator object */ + int nLeTerm; /* Size of zLeTerm in bytes */ + char *zLeTerm; /* (term <= $zLeTerm) paramater, or NULL */ + /* These are used by 'col' tables only */ - int nCol; + Fts5Config *pConfig; /* Fts5 table configuration */ int iCol; i64 *aCnt; i64 *aDoc; - /* Output values */ + /* Output values used by 'row' and 'col' tables */ i64 rowid; /* This table's current rowid value */ Fts5Buffer term; /* Current value of 'term' column */ - i64 aVal[3]; /* Up to three columns left of 'term' */ }; #define FTS5_VOCAB_COL 0 @@ -183831,6 +196270,14 @@ struct Fts5VocabCursor { #define FTS5_VOCAB_COL_SCHEMA "term, col, doc, cnt" #define FTS5_VOCAB_ROW_SCHEMA "term, doc, cnt" +/* +** Bits for the mask used as the idxNum value by xBestIndex/xFilter. +*/ +#define FTS5_VOCAB_TERM_EQ 0x01 +#define FTS5_VOCAB_TERM_GE 0x02 +#define FTS5_VOCAB_TERM_LE 0x04 + + /* ** Translate a string containing an fts5vocab table type to an ** FTS5_VOCAB_XXX constant. If successful, set *peType to the output @@ -183926,13 +196373,13 @@ static int fts5VocabInitVtab( const char *zDb = bDb ? argv[3] : argv[1]; const char *zTab = bDb ? argv[4] : argv[3]; const char *zType = bDb ? argv[5] : argv[4]; - int nDb = strlen(zDb)+1; - int nTab = strlen(zTab)+1; - int eType; + int nDb = (int)strlen(zDb)+1; + int nTab = (int)strlen(zTab)+1; + int eType = 0; rc = fts5VocabTableType(zType, pzErr, &eType); if( rc==SQLITE_OK ){ - assert( eType>=0 && eType=0 && eTypenConstraint; i++){ + struct sqlite3_index_constraint *p = &pInfo->aConstraint[i]; + if( p->usable==0 ) continue; + if( p->iColumn==0 ){ /* term column */ + if( p->op==SQLITE_INDEX_CONSTRAINT_EQ ) iTermEq = i; + if( p->op==SQLITE_INDEX_CONSTRAINT_LE ) iTermLe = i; + if( p->op==SQLITE_INDEX_CONSTRAINT_LT ) iTermLe = i; + if( p->op==SQLITE_INDEX_CONSTRAINT_GE ) iTermGe = i; + if( p->op==SQLITE_INDEX_CONSTRAINT_GT ) iTermGe = i; + } + } + + if( iTermEq>=0 ){ + idxNum |= FTS5_VOCAB_TERM_EQ; + pInfo->aConstraintUsage[iTermEq].argvIndex = ++nArg; + pInfo->estimatedCost = 100; + }else{ + pInfo->estimatedCost = 1000000; + if( iTermGe>=0 ){ + idxNum |= FTS5_VOCAB_TERM_GE; + pInfo->aConstraintUsage[iTermGe].argvIndex = ++nArg; + pInfo->estimatedCost = pInfo->estimatedCost / 2; + } + if( iTermLe>=0 ){ + idxNum |= FTS5_VOCAB_TERM_LE; + pInfo->aConstraintUsage[iTermLe].argvIndex = ++nArg; + pInfo->estimatedCost = pInfo->estimatedCost / 2; + } + } + + pInfo->idxNum = idxNum; + return SQLITE_OK; } @@ -184000,12 +196488,11 @@ static int fts5VocabOpenMethod( ){ Fts5VocabTable *pTab = (Fts5VocabTable*)pVTab; Fts5Index *pIndex = 0; - int nCol = 0; + Fts5Config *pConfig = 0; Fts5VocabCursor *pCsr = 0; int rc = SQLITE_OK; sqlite3_stmt *pStmt = 0; char *zSql = 0; - int nByte; zSql = sqlite3Fts5Mprintf(&rc, "SELECT t.%Q FROM %Q.%Q AS t WHERE t.%Q MATCH '*id'", @@ -184020,7 +196507,7 @@ static int fts5VocabOpenMethod( if( pStmt && sqlite3_step(pStmt)==SQLITE_ROW ){ i64 iId = sqlite3_column_int64(pStmt, 0); - pIndex = sqlite3Fts5IndexFromCsrid(pTab->pGlobal, iId, &nCol); + pIndex = sqlite3Fts5IndexFromCsrid(pTab->pGlobal, iId, &pConfig); } if( rc==SQLITE_OK && pIndex==0 ){ @@ -184034,14 +196521,17 @@ static int fts5VocabOpenMethod( } } - nByte = nCol * sizeof(i64) * 2 + sizeof(Fts5VocabCursor); - pCsr = (Fts5VocabCursor*)sqlite3Fts5MallocZero(&rc, nByte); + if( rc==SQLITE_OK ){ + int nByte = pConfig->nCol * sizeof(i64) * 2 + sizeof(Fts5VocabCursor); + pCsr = (Fts5VocabCursor*)sqlite3Fts5MallocZero(&rc, nByte); + } + if( pCsr ){ pCsr->pIndex = pIndex; pCsr->pStmt = pStmt; - pCsr->nCol = nCol; + pCsr->pConfig = pConfig; pCsr->aCnt = (i64*)&pCsr[1]; - pCsr->aDoc = &pCsr->aCnt[nCol]; + pCsr->aDoc = &pCsr->aCnt[pConfig->nCol]; }else{ sqlite3_finalize(pStmt); } @@ -184054,6 +196544,9 @@ static void fts5VocabResetCursor(Fts5VocabCursor *pCsr){ pCsr->rowid = 0; sqlite3Fts5IterClose(pCsr->pIter); pCsr->pIter = 0; + sqlite3_free(pCsr->zLeTerm); + pCsr->nLeTerm = -1; + pCsr->zLeTerm = 0; } /* @@ -184077,16 +196570,17 @@ static int fts5VocabNextMethod(sqlite3_vtab_cursor *pCursor){ Fts5VocabCursor *pCsr = (Fts5VocabCursor*)pCursor; Fts5VocabTable *pTab = (Fts5VocabTable*)pCursor->pVtab; int rc = SQLITE_OK; + int nCol = pCsr->pConfig->nCol; pCsr->rowid++; if( pTab->eType==FTS5_VOCAB_COL ){ - for(pCsr->iCol++; pCsr->iColnCol; pCsr->iCol++){ - if( pCsr->aCnt[pCsr->iCol] ) break; + for(pCsr->iCol++; pCsr->iColiCol++){ + if( pCsr->aDoc[pCsr->iCol] ) break; } } - if( pTab->eType==FTS5_VOCAB_ROW || pCsr->iCol>=pCsr->nCol ){ + if( pTab->eType==FTS5_VOCAB_ROW || pCsr->iCol>=nCol ){ if( sqlite3Fts5IterEof(pCsr->pIter) ){ pCsr->bEof = 1; }else{ @@ -184094,53 +196588,93 @@ static int fts5VocabNextMethod(sqlite3_vtab_cursor *pCursor){ int nTerm; zTerm = sqlite3Fts5IterTerm(pCsr->pIter, &nTerm); + if( pCsr->nLeTerm>=0 ){ + int nCmp = MIN(nTerm, pCsr->nLeTerm); + int bCmp = memcmp(pCsr->zLeTerm, zTerm, nCmp); + if( bCmp<0 || (bCmp==0 && pCsr->nLeTermbEof = 1; + return SQLITE_OK; + } + } + sqlite3Fts5BufferSet(&rc, &pCsr->term, nTerm, (const u8*)zTerm); - memset(pCsr->aVal, 0, sizeof(pCsr->aVal)); - memset(pCsr->aCnt, 0, pCsr->nCol * sizeof(i64)); - memset(pCsr->aDoc, 0, pCsr->nCol * sizeof(i64)); + memset(pCsr->aCnt, 0, nCol * sizeof(i64)); + memset(pCsr->aDoc, 0, nCol * sizeof(i64)); pCsr->iCol = 0; assert( pTab->eType==FTS5_VOCAB_COL || pTab->eType==FTS5_VOCAB_ROW ); while( rc==SQLITE_OK ){ - i64 dummy; const u8 *pPos; int nPos; /* Position list */ i64 iPos = 0; /* 64-bit position read from poslist */ int iOff = 0; /* Current offset within position list */ - rc = sqlite3Fts5IterPoslist(pCsr->pIter, 0, &pPos, &nPos, &dummy); - if( rc==SQLITE_OK ){ - if( pTab->eType==FTS5_VOCAB_ROW ){ - while( 0==sqlite3Fts5PoslistNext64(pPos, nPos, &iOff, &iPos) ){ - pCsr->aVal[1]++; - } - pCsr->aVal[0]++; - }else{ - int iCol = -1; - while( 0==sqlite3Fts5PoslistNext64(pPos, nPos, &iOff, &iPos) ){ - int ii = FTS5_POS2COLUMN(iPos); - pCsr->aCnt[ii]++; - if( iCol!=ii ){ - pCsr->aDoc[ii]++; - iCol = ii; + pPos = pCsr->pIter->pData; + nPos = pCsr->pIter->nData; + switch( pCsr->pConfig->eDetail ){ + case FTS5_DETAIL_FULL: + pPos = pCsr->pIter->pData; + nPos = pCsr->pIter->nData; + if( pTab->eType==FTS5_VOCAB_ROW ){ + while( 0==sqlite3Fts5PoslistNext64(pPos, nPos, &iOff, &iPos) ){ + pCsr->aCnt[0]++; + } + pCsr->aDoc[0]++; + }else{ + int iCol = -1; + while( 0==sqlite3Fts5PoslistNext64(pPos, nPos, &iOff, &iPos) ){ + int ii = FTS5_POS2COLUMN(iPos); + pCsr->aCnt[ii]++; + if( iCol!=ii ){ + if( ii>=nCol ){ + rc = FTS5_CORRUPT; + break; + } + pCsr->aDoc[ii]++; + iCol = ii; + } } } - } + break; + + case FTS5_DETAIL_COLUMNS: + if( pTab->eType==FTS5_VOCAB_ROW ){ + pCsr->aDoc[0]++; + }else{ + while( 0==sqlite3Fts5PoslistNext64(pPos, nPos, &iOff,&iPos) ){ + assert_nc( iPos>=0 && iPos=nCol ){ + rc = FTS5_CORRUPT; + break; + } + pCsr->aDoc[iPos]++; + } + } + break; + + default: + assert( pCsr->pConfig->eDetail==FTS5_DETAIL_NONE ); + pCsr->aDoc[0]++; + break; + } + + if( rc==SQLITE_OK ){ rc = sqlite3Fts5IterNextScan(pCsr->pIter); } + if( rc==SQLITE_OK ){ zTerm = sqlite3Fts5IterTerm(pCsr->pIter, &nTerm); - if( nTerm!=pCsr->term.n || memcmp(zTerm, pCsr->term.p, nTerm) ) break; + if( nTerm!=pCsr->term.n || memcmp(zTerm, pCsr->term.p, nTerm) ){ + break; + } if( sqlite3Fts5IterEof(pCsr->pIter) ) break; } } } } - if( pCsr->bEof==0 && pTab->eType==FTS5_VOCAB_COL ){ - while( pCsr->aCnt[pCsr->iCol]==0 ) pCsr->iCol++; - pCsr->aVal[0] = pCsr->iCol; - pCsr->aVal[1] = pCsr->aDoc[pCsr->iCol]; - pCsr->aVal[2] = pCsr->aCnt[pCsr->iCol]; + if( rc==SQLITE_OK && pCsr->bEof==0 && pTab->eType==FTS5_VOCAB_COL ){ + while( pCsr->aDoc[pCsr->iCol]==0 ) pCsr->iCol++; + assert( pCsr->iColpConfig->nCol ); } return rc; } @@ -184151,16 +196685,54 @@ static int fts5VocabNextMethod(sqlite3_vtab_cursor *pCursor){ static int fts5VocabFilterMethod( sqlite3_vtab_cursor *pCursor, /* The cursor used for this query */ int idxNum, /* Strategy index */ - const char *idxStr, /* Unused */ - int nVal, /* Number of elements in apVal */ + const char *zUnused, /* Unused */ + int nUnused, /* Number of elements in apVal */ sqlite3_value **apVal /* Arguments for the indexing scheme */ ){ Fts5VocabCursor *pCsr = (Fts5VocabCursor*)pCursor; - int rc; - const int flags = FTS5INDEX_QUERY_SCAN; + int rc = SQLITE_OK; + + int iVal = 0; + int f = FTS5INDEX_QUERY_SCAN; + const char *zTerm = 0; + int nTerm = 0; + + sqlite3_value *pEq = 0; + sqlite3_value *pGe = 0; + sqlite3_value *pLe = 0; + + UNUSED_PARAM2(zUnused, nUnused); fts5VocabResetCursor(pCsr); - rc = sqlite3Fts5IndexQuery(pCsr->pIndex, 0, 0, flags, 0, &pCsr->pIter); + if( idxNum & FTS5_VOCAB_TERM_EQ ) pEq = apVal[iVal++]; + if( idxNum & FTS5_VOCAB_TERM_GE ) pGe = apVal[iVal++]; + if( idxNum & FTS5_VOCAB_TERM_LE ) pLe = apVal[iVal++]; + + if( pEq ){ + zTerm = (const char *)sqlite3_value_text(pEq); + nTerm = sqlite3_value_bytes(pEq); + f = 0; + }else{ + if( pGe ){ + zTerm = (const char *)sqlite3_value_text(pGe); + nTerm = sqlite3_value_bytes(pGe); + } + if( pLe ){ + const char *zCopy = (const char *)sqlite3_value_text(pLe); + pCsr->nLeTerm = sqlite3_value_bytes(pLe); + pCsr->zLeTerm = sqlite3_malloc(pCsr->nLeTerm+1); + if( pCsr->zLeTerm==0 ){ + rc = SQLITE_NOMEM; + }else{ + memcpy(pCsr->zLeTerm, zCopy, pCsr->nLeTerm+1); + } + } + } + + + if( rc==SQLITE_OK ){ + rc = sqlite3Fts5IndexQuery(pCsr->pIndex, zTerm, nTerm, f, 0, &pCsr->pIter); + } if( rc==SQLITE_OK ){ rc = fts5VocabNextMethod(pCursor); } @@ -184183,18 +196755,36 @@ static int fts5VocabColumnMethod( int iCol /* Index of column to read value from */ ){ Fts5VocabCursor *pCsr = (Fts5VocabCursor*)pCursor; - switch( iCol ){ - case 0: /* term */ - sqlite3_result_text( - pCtx, (const char*)pCsr->term.p, pCsr->term.n, SQLITE_TRANSIENT - ); - break; + int eDetail = pCsr->pConfig->eDetail; + int eType = ((Fts5VocabTable*)(pCursor->pVtab))->eType; + i64 iVal = 0; - default: - assert( iCol<4 && iCol>0 ); - sqlite3_result_int64(pCtx, pCsr->aVal[iCol-1]); - break; + if( iCol==0 ){ + sqlite3_result_text( + pCtx, (const char*)pCsr->term.p, pCsr->term.n, SQLITE_TRANSIENT + ); + }else if( eType==FTS5_VOCAB_COL ){ + assert( iCol==1 || iCol==2 || iCol==3 ); + if( iCol==1 ){ + if( eDetail!=FTS5_DETAIL_NONE ){ + const char *z = pCsr->pConfig->azCol[pCsr->iCol]; + sqlite3_result_text(pCtx, z, -1, SQLITE_STATIC); + } + }else if( iCol==2 ){ + iVal = pCsr->aDoc[pCsr->iCol]; + }else{ + iVal = pCsr->aCnt[pCsr->iCol]; + } + }else{ + assert( iCol==1 || iCol==2 ); + if( iCol==1 ){ + iVal = pCsr->aDoc[0]; + }else{ + iVal = pCsr->aCnt[0]; + } } + + if( iVal>0 ) sqlite3_result_int64(pCtx, iVal); return SQLITE_OK; } diff --git a/TMessagesProj/jni/sqlite/sqlite3.h b/TMessagesProj/jni/sqlite/sqlite3.h index 928b43077..313b5ec3c 100644 --- a/TMessagesProj/jni/sqlite/sqlite3.h +++ b/TMessagesProj/jni/sqlite/sqlite3.h @@ -31,7 +31,7 @@ ** part of the build process. */ #ifndef _SQLITE3_H_ -#define _SQLITE3_H_ +#define _SQLITE3_H_ #include /* Needed for the definition of va_list */ /* @@ -111,9 +111,9 @@ extern "C" { ** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite_version()] and [sqlite_source_id()]. */ -#define SQLITE_VERSION "3.9.2" -#define SQLITE_VERSION_NUMBER 3009002 -#define SQLITE_SOURCE_ID "2015-11-02 18:31:45 bda77dda9697c463c3d0704014d51627fceee328" +#define SQLITE_VERSION "3.13.0" +#define SQLITE_VERSION_NUMBER 3013000 +#define SQLITE_SOURCE_ID "2016-05-18 10:57:30 fc49f556e48970561d7ab6a2f24fdd7d9eb81ff2" /* ** CAPI3REF: Run-Time Library Version Numbers @@ -347,7 +347,7 @@ typedef int (*sqlite3_callback)(void*,int,char**, char**); ** from [sqlite3_malloc()] and passed back through the 5th parameter. ** To avoid memory leaks, the application should invoke [sqlite3_free()] ** on error message strings returned through the 5th parameter of -** of sqlite3_exec() after the error message string is no longer needed. +** sqlite3_exec() after the error message string is no longer needed. ** ^If the 5th parameter to sqlite3_exec() is not NULL and no errors ** occur, then sqlite3_exec() sets the pointer in its 5th parameter to ** NULL before returning. @@ -478,6 +478,7 @@ SQLITE_API int SQLITE_STDCALL sqlite3_exec( #define SQLITE_IOERR_GETTEMPPATH (SQLITE_IOERR | (25<<8)) #define SQLITE_IOERR_CONVPATH (SQLITE_IOERR | (26<<8)) #define SQLITE_IOERR_VNODE (SQLITE_IOERR | (27<<8)) +#define SQLITE_IOERR_AUTH (SQLITE_IOERR | (28<<8)) #define SQLITE_LOCKED_SHAREDCACHE (SQLITE_LOCKED | (1<<8)) #define SQLITE_BUSY_RECOVERY (SQLITE_BUSY | (1<<8)) #define SQLITE_BUSY_SNAPSHOT (SQLITE_BUSY | (2<<8)) @@ -793,8 +794,13 @@ struct sqlite3_io_methods { **
  • [[SQLITE_FCNTL_FILE_POINTER]] ** The [SQLITE_FCNTL_FILE_POINTER] opcode is used to obtain a pointer ** to the [sqlite3_file] object associated with a particular database -** connection. See the [sqlite3_file_control()] documentation for -** additional information. +** connection. See also [SQLITE_FCNTL_JOURNAL_POINTER]. +** +**
  • [[SQLITE_FCNTL_JOURNAL_POINTER]] +** The [SQLITE_FCNTL_JOURNAL_POINTER] opcode is used to obtain a pointer +** to the [sqlite3_file] object associated with the journal file (either +** the [rollback journal] or the [write-ahead log]) for a particular database +** connection. See also [SQLITE_FCNTL_FILE_POINTER]. ** **
  • [[SQLITE_FCNTL_SYNC_OMITTED]] ** No longer in use. @@ -881,6 +887,15 @@ struct sqlite3_io_methods { ** pointer in case this file-control is not implemented. This file-control ** is intended for diagnostic use only. ** +**
  • [[SQLITE_FCNTL_VFS_POINTER]] +** ^The [SQLITE_FCNTL_VFS_POINTER] opcode finds a pointer to the top-level +** [VFSes] currently in use. ^(The argument X in +** sqlite3_file_control(db,SQLITE_FCNTL_VFS_POINTER,X) must be +** of type "[sqlite3_vfs] **". This opcodes will set *X +** to a pointer to the top-level VFS.)^ +** ^When there are multiple VFS shims in the stack, this opcode finds the +** upper-most shim only. +** **
  • [[SQLITE_FCNTL_PRAGMA]] ** ^Whenever a [PRAGMA] statement is parsed, an [SQLITE_FCNTL_PRAGMA] ** file control is sent to the open [sqlite3_file] object corresponding @@ -999,6 +1014,8 @@ struct sqlite3_io_methods { #define SQLITE_FCNTL_WAL_BLOCK 24 #define SQLITE_FCNTL_ZIPVFS 25 #define SQLITE_FCNTL_RBU 26 +#define SQLITE_FCNTL_VFS_POINTER 27 +#define SQLITE_FCNTL_JOURNAL_POINTER 28 /* deprecated names */ #define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE @@ -1211,7 +1228,7 @@ struct sqlite3_vfs { const char *(*xNextSystemCall)(sqlite3_vfs*, const char *zName); /* ** The methods above are in versions 1 through 3 of the sqlite_vfs object. - ** New fields may be appended in figure versions. The iVersion + ** New fields may be appended in future versions. The iVersion ** value will increment whenever this happens. */ }; @@ -1598,29 +1615,34 @@ struct sqlite3_mem_methods { ** ** ** [[SQLITE_CONFIG_PAGECACHE]]
    SQLITE_CONFIG_PAGECACHE
    -**
    ^The SQLITE_CONFIG_PAGECACHE option specifies a static memory buffer +**
    ^The SQLITE_CONFIG_PAGECACHE option specifies a memory pool ** that SQLite can use for the database page cache with the default page ** cache implementation. -** This configuration should not be used if an application-define page -** cache implementation is loaded using the [SQLITE_CONFIG_PCACHE2] -** configuration option. +** This configuration option is a no-op if an application-define page +** cache implementation is loaded using the [SQLITE_CONFIG_PCACHE2]. ** ^There are three arguments to SQLITE_CONFIG_PAGECACHE: A pointer to -** 8-byte aligned -** memory, the size of each page buffer (sz), and the number of pages (N). +** 8-byte aligned memory (pMem), the size of each page cache line (sz), +** and the number of cache lines (N). ** The sz argument should be the size of the largest database page ** (a power of two between 512 and 65536) plus some extra bytes for each ** page header. ^The number of extra bytes needed by the page header -** can be determined using the [SQLITE_CONFIG_PCACHE_HDRSZ] option -** to [sqlite3_config()]. +** can be determined using [SQLITE_CONFIG_PCACHE_HDRSZ]. ** ^It is harmless, apart from the wasted memory, -** for the sz parameter to be larger than necessary. The first -** argument should pointer to an 8-byte aligned block of memory that -** is at least sz*N bytes of memory, otherwise subsequent behavior is -** undefined. -** ^SQLite will use the memory provided by the first argument to satisfy its -** memory needs for the first N pages that it adds to cache. ^If additional -** page cache memory is needed beyond what is provided by this option, then -** SQLite goes to [sqlite3_malloc()] for the additional storage space.
    +** for the sz parameter to be larger than necessary. The pMem +** argument must be either a NULL pointer or a pointer to an 8-byte +** aligned block of memory of at least sz*N bytes, otherwise +** subsequent behavior is undefined. +** ^When pMem is not NULL, SQLite will strive to use the memory provided +** to satisfy page cache needs, falling back to [sqlite3_malloc()] if +** a page cache line is larger than sz bytes or if all of the pMem buffer +** is exhausted. +** ^If pMem is NULL and N is non-zero, then each database connection +** does an initial bulk allocation for page cache memory +** from [sqlite3_malloc()] sufficient for N cache lines if N is positive or +** of -1024*N bytes if N is negative, . ^If additional +** page cache memory is needed beyond what is provided by the initial +** allocation, then SQLite goes to [sqlite3_malloc()] separately for each +** additional cache line. ** ** [[SQLITE_CONFIG_HEAP]]
    SQLITE_CONFIG_HEAP
    **
    ^The SQLITE_CONFIG_HEAP option specifies a static memory buffer @@ -1798,6 +1820,20 @@ struct sqlite3_mem_methods { ** is enabled (using the [PRAGMA threads] command) and the amount of content ** to be sorted exceeds the page size times the minimum of the ** [PRAGMA cache_size] setting and this value. +** +** [[SQLITE_CONFIG_STMTJRNL_SPILL]] +**
    SQLITE_CONFIG_STMTJRNL_SPILL +**
    ^The SQLITE_CONFIG_STMTJRNL_SPILL option takes a single parameter which +** becomes the [statement journal] spill-to-disk threshold. +** [Statement journals] are held in memory until their size (in bytes) +** exceeds this threshold, at which point they are written to disk. +** Or if the threshold is -1, statement journals are always held +** exclusively in memory. +** Since many statement journals never become large, setting the spill +** threshold to a value such as 64KiB can greatly reduce the amount of +** I/O required to support statement rollback. +** The default value for this setting is controlled by the +** [SQLITE_STMTJRNL_SPILL] compile-time option. ** */ #define SQLITE_CONFIG_SINGLETHREAD 1 /* nil */ @@ -1825,6 +1861,7 @@ struct sqlite3_mem_methods { #define SQLITE_CONFIG_WIN32_HEAPSIZE 23 /* int nByte */ #define SQLITE_CONFIG_PCACHE_HDRSZ 24 /* int *psz */ #define SQLITE_CONFIG_PMASZ 25 /* unsigned int szPma */ +#define SQLITE_CONFIG_STMTJRNL_SPILL 26 /* int nByte */ /* ** CAPI3REF: Database Connection Configuration Options @@ -1882,11 +1919,43 @@ struct sqlite3_mem_methods { ** following this call. The second parameter may be a NULL pointer, in ** which case the trigger setting is not reported back.
    ** +**
    SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER
    +**
    ^This option is used to enable or disable the two-argument +** version of the [fts3_tokenizer()] function which is part of the +** [FTS3] full-text search engine extension. +** There should be two additional arguments. +** The first argument is an integer which is 0 to disable fts3_tokenizer() or +** positive to enable fts3_tokenizer() or negative to leave the setting +** unchanged. +** The second parameter is a pointer to an integer into which +** is written 0 or 1 to indicate whether fts3_tokenizer is disabled or enabled +** following this call. The second parameter may be a NULL pointer, in +** which case the new setting is not reported back.
    +** +**
    SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION
    +**
    ^This option is used to enable or disable the [sqlite3_load_extension()] +** interface independently of the [load_extension()] SQL function. +** The [sqlite3_enable_load_extension()] API enables or disables both the +** C-API [sqlite3_load_extension()] and the SQL function [load_extension()]. +** There should be two additional arguments. +** When the first argument to this interface is 1, then only the C-API is +** enabled and the SQL function remains disabled. If the first argment to +** this interface is 0, then both the C-API and the SQL function are disabled. +** If the first argument is -1, then no changes are made to state of either the +** C-API or the SQL function. +** The second parameter is a pointer to an integer into which +** is written 0 or 1 to indicate whether [sqlite3_load_extension()] interface +** is disabled or enabled following this call. The second parameter may +** be a NULL pointer, in which case the new setting is not reported back. +**
    +** ** */ -#define SQLITE_DBCONFIG_LOOKASIDE 1001 /* void* int int */ -#define SQLITE_DBCONFIG_ENABLE_FKEY 1002 /* int int* */ -#define SQLITE_DBCONFIG_ENABLE_TRIGGER 1003 /* int int* */ +#define SQLITE_DBCONFIG_LOOKASIDE 1001 /* void* int int */ +#define SQLITE_DBCONFIG_ENABLE_FKEY 1002 /* int int* */ +#define SQLITE_DBCONFIG_ENABLE_TRIGGER 1003 /* int int* */ +#define SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER 1004 /* int int* */ +#define SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION 1005 /* int int* */ /* @@ -4389,8 +4458,8 @@ SQLITE_API unsigned int SQLITE_STDCALL sqlite3_value_subtype(sqlite3_value*); ** previously obtained from [sqlite3_value_dup()]. ^If V is a NULL pointer ** then sqlite3_value_free(V) is a harmless no-op. */ -SQLITE_API SQLITE_EXPERIMENTAL sqlite3_value *SQLITE_STDCALL sqlite3_value_dup(const sqlite3_value*); -SQLITE_API SQLITE_EXPERIMENTAL void SQLITE_STDCALL sqlite3_value_free(sqlite3_value*); +SQLITE_API sqlite3_value *SQLITE_STDCALL sqlite3_value_dup(const sqlite3_value*); +SQLITE_API void SQLITE_STDCALL sqlite3_value_free(sqlite3_value*); /* ** CAPI3REF: Obtain Aggregate Function Context @@ -5136,7 +5205,7 @@ SQLITE_API void *SQLITE_STDCALL sqlite3_rollback_hook(sqlite3*, void(*)(void *), ** ^The sqlite3_update_hook() interface registers a callback function ** with the [database connection] identified by the first argument ** to be invoked whenever a row is updated, inserted or deleted in -** a rowid table. +** a [rowid table]. ** ^Any callback set by a previous call to this function ** for the same database connection is overridden. ** @@ -5175,8 +5244,8 @@ SQLITE_API void *SQLITE_STDCALL sqlite3_rollback_hook(sqlite3*, void(*)(void *), ** on the same [database connection] D, or NULL for ** the first call on D. ** -** See also the [sqlite3_commit_hook()] and [sqlite3_rollback_hook()] -** interfaces. +** See also the [sqlite3_commit_hook()], [sqlite3_rollback_hook()], +** and [sqlite3_preupdate_hook()] interfaces. */ SQLITE_API void *SQLITE_STDCALL sqlite3_update_hook( sqlite3*, @@ -5423,9 +5492,18 @@ SQLITE_API int SQLITE_STDCALL sqlite3_table_column_metadata( ** should free this memory by calling [sqlite3_free()]. ** ** ^Extension loading must be enabled using -** [sqlite3_enable_load_extension()] prior to calling this API, +** [sqlite3_enable_load_extension()] or +** [sqlite3_db_config](db,[SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION],1,NULL) +** prior to calling this API, ** otherwise an error will be returned. ** +** Security warning: It is recommended that the +** [SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION] method be used to enable only this +** interface. The use of the [sqlite3_enable_load_extension()] interface +** should be avoided. This will keep the SQL function [load_extension()] +** disabled and prevent SQL injections from giving attackers +** access to extension loading capabilities. +** ** See also the [load_extension() SQL function]. */ SQLITE_API int SQLITE_STDCALL sqlite3_load_extension( @@ -5448,6 +5526,17 @@ SQLITE_API int SQLITE_STDCALL sqlite3_load_extension( ** ^Call the sqlite3_enable_load_extension() routine with onoff==1 ** to turn extension loading on and call it with onoff==0 to turn ** it back off again. +** +** ^This interface enables or disables both the C-API +** [sqlite3_load_extension()] and the SQL function [load_extension()]. +** Use [sqlite3_db_config](db,[SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION],..) +** to enable or disable only the C-API. +** +** Security warning: It is recommended that extension loading +** be disabled using the [SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION] method +** rather than this interface, so the [load_extension()] SQL function +** remains disabled. This will prevent SQL injections from giving attackers +** access to extension loading capabilities. */ SQLITE_API int SQLITE_STDCALL sqlite3_enable_load_extension(sqlite3 *db, int onoff); @@ -5609,6 +5698,17 @@ struct sqlite3_module { ** ^Information about the ORDER BY clause is stored in aOrderBy[]. ** ^Each term of aOrderBy records a column of the ORDER BY clause. ** +** The colUsed field indicates which columns of the virtual table may be +** required by the current scan. Virtual table columns are numbered from +** zero in the order in which they appear within the CREATE TABLE statement +** passed to sqlite3_declare_vtab(). For the first 63 columns (columns 0-62), +** the corresponding bit is set within the colUsed mask if the column may be +** required by SQLite. If the table has at least 64 columns and any column +** to the right of the first 63 is required, then bit 63 of colUsed is also +** set. In other words, column iCol may be required if the expression +** (colUsed & ((sqlite3_uint64)1 << (iCol>=63 ? 63 : iCol))) evaluates to +** non-zero. +** ** The [xBestIndex] method must fill aConstraintUsage[] with information ** about what parameters to pass to xFilter. ^If argvIndex>0 then ** the right-hand side of the corresponding aConstraint[] is evaluated @@ -5664,7 +5764,7 @@ struct sqlite3_index_info { /* Inputs */ int nConstraint; /* Number of entries in aConstraint */ struct sqlite3_index_constraint { - int iColumn; /* Column on left-hand side of constraint */ + int iColumn; /* Column constrained. -1 for ROWID */ unsigned char op; /* Constraint operator */ unsigned char usable; /* True if this constraint is usable */ int iTermOffset; /* Used internally - xBestIndex should ignore */ @@ -5688,6 +5788,8 @@ struct sqlite3_index_info { sqlite3_int64 estimatedRows; /* Estimated number of rows returned */ /* Fields below are only available in SQLite 3.9.0 and later */ int idxFlags; /* Mask of SQLITE_INDEX_SCAN_* flags */ + /* Fields below are only available in SQLite 3.10.0 and later */ + sqlite3_uint64 colUsed; /* Input: Mask of columns used by statement */ }; /* @@ -5703,12 +5805,15 @@ struct sqlite3_index_info { ** an operator that is part of a constraint term in the wHERE clause of ** a query that uses a [virtual table]. */ -#define SQLITE_INDEX_CONSTRAINT_EQ 2 -#define SQLITE_INDEX_CONSTRAINT_GT 4 -#define SQLITE_INDEX_CONSTRAINT_LE 8 -#define SQLITE_INDEX_CONSTRAINT_LT 16 -#define SQLITE_INDEX_CONSTRAINT_GE 32 -#define SQLITE_INDEX_CONSTRAINT_MATCH 64 +#define SQLITE_INDEX_CONSTRAINT_EQ 2 +#define SQLITE_INDEX_CONSTRAINT_GT 4 +#define SQLITE_INDEX_CONSTRAINT_LE 8 +#define SQLITE_INDEX_CONSTRAINT_LT 16 +#define SQLITE_INDEX_CONSTRAINT_GE 32 +#define SQLITE_INDEX_CONSTRAINT_MATCH 64 +#define SQLITE_INDEX_CONSTRAINT_LIKE 65 +#define SQLITE_INDEX_CONSTRAINT_GLOB 66 +#define SQLITE_INDEX_CONSTRAINT_REGEXP 67 /* ** CAPI3REF: Register A Virtual Table Implementation @@ -6572,7 +6677,8 @@ SQLITE_API int SQLITE_STDCALL sqlite3_status64( ** The value written into the *pCurrent parameter is undefined.)^ ** ** [[SQLITE_STATUS_PARSER_STACK]] ^(
    SQLITE_STATUS_PARSER_STACK
    -**
    This parameter records the deepest parser stack. It is only +**
    The *pHighwater parameter records the deepest parser stack. +** The *pCurrent value is undefined. The *pHighwater value is only ** meaningful if SQLite is compiled with [YYTRACKMAXSTACKDEPTH].
    )^ ** ** @@ -7069,7 +7175,7 @@ typedef struct sqlite3_backup sqlite3_backup; ** must be different or else sqlite3_backup_init(D,N,S,M) will fail with ** an error. ** -** ^A call to sqlite3_backup_init() will fail, returning SQLITE_ERROR, if +** ^A call to sqlite3_backup_init() will fail, returning NULL, if ** there is already a read or read-write transaction open on the ** destination database. ** @@ -7358,18 +7464,43 @@ SQLITE_API int SQLITE_STDCALL sqlite3_strnicmp(const char *, const char *, int); /* ** CAPI3REF: String Globbing * -** ^The [sqlite3_strglob(P,X)] interface returns zero if string X matches -** the glob pattern P, and it returns non-zero if string X does not match -** the glob pattern P. ^The definition of glob pattern matching used in +** ^The [sqlite3_strglob(P,X)] interface returns zero if and only if +** string X matches the [GLOB] pattern P. +** ^The definition of [GLOB] pattern matching used in ** [sqlite3_strglob(P,X)] is the same as for the "X GLOB P" operator in the -** SQL dialect used by SQLite. ^The sqlite3_strglob(P,X) function is case -** sensitive. +** SQL dialect understood by SQLite. ^The [sqlite3_strglob(P,X)] function +** is case sensitive. ** ** Note that this routine returns zero on a match and non-zero if the strings ** do not match, the same as [sqlite3_stricmp()] and [sqlite3_strnicmp()]. +** +** See also: [sqlite3_strlike()]. */ SQLITE_API int SQLITE_STDCALL sqlite3_strglob(const char *zGlob, const char *zStr); +/* +** CAPI3REF: String LIKE Matching +* +** ^The [sqlite3_strlike(P,X,E)] interface returns zero if and only if +** string X matches the [LIKE] pattern P with escape character E. +** ^The definition of [LIKE] pattern matching used in +** [sqlite3_strlike(P,X,E)] is the same as for the "X LIKE P ESCAPE E" +** operator in the SQL dialect understood by SQLite. ^For "X LIKE P" without +** the ESCAPE clause, set the E parameter of [sqlite3_strlike(P,X,E)] to 0. +** ^As with the LIKE operator, the [sqlite3_strlike(P,X,E)] function is case +** insensitive - equivalent upper and lower case ASCII characters match +** one another. +** +** ^The [sqlite3_strlike(P,X,E)] function matches Unicode characters, though +** only ASCII characters are case folded. +** +** Note that this routine returns zero on a match and non-zero if the strings +** do not match, the same as [sqlite3_stricmp()] and [sqlite3_strnicmp()]. +** +** See also: [sqlite3_strglob()]. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_strlike(const char *zGlob, const char *zStr, unsigned int cEsc); + /* ** CAPI3REF: Error Logging Interface ** @@ -7425,7 +7556,7 @@ SQLITE_API void SQLITE_CDECL sqlite3_log(int iErrCode, const char *zFormat, ...) ** previously registered write-ahead log callback. ^Note that the ** [sqlite3_wal_autocheckpoint()] interface and the ** [wal_autocheckpoint pragma] both invoke [sqlite3_wal_hook()] and will -** those overwrite any prior [sqlite3_wal_hook()] settings. +** overwrite any prior [sqlite3_wal_hook()] settings. */ SQLITE_API void *SQLITE_STDCALL sqlite3_wal_hook( sqlite3*, @@ -7790,6 +7921,277 @@ SQLITE_API int SQLITE_STDCALL sqlite3_stmt_scanstatus( */ SQLITE_API void SQLITE_STDCALL sqlite3_stmt_scanstatus_reset(sqlite3_stmt*); +/* +** CAPI3REF: Flush caches to disk mid-transaction +** +** ^If a write-transaction is open on [database connection] D when the +** [sqlite3_db_cacheflush(D)] interface invoked, any dirty +** pages in the pager-cache that are not currently in use are written out +** to disk. A dirty page may be in use if a database cursor created by an +** active SQL statement is reading from it, or if it is page 1 of a database +** file (page 1 is always "in use"). ^The [sqlite3_db_cacheflush(D)] +** interface flushes caches for all schemas - "main", "temp", and +** any [attached] databases. +** +** ^If this function needs to obtain extra database locks before dirty pages +** can be flushed to disk, it does so. ^If those locks cannot be obtained +** immediately and there is a busy-handler callback configured, it is invoked +** in the usual manner. ^If the required lock still cannot be obtained, then +** the database is skipped and an attempt made to flush any dirty pages +** belonging to the next (if any) database. ^If any databases are skipped +** because locks cannot be obtained, but no other error occurs, this +** function returns SQLITE_BUSY. +** +** ^If any other error occurs while flushing dirty pages to disk (for +** example an IO error or out-of-memory condition), then processing is +** abandoned and an SQLite [error code] is returned to the caller immediately. +** +** ^Otherwise, if no error occurs, [sqlite3_db_cacheflush()] returns SQLITE_OK. +** +** ^This function does not set the database handle error code or message +** returned by the [sqlite3_errcode()] and [sqlite3_errmsg()] functions. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_db_cacheflush(sqlite3*); + +/* +** CAPI3REF: The pre-update hook. +** +** ^These interfaces are only available if SQLite is compiled using the +** [SQLITE_ENABLE_PREUPDATE_HOOK] compile-time option. +** +** ^The [sqlite3_preupdate_hook()] interface registers a callback function +** that is invoked prior to each [INSERT], [UPDATE], and [DELETE] operation +** on a [rowid table]. +** ^At most one preupdate hook may be registered at a time on a single +** [database connection]; each call to [sqlite3_preupdate_hook()] overrides +** the previous setting. +** ^The preupdate hook is disabled by invoking [sqlite3_preupdate_hook()] +** with a NULL pointer as the second parameter. +** ^The third parameter to [sqlite3_preupdate_hook()] is passed through as +** the first parameter to callbacks. +** +** ^The preupdate hook only fires for changes to [rowid tables]; the preupdate +** hook is not invoked for changes to [virtual tables] or [WITHOUT ROWID] +** tables. +** +** ^The second parameter to the preupdate callback is a pointer to +** the [database connection] that registered the preupdate hook. +** ^The third parameter to the preupdate callback is one of the constants +** [SQLITE_INSERT], [SQLITE_DELETE], or [SQLITE_UPDATE] to indentify the +** kind of update operation that is about to occur. +** ^(The fourth parameter to the preupdate callback is the name of the +** database within the database connection that is being modified. This +** will be "main" for the main database or "temp" for TEMP tables or +** the name given after the AS keyword in the [ATTACH] statement for attached +** databases.)^ +** ^The fifth parameter to the preupdate callback is the name of the +** table that is being modified. +** ^The sixth parameter to the preupdate callback is the initial [rowid] of the +** row being changes for SQLITE_UPDATE and SQLITE_DELETE changes and is +** undefined for SQLITE_INSERT changes. +** ^The seventh parameter to the preupdate callback is the final [rowid] of +** the row being changed for SQLITE_UPDATE and SQLITE_INSERT changes and is +** undefined for SQLITE_DELETE changes. +** +** The [sqlite3_preupdate_old()], [sqlite3_preupdate_new()], +** [sqlite3_preupdate_count()], and [sqlite3_preupdate_depth()] interfaces +** provide additional information about a preupdate event. These routines +** may only be called from within a preupdate callback. Invoking any of +** these routines from outside of a preupdate callback or with a +** [database connection] pointer that is different from the one supplied +** to the preupdate callback results in undefined and probably undesirable +** behavior. +** +** ^The [sqlite3_preupdate_count(D)] interface returns the number of columns +** in the row that is being inserted, updated, or deleted. +** +** ^The [sqlite3_preupdate_old(D,N,P)] interface writes into P a pointer to +** a [protected sqlite3_value] that contains the value of the Nth column of +** the table row before it is updated. The N parameter must be between 0 +** and one less than the number of columns or the behavior will be +** undefined. This must only be used within SQLITE_UPDATE and SQLITE_DELETE +** preupdate callbacks; if it is used by an SQLITE_INSERT callback then the +** behavior is undefined. The [sqlite3_value] that P points to +** will be destroyed when the preupdate callback returns. +** +** ^The [sqlite3_preupdate_new(D,N,P)] interface writes into P a pointer to +** a [protected sqlite3_value] that contains the value of the Nth column of +** the table row after it is updated. The N parameter must be between 0 +** and one less than the number of columns or the behavior will be +** undefined. This must only be used within SQLITE_INSERT and SQLITE_UPDATE +** preupdate callbacks; if it is used by an SQLITE_DELETE callback then the +** behavior is undefined. The [sqlite3_value] that P points to +** will be destroyed when the preupdate callback returns. +** +** ^The [sqlite3_preupdate_depth(D)] interface returns 0 if the preupdate +** callback was invoked as a result of a direct insert, update, or delete +** operation; or 1 for inserts, updates, or deletes invoked by top-level +** triggers; or 2 for changes resulting from triggers called by top-level +** triggers; and so forth. +** +** See also: [sqlite3_update_hook()] +*/ +SQLITE_API SQLITE_EXPERIMENTAL void *SQLITE_STDCALL sqlite3_preupdate_hook( + sqlite3 *db, + void(*xPreUpdate)( + void *pCtx, /* Copy of third arg to preupdate_hook() */ + sqlite3 *db, /* Database handle */ + int op, /* SQLITE_UPDATE, DELETE or INSERT */ + char const *zDb, /* Database name */ + char const *zName, /* Table name */ + sqlite3_int64 iKey1, /* Rowid of row about to be deleted/updated */ + sqlite3_int64 iKey2 /* New rowid value (for a rowid UPDATE) */ + ), + void* +); +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_preupdate_old(sqlite3 *, int, sqlite3_value **); +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_preupdate_count(sqlite3 *); +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_preupdate_depth(sqlite3 *); +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_preupdate_new(sqlite3 *, int, sqlite3_value **); + +/* +** CAPI3REF: Low-level system error code +** +** ^Attempt to return the underlying operating system error code or error +** number that caused the most recent I/O error or failure to open a file. +** The return value is OS-dependent. For example, on unix systems, after +** [sqlite3_open_v2()] returns [SQLITE_CANTOPEN], this interface could be +** called to get back the underlying "errno" that caused the problem, such +** as ENOSPC, EAUTH, EISDIR, and so forth. +*/ +SQLITE_API int SQLITE_STDCALL sqlite3_system_errno(sqlite3*); + +/* +** CAPI3REF: Database Snapshot +** KEYWORDS: {snapshot} +** EXPERIMENTAL +** +** An instance of the snapshot object records the state of a [WAL mode] +** database for some specific point in history. +** +** In [WAL mode], multiple [database connections] that are open on the +** same database file can each be reading a different historical version +** of the database file. When a [database connection] begins a read +** transaction, that connection sees an unchanging copy of the database +** as it existed for the point in time when the transaction first started. +** Subsequent changes to the database from other connections are not seen +** by the reader until a new read transaction is started. +** +** The sqlite3_snapshot object records state information about an historical +** version of the database file so that it is possible to later open a new read +** transaction that sees that historical version of the database rather than +** the most recent version. +** +** The constructor for this object is [sqlite3_snapshot_get()]. The +** [sqlite3_snapshot_open()] method causes a fresh read transaction to refer +** to an historical snapshot (if possible). The destructor for +** sqlite3_snapshot objects is [sqlite3_snapshot_free()]. +*/ +typedef struct sqlite3_snapshot sqlite3_snapshot; + +/* +** CAPI3REF: Record A Database Snapshot +** EXPERIMENTAL +** +** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a +** new [sqlite3_snapshot] object that records the current state of +** schema S in database connection D. ^On success, the +** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly +** created [sqlite3_snapshot] object into *P and returns SQLITE_OK. +** ^If schema S of [database connection] D is not a [WAL mode] database +** that is in a read transaction, then [sqlite3_snapshot_get(D,S,P)] +** leaves the *P value unchanged and returns an appropriate [error code]. +** +** The [sqlite3_snapshot] object returned from a successful call to +** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()] +** to avoid a memory leak. +** +** The [sqlite3_snapshot_get()] interface is only available when the +** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +*/ +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_snapshot_get( + sqlite3 *db, + const char *zSchema, + sqlite3_snapshot **ppSnapshot +); + +/* +** CAPI3REF: Start a read transaction on an historical snapshot +** EXPERIMENTAL +** +** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a +** read transaction for schema S of +** [database connection] D such that the read transaction +** refers to historical [snapshot] P, rather than the most +** recent change to the database. +** ^The [sqlite3_snapshot_open()] interface returns SQLITE_OK on success +** or an appropriate [error code] if it fails. +** +** ^In order to succeed, a call to [sqlite3_snapshot_open(D,S,P)] must be +** the first operation following the [BEGIN] that takes the schema S +** out of [autocommit mode]. +** ^In other words, schema S must not currently be in +** a transaction for [sqlite3_snapshot_open(D,S,P)] to work, but the +** database connection D must be out of [autocommit mode]. +** ^A [snapshot] will fail to open if it has been overwritten by a +** [checkpoint]. +** ^(A call to [sqlite3_snapshot_open(D,S,P)] will fail if the +** database connection D does not know that the database file for +** schema S is in [WAL mode]. A database connection might not know +** that the database file is in [WAL mode] if there has been no prior +** I/O on that database connection, or if the database entered [WAL mode] +** after the most recent I/O on the database connection.)^ +** (Hint: Run "[PRAGMA application_id]" against a newly opened +** database connection in order to make it ready to use snapshots.) +** +** The [sqlite3_snapshot_open()] interface is only available when the +** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +*/ +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_snapshot_open( + sqlite3 *db, + const char *zSchema, + sqlite3_snapshot *pSnapshot +); + +/* +** CAPI3REF: Destroy a snapshot +** EXPERIMENTAL +** +** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P. +** The application must eventually free every [sqlite3_snapshot] object +** using this routine to avoid a memory leak. +** +** The [sqlite3_snapshot_free()] interface is only available when the +** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +*/ +SQLITE_API SQLITE_EXPERIMENTAL void SQLITE_STDCALL sqlite3_snapshot_free(sqlite3_snapshot*); + +/* +** CAPI3REF: Compare the ages of two snapshot handles. +** EXPERIMENTAL +** +** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages +** of two valid snapshot handles. +** +** If the two snapshot handles are not associated with the same database +** file, the result of the comparison is undefined. +** +** Additionally, the result of the comparison is only valid if both of the +** snapshot handles were obtained by calling sqlite3_snapshot_get() since the +** last time the wal file was deleted. The wal file is deleted when the +** database is changed back to rollback mode or when the number of database +** clients drops to zero. If either snapshot handle was obtained before the +** wal file was last deleted, the value returned by this function +** is undefined. +** +** Otherwise, this API returns a negative value if P1 refers to an older +** snapshot than P2, zero if the two handles refer to the same database +** snapshot, and a positive value if P1 is a newer snapshot than P2. +*/ +SQLITE_API SQLITE_EXPERIMENTAL int SQLITE_STDCALL sqlite3_snapshot_cmp( + sqlite3_snapshot *p1, + sqlite3_snapshot *p2 +); /* ** Undo the hack that converts floating point types to integer for @@ -7804,6 +8206,7 @@ SQLITE_API void SQLITE_STDCALL sqlite3_stmt_scanstatus_reset(sqlite3_stmt*); #endif #endif /* _SQLITE3_H_ */ +/******** Begin file sqlite3rtree.h *********/ /* ** 2010 August 30 ** @@ -7921,6 +8324,1287 @@ struct sqlite3_rtree_query_info { #endif /* ifndef _SQLITE3RTREE_H_ */ +/******** End of sqlite3rtree.h *********/ +/******** Begin file sqlite3session.h *********/ + +#if !defined(__SQLITESESSION_H_) && defined(SQLITE_ENABLE_SESSION) +#define __SQLITESESSION_H_ 1 + +/* +** Make sure we can call this stuff from C++. +*/ +#ifdef __cplusplus +extern "C" { +#endif + + +/* +** CAPI3REF: Session Object Handle +*/ +typedef struct sqlite3_session sqlite3_session; + +/* +** CAPI3REF: Changeset Iterator Handle +*/ +typedef struct sqlite3_changeset_iter sqlite3_changeset_iter; + +/* +** CAPI3REF: Create A New Session Object +** +** Create a new session object attached to database handle db. If successful, +** a pointer to the new object is written to *ppSession and SQLITE_OK is +** returned. If an error occurs, *ppSession is set to NULL and an SQLite +** error code (e.g. SQLITE_NOMEM) is returned. +** +** It is possible to create multiple session objects attached to a single +** database handle. +** +** Session objects created using this function should be deleted using the +** [sqlite3session_delete()] function before the database handle that they +** are attached to is itself closed. If the database handle is closed before +** the session object is deleted, then the results of calling any session +** module function, including [sqlite3session_delete()] on the session object +** are undefined. +** +** Because the session module uses the [sqlite3_preupdate_hook()] API, it +** is not possible for an application to register a pre-update hook on a +** database handle that has one or more session objects attached. Nor is +** it possible to create a session object attached to a database handle for +** which a pre-update hook is already defined. The results of attempting +** either of these things are undefined. +** +** The session object will be used to create changesets for tables in +** database zDb, where zDb is either "main", or "temp", or the name of an +** attached database. It is not an error if database zDb is not attached +** to the database when the session object is created. +*/ +int sqlite3session_create( + sqlite3 *db, /* Database handle */ + const char *zDb, /* Name of db (e.g. "main") */ + sqlite3_session **ppSession /* OUT: New session object */ +); + +/* +** CAPI3REF: Delete A Session Object +** +** Delete a session object previously allocated using +** [sqlite3session_create()]. Once a session object has been deleted, the +** results of attempting to use pSession with any other session module +** function are undefined. +** +** Session objects must be deleted before the database handle to which they +** are attached is closed. Refer to the documentation for +** [sqlite3session_create()] for details. +*/ +void sqlite3session_delete(sqlite3_session *pSession); + + +/* +** CAPI3REF: Enable Or Disable A Session Object +** +** Enable or disable the recording of changes by a session object. When +** enabled, a session object records changes made to the database. When +** disabled - it does not. A newly created session object is enabled. +** Refer to the documentation for [sqlite3session_changeset()] for further +** details regarding how enabling and disabling a session object affects +** the eventual changesets. +** +** Passing zero to this function disables the session. Passing a value +** greater than zero enables it. Passing a value less than zero is a +** no-op, and may be used to query the current state of the session. +** +** The return value indicates the final state of the session object: 0 if +** the session is disabled, or 1 if it is enabled. +*/ +int sqlite3session_enable(sqlite3_session *pSession, int bEnable); + +/* +** CAPI3REF: Set Or Clear the Indirect Change Flag +** +** Each change recorded by a session object is marked as either direct or +** indirect. A change is marked as indirect if either: +** +**
      +**
    • The session object "indirect" flag is set when the change is +** made, or +**
    • The change is made by an SQL trigger or foreign key action +** instead of directly as a result of a users SQL statement. +**
    +** +** If a single row is affected by more than one operation within a session, +** then the change is considered indirect if all operations meet the criteria +** for an indirect change above, or direct otherwise. +** +** This function is used to set, clear or query the session object indirect +** flag. If the second argument passed to this function is zero, then the +** indirect flag is cleared. If it is greater than zero, the indirect flag +** is set. Passing a value less than zero does not modify the current value +** of the indirect flag, and may be used to query the current state of the +** indirect flag for the specified session object. +** +** The return value indicates the final state of the indirect flag: 0 if +** it is clear, or 1 if it is set. +*/ +int sqlite3session_indirect(sqlite3_session *pSession, int bIndirect); + +/* +** CAPI3REF: Attach A Table To A Session Object +** +** If argument zTab is not NULL, then it is the name of a table to attach +** to the session object passed as the first argument. All subsequent changes +** made to the table while the session object is enabled will be recorded. See +** documentation for [sqlite3session_changeset()] for further details. +** +** Or, if argument zTab is NULL, then changes are recorded for all tables +** in the database. If additional tables are added to the database (by +** executing "CREATE TABLE" statements) after this call is made, changes for +** the new tables are also recorded. +** +** Changes can only be recorded for tables that have a PRIMARY KEY explicitly +** defined as part of their CREATE TABLE statement. It does not matter if the +** PRIMARY KEY is an "INTEGER PRIMARY KEY" (rowid alias) or not. The PRIMARY +** KEY may consist of a single column, or may be a composite key. +** +** It is not an error if the named table does not exist in the database. Nor +** is it an error if the named table does not have a PRIMARY KEY. However, +** no changes will be recorded in either of these scenarios. +** +** Changes are not recorded for individual rows that have NULL values stored +** in one or more of their PRIMARY KEY columns. +** +** SQLITE_OK is returned if the call completes without error. Or, if an error +** occurs, an SQLite error code (e.g. SQLITE_NOMEM) is returned. +*/ +int sqlite3session_attach( + sqlite3_session *pSession, /* Session object */ + const char *zTab /* Table name */ +); + +/* +** CAPI3REF: Set a table filter on a Session Object. +** +** The second argument (xFilter) is the "filter callback". For changes to rows +** in tables that are not attached to the Session oject, the filter is called +** to determine whether changes to the table's rows should be tracked or not. +** If xFilter returns 0, changes is not tracked. Note that once a table is +** attached, xFilter will not be called again. +*/ +void sqlite3session_table_filter( + sqlite3_session *pSession, /* Session object */ + int(*xFilter)( + void *pCtx, /* Copy of third arg to _filter_table() */ + const char *zTab /* Table name */ + ), + void *pCtx /* First argument passed to xFilter */ +); + +/* +** CAPI3REF: Generate A Changeset From A Session Object +** +** Obtain a changeset containing changes to the tables attached to the +** session object passed as the first argument. If successful, +** set *ppChangeset to point to a buffer containing the changeset +** and *pnChangeset to the size of the changeset in bytes before returning +** SQLITE_OK. If an error occurs, set both *ppChangeset and *pnChangeset to +** zero and return an SQLite error code. +** +** A changeset consists of zero or more INSERT, UPDATE and/or DELETE changes, +** each representing a change to a single row of an attached table. An INSERT +** change contains the values of each field of a new database row. A DELETE +** contains the original values of each field of a deleted database row. An +** UPDATE change contains the original values of each field of an updated +** database row along with the updated values for each updated non-primary-key +** column. It is not possible for an UPDATE change to represent a change that +** modifies the values of primary key columns. If such a change is made, it +** is represented in a changeset as a DELETE followed by an INSERT. +** +** Changes are not recorded for rows that have NULL values stored in one or +** more of their PRIMARY KEY columns. If such a row is inserted or deleted, +** no corresponding change is present in the changesets returned by this +** function. If an existing row with one or more NULL values stored in +** PRIMARY KEY columns is updated so that all PRIMARY KEY columns are non-NULL, +** only an INSERT is appears in the changeset. Similarly, if an existing row +** with non-NULL PRIMARY KEY values is updated so that one or more of its +** PRIMARY KEY columns are set to NULL, the resulting changeset contains a +** DELETE change only. +** +** The contents of a changeset may be traversed using an iterator created +** using the [sqlite3changeset_start()] API. A changeset may be applied to +** a database with a compatible schema using the [sqlite3changeset_apply()] +** API. +** +** Within a changeset generated by this function, all changes related to a +** single table are grouped together. In other words, when iterating through +** a changeset or when applying a changeset to a database, all changes related +** to a single table are processed before moving on to the next table. Tables +** are sorted in the same order in which they were attached (or auto-attached) +** to the sqlite3_session object. The order in which the changes related to +** a single table are stored is undefined. +** +** Following a successful call to this function, it is the responsibility of +** the caller to eventually free the buffer that *ppChangeset points to using +** [sqlite3_free()]. +** +**

    Changeset Generation

    +** +** Once a table has been attached to a session object, the session object +** records the primary key values of all new rows inserted into the table. +** It also records the original primary key and other column values of any +** deleted or updated rows. For each unique primary key value, data is only +** recorded once - the first time a row with said primary key is inserted, +** updated or deleted in the lifetime of the session. +** +** There is one exception to the previous paragraph: when a row is inserted, +** updated or deleted, if one or more of its primary key columns contain a +** NULL value, no record of the change is made. +** +** The session object therefore accumulates two types of records - those +** that consist of primary key values only (created when the user inserts +** a new record) and those that consist of the primary key values and the +** original values of other table columns (created when the users deletes +** or updates a record). +** +** When this function is called, the requested changeset is created using +** both the accumulated records and the current contents of the database +** file. Specifically: +** +**
      +**
    • For each record generated by an insert, the database is queried +** for a row with a matching primary key. If one is found, an INSERT +** change is added to the changeset. If no such row is found, no change +** is added to the changeset. +** +**
    • For each record generated by an update or delete, the database is +** queried for a row with a matching primary key. If such a row is +** found and one or more of the non-primary key fields have been +** modified from their original values, an UPDATE change is added to +** the changeset. Or, if no such row is found in the table, a DELETE +** change is added to the changeset. If there is a row with a matching +** primary key in the database, but all fields contain their original +** values, no change is added to the changeset. +**
    +** +** This means, amongst other things, that if a row is inserted and then later +** deleted while a session object is active, neither the insert nor the delete +** will be present in the changeset. Or if a row is deleted and then later a +** row with the same primary key values inserted while a session object is +** active, the resulting changeset will contain an UPDATE change instead of +** a DELETE and an INSERT. +** +** When a session object is disabled (see the [sqlite3session_enable()] API), +** it does not accumulate records when rows are inserted, updated or deleted. +** This may appear to have some counter-intuitive effects if a single row +** is written to more than once during a session. For example, if a row +** is inserted while a session object is enabled, then later deleted while +** the same session object is disabled, no INSERT record will appear in the +** changeset, even though the delete took place while the session was disabled. +** Or, if one field of a row is updated while a session is disabled, and +** another field of the same row is updated while the session is enabled, the +** resulting changeset will contain an UPDATE change that updates both fields. +*/ +int sqlite3session_changeset( + sqlite3_session *pSession, /* Session object */ + int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + void **ppChangeset /* OUT: Buffer containing changeset */ +); + +/* +** CAPI3REF: Load The Difference Between Tables Into A Session +** +** If it is not already attached to the session object passed as the first +** argument, this function attaches table zTbl in the same manner as the +** [sqlite3session_attach()] function. If zTbl does not exist, or if it +** does not have a primary key, this function is a no-op (but does not return +** an error). +** +** Argument zFromDb must be the name of a database ("main", "temp" etc.) +** attached to the same database handle as the session object that contains +** a table compatible with the table attached to the session by this function. +** A table is considered compatible if it: +** +**
      +**
    • Has the same name, +**
    • Has the same set of columns declared in the same order, and +**
    • Has the same PRIMARY KEY definition. +**
    +** +** If the tables are not compatible, SQLITE_SCHEMA is returned. If the tables +** are compatible but do not have any PRIMARY KEY columns, it is not an error +** but no changes are added to the session object. As with other session +** APIs, tables without PRIMARY KEYs are simply ignored. +** +** This function adds a set of changes to the session object that could be +** used to update the table in database zFrom (call this the "from-table") +** so that its content is the same as the table attached to the session +** object (call this the "to-table"). Specifically: +** +**
      +**
    • For each row (primary key) that exists in the to-table but not in +** the from-table, an INSERT record is added to the session object. +** +**
    • For each row (primary key) that exists in the to-table but not in +** the from-table, a DELETE record is added to the session object. +** +**
    • For each row (primary key) that exists in both tables, but features +** different in each, an UPDATE record is added to the session. +**
    +** +** To clarify, if this function is called and then a changeset constructed +** using [sqlite3session_changeset()], then after applying that changeset to +** database zFrom the contents of the two compatible tables would be +** identical. +** +** It an error if database zFrom does not exist or does not contain the +** required compatible table. +** +** If the operation successful, SQLITE_OK is returned. Otherwise, an SQLite +** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg +** may be set to point to a buffer containing an English language error +** message. It is the responsibility of the caller to free this buffer using +** sqlite3_free(). +*/ +int sqlite3session_diff( + sqlite3_session *pSession, + const char *zFromDb, + const char *zTbl, + char **pzErrMsg +); + + +/* +** CAPI3REF: Generate A Patchset From A Session Object +** +** The differences between a patchset and a changeset are that: +** +**
      +**
    • DELETE records consist of the primary key fields only. The +** original values of other fields are omitted. +**
    • The original values of any modified fields are omitted from +** UPDATE records. +**
    +** +** A patchset blob may be used with up to date versions of all +** sqlite3changeset_xxx API functions except for sqlite3changeset_invert(), +** which returns SQLITE_CORRUPT if it is passed a patchset. Similarly, +** attempting to use a patchset blob with old versions of the +** sqlite3changeset_xxx APIs also provokes an SQLITE_CORRUPT error. +** +** Because the non-primary key "old.*" fields are omitted, no +** SQLITE_CHANGESET_DATA conflicts can be detected or reported if a patchset +** is passed to the sqlite3changeset_apply() API. Other conflict types work +** in the same way as for changesets. +** +** Changes within a patchset are ordered in the same way as for changesets +** generated by the sqlite3session_changeset() function (i.e. all changes for +** a single table are grouped together, tables appear in the order in which +** they were attached to the session object). +*/ +int sqlite3session_patchset( + sqlite3_session *pSession, /* Session object */ + int *pnPatchset, /* OUT: Size of buffer at *ppChangeset */ + void **ppPatchset /* OUT: Buffer containing changeset */ +); + +/* +** CAPI3REF: Test if a changeset has recorded any changes. +** +** Return non-zero if no changes to attached tables have been recorded by +** the session object passed as the first argument. Otherwise, if one or +** more changes have been recorded, return zero. +** +** Even if this function returns zero, it is possible that calling +** [sqlite3session_changeset()] on the session handle may still return a +** changeset that contains no changes. This can happen when a row in +** an attached table is modified and then later on the original values +** are restored. However, if this function returns non-zero, then it is +** guaranteed that a call to sqlite3session_changeset() will return a +** changeset containing zero changes. +*/ +int sqlite3session_isempty(sqlite3_session *pSession); + +/* +** CAPI3REF: Create An Iterator To Traverse A Changeset +** +** Create an iterator used to iterate through the contents of a changeset. +** If successful, *pp is set to point to the iterator handle and SQLITE_OK +** is returned. Otherwise, if an error occurs, *pp is set to zero and an +** SQLite error code is returned. +** +** The following functions can be used to advance and query a changeset +** iterator created by this function: +** +**
      +**
    • [sqlite3changeset_next()] +**
    • [sqlite3changeset_op()] +**
    • [sqlite3changeset_new()] +**
    • [sqlite3changeset_old()] +**
    +** +** It is the responsibility of the caller to eventually destroy the iterator +** by passing it to [sqlite3changeset_finalize()]. The buffer containing the +** changeset (pChangeset) must remain valid until after the iterator is +** destroyed. +** +** Assuming the changeset blob was created by one of the +** [sqlite3session_changeset()], [sqlite3changeset_concat()] or +** [sqlite3changeset_invert()] functions, all changes within the changeset +** that apply to a single table are grouped together. This means that when +** an application iterates through a changeset using an iterator created by +** this function, all changes that relate to a single table are visted +** consecutively. There is no chance that the iterator will visit a change +** the applies to table X, then one for table Y, and then later on visit +** another change for table X. +*/ +int sqlite3changeset_start( + sqlite3_changeset_iter **pp, /* OUT: New changeset iterator handle */ + int nChangeset, /* Size of changeset blob in bytes */ + void *pChangeset /* Pointer to blob containing changeset */ +); + + +/* +** CAPI3REF: Advance A Changeset Iterator +** +** This function may only be used with iterators created by function +** [sqlite3changeset_start()]. If it is called on an iterator passed to +** a conflict-handler callback by [sqlite3changeset_apply()], SQLITE_MISUSE +** is returned and the call has no effect. +** +** Immediately after an iterator is created by sqlite3changeset_start(), it +** does not point to any change in the changeset. Assuming the changeset +** is not empty, the first call to this function advances the iterator to +** point to the first change in the changeset. Each subsequent call advances +** the iterator to point to the next change in the changeset (if any). If +** no error occurs and the iterator points to a valid change after a call +** to sqlite3changeset_next() has advanced it, SQLITE_ROW is returned. +** Otherwise, if all changes in the changeset have already been visited, +** SQLITE_DONE is returned. +** +** If an error occurs, an SQLite error code is returned. Possible error +** codes include SQLITE_CORRUPT (if the changeset buffer is corrupt) or +** SQLITE_NOMEM. +*/ +int sqlite3changeset_next(sqlite3_changeset_iter *pIter); + +/* +** CAPI3REF: Obtain The Current Operation From A Changeset Iterator +** +** The pIter argument passed to this function may either be an iterator +** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator +** created by [sqlite3changeset_start()]. In the latter case, the most recent +** call to [sqlite3changeset_next()] must have returned [SQLITE_ROW]. If this +** is not the case, this function returns [SQLITE_MISUSE]. +** +** If argument pzTab is not NULL, then *pzTab is set to point to a +** nul-terminated utf-8 encoded string containing the name of the table +** affected by the current change. The buffer remains valid until either +** sqlite3changeset_next() is called on the iterator or until the +** conflict-handler function returns. If pnCol is not NULL, then *pnCol is +** set to the number of columns in the table affected by the change. If +** pbIncorrect is not NULL, then *pbIndirect is set to true (1) if the change +** is an indirect change, or false (0) otherwise. See the documentation for +** [sqlite3session_indirect()] for a description of direct and indirect +** changes. Finally, if pOp is not NULL, then *pOp is set to one of +** [SQLITE_INSERT], [SQLITE_DELETE] or [SQLITE_UPDATE], depending on the +** type of change that the iterator currently points to. +** +** If no error occurs, SQLITE_OK is returned. If an error does occur, an +** SQLite error code is returned. The values of the output variables may not +** be trusted in this case. +*/ +int sqlite3changeset_op( + sqlite3_changeset_iter *pIter, /* Iterator object */ + const char **pzTab, /* OUT: Pointer to table name */ + int *pnCol, /* OUT: Number of columns in table */ + int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + int *pbIndirect /* OUT: True for an 'indirect' change */ +); + +/* +** CAPI3REF: Obtain The Primary Key Definition Of A Table +** +** For each modified table, a changeset includes the following: +** +**
      +**
    • The number of columns in the table, and +**
    • Which of those columns make up the tables PRIMARY KEY. +**
    +** +** This function is used to find which columns comprise the PRIMARY KEY of +** the table modified by the change that iterator pIter currently points to. +** If successful, *pabPK is set to point to an array of nCol entries, where +** nCol is the number of columns in the table. Elements of *pabPK are set to +** 0x01 if the corresponding column is part of the tables primary key, or +** 0x00 if it is not. +** +** If argumet pnCol is not NULL, then *pnCol is set to the number of columns +** in the table. +** +** If this function is called when the iterator does not point to a valid +** entry, SQLITE_MISUSE is returned and the output variables zeroed. Otherwise, +** SQLITE_OK is returned and the output variables populated as described +** above. +*/ +int sqlite3changeset_pk( + sqlite3_changeset_iter *pIter, /* Iterator object */ + unsigned char **pabPK, /* OUT: Array of boolean - true for PK cols */ + int *pnCol /* OUT: Number of entries in output array */ +); + +/* +** CAPI3REF: Obtain old.* Values From A Changeset Iterator +** +** The pIter argument passed to this function may either be an iterator +** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator +** created by [sqlite3changeset_start()]. In the latter case, the most recent +** call to [sqlite3changeset_next()] must have returned SQLITE_ROW. +** Furthermore, it may only be called if the type of change that the iterator +** currently points to is either [SQLITE_DELETE] or [SQLITE_UPDATE]. Otherwise, +** this function returns [SQLITE_MISUSE] and sets *ppValue to NULL. +** +** Argument iVal must be greater than or equal to 0, and less than the number +** of columns in the table affected by the current change. Otherwise, +** [SQLITE_RANGE] is returned and *ppValue is set to NULL. +** +** If successful, this function sets *ppValue to point to a protected +** sqlite3_value object containing the iVal'th value from the vector of +** original row values stored as part of the UPDATE or DELETE change and +** returns SQLITE_OK. The name of the function comes from the fact that this +** is similar to the "old.*" columns available to update or delete triggers. +** +** If some other error occurs (e.g. an OOM condition), an SQLite error code +** is returned and *ppValue is set to NULL. +*/ +int sqlite3changeset_old( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Column number */ + sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */ +); + +/* +** CAPI3REF: Obtain new.* Values From A Changeset Iterator +** +** The pIter argument passed to this function may either be an iterator +** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator +** created by [sqlite3changeset_start()]. In the latter case, the most recent +** call to [sqlite3changeset_next()] must have returned SQLITE_ROW. +** Furthermore, it may only be called if the type of change that the iterator +** currently points to is either [SQLITE_UPDATE] or [SQLITE_INSERT]. Otherwise, +** this function returns [SQLITE_MISUSE] and sets *ppValue to NULL. +** +** Argument iVal must be greater than or equal to 0, and less than the number +** of columns in the table affected by the current change. Otherwise, +** [SQLITE_RANGE] is returned and *ppValue is set to NULL. +** +** If successful, this function sets *ppValue to point to a protected +** sqlite3_value object containing the iVal'th value from the vector of +** new row values stored as part of the UPDATE or INSERT change and +** returns SQLITE_OK. If the change is an UPDATE and does not include +** a new value for the requested column, *ppValue is set to NULL and +** SQLITE_OK returned. The name of the function comes from the fact that +** this is similar to the "new.*" columns available to update or delete +** triggers. +** +** If some other error occurs (e.g. an OOM condition), an SQLite error code +** is returned and *ppValue is set to NULL. +*/ +int sqlite3changeset_new( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Column number */ + sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */ +); + +/* +** CAPI3REF: Obtain Conflicting Row Values From A Changeset Iterator +** +** This function should only be used with iterator objects passed to a +** conflict-handler callback by [sqlite3changeset_apply()] with either +** [SQLITE_CHANGESET_DATA] or [SQLITE_CHANGESET_CONFLICT]. If this function +** is called on any other iterator, [SQLITE_MISUSE] is returned and *ppValue +** is set to NULL. +** +** Argument iVal must be greater than or equal to 0, and less than the number +** of columns in the table affected by the current change. Otherwise, +** [SQLITE_RANGE] is returned and *ppValue is set to NULL. +** +** If successful, this function sets *ppValue to point to a protected +** sqlite3_value object containing the iVal'th value from the +** "conflicting row" associated with the current conflict-handler callback +** and returns SQLITE_OK. +** +** If some other error occurs (e.g. an OOM condition), an SQLite error code +** is returned and *ppValue is set to NULL. +*/ +int sqlite3changeset_conflict( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Column number */ + sqlite3_value **ppValue /* OUT: Value from conflicting row */ +); + +/* +** CAPI3REF: Determine The Number Of Foreign Key Constraint Violations +** +** This function may only be called with an iterator passed to an +** SQLITE_CHANGESET_FOREIGN_KEY conflict handler callback. In this case +** it sets the output variable to the total number of known foreign key +** violations in the destination database and returns SQLITE_OK. +** +** In all other cases this function returns SQLITE_MISUSE. +*/ +int sqlite3changeset_fk_conflicts( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int *pnOut /* OUT: Number of FK violations */ +); + + +/* +** CAPI3REF: Finalize A Changeset Iterator +** +** This function is used to finalize an iterator allocated with +** [sqlite3changeset_start()]. +** +** This function should only be called on iterators created using the +** [sqlite3changeset_start()] function. If an application calls this +** function with an iterator passed to a conflict-handler by +** [sqlite3changeset_apply()], [SQLITE_MISUSE] is immediately returned and the +** call has no effect. +** +** If an error was encountered within a call to an sqlite3changeset_xxx() +** function (for example an [SQLITE_CORRUPT] in [sqlite3changeset_next()] or an +** [SQLITE_NOMEM] in [sqlite3changeset_new()]) then an error code corresponding +** to that error is returned by this function. Otherwise, SQLITE_OK is +** returned. This is to allow the following pattern (pseudo-code): +** +** sqlite3changeset_start(); +** while( SQLITE_ROW==sqlite3changeset_next() ){ +** // Do something with change. +** } +** rc = sqlite3changeset_finalize(); +** if( rc!=SQLITE_OK ){ +** // An error has occurred +** } +*/ +int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter); + +/* +** CAPI3REF: Invert A Changeset +** +** This function is used to "invert" a changeset object. Applying an inverted +** changeset to a database reverses the effects of applying the uninverted +** changeset. Specifically: +** +**
      +**
    • Each DELETE change is changed to an INSERT, and +**
    • Each INSERT change is changed to a DELETE, and +**
    • For each UPDATE change, the old.* and new.* values are exchanged. +**
    +** +** This function does not change the order in which changes appear within +** the changeset. It merely reverses the sense of each individual change. +** +** If successful, a pointer to a buffer containing the inverted changeset +** is stored in *ppOut, the size of the same buffer is stored in *pnOut, and +** SQLITE_OK is returned. If an error occurs, both *pnOut and *ppOut are +** zeroed and an SQLite error code returned. +** +** It is the responsibility of the caller to eventually call sqlite3_free() +** on the *ppOut pointer to free the buffer allocation following a successful +** call to this function. +** +** WARNING/TODO: This function currently assumes that the input is a valid +** changeset. If it is not, the results are undefined. +*/ +int sqlite3changeset_invert( + int nIn, const void *pIn, /* Input changeset */ + int *pnOut, void **ppOut /* OUT: Inverse of input */ +); + +/* +** CAPI3REF: Concatenate Two Changeset Objects +** +** This function is used to concatenate two changesets, A and B, into a +** single changeset. The result is a changeset equivalent to applying +** changeset A followed by changeset B. +** +** This function combines the two input changesets using an +** sqlite3_changegroup object. Calling it produces similar results as the +** following code fragment: +** +** sqlite3_changegroup *pGrp; +** rc = sqlite3_changegroup_new(&pGrp); +** if( rc==SQLITE_OK ) rc = sqlite3changegroup_add(pGrp, nA, pA); +** if( rc==SQLITE_OK ) rc = sqlite3changegroup_add(pGrp, nB, pB); +** if( rc==SQLITE_OK ){ +** rc = sqlite3changegroup_output(pGrp, pnOut, ppOut); +** }else{ +** *ppOut = 0; +** *pnOut = 0; +** } +** +** Refer to the sqlite3_changegroup documentation below for details. +*/ +int sqlite3changeset_concat( + int nA, /* Number of bytes in buffer pA */ + void *pA, /* Pointer to buffer containing changeset A */ + int nB, /* Number of bytes in buffer pB */ + void *pB, /* Pointer to buffer containing changeset B */ + int *pnOut, /* OUT: Number of bytes in output changeset */ + void **ppOut /* OUT: Buffer containing output changeset */ +); + + +/* +** Changegroup handle. +*/ +typedef struct sqlite3_changegroup sqlite3_changegroup; + +/* +** CAPI3REF: Combine two or more changesets into a single changeset. +** +** An sqlite3_changegroup object is used to combine two or more changesets +** (or patchsets) into a single changeset (or patchset). A single changegroup +** object may combine changesets or patchsets, but not both. The output is +** always in the same format as the input. +** +** If successful, this function returns SQLITE_OK and populates (*pp) with +** a pointer to a new sqlite3_changegroup object before returning. The caller +** should eventually free the returned object using a call to +** sqlite3changegroup_delete(). If an error occurs, an SQLite error code +** (i.e. SQLITE_NOMEM) is returned and *pp is set to NULL. +** +** The usual usage pattern for an sqlite3_changegroup object is as follows: +** +**
      +**
    • It is created using a call to sqlite3changegroup_new(). +** +**
    • Zero or more changesets (or patchsets) are added to the object +** by calling sqlite3changegroup_add(). +** +**
    • The result of combining all input changesets together is obtained +** by the application via a call to sqlite3changegroup_output(). +** +**
    • The object is deleted using a call to sqlite3changegroup_delete(). +**
    +** +** Any number of calls to add() and output() may be made between the calls to +** new() and delete(), and in any order. +** +** As well as the regular sqlite3changegroup_add() and +** sqlite3changegroup_output() functions, also available are the streaming +** versions sqlite3changegroup_add_strm() and sqlite3changegroup_output_strm(). +*/ +int sqlite3changegroup_new(sqlite3_changegroup **pp); + +/* +** Add all changes within the changeset (or patchset) in buffer pData (size +** nData bytes) to the changegroup. +** +** If the buffer contains a patchset, then all prior calls to this function +** on the same changegroup object must also have specified patchsets. Or, if +** the buffer contains a changeset, so must have the earlier calls to this +** function. Otherwise, SQLITE_ERROR is returned and no changes are added +** to the changegroup. +** +** Rows within the changeset and changegroup are identified by the values in +** their PRIMARY KEY columns. A change in the changeset is considered to +** apply to the same row as a change already present in the changegroup if +** the two rows have the same primary key. +** +** Changes to rows that that do not already appear in the changegroup are +** simply copied into it. Or, if both the new changeset and the changegroup +** contain changes that apply to a single row, the final contents of the +** changegroup depends on the type of each change, as follows: +** +**
  • +** +** +**
    Existing Change New Change Output Change +**
    INSERT INSERT +** The new change is ignored. This case does not occur if the new +** changeset was recorded immediately after the changesets already +** added to the changegroup. +**
    INSERT UPDATE +** The INSERT change remains in the changegroup. The values in the +** INSERT change are modified as if the row was inserted by the +** existing change and then updated according to the new change. +**
    INSERT DELETE +** The existing INSERT is removed from the changegroup. The DELETE is +** not added. +**
    UPDATE INSERT +** The new change is ignored. This case does not occur if the new +** changeset was recorded immediately after the changesets already +** added to the changegroup. +**
    UPDATE UPDATE +** The existing UPDATE remains within the changegroup. It is amended +** so that the accompanying values are as if the row was updated once +** by the existing change and then again by the new change. +**
    UPDATE DELETE +** The existing UPDATE is replaced by the new DELETE within the +** changegroup. +**
    DELETE INSERT +** If one or more of the column values in the row inserted by the +** new change differ from those in the row deleted by the existing +** change, the existing DELETE is replaced by an UPDATE within the +** changegroup. Otherwise, if the inserted row is exactly the same +** as the deleted row, the existing DELETE is simply discarded. +**
    DELETE UPDATE +** The new change is ignored. This case does not occur if the new +** changeset was recorded immediately after the changesets already +** added to the changegroup. +**
    DELETE DELETE +** The new change is ignored. This case does not occur if the new +** changeset was recorded immediately after the changesets already +** added to the changegroup. +**
    +** +** If the new changeset contains changes to a table that is already present +** in the changegroup, then the number of columns and the position of the +** primary key columns for the table must be consistent. If this is not the +** case, this function fails with SQLITE_SCHEMA. If the input changeset +** appears to be corrupt and the corruption is detected, SQLITE_CORRUPT is +** returned. Or, if an out-of-memory condition occurs during processing, this +** function returns SQLITE_NOMEM. In all cases, if an error occurs the +** final contents of the changegroup is undefined. +** +** If no error occurs, SQLITE_OK is returned. +*/ +int sqlite3changegroup_add(sqlite3_changegroup*, int nData, void *pData); + +/* +** Obtain a buffer containing a changeset (or patchset) representing the +** current contents of the changegroup. If the inputs to the changegroup +** were themselves changesets, the output is a changeset. Or, if the +** inputs were patchsets, the output is also a patchset. +** +** As with the output of the sqlite3session_changeset() and +** sqlite3session_patchset() functions, all changes related to a single +** table are grouped together in the output of this function. Tables appear +** in the same order as for the very first changeset added to the changegroup. +** If the second or subsequent changesets added to the changegroup contain +** changes for tables that do not appear in the first changeset, they are +** appended onto the end of the output changeset, again in the order in +** which they are first encountered. +** +** If an error occurs, an SQLite error code is returned and the output +** variables (*pnData) and (*ppData) are set to 0. Otherwise, SQLITE_OK +** is returned and the output variables are set to the size of and a +** pointer to the output buffer, respectively. In this case it is the +** responsibility of the caller to eventually free the buffer using a +** call to sqlite3_free(). +*/ +int sqlite3changegroup_output( + sqlite3_changegroup*, + int *pnData, /* OUT: Size of output buffer in bytes */ + void **ppData /* OUT: Pointer to output buffer */ +); + +/* +** Delete a changegroup object. +*/ +void sqlite3changegroup_delete(sqlite3_changegroup*); + +/* +** CAPI3REF: Apply A Changeset To A Database +** +** Apply a changeset to a database. This function attempts to update the +** "main" database attached to handle db with the changes found in the +** changeset passed via the second and third arguments. +** +** The fourth argument (xFilter) passed to this function is the "filter +** callback". If it is not NULL, then for each table affected by at least one +** change in the changeset, the filter callback is invoked with +** the table name as the second argument, and a copy of the context pointer +** passed as the sixth argument to this function as the first. If the "filter +** callback" returns zero, then no attempt is made to apply any changes to +** the table. Otherwise, if the return value is non-zero or the xFilter +** argument to this function is NULL, all changes related to the table are +** attempted. +** +** For each table that is not excluded by the filter callback, this function +** tests that the target database contains a compatible table. A table is +** considered compatible if all of the following are true: +** +**
      +**
    • The table has the same name as the name recorded in the +** changeset, and +**
    • The table has the same number of columns as recorded in the +** changeset, and +**
    • The table has primary key columns in the same position as +** recorded in the changeset. +**
    +** +** If there is no compatible table, it is not an error, but none of the +** changes associated with the table are applied. A warning message is issued +** via the sqlite3_log() mechanism with the error code SQLITE_SCHEMA. At most +** one such warning is issued for each table in the changeset. +** +** For each change for which there is a compatible table, an attempt is made +** to modify the table contents according to the UPDATE, INSERT or DELETE +** change. If a change cannot be applied cleanly, the conflict handler +** function passed as the fifth argument to sqlite3changeset_apply() may be +** invoked. A description of exactly when the conflict handler is invoked for +** each type of change is below. +** +** Unlike the xFilter argument, xConflict may not be passed NULL. The results +** of passing anything other than a valid function pointer as the xConflict +** argument are undefined. +** +** Each time the conflict handler function is invoked, it must return one +** of [SQLITE_CHANGESET_OMIT], [SQLITE_CHANGESET_ABORT] or +** [SQLITE_CHANGESET_REPLACE]. SQLITE_CHANGESET_REPLACE may only be returned +** if the second argument passed to the conflict handler is either +** SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT. If the conflict-handler +** returns an illegal value, any changes already made are rolled back and +** the call to sqlite3changeset_apply() returns SQLITE_MISUSE. Different +** actions are taken by sqlite3changeset_apply() depending on the value +** returned by each invocation of the conflict-handler function. Refer to +** the documentation for the three +** [SQLITE_CHANGESET_OMIT|available return values] for details. +** +**
    +**
    DELETE Changes
    +** For each DELETE change, this function checks if the target database +** contains a row with the same primary key value (or values) as the +** original row values stored in the changeset. If it does, and the values +** stored in all non-primary key columns also match the values stored in +** the changeset the row is deleted from the target database. +** +** If a row with matching primary key values is found, but one or more of +** the non-primary key fields contains a value different from the original +** row value stored in the changeset, the conflict-handler function is +** invoked with [SQLITE_CHANGESET_DATA] as the second argument. +** +** If no row with matching primary key values is found in the database, +** the conflict-handler function is invoked with [SQLITE_CHANGESET_NOTFOUND] +** passed as the second argument. +** +** If the DELETE operation is attempted, but SQLite returns SQLITE_CONSTRAINT +** (which can only happen if a foreign key constraint is violated), the +** conflict-handler function is invoked with [SQLITE_CHANGESET_CONSTRAINT] +** passed as the second argument. This includes the case where the DELETE +** operation is attempted because an earlier call to the conflict handler +** function returned [SQLITE_CHANGESET_REPLACE]. +** +**
    INSERT Changes
    +** For each INSERT change, an attempt is made to insert the new row into +** the database. +** +** If the attempt to insert the row fails because the database already +** contains a row with the same primary key values, the conflict handler +** function is invoked with the second argument set to +** [SQLITE_CHANGESET_CONFLICT]. +** +** If the attempt to insert the row fails because of some other constraint +** violation (e.g. NOT NULL or UNIQUE), the conflict handler function is +** invoked with the second argument set to [SQLITE_CHANGESET_CONSTRAINT]. +** This includes the case where the INSERT operation is re-attempted because +** an earlier call to the conflict handler function returned +** [SQLITE_CHANGESET_REPLACE]. +** +**
    UPDATE Changes
    +** For each UPDATE change, this function checks if the target database +** contains a row with the same primary key value (or values) as the +** original row values stored in the changeset. If it does, and the values +** stored in all non-primary key columns also match the values stored in +** the changeset the row is updated within the target database. +** +** If a row with matching primary key values is found, but one or more of +** the non-primary key fields contains a value different from an original +** row value stored in the changeset, the conflict-handler function is +** invoked with [SQLITE_CHANGESET_DATA] as the second argument. Since +** UPDATE changes only contain values for non-primary key fields that are +** to be modified, only those fields need to match the original values to +** avoid the SQLITE_CHANGESET_DATA conflict-handler callback. +** +** If no row with matching primary key values is found in the database, +** the conflict-handler function is invoked with [SQLITE_CHANGESET_NOTFOUND] +** passed as the second argument. +** +** If the UPDATE operation is attempted, but SQLite returns +** SQLITE_CONSTRAINT, the conflict-handler function is invoked with +** [SQLITE_CHANGESET_CONSTRAINT] passed as the second argument. +** This includes the case where the UPDATE operation is attempted after +** an earlier call to the conflict handler function returned +** [SQLITE_CHANGESET_REPLACE]. +**
    +** +** It is safe to execute SQL statements, including those that write to the +** table that the callback related to, from within the xConflict callback. +** This can be used to further customize the applications conflict +** resolution strategy. +** +** All changes made by this function are enclosed in a savepoint transaction. +** If any other error (aside from a constraint failure when attempting to +** write to the target database) occurs, then the savepoint transaction is +** rolled back, restoring the target database to its original state, and an +** SQLite error code returned. +*/ +int sqlite3changeset_apply( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int nChangeset, /* Size of changeset in bytes */ + void *pChangeset, /* Changeset blob */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +); + +/* +** CAPI3REF: Constants Passed To The Conflict Handler +** +** Values that may be passed as the second argument to a conflict-handler. +** +**
    +**
    SQLITE_CHANGESET_DATA
    +** The conflict handler is invoked with CHANGESET_DATA as the second argument +** when processing a DELETE or UPDATE change if a row with the required +** PRIMARY KEY fields is present in the database, but one or more other +** (non primary-key) fields modified by the update do not contain the +** expected "before" values. +** +** The conflicting row, in this case, is the database row with the matching +** primary key. +** +**
    SQLITE_CHANGESET_NOTFOUND
    +** The conflict handler is invoked with CHANGESET_NOTFOUND as the second +** argument when processing a DELETE or UPDATE change if a row with the +** required PRIMARY KEY fields is not present in the database. +** +** There is no conflicting row in this case. The results of invoking the +** sqlite3changeset_conflict() API are undefined. +** +**
    SQLITE_CHANGESET_CONFLICT
    +** CHANGESET_CONFLICT is passed as the second argument to the conflict +** handler while processing an INSERT change if the operation would result +** in duplicate primary key values. +** +** The conflicting row in this case is the database row with the matching +** primary key. +** +**
    SQLITE_CHANGESET_FOREIGN_KEY
    +** If foreign key handling is enabled, and applying a changeset leaves the +** database in a state containing foreign key violations, the conflict +** handler is invoked with CHANGESET_FOREIGN_KEY as the second argument +** exactly once before the changeset is committed. If the conflict handler +** returns CHANGESET_OMIT, the changes, including those that caused the +** foreign key constraint violation, are committed. Or, if it returns +** CHANGESET_ABORT, the changeset is rolled back. +** +** No current or conflicting row information is provided. The only function +** it is possible to call on the supplied sqlite3_changeset_iter handle +** is sqlite3changeset_fk_conflicts(). +** +**
    SQLITE_CHANGESET_CONSTRAINT
    +** If any other constraint violation occurs while applying a change (i.e. +** a UNIQUE, CHECK or NOT NULL constraint), the conflict handler is +** invoked with CHANGESET_CONSTRAINT as the second argument. +** +** There is no conflicting row in this case. The results of invoking the +** sqlite3changeset_conflict() API are undefined. +** +**
    +*/ +#define SQLITE_CHANGESET_DATA 1 +#define SQLITE_CHANGESET_NOTFOUND 2 +#define SQLITE_CHANGESET_CONFLICT 3 +#define SQLITE_CHANGESET_CONSTRAINT 4 +#define SQLITE_CHANGESET_FOREIGN_KEY 5 + +/* +** CAPI3REF: Constants Returned By The Conflict Handler +** +** A conflict handler callback must return one of the following three values. +** +**
    +**
    SQLITE_CHANGESET_OMIT
    +** If a conflict handler returns this value no special action is taken. The +** change that caused the conflict is not applied. The session module +** continues to the next change in the changeset. +** +**
    SQLITE_CHANGESET_REPLACE
    +** This value may only be returned if the second argument to the conflict +** handler was SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT. If this +** is not the case, any changes applied so far are rolled back and the +** call to sqlite3changeset_apply() returns SQLITE_MISUSE. +** +** If CHANGESET_REPLACE is returned by an SQLITE_CHANGESET_DATA conflict +** handler, then the conflicting row is either updated or deleted, depending +** on the type of change. +** +** If CHANGESET_REPLACE is returned by an SQLITE_CHANGESET_CONFLICT conflict +** handler, then the conflicting row is removed from the database and a +** second attempt to apply the change is made. If this second attempt fails, +** the original row is restored to the database before continuing. +** +**
    SQLITE_CHANGESET_ABORT
    +** If this value is returned, any changes applied so far are rolled back +** and the call to sqlite3changeset_apply() returns SQLITE_ABORT. +**
    +*/ +#define SQLITE_CHANGESET_OMIT 0 +#define SQLITE_CHANGESET_REPLACE 1 +#define SQLITE_CHANGESET_ABORT 2 + +/* +** CAPI3REF: Streaming Versions of API functions. +** +** The six streaming API xxx_strm() functions serve similar purposes to the +** corresponding non-streaming API functions: +** +** +** +**
    Streaming functionNon-streaming equivalent
    sqlite3changeset_apply_str[sqlite3changeset_apply] +**
    sqlite3changeset_concat_str[sqlite3changeset_concat] +**
    sqlite3changeset_invert_str[sqlite3changeset_invert] +**
    sqlite3changeset_start_str[sqlite3changeset_start] +**
    sqlite3session_changeset_str[sqlite3session_changeset] +**
    sqlite3session_patchset_str[sqlite3session_patchset] +**
    +** +** Non-streaming functions that accept changesets (or patchsets) as input +** require that the entire changeset be stored in a single buffer in memory. +** Similarly, those that return a changeset or patchset do so by returning +** a pointer to a single large buffer allocated using sqlite3_malloc(). +** Normally this is convenient. However, if an application running in a +** low-memory environment is required to handle very large changesets, the +** large contiguous memory allocations required can become onerous. +** +** In order to avoid this problem, instead of a single large buffer, input +** is passed to a streaming API functions by way of a callback function that +** the sessions module invokes to incrementally request input data as it is +** required. In all cases, a pair of API function parameters such as +** +**
    +**        int nChangeset,
    +**        void *pChangeset,
    +**  
    +** +** Is replaced by: +** +**
    +**        int (*xInput)(void *pIn, void *pData, int *pnData),
    +**        void *pIn,
    +**  
    +** +** Each time the xInput callback is invoked by the sessions module, the first +** argument passed is a copy of the supplied pIn context pointer. The second +** argument, pData, points to a buffer (*pnData) bytes in size. Assuming no +** error occurs the xInput method should copy up to (*pnData) bytes of data +** into the buffer and set (*pnData) to the actual number of bytes copied +** before returning SQLITE_OK. If the input is completely exhausted, (*pnData) +** should be set to zero to indicate this. Or, if an error occurs, an SQLite +** error code should be returned. In all cases, if an xInput callback returns +** an error, all processing is abandoned and the streaming API function +** returns a copy of the error code to the caller. +** +** In the case of sqlite3changeset_start_strm(), the xInput callback may be +** invoked by the sessions module at any point during the lifetime of the +** iterator. If such an xInput callback returns an error, the iterator enters +** an error state, whereby all subsequent calls to iterator functions +** immediately fail with the same error code as returned by xInput. +** +** Similarly, streaming API functions that return changesets (or patchsets) +** return them in chunks by way of a callback function instead of via a +** pointer to a single large buffer. In this case, a pair of parameters such +** as: +** +**
    +**        int *pnChangeset,
    +**        void **ppChangeset,
    +**  
    +** +** Is replaced by: +** +**
    +**        int (*xOutput)(void *pOut, const void *pData, int nData),
    +**        void *pOut
    +**  
    +** +** The xOutput callback is invoked zero or more times to return data to +** the application. The first parameter passed to each call is a copy of the +** pOut pointer supplied by the application. The second parameter, pData, +** points to a buffer nData bytes in size containing the chunk of output +** data being returned. If the xOutput callback successfully processes the +** supplied data, it should return SQLITE_OK to indicate success. Otherwise, +** it should return some other SQLite error code. In this case processing +** is immediately abandoned and the streaming API function returns a copy +** of the xOutput error code to the application. +** +** The sessions module never invokes an xOutput callback with the third +** parameter set to a value less than or equal to zero. Other than this, +** no guarantees are made as to the size of the chunks of data returned. +*/ +int sqlite3changeset_apply_strm( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int (*xInput)(void *pIn, void *pData, int *pnData), /* Input function */ + void *pIn, /* First arg for xInput */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +); +int sqlite3changeset_concat_strm( + int (*xInputA)(void *pIn, void *pData, int *pnData), + void *pInA, + int (*xInputB)(void *pIn, void *pData, int *pnData), + void *pInB, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); +int sqlite3changeset_invert_strm( + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); +int sqlite3changeset_start_strm( + sqlite3_changeset_iter **pp, + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn +); +int sqlite3session_changeset_strm( + sqlite3_session *pSession, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); +int sqlite3session_patchset_strm( + sqlite3_session *pSession, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); +int sqlite3changegroup_add_strm(sqlite3_changegroup*, + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn +); +int sqlite3changegroup_output_strm(sqlite3_changegroup*, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); + + +/* +** Make sure we can call this stuff from C++. +*/ +#ifdef __cplusplus +} +#endif + +#endif /* !defined(__SQLITESESSION_H_) && defined(SQLITE_ENABLE_SESSION) */ + +/******** End of sqlite3session.h *********/ +/******** Begin file fts5.h *********/ /* ** 2014 May 31 ** @@ -8006,6 +9690,9 @@ struct Fts5PhraseIter { ** an OOM condition or IO error), an appropriate SQLite error code is ** returned. ** +** This function may be quite inefficient if used with an FTS5 table +** created with the "columnsize=0" option. +** ** xColumnText: ** This function attempts to retrieve the text of column iCol of the ** current document. If successful, (*pz) is set to point to a buffer @@ -8026,15 +9713,29 @@ struct Fts5PhraseIter { ** the query within the current row. Return SQLITE_OK if successful, or ** an error code (i.e. SQLITE_NOMEM) if an error occurs. ** +** This API can be quite slow if used with an FTS5 table created with the +** "detail=none" or "detail=column" option. If the FTS5 table is created +** with either "detail=none" or "detail=column" and "content=" option +** (i.e. if it is a contentless table), then this API always returns 0. +** ** xInst: ** Query for the details of phrase match iIdx within the current row. ** Phrase matches are numbered starting from zero, so the iIdx argument ** should be greater than or equal to zero and smaller than the value ** output by xInstCount(). ** +** Usually, output parameter *piPhrase is set to the phrase number, *piCol +** to the column in which it occurs and *piOff the token offset of the +** first token of the phrase. The exception is if the table was created +** with the offsets=0 option specified. In this case *piOff is always +** set to -1. +** ** Returns SQLITE_OK if successful, or an error code (i.e. SQLITE_NOMEM) ** if an error occurs. ** +** This API can be quite slow if used with an FTS5 table created with the +** "detail=none" or "detail=column" option. +** ** xRowid: ** Returns the rowid of the current row. ** @@ -8048,11 +9749,13 @@ struct Fts5PhraseIter { ** ... FROM ftstable WHERE ftstable MATCH $p ORDER BY rowid ** ** with $p set to a phrase equivalent to the phrase iPhrase of the -** current query is executed. For each row visited, the callback function -** passed as the fourth argument is invoked. The context and API objects -** passed to the callback function may be used to access the properties of -** each matched row. Invoking Api.xUserData() returns a copy of the pointer -** passed as the third argument to pUserData. +** current query is executed. Any column filter that applies to +** phrase iPhrase of the current query is included in $p. For each +** row visited, the callback function passed as the fourth argument +** is invoked. The context and API objects passed to the callback +** function may be used to access the properties of each matched row. +** Invoking Api.xUserData() returns a copy of the pointer passed as +** the third argument to pUserData. ** ** If the callback function returns any value other than SQLITE_OK, the ** query is abandoned and the xQueryPhrase function returns immediately. @@ -8118,7 +9821,7 @@ struct Fts5PhraseIter { ** Fts5PhraseIter iter; ** int iCol, iOff; ** for(pApi->xPhraseFirst(pFts, iPhrase, &iter, &iCol, &iOff); -** iOff>=0; +** iCol>=0; ** pApi->xPhraseNext(pFts, &iter, &iCol, &iOff) ** ){ ** // An instance of phrase iPhrase at offset iOff of column iCol @@ -8126,13 +9829,51 @@ struct Fts5PhraseIter { ** ** The Fts5PhraseIter structure is defined above. Applications should not ** modify this structure directly - it should only be used as shown above -** with the xPhraseFirst() and xPhraseNext() API methods. +** with the xPhraseFirst() and xPhraseNext() API methods (and by +** xPhraseFirstColumn() and xPhraseNextColumn() as illustrated below). +** +** This API can be quite slow if used with an FTS5 table created with the +** "detail=none" or "detail=column" option. If the FTS5 table is created +** with either "detail=none" or "detail=column" and "content=" option +** (i.e. if it is a contentless table), then this API always iterates +** through an empty set (all calls to xPhraseFirst() set iCol to -1). ** ** xPhraseNext() ** See xPhraseFirst above. +** +** xPhraseFirstColumn() +** This function and xPhraseNextColumn() are similar to the xPhraseFirst() +** and xPhraseNext() APIs described above. The difference is that instead +** of iterating through all instances of a phrase in the current row, these +** APIs are used to iterate through the set of columns in the current row +** that contain one or more instances of a specified phrase. For example: +** +** Fts5PhraseIter iter; +** int iCol; +** for(pApi->xPhraseFirstColumn(pFts, iPhrase, &iter, &iCol); +** iCol>=0; +** pApi->xPhraseNextColumn(pFts, &iter, &iCol) +** ){ +** // Column iCol contains at least one instance of phrase iPhrase +** } +** +** This API can be quite slow if used with an FTS5 table created with the +** "detail=none" option. If the FTS5 table is created with either +** "detail=none" "content=" option (i.e. if it is a contentless table), +** then this API always iterates through an empty set (all calls to +** xPhraseFirstColumn() set iCol to -1). +** +** The information accessed using this API and its companion +** xPhraseFirstColumn() may also be obtained using xPhraseFirst/xPhraseNext +** (or xInst/xInstCount). The chief advantage of this API is that it is +** significantly more efficient than those alternatives when used with +** "detail=column" tables. +** +** xPhraseNextColumn() +** See xPhraseFirstColumn above. */ struct Fts5ExtensionApi { - int iVersion; /* Currently always set to 1 */ + int iVersion; /* Currently always set to 3 */ void *(*xUserData)(Fts5Context*); @@ -8162,8 +9903,11 @@ struct Fts5ExtensionApi { int (*xSetAuxdata)(Fts5Context*, void *pAux, void(*xDelete)(void*)); void *(*xGetAuxdata)(Fts5Context*, int bClear); - void (*xPhraseFirst)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*, int*); + int (*xPhraseFirst)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*, int*); void (*xPhraseNext)(Fts5Context*, Fts5PhraseIter*, int *piCol, int *piOff); + + int (*xPhraseFirstColumn)(Fts5Context*, int iPhrase, Fts5PhraseIter*, int*); + void (*xPhraseNextColumn)(Fts5Context*, Fts5PhraseIter*, int *piCol); }; /* @@ -8441,3 +10185,4 @@ struct fts5_api { #endif /* _FTS5_H */ +/******** End of fts5.h *********/ diff --git a/TMessagesProj/jni/sqlite_cursor.c b/TMessagesProj/jni/sqlite_cursor.c deleted file mode 100755 index de3f7469d..000000000 --- a/TMessagesProj/jni/sqlite_cursor.c +++ /dev/null @@ -1,79 +0,0 @@ -#include "sqlite.h" - -int Java_org_telegram_SQLite_SQLiteCursor_columnType(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { - sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; - return sqlite3_column_type(handle, columnIndex); -} - -int Java_org_telegram_SQLite_SQLiteCursor_columnIsNull(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { - sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; - int valType = sqlite3_column_type(handle, columnIndex); - return SQLITE_NULL == valType; -} - -int Java_org_telegram_SQLite_SQLiteCursor_columnIntValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { - sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; - int valType = sqlite3_column_type(handle, columnIndex); - if (SQLITE_NULL == valType) { - return 0; - } - return sqlite3_column_int(handle, columnIndex); -} - -long long Java_org_telegram_SQLite_SQLiteCursor_columnLongValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { - sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; - int valType = sqlite3_column_type(handle, columnIndex); - if (SQLITE_NULL == valType) { - return 0; - } - return sqlite3_column_int64(handle, columnIndex); -} - -double Java_org_telegram_SQLite_SQLiteCursor_columnDoubleValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { - sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; - int valType = sqlite3_column_type(handle, columnIndex); - if (SQLITE_NULL == valType) { - return 0; - } - return sqlite3_column_double(handle, columnIndex); -} - -jstring Java_org_telegram_SQLite_SQLiteCursor_columnStringValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { - sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; - const char *str = sqlite3_column_text(handle, columnIndex); - if (str != 0) { - return (*env)->NewStringUTF(env, str); - } - return 0; -} - -jbyteArray Java_org_telegram_SQLite_SQLiteCursor_columnByteArrayValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { - sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; - void *buf = sqlite3_column_blob(handle, columnIndex); - int length = sqlite3_column_bytes(handle, columnIndex); - if (buf != 0 && length > 0) { - jbyteArray result = (*env)->NewByteArray(env, length); - (*env)->SetByteArrayRegion(env, result, 0, length, buf); - return result; - } - return 0; -} - -int Java_org_telegram_SQLite_SQLiteCursor_columnByteArrayLength(JNIEnv *env, jobject object, int statementHandle, int columnIndex) { - return sqlite3_column_bytes((sqlite3_stmt *)statementHandle, columnIndex); -} - -int Java_org_telegram_SQLite_SQLiteCursor_columnByteBufferValue(JNIEnv *env, jobject object, int statementHandle, int columnIndex, jobject buffer) { - if (!buffer) { - return 0; - } - sqlite3_stmt *handle = (sqlite3_stmt *)statementHandle; - void *buf = sqlite3_column_blob(handle, columnIndex); - int length = sqlite3_column_bytes(handle, columnIndex); - if (buf != 0 && length > 0) { - jbyte *byteBuff = (*env)->GetDirectBufferAddress(env, buffer); - memcpy(byteBuff, buf, length); - return length; - } - return 0; -} diff --git a/TMessagesProj/jni/sqlite_database.c b/TMessagesProj/jni/sqlite_database.c deleted file mode 100755 index a249cff43..000000000 --- a/TMessagesProj/jni/sqlite_database.c +++ /dev/null @@ -1,42 +0,0 @@ -#include "sqlite.h" - -void Java_org_telegram_SQLite_SQLiteDatabase_closedb(JNIEnv *env, jobject object, int sqliteHandle) { - sqlite3 *handle = (sqlite3 *)sqliteHandle; - int err = sqlite3_close(handle); - if (SQLITE_OK != err) { - throw_sqlite3_exception(env, handle, err); - } -} - -void Java_org_telegram_SQLite_SQLiteDatabase_beginTransaction(JNIEnv *env, jobject object, int sqliteHandle) { - sqlite3 *handle = (sqlite3 *)sqliteHandle; - sqlite3_exec(handle, "BEGIN", 0, 0, 0); -} - -void Java_org_telegram_SQLite_SQLiteDatabase_commitTransaction(JNIEnv *env, jobject object, int sqliteHandle) { - sqlite3 *handle = (sqlite3 *)sqliteHandle; - sqlite3_exec(handle, "COMMIT", 0, 0, 0); -} - -int Java_org_telegram_SQLite_SQLiteDatabase_opendb(JNIEnv *env, jobject object, jstring fileName, jstring tempDir) { - char const *fileNameStr = (*env)->GetStringUTFChars(env, fileName, 0); - char const *tempDirStr = (*env)->GetStringUTFChars(env, tempDir, 0); - - if (sqlite3_temp_directory != 0) { - sqlite3_free(sqlite3_temp_directory); - } - sqlite3_temp_directory = sqlite3_mprintf("%s", tempDirStr); - - sqlite3 *handle = 0; - int err = sqlite3_open(fileNameStr, &handle); - if (SQLITE_OK != err) { - throw_sqlite3_exception(env, handle, err); - } - if (fileNameStr != 0) { - (*env)->ReleaseStringUTFChars(env, fileName, fileNameStr); - } - if (tempDirStr != 0) { - (*env)->ReleaseStringUTFChars(env, tempDir, tempDirStr); - } - return (int)handle; -} diff --git a/TMessagesProj/jni/sqlite_statement.c b/TMessagesProj/jni/sqlite_statement.c deleted file mode 100755 index 4957f790b..000000000 --- a/TMessagesProj/jni/sqlite_statement.c +++ /dev/null @@ -1,120 +0,0 @@ -#include "sqlite.h" - -jfieldID queryArgsCountField; - -jint sqliteOnJNILoad(JavaVM *vm, void *reserved, JNIEnv *env) { - jclass class = (*env)->FindClass(env, "org/telegram/SQLite/SQLitePreparedStatement"); - queryArgsCountField = (*env)->GetFieldID(env, class, "queryArgsCount", "I"); - return JNI_VERSION_1_6; -} - -int Java_org_telegram_SQLite_SQLitePreparedStatement_step(JNIEnv *env, jobject object, int statementHandle) { - sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; - - int errcode = sqlite3_step(handle); - if (errcode == SQLITE_ROW) { - return 0; - } else if(errcode == SQLITE_DONE) { - return 1; - } else if(errcode == SQLITE_BUSY) { - return -1; - } - throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); -} - -int Java_org_telegram_SQLite_SQLitePreparedStatement_prepare(JNIEnv *env, jobject object, int sqliteHandle, jstring sql) { - sqlite3 *handle = (sqlite3 *) sqliteHandle; - - char const *sqlStr = (*env)->GetStringUTFChars(env, sql, 0); - - sqlite3_stmt *stmt_handle; - - int errcode = sqlite3_prepare_v2(handle, sqlStr, -1, &stmt_handle, 0); - if (SQLITE_OK != errcode) { - throw_sqlite3_exception(env, handle, errcode); - } else { - int argsCount = sqlite3_bind_parameter_count(stmt_handle); - (*env)->SetIntField(env, object, queryArgsCountField, argsCount); - } - - if (sqlStr != 0) { - (*env)->ReleaseStringUTFChars(env, sql, sqlStr); - } - - return (int) stmt_handle; -} - -void Java_org_telegram_SQLite_SQLitePreparedStatement_reset(JNIEnv *env, jobject object, int statementHandle) { - sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; - - int errcode = sqlite3_reset(handle); - if (SQLITE_OK != errcode) { - throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); - } -} - -void Java_org_telegram_SQLite_SQLitePreparedStatement_finalize(JNIEnv *env, jobject object, int statementHandle) { - sqlite3_finalize((sqlite3_stmt *) statementHandle); -} - -void Java_org_telegram_SQLite_SQLitePreparedStatement_bindByteBuffer(JNIEnv *env, jobject object, int statementHandle, int index, jobject value, int length) { - sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; - jbyte *buf = (*env)->GetDirectBufferAddress(env, value); - - int errcode = sqlite3_bind_blob(handle, index, buf, length, SQLITE_STATIC); - if (SQLITE_OK != errcode) { - throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); - } -} - -void Java_org_telegram_SQLite_SQLitePreparedStatement_bindString(JNIEnv *env, jobject object, int statementHandle, int index, jstring value) { - sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; - - char const *valueStr = (*env)->GetStringUTFChars(env, value, 0); - - int errcode = sqlite3_bind_text(handle, index, valueStr, -1, SQLITE_TRANSIENT); - if (SQLITE_OK != errcode) { - throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); - } - - if (valueStr != 0) { - (*env)->ReleaseStringUTFChars(env, value, valueStr); - } -} - -void Java_org_telegram_SQLite_SQLitePreparedStatement_bindInt(JNIEnv *env, jobject object, int statementHandle, int index, int value) { - sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; - - int errcode = sqlite3_bind_int(handle, index, value); - if (SQLITE_OK != errcode) { - throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); - } -} - -void Java_org_telegram_SQLite_SQLitePreparedStatement_bindLong(JNIEnv *env, jobject object, int statementHandle, int index, long long value) { - sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; - - int errcode = sqlite3_bind_int64(handle, index, value); - if (SQLITE_OK != errcode) { - throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); - } -} - -void Java_org_telegram_SQLite_SQLitePreparedStatement_bindDouble(JNIEnv *env, jobject object, int statementHandle, int index, double value) { - sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; - - int errcode = sqlite3_bind_double(handle, index, value); - if (SQLITE_OK != errcode) { - throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); - } -} - -void Java_org_telegram_SQLite_SQLitePreparedStatement_bindNull(JNIEnv *env, jobject object, int statementHandle, int index) { - sqlite3_stmt *handle = (sqlite3_stmt *) statementHandle; - - int errcode = sqlite3_bind_null(handle, index); - if (SQLITE_OK != errcode) { - throw_sqlite3_exception(env, sqlite3_db_handle(handle), errcode); - } -} - diff --git a/TMessagesProj/jni/tgnet/ConnectionsManager.cpp b/TMessagesProj/jni/tgnet/ConnectionsManager.cpp index bf0392161..982ba2497 100644 --- a/TMessagesProj/jni/tgnet/ConnectionsManager.cpp +++ b/TMessagesProj/jni/tgnet/ConnectionsManager.cpp @@ -33,8 +33,6 @@ JavaVM *javaVm = nullptr; JNIEnv *jniEnv = nullptr; jclass jclass_ByteBuffer = nullptr; jmethodID jclass_ByteBuffer_allocateDirect = 0; -jclass jclass_ApplicationLoader = nullptr; -jmethodID jclass_ApplicationLoader_setAlarm = 0; #endif static bool done = false; @@ -139,17 +137,6 @@ int ConnectionsManager::callEvents(int64_t now) { } DEBUG_D("schedule next epoll wakeup in %d ms", timeToPushPing); - if (networkAvailable) { - DEBUG_D("setting network alarm to wake us in %d ms", timeToPushPing); - JNIEnv *env = 0; - if (javaVm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { - DEBUG_E("can't get jnienv"); - } - env->CallStaticVoidMethod(jclass_ApplicationLoader, jclass_ApplicationLoader_setAlarm, (jint) timeToPushPing); - } else { - DEBUG_D("omitting network alarm since there's no available connection"); - } - return timeToPushPing; } @@ -2509,18 +2496,6 @@ void ConnectionsManager::useJavaVM(JavaVM *vm, bool useJavaByteBuffers) { exit(1); } - jclass_ApplicationLoader = (jclass) env->NewGlobalRef(env->FindClass("org/telegram/messenger/ApplicationLoader")); - if (jclass_ApplicationLoader == 0) { - DEBUG_E("can't find ApplicationLoader class"); - exit(1); - } - - jclass_ApplicationLoader_setAlarm = env->GetStaticMethodID(jclass_ApplicationLoader, "setAlarm", "(I)V"); - if (jclass_ApplicationLoader_setAlarm == 0) { - DEBUG_E("can't find ApplicationLoader setAlarm method"); - exit(1); - } - if (useJavaByteBuffers) { jclass_ByteBuffer = (jclass) env->NewGlobalRef(env->FindClass("java/nio/ByteBuffer")); if (jclass_ByteBuffer == 0) { diff --git a/TMessagesProj/jni/tgnet/Datacenter.cpp b/TMessagesProj/jni/tgnet/Datacenter.cpp index 74050504a..fc0cecdd7 100644 --- a/TMessagesProj/jni/tgnet/Datacenter.cpp +++ b/TMessagesProj/jni/tgnet/Datacenter.cpp @@ -1381,7 +1381,7 @@ NativeByteBuffer *Datacenter::createRequestsData(std::vectorgetConnectionType(), (uint64_t) connection->getSissionId(), networkMessage->message->seqno, (uint64_t) networkMessage->message->msg_id, typeid(*messageBody).name(), messageBody); int64_t messageTime = (int64_t) (networkMessage->message->msg_id / 4294967296.0 * 1000); - int64_t currentTime = ConnectionsManager::getInstance().getCurrentTimeMillis() + (int64_t) timeDifference * 1000; + int64_t currentTime = ConnectionsManager::getInstance().getCurrentTimeMillis() + (int64_t) ConnectionsManager::getInstance().getTimeDifference() * 1000; if (messageTime < currentTime - 30000 || messageTime > currentTime + 25000) { DEBUG_D("wrap message in container"); diff --git a/TMessagesProj/jni/tgnet/MTProtoScheme.cpp b/TMessagesProj/jni/tgnet/MTProtoScheme.cpp index 7921031dc..efe399897 100644 --- a/TMessagesProj/jni/tgnet/MTProtoScheme.cpp +++ b/TMessagesProj/jni/tgnet/MTProtoScheme.cpp @@ -947,6 +947,7 @@ void TL_config::readParams(NativeByteBuffer *stream, bool &error) { push_chat_limit = stream->readInt32(&error); saved_gifs_limit = stream->readInt32(&error); edit_time_limit = stream->readInt32(&error); + rating_e_decay = stream->readInt32(&error); magic = stream->readUint32(&error); if (magic != 0x1cb5c415) { error = true; @@ -989,6 +990,7 @@ void TL_config::serializeToStream(NativeByteBuffer *stream) { stream->writeInt32(push_chat_limit); stream->writeInt32(saved_gifs_limit); stream->writeInt32(edit_time_limit); + stream->writeInt32(rating_e_decay); stream->writeInt32(0x1cb5c415); count = (uint32_t) disabled_features.size(); stream->writeInt32(count); diff --git a/TMessagesProj/jni/tgnet/MTProtoScheme.h b/TMessagesProj/jni/tgnet/MTProtoScheme.h index 75922fefc..cb6b92148 100644 --- a/TMessagesProj/jni/tgnet/MTProtoScheme.h +++ b/TMessagesProj/jni/tgnet/MTProtoScheme.h @@ -657,7 +657,7 @@ public: class TL_config : public TLObject { public: - static const uint32_t constructor = 0x317ceef4; + static const uint32_t constructor = 0xc9411388; int32_t date; int32_t expires; @@ -678,6 +678,7 @@ public: int32_t push_chat_limit; int32_t saved_gifs_limit; int32_t edit_time_limit; + int32_t rating_e_decay; std::vector> disabled_features; static TL_config *TLdeserialize(NativeByteBuffer *stream, uint32_t constructor, bool &error); diff --git a/TMessagesProj/src/main/AndroidManifest.xml b/TMessagesProj/src/main/AndroidManifest.xml index 505f13dae..7509a1366 100644 --- a/TMessagesProj/src/main/AndroidManifest.xml +++ b/TMessagesProj/src/main/AndroidManifest.xml @@ -15,6 +15,8 @@ + + @@ -36,6 +38,10 @@ + + + + + + + + + + + @@ -209,6 +225,8 @@ + + @@ -216,6 +234,7 @@ + diff --git a/TMessagesProj/src/main/java/org/telegram/PhoneFormat/PhoneFormat.java b/TMessagesProj/src/main/java/org/telegram/PhoneFormat/PhoneFormat.java index 63081ef8b..89066116d 100644 --- a/TMessagesProj/src/main/java/org/telegram/PhoneFormat/PhoneFormat.java +++ b/TMessagesProj/src/main/java/org/telegram/PhoneFormat/PhoneFormat.java @@ -182,41 +182,47 @@ public class PhoneFormat { if (!initialzed) { return orig; } - String str = strip(orig); + try { + String str = strip(orig); - if (str.startsWith("+")) { - String rest = str.substring(1); - CallingCodeInfo info = findCallingCodeInfo(rest); - if (info != null) { - String phone = info.format(rest); - return "+" + phone; - } else { - return orig; - } - } else { - CallingCodeInfo info = callingCodeInfo(defaultCallingCode); - if (info == null) { - return orig; - } - - String accessCode = info.matchingAccessCode(str); - if (accessCode != null) { - String rest = str.substring(accessCode.length()); - String phone = rest; - CallingCodeInfo info2 = findCallingCodeInfo(rest); - if (info2 != null) { - phone = info2.format(rest); - } - - if (phone.length() == 0) { - return accessCode; + if (str.startsWith("+")) { + String rest = str.substring(1); + CallingCodeInfo info = findCallingCodeInfo(rest); + if (info != null) { + String phone = info.format(rest); + return "+" + phone; } else { - return String.format("%s %s", accessCode, phone); + return orig; } } else { - return info.format(str); + CallingCodeInfo info = callingCodeInfo(defaultCallingCode); + if (info == null) { + return orig; + } + + String accessCode = info.matchingAccessCode(str); + if (accessCode != null) { + String rest = str.substring(accessCode.length()); + String phone = rest; + CallingCodeInfo info2 = findCallingCodeInfo(rest); + if (info2 != null) { + phone = info2.format(rest); + } + + if (phone.length() == 0) { + return accessCode; + } else { + return String.format("%s %s", accessCode, phone); + } + } else { + return info.format(str); + } } + } catch (Exception e) { + FileLog.e("tmessages", e); + return orig; } + } public boolean isPhoneNumberValid(String phoneNumber) { diff --git a/TMessagesProj/src/main/java/org/telegram/SQLite/SQLiteCursor.java b/TMessagesProj/src/main/java/org/telegram/SQLite/SQLiteCursor.java index b667c7fca..49a74fa0a 100755 --- a/TMessagesProj/src/main/java/org/telegram/SQLite/SQLiteCursor.java +++ b/TMessagesProj/src/main/java/org/telegram/SQLite/SQLiteCursor.java @@ -11,8 +11,6 @@ package org.telegram.SQLite; import org.telegram.messenger.FileLog; import org.telegram.tgnet.NativeByteBuffer; -import java.nio.ByteBuffer; - public class SQLiteCursor { public static final int FIELD_TYPE_INT = 1; @@ -58,19 +56,13 @@ public class SQLiteCursor { return columnByteArrayValue(preparedStatement.getStatementHandle(), columnIndex); } - public int byteArrayLength(int columnIndex) throws SQLiteException { - checkRow(); - return columnByteArrayLength(preparedStatement.getStatementHandle(), columnIndex); - } - - public int byteBufferValue(int columnIndex, ByteBuffer buffer) throws SQLiteException { - checkRow(); - return columnByteBufferValue(preparedStatement.getStatementHandle(), columnIndex, buffer); - } - - public int byteBufferValue(int columnIndex, NativeByteBuffer buffer) throws SQLiteException { + public NativeByteBuffer byteBufferValue(int columnIndex) throws SQLiteException { checkRow(); - return columnByteBufferValue(preparedStatement.getStatementHandle(), columnIndex, buffer.buffer); + int ptr = columnByteBufferValue(preparedStatement.getStatementHandle(), columnIndex); + if (ptr != 0) { + return NativeByteBuffer.wrap(ptr); + } + return null; } public int getTypeOf(int columnIndex) throws SQLiteException { @@ -123,6 +115,5 @@ public class SQLiteCursor { native double columnDoubleValue(int statementHandle, int columnIndex); native String columnStringValue(int statementHandle, int columnIndex); native byte[] columnByteArrayValue(int statementHandle, int columnIndex); - native int columnByteArrayLength(int statementHandle, int columnIndex); - native int columnByteBufferValue(int statementHandle, int columnIndex, ByteBuffer buffer); + native int columnByteBufferValue(int statementHandle, int columnIndex); } diff --git a/TMessagesProj/src/main/java/org/telegram/SQLite/SQLitePreparedStatement.java b/TMessagesProj/src/main/java/org/telegram/SQLite/SQLitePreparedStatement.java index 6135e037f..cae02b41f 100755 --- a/TMessagesProj/src/main/java/org/telegram/SQLite/SQLitePreparedStatement.java +++ b/TMessagesProj/src/main/java/org/telegram/SQLite/SQLitePreparedStatement.java @@ -12,16 +12,14 @@ import org.telegram.messenger.FileLog; import org.telegram.tgnet.NativeByteBuffer; import java.nio.ByteBuffer; -import java.util.HashMap; public class SQLitePreparedStatement { + private boolean isFinalized = false; private int sqliteStatementHandle; - - private int queryArgsCount; private boolean finalizeAfterQuery = false; - private static HashMap hashMap; + //private static HashMap hashMap; public int getStatementHandle() { return sqliteStatementHandle; @@ -43,7 +41,7 @@ public class SQLitePreparedStatement { public SQLiteCursor query(Object[] args) throws SQLiteException { - if (args == null || args.length != queryArgsCount) { + if (args == null) { throw new IllegalArgumentException(); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java index 6a88f2dbd..e5222ef07 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java @@ -8,6 +8,9 @@ package org.telegram.messenger; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; @@ -20,9 +23,15 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapShader; +import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; @@ -42,6 +51,7 @@ import android.view.Surface; import android.view.View; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; +import android.webkit.MimeTypeMap; import android.widget.AbsListView; import android.widget.EdgeEffect; import android.widget.EditText; @@ -51,10 +61,6 @@ import android.widget.TextView; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.TLRPC; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.Components.ForegroundDetector; import org.telegram.ui.Components.NumberPicker; @@ -95,6 +101,9 @@ public class AndroidUtilities { private static Boolean isTablet = null; private static int adjustOwnerClassGuid = 0; + private static Paint roundPaint; + private static RectF bitmapRect; + public static Pattern WEB_URL = null; static { try { @@ -143,11 +152,7 @@ public class AndroidUtilities { } } } else if (drawable instanceof ColorDrawable) { - if (Build.VERSION.SDK_INT >= 11) { - bitmapColor = ((ColorDrawable) drawable).getColor(); - } else { - bitmapColor = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).getInt("selectedColor", 0xff000000); - } + bitmapColor = ((ColorDrawable) drawable).getColor(); } } catch (Exception e) { FileLog.e("tmessages", e); @@ -273,6 +278,21 @@ public class AndroidUtilities { } } + public static boolean isInternalUri(Uri uri) { + String pathString = uri.getPath(); + if (pathString == null) { + return false; + } + while (true) { + String newPath = Utilities.readlink(pathString); + if (newPath == null || newPath.equals(pathString)) { + break; + } + pathString = newPath; + } + return pathString != null && pathString.toLowerCase().contains("/data/data/" + ApplicationLoader.applicationContext.getPackageName() + "/files"); + } + public static void lockOrientation(Activity activity) { if (activity == null || prevOrientation != -10) { return; @@ -376,27 +396,40 @@ public class AndroidUtilities { if (view == null) { return; } - InputMethodManager inputManager = (InputMethodManager)view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - inputManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + try { + InputMethodManager inputManager = (InputMethodManager)view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + } catch (Exception e) { + FileLog.e("tmessages", e); + } } public static boolean isKeyboardShowed(View view) { if (view == null) { return false; } - InputMethodManager inputManager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - return inputManager.isActive(view); + try { + InputMethodManager inputManager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + return inputManager.isActive(view); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + return false; } public static void hideKeyboard(View view) { if (view == null) { return; } - InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (!imm.isActive()) { - return; + try { + InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (!imm.isActive()) { + return; + } + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } catch (Exception e) { + FileLog.e("tmessages", e); } - imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } public static File getCacheDir() { @@ -459,11 +492,7 @@ public class AndroidUtilities { Display display = manager.getDefaultDisplay(); if (display != null) { display.getMetrics(displayMetrics); - if (android.os.Build.VERSION.SDK_INT < 13) { - displaySize.set(display.getWidth(), display.getHeight()); - } else { - display.getSize(displaySize); - } + display.getSize(displaySize); FileLog.e("tmessages", "display size = " + displaySize.x + " " + displaySize.y + " " + displayMetrics.xdpi + "x" + displayMetrics.ydpi); } } @@ -644,7 +673,7 @@ public class AndroidUtilities { } public static void clearCursorDrawable(EditText editText) { - if (editText == null || Build.VERSION.SDK_INT < 12) { + if (editText == null) { return; } try { @@ -669,6 +698,137 @@ public class AndroidUtilities { } } + private static Intent createShortcutIntent(long did, boolean forDelete) { + Intent shortcutIntent = new Intent(ApplicationLoader.applicationContext, OpenChatReceiver.class); + + int lower_id = (int) did; + int high_id = (int) (did >> 32); + + TLRPC.User user = null; + TLRPC.Chat chat = null; + if (lower_id == 0) { + shortcutIntent.putExtra("encId", high_id); + TLRPC.EncryptedChat encryptedChat = MessagesController.getInstance().getEncryptedChat(high_id); + if (encryptedChat == null) { + return null; + } + user = MessagesController.getInstance().getUser(encryptedChat.user_id); + } else if (lower_id > 0) { + shortcutIntent.putExtra("userId", lower_id); + user = MessagesController.getInstance().getUser(lower_id); + } else if (lower_id < 0) { + chat = MessagesController.getInstance().getChat(-lower_id); + shortcutIntent.putExtra("chatId", -lower_id); + } else { + return null; + } + if (user == null && chat == null) { + return null; + } + + String name; + TLRPC.FileLocation photo = null; + + if (user != null) { + name = ContactsController.formatName(user.first_name, user.last_name); + if (user.photo != null) { + photo = user.photo.photo_small; + } + } else { + name = chat.title; + if (chat.photo != null) { + photo = chat.photo.photo_small; + } + } + + shortcutIntent.setAction("com.tmessages.openchat" + did); + shortcutIntent.addFlags(0x4000000); + + Intent addIntent = new Intent(); + addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); + addIntent.putExtra("duplicate", false); + if (!forDelete) { + Bitmap bitmap = null; + if (photo != null) { + try { + File path = FileLoader.getPathToAttach(photo, true); + bitmap = BitmapFactory.decodeFile(path.toString()); + if (bitmap != null) { + int size = AndroidUtilities.dp(58); + Bitmap result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + result.eraseColor(Color.TRANSPARENT); + Canvas canvas = new Canvas(result); + BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + if (roundPaint == null) { + roundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + bitmapRect = new RectF(); + } + float scale = size / (float) bitmap.getWidth(); + canvas.save(); + canvas.scale(scale, scale); + roundPaint.setShader(shader); + bitmapRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight()); + canvas.drawRoundRect(bitmapRect, bitmap.getWidth(), bitmap.getHeight(), roundPaint); + canvas.restore(); + Drawable drawable = ApplicationLoader.applicationContext.getResources().getDrawable(R.drawable.book_logo); + int w = AndroidUtilities.dp(15); + int left = size - w - AndroidUtilities.dp(2); + int top = size - w - AndroidUtilities.dp(2); + drawable.setBounds(left, top, left + w, top + w); + drawable.draw(canvas); + try { + canvas.setBitmap(null); + } catch (Exception e) { + //don't promt, this will crash on 2.x + } + bitmap = result; + } + } catch (Throwable e) { + FileLog.e("tmessages", e); + } + } + if (bitmap != null) { + addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bitmap); + } else { + if (user != null) { + if (user.bot) { + addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(ApplicationLoader.applicationContext, R.drawable.book_bot)); + } else { + addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(ApplicationLoader.applicationContext, R.drawable.book_user)); + } + } else if (chat != null) { + if (ChatObject.isChannel(chat) && !chat.megagroup) { + addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(ApplicationLoader.applicationContext, R.drawable.book_channel)); + } else { + addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(ApplicationLoader.applicationContext, R.drawable.book_group)); + } + } + } + } + return addIntent; + } + + public static void installShortcut(long did) { + try { + Intent addIntent = createShortcutIntent(did, false); + addIntent.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); + ApplicationLoader.applicationContext.sendBroadcast(addIntent); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + + public static void uninstallShortcut(long did) { + try { + Intent addIntent = createShortcutIntent(did, true); + addIntent.setAction("com.android.launcher.action.UNINSTALL_SHORTCUT"); + ApplicationLoader.applicationContext.sendBroadcast(addIntent); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + public static int getViewInset(View view) { if (view == null || Build.VERSION.SDK_INT < 21 || view.getHeight() == AndroidUtilities.displaySize.y || view.getHeight() == AndroidUtilities.displaySize.y - statusBarHeight) { return 0; @@ -711,6 +871,19 @@ public class AndroidUtilities { return size; } + public static CharSequence getTrimmedString(CharSequence src) { + if (src == null || src.length() == 0) { + return src; + } + while (src.length() > 0 && (src.charAt(0) == '\n' || src.charAt(0) == ' ')) { + src = src.subSequence(1, src.length()); + } + while (src.length() > 0 && (src.charAt(src.length() - 1) == '\n' || src.charAt(src.length() - 1) == ' ')) { + src = src.subSequence(0, src.length() - 1); + } + return src; + } + public static void setListViewEdgeEffectColor(AbsListView listView, int color) { if (Build.VERSION.SDK_INT >= 21) { try { @@ -817,14 +990,9 @@ public class AndroidUtilities { } public static boolean needShowPasscode(boolean reset) { - boolean wasInBackground; - if (Build.VERSION.SDK_INT >= 14) { - wasInBackground = ForegroundDetector.getInstance().isWasInBackground(reset); - if (reset) { - ForegroundDetector.getInstance().resetBackgroundVar(); - } - } else { - wasInBackground = UserConfig.lastPauseTime != 0; + boolean wasInBackground = ForegroundDetector.getInstance().isWasInBackground(reset); + if (reset) { + ForegroundDetector.getInstance().resetBackgroundVar(); } return UserConfig.passcodeHash.length() > 0 && wasInBackground && (UserConfig.appLocked || UserConfig.autoLockIn != 0 && UserConfig.lastPauseTime != 0 && !UserConfig.appLocked && (UserConfig.lastPauseTime + UserConfig.autoLockIn) <= ConnectionsManager.getInstance().getCurrentTime()); @@ -832,24 +1000,21 @@ public class AndroidUtilities { public static void shakeView(final View view, final float x, final int num) { if (num == 6) { - ViewProxy.setTranslationX(view, 0); - view.clearAnimation(); + view.setTranslationX(0); return; } - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether(ObjectAnimatorProxy.ofFloat(view, "translationX", AndroidUtilities.dp(x))); - animatorSetProxy.setDuration(50); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(view, "translationX", AndroidUtilities.dp(x))); + animatorSet.setDuration(50); + animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { shakeView(view, num == 5 ? 0 : -x, num + 1); } }); - animatorSetProxy.start(); + animatorSet.start(); } - - /*public static String ellipsize(String text, int maxLines, int maxWidth, TextPaint paint) { if (text == null || paint == null) { return null; @@ -886,7 +1051,7 @@ public class AndroidUtilities { }*/ /*public static void turnOffHardwareAcceleration(Window window) { - if (window == null || Build.MODEL == null || Build.VERSION.SDK_INT < 11) { + if (window == null || Build.MODEL == null) { return; } if (Build.MODEL.contains("GT-S5301") || @@ -899,6 +1064,16 @@ public class AndroidUtilities { } }*/ + public static void addToClipboard(CharSequence str) { + try { + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", str); + clipboard.setPrimaryClip(clip); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + public static void addMediaToGallery(String fromPath) { if (fromPath == null) { return; @@ -1181,4 +1356,50 @@ public class AndroidUtilities { System.arraycopy(sha1, 0, key_hash, 0, 16); return key_hash; } + + public static void openForView(MessageObject message, Activity activity) throws Exception { + File f = null; + String fileName = message.getFileName(); + if (message.messageOwner.attachPath != null && message.messageOwner.attachPath.length() != 0) { + f = new File(message.messageOwner.attachPath); + } + if (f == null || !f.exists()) { + f = FileLoader.getPathToMessage(message.messageOwner); + } + if (f != null && f.exists()) { + String realMimeType = null; + Intent intent = new Intent(Intent.ACTION_VIEW); + MimeTypeMap myMime = MimeTypeMap.getSingleton(); + int idx = fileName.lastIndexOf('.'); + if (idx != -1) { + String ext = fileName.substring(idx + 1); + realMimeType = myMime.getMimeTypeFromExtension(ext.toLowerCase()); + if (realMimeType == null) { + if (message.type == 9 || message.type == 0) { + realMimeType = message.getDocument().mime_type; + } + if (realMimeType == null || realMimeType.length() == 0) { + realMimeType = null; + } + } + if (realMimeType != null) { + intent.setDataAndType(Uri.fromFile(f), realMimeType); + } else { + intent.setDataAndType(Uri.fromFile(f), "text/plain"); + } + } else { + intent.setDataAndType(Uri.fromFile(f), "text/plain"); + } + if (realMimeType != null) { + try { + activity.startActivityForResult(intent, 500); + } catch (Exception e) { + intent.setDataAndType(Uri.fromFile(f), "text/plain"); + activity.startActivityForResult(intent, 500); + } + } else { + activity.startActivityForResult(intent, 500); + } + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Animator10.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Animator10.java deleted file mode 100644 index 6defed840..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Animator10.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.telegram.messenger.Animation; - -import android.view.animation.Interpolator; - -import java.util.ArrayList; - -public abstract class Animator10 implements Cloneable { - - ArrayList mListeners = null; - ArrayList mPauseListeners = null; - boolean mPaused = false; - - public abstract long getStartDelay(); - - public abstract void setStartDelay(long startDelay); - - public abstract Animator10 setDuration(long duration); - - public abstract long getDuration(); - - public abstract void setInterpolator(Interpolator value); - - public abstract boolean isRunning(); - - public void start() { - - } - - public void cancel() { - - } - - public void end() { - - } - - @SuppressWarnings("unchecked") - public void pause() { - if (isStarted() && !mPaused) { - mPaused = true; - if (mPauseListeners != null) { - ArrayList tmpListeners = (ArrayList) mPauseListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorPauseListener tmpListener : tmpListeners) { - tmpListener.onAnimationPause(this); - } - } - } - } - - @SuppressWarnings("unchecked") - public void resume() { - if (mPaused) { - mPaused = false; - if (mPauseListeners != null) { - ArrayList tmpListeners = (ArrayList) mPauseListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorPauseListener tmpListener : tmpListeners) { - tmpListener.onAnimationResume(this); - } - } - } - } - - public boolean isPaused() { - return mPaused; - } - - public boolean isStarted() { - return isRunning(); - } - - public Interpolator getInterpolator() { - return null; - } - - public void addListener(AnimatorListener listener) { - if (mListeners == null) { - mListeners = new ArrayList(); - } - mListeners.add(listener); - } - - public void removeListener(AnimatorListener listener) { - if (mListeners == null) { - return; - } - mListeners.remove(listener); - if (mListeners.size() == 0) { - mListeners = null; - } - } - - public ArrayList getListeners() { - return mListeners; - } - - public void addPauseListener(AnimatorPauseListener listener) { - if (mPauseListeners == null) { - mPauseListeners = new ArrayList(); - } - mPauseListeners.add(listener); - } - - public void removePauseListener(AnimatorPauseListener listener) { - if (mPauseListeners == null) { - return; - } - mPauseListeners.remove(listener); - if (mPauseListeners.size() == 0) { - mPauseListeners = null; - } - } - - public void removeAllListeners() { - if (mListeners != null) { - mListeners.clear(); - mListeners = null; - } - if (mPauseListeners != null) { - mPauseListeners.clear(); - mPauseListeners = null; - } - } - - @Override - public Animator10 clone() { - try { - final Animator10 anim = (Animator10) super.clone(); - if (mListeners != null) { - ArrayList oldListeners = mListeners; - anim.mListeners = new ArrayList(); - int numListeners = oldListeners.size(); - for (AnimatorListener oldListener : oldListeners) { - anim.mListeners.add(oldListener); - } - } - if (mPauseListeners != null) { - ArrayList oldListeners = mPauseListeners; - anim.mPauseListeners = new ArrayList(); - int numListeners = oldListeners.size(); - for (AnimatorPauseListener oldListener : oldListeners) { - anim.mPauseListeners.add(oldListener); - } - } - return anim; - } catch (CloneNotSupportedException e) { - throw new AssertionError(); - } - } - - public void setupStartValues() { - - } - - public void setupEndValues() { - - } - - public void setTarget(Object target) { - - } - - public interface AnimatorListener { - void onAnimationStart(Animator10 animation); - void onAnimationEnd(Animator10 animation); - void onAnimationCancel(Animator10 animation); - void onAnimationRepeat(Animator10 animation); - } - - public interface AnimatorPauseListener { - void onAnimationPause(Animator10 animation); - void onAnimationResume(Animator10 animation); - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorListenerAdapter10.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorListenerAdapter10.java deleted file mode 100644 index f5aefe9a8..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorListenerAdapter10.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.telegram.messenger.Animation; - -public abstract class AnimatorListenerAdapter10 implements Animator10.AnimatorListener, Animator10.AnimatorPauseListener { - - @Override - public void onAnimationCancel(Animator10 animation) { - - } - - @Override - public void onAnimationEnd(Animator10 animation) { - - } - - @Override - public void onAnimationRepeat(Animator10 animation) { - - } - - @Override - public void onAnimationStart(Animator10 animation) { - - } - - @Override - public void onAnimationPause(Animator10 animation) { - - } - - @Override - public void onAnimationResume(Animator10 animation) { - - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorSet10.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorSet10.java deleted file mode 100644 index 2f749f2f0..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorSet10.java +++ /dev/null @@ -1,705 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.telegram.messenger.Animation; - -import android.view.animation.Interpolator; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; - -public final class AnimatorSet10 extends Animator10 { - - private ArrayList mPlayingSet = new ArrayList<>(); - private HashMap mNodeMap = new HashMap<>(); - private ArrayList mNodes = new ArrayList<>(); - private ArrayList mSortedNodes = new ArrayList<>(); - private boolean mNeedsSort = true; - private AnimatorSetListener mSetListener = null; - boolean mTerminated = false; - private boolean mStarted = false; - private long mStartDelay = 0; - private ValueAnimator mDelayAnim = null; - private long mDuration = -1; - private Interpolator mInterpolator = null; - - public void playTogether(Animator10... items) { - if (items != null) { - mNeedsSort = true; - Builder builder = play(items[0]); - for (int i = 1; i < items.length; ++i) { - builder.with(items[i]); - } - } - } - - public void playTogether(Collection items) { - if (items != null && items.size() > 0) { - mNeedsSort = true; - Builder builder = null; - for (Animator10 anim : items) { - if (builder == null) { - builder = play(anim); - } else { - builder.with(anim); - } - } - } - } - - public void playSequentially(Animator10... items) { - if (items != null) { - mNeedsSort = true; - if (items.length == 1) { - play(items[0]); - } else { - for (int i = 0; i < items.length - 1; ++i) { - play(items[i]).before(items[i+1]); - } - } - } - } - - public void playSequentially(List items) { - if (items != null && items.size() > 0) { - mNeedsSort = true; - if (items.size() == 1) { - play(items.get(0)); - } else { - for (int i = 0; i < items.size() - 1; ++i) { - play(items.get(i)).before(items.get(i+1)); - } - } - } - } - - public ArrayList getChildAnimations() { - ArrayList childList = new ArrayList<>(); - for (Node node : mNodes) { - childList.add(node.animation); - } - return childList; - } - - @Override - public void setTarget(Object target) { - for (Node node : mNodes) { - Animator10 animation = node.animation; - if (animation instanceof AnimatorSet10) { - animation.setTarget(target); - } else if (animation instanceof ObjectAnimator10) { - animation.setTarget(target); - } - } - } - - @Override - public void setInterpolator(Interpolator interpolator) { - mInterpolator = interpolator; - } - - @Override - public Interpolator getInterpolator() { - return mInterpolator; - } - - public Builder play(Animator10 anim) { - if (anim != null) { - mNeedsSort = true; - return new Builder(anim); - } - return null; - } - - @SuppressWarnings("unchecked") - @Override - public void cancel() { - mTerminated = true; - if (isStarted()) { - ArrayList tmpListeners = null; - if (mListeners != null) { - tmpListeners = (ArrayList) mListeners.clone(); - for (AnimatorListener listener : tmpListeners) { - listener.onAnimationCancel(this); - } - } - if (mDelayAnim != null && mDelayAnim.isRunning()) { - mDelayAnim.cancel(); - } else if (mSortedNodes.size() > 0) { - for (Node node : mSortedNodes) { - node.animation.cancel(); - } - } - if (tmpListeners != null) { - for (AnimatorListener listener : tmpListeners) { - listener.onAnimationEnd(this); - } - } - mStarted = false; - } - } - - @SuppressWarnings("unchecked") - @Override - public void end() { - mTerminated = true; - if (isStarted()) { - if (mSortedNodes.size() != mNodes.size()) { - // hasn't been started yet - sort the nodes now, then end them - sortNodes(); - for (Node node : mSortedNodes) { - if (mSetListener == null) { - mSetListener = new AnimatorSetListener(this); - } - node.animation.addListener(mSetListener); - } - } - if (mDelayAnim != null) { - mDelayAnim.cancel(); - } - if (mSortedNodes.size() > 0) { - for (Node node : mSortedNodes) { - node.animation.end(); - } - } - if (mListeners != null) { - ArrayList tmpListeners = (ArrayList) mListeners.clone(); - for (AnimatorListener listener : tmpListeners) { - listener.onAnimationEnd(this); - } - } - mStarted = false; - } - } - - @Override - public boolean isRunning() { - for (Node node : mNodes) { - if (node.animation.isRunning()) { - return true; - } - } - return false; - } - - @Override - public boolean isStarted() { - return mStarted; - } - - @Override - public long getStartDelay() { - return mStartDelay; - } - - @Override - public void setStartDelay(long startDelay) { - mStartDelay = startDelay; - } - - @Override - public long getDuration() { - return mDuration; - } - - @Override - public AnimatorSet10 setDuration(long duration) { - if (duration < 0) { - throw new IllegalArgumentException("duration must be a value of zero or greater"); - } - mDuration = duration; - return this; - } - - @Override - public void setupStartValues() { - for (Node node : mNodes) { - node.animation.setupStartValues(); - } - } - - @Override - public void setupEndValues() { - for (Node node : mNodes) { - node.animation.setupEndValues(); - } - } - - @Override - public void pause() { - boolean previouslyPaused = mPaused; - super.pause(); - if (!previouslyPaused && mPaused) { - if (mDelayAnim != null) { - mDelayAnim.pause(); - } else { - for (Node node : mNodes) { - node.animation.pause(); - } - } - } - } - - @Override - public void resume() { - boolean previouslyPaused = mPaused; - super.resume(); - if (previouslyPaused && !mPaused) { - if (mDelayAnim != null) { - mDelayAnim.resume(); - } else { - for (Node node : mNodes) { - node.animation.resume(); - } - } - } - } - - @SuppressWarnings("unchecked") - @Override - public void start() { - mTerminated = false; - mStarted = true; - mPaused = false; - - if (mDuration >= 0) { - for (Node node : mNodes) { - node.animation.setDuration(mDuration); - } - } - if (mInterpolator != null) { - for (Node node : mNodes) { - node.animation.setInterpolator(mInterpolator); - } - } - - sortNodes(); - - int numSortedNodes = mSortedNodes.size(); - for (Node node : mSortedNodes) { - ArrayList oldListeners = node.animation.getListeners(); - if (oldListeners != null && oldListeners.size() > 0) { - final ArrayList clonedListeners = new - ArrayList<>(oldListeners); - - for (AnimatorListener listener : clonedListeners) { - if (listener instanceof DependencyListener || - listener instanceof AnimatorSetListener) { - node.animation.removeListener(listener); - } - } - } - } - - final ArrayList nodesToStart = new ArrayList<>(); - for (Node node : mSortedNodes) { - if (mSetListener == null) { - mSetListener = new AnimatorSetListener(this); - } - if (node.dependencies == null || node.dependencies.size() == 0) { - nodesToStart.add(node); - } else { - int numDependencies = node.dependencies.size(); - for (int j = 0; j < numDependencies; ++j) { - Dependency dependency = node.dependencies.get(j); - dependency.node.animation.addListener( - new DependencyListener(this, node, dependency.rule)); - } - node.tmpDependencies = (ArrayList) node.dependencies.clone(); - } - node.animation.addListener(mSetListener); - } - - if (mStartDelay <= 0) { - for (Node node : nodesToStart) { - node.animation.start(); - mPlayingSet.add(node.animation); - } - } else { - mDelayAnim = ValueAnimator.ofFloat(0f, 1f); - mDelayAnim.setDuration(mStartDelay); - mDelayAnim.addListener(new AnimatorListenerAdapter10() { - boolean canceled = false; - public void onAnimationCancel(Animator10 anim) { - canceled = true; - } - public void onAnimationEnd(Animator10 anim) { - if (!canceled) { - int numNodes = nodesToStart.size(); - for (Node node : nodesToStart) { - node.animation.start(); - mPlayingSet.add(node.animation); - } - } - mDelayAnim = null; - } - }); - mDelayAnim.start(); - } - if (mListeners != null) { - ArrayList tmpListeners = - (ArrayList) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorListener tmpListener : tmpListeners) { - tmpListener.onAnimationStart(this); - } - } - if (mNodes.size() == 0 && mStartDelay == 0) { - mStarted = false; - if (mListeners != null) { - ArrayList tmpListeners = - (ArrayList) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorListener tmpListener : tmpListeners) { - tmpListener.onAnimationEnd(this); - } - } - } - } - - @Override - public AnimatorSet10 clone() { - final AnimatorSet10 anim = (AnimatorSet10) super.clone(); - - anim.mNeedsSort = true; - anim.mTerminated = false; - anim.mStarted = false; - anim.mPlayingSet = new ArrayList<>(); - anim.mNodeMap = new HashMap<>(); - anim.mNodes = new ArrayList<>(); - anim.mSortedNodes = new ArrayList<>(); - - HashMap nodeCloneMap = new HashMap<>(); - for (Node node : mNodes) { - Node nodeClone = node.clone(); - nodeCloneMap.put(node, nodeClone); - anim.mNodes.add(nodeClone); - anim.mNodeMap.put(nodeClone.animation, nodeClone); - nodeClone.dependencies = null; - nodeClone.tmpDependencies = null; - nodeClone.nodeDependents = null; - nodeClone.nodeDependencies = null; - ArrayList cloneListeners = nodeClone.animation.getListeners(); - if (cloneListeners != null) { - ArrayList listenersToRemove = null; - for (AnimatorListener listener : cloneListeners) { - if (listener instanceof AnimatorSetListener) { - if (listenersToRemove == null) { - listenersToRemove = new ArrayList<>(); - } - listenersToRemove.add(listener); - } - } - if (listenersToRemove != null) { - for (AnimatorListener listener : listenersToRemove) { - cloneListeners.remove(listener); - } - } - } - } - for (Node node : mNodes) { - Node nodeClone = nodeCloneMap.get(node); - if (node.dependencies != null) { - for (Dependency dependency : node.dependencies) { - Node clonedDependencyNode = nodeCloneMap.get(dependency.node); - Dependency cloneDependency = new Dependency(clonedDependencyNode, dependency.rule); - nodeClone.addDependency(cloneDependency); - } - } - } - return anim; - } - - private static class DependencyListener implements AnimatorListener { - - private AnimatorSet10 mAnimatorSet; - private Node mNode; - private int mRule; - - public DependencyListener(AnimatorSet10 animatorSet, Node node, int rule) { - this.mAnimatorSet = animatorSet; - this.mNode = node; - this.mRule = rule; - } - - public void onAnimationCancel(Animator10 animation) { - - } - - public void onAnimationEnd(Animator10 animation) { - if (mRule == Dependency.AFTER) { - startIfReady(animation); - } - } - - public void onAnimationRepeat(Animator10 animation) { - - } - - public void onAnimationStart(Animator10 animation) { - if (mRule == Dependency.WITH) { - startIfReady(animation); - } - } - - private void startIfReady(Animator10 dependencyAnimation) { - if (mAnimatorSet.mTerminated) { - return; - } - Dependency dependencyToRemove = null; - int numDependencies = mNode.tmpDependencies.size(); - for (int i = 0; i < numDependencies; ++i) { - Dependency dependency = mNode.tmpDependencies.get(i); - if (dependency.rule == mRule && dependency.node.animation == dependencyAnimation) { - dependencyToRemove = dependency; - dependencyAnimation.removeListener(this); - break; - } - } - mNode.tmpDependencies.remove(dependencyToRemove); - if (mNode.tmpDependencies.size() == 0) { - mNode.animation.start(); - mAnimatorSet.mPlayingSet.add(mNode.animation); - } - } - } - - private class AnimatorSetListener implements AnimatorListener { - - private AnimatorSet10 mAnimatorSet; - - AnimatorSetListener(AnimatorSet10 animatorSet) { - mAnimatorSet = animatorSet; - } - - public void onAnimationCancel(Animator10 animation) { - if (!mTerminated) { - if (mPlayingSet.size() == 0) { - if (mListeners != null) { - int numListeners = mListeners.size(); - for (AnimatorListener mListener : mListeners) { - mListener.onAnimationCancel(mAnimatorSet); - } - } - } - } - } - - @SuppressWarnings("unchecked") - public void onAnimationEnd(Animator10 animation) { - animation.removeListener(this); - mPlayingSet.remove(animation); - Node animNode = mAnimatorSet.mNodeMap.get(animation); - animNode.done = true; - if (!mTerminated) { - ArrayList sortedNodes = mAnimatorSet.mSortedNodes; - boolean allDone = true; - int numSortedNodes = sortedNodes.size(); - for (Node sortedNode : sortedNodes) { - if (!sortedNode.done) { - allDone = false; - break; - } - } - if (allDone) { - if (mListeners != null) { - ArrayList tmpListeners = - (ArrayList) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorListener tmpListener : tmpListeners) { - tmpListener.onAnimationEnd(mAnimatorSet); - } - } - mAnimatorSet.mStarted = false; - mAnimatorSet.mPaused = false; - } - } - } - - public void onAnimationRepeat(Animator10 animation) { - - } - - public void onAnimationStart(Animator10 animation) { - - } - } - - private void sortNodes() { - if (mNeedsSort) { - mSortedNodes.clear(); - ArrayList roots = new ArrayList<>(); - int numNodes = mNodes.size(); - for (Node node : mNodes) { - if (node.dependencies == null || node.dependencies.size() == 0) { - roots.add(node); - } - } - ArrayList tmpRoots = new ArrayList<>(); - while (roots.size() > 0) { - int numRoots = roots.size(); - for (Node root : roots) { - mSortedNodes.add(root); - if (root.nodeDependents != null) { - int numDependents = root.nodeDependents.size(); - for (int j = 0; j < numDependents; ++j) { - Node node = root.nodeDependents.get(j); - node.nodeDependencies.remove(root); - if (node.nodeDependencies.size() == 0) { - tmpRoots.add(node); - } - } - } - } - roots.clear(); - roots.addAll(tmpRoots); - tmpRoots.clear(); - } - mNeedsSort = false; - if (mSortedNodes.size() != mNodes.size()) { - throw new IllegalStateException("Circular dependencies cannot exist in AnimatorSet"); - } - } else { - int numNodes = mNodes.size(); - for (Node node : mNodes) { - if (node.dependencies != null && node.dependencies.size() > 0) { - int numDependencies = node.dependencies.size(); - for (int j = 0; j < numDependencies; ++j) { - Dependency dependency = node.dependencies.get(j); - if (node.nodeDependencies == null) { - node.nodeDependencies = new ArrayList<>(); - } - if (!node.nodeDependencies.contains(dependency.node)) { - node.nodeDependencies.add(dependency.node); - } - } - } - node.done = false; - } - } - } - - private static class Dependency { - static final int WITH = 0; - static final int AFTER = 1; - public Node node; - public int rule; - - public Dependency(Node node, int rule) { - this.node = node; - this.rule = rule; - } - } - - private static class Node implements Cloneable { - public Animator10 animation; - public ArrayList dependencies = null; - public ArrayList tmpDependencies = null; - public ArrayList nodeDependencies = null; - public ArrayList nodeDependents = null; - public boolean done = false; - - public Node(Animator10 animation) { - this.animation = animation; - } - - public void addDependency(Dependency dependency) { - if (dependencies == null) { - dependencies = new ArrayList<>(); - nodeDependencies = new ArrayList<>(); - } - dependencies.add(dependency); - if (!nodeDependencies.contains(dependency.node)) { - nodeDependencies.add(dependency.node); - } - Node dependencyNode = dependency.node; - if (dependencyNode.nodeDependents == null) { - dependencyNode.nodeDependents = new ArrayList<>(); - } - dependencyNode.nodeDependents.add(this); - } - - @Override - public Node clone() { - try { - Node node = (Node) super.clone(); - node.animation = animation.clone(); - return node; - } catch (CloneNotSupportedException e) { - throw new AssertionError(); - } - } - } - - public class Builder { - - private Node mCurrentNode; - - Builder(Animator10 anim) { - mCurrentNode = mNodeMap.get(anim); - if (mCurrentNode == null) { - mCurrentNode = new Node(anim); - mNodeMap.put(anim, mCurrentNode); - mNodes.add(mCurrentNode); - } - } - - public Builder with(Animator10 anim) { - Node node = mNodeMap.get(anim); - if (node == null) { - node = new Node(anim); - mNodeMap.put(anim, node); - mNodes.add(node); - } - Dependency dependency = new Dependency(mCurrentNode, Dependency.WITH); - node.addDependency(dependency); - return this; - } - - public Builder before(Animator10 anim) { - Node node = mNodeMap.get(anim); - if (node == null) { - node = new Node(anim); - mNodeMap.put(anim, node); - mNodes.add(node); - } - Dependency dependency = new Dependency(mCurrentNode, Dependency.AFTER); - node.addDependency(dependency); - return this; - } - - public Builder after(Animator10 anim) { - Node node = mNodeMap.get(anim); - if (node == null) { - node = new Node(anim); - mNodeMap.put(anim, node); - mNodes.add(node); - } - Dependency dependency = new Dependency(node, Dependency.AFTER); - mCurrentNode.addDependency(dependency); - return this; - } - - public Builder after(long delay) { - ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); - anim.setDuration(delay); - after(anim); - return this; - } - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatKeyframeSet.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatKeyframeSet.java deleted file mode 100644 index e7865e05c..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatKeyframeSet.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.telegram.messenger.Animation; - -import android.view.animation.Interpolator; - -import org.telegram.messenger.Animation.Keyframe.FloatKeyframe; - -import java.util.ArrayList; - -class FloatKeyframeSet extends KeyframeSet { - private float firstValue; - private float lastValue; - private float deltaValue; - private boolean firstTime = true; - - public FloatKeyframeSet(FloatKeyframe... keyframes) { - super(keyframes); - } - - @Override - public Object getValue(float fraction) { - return getFloatValue(fraction); - } - - @Override - public FloatKeyframeSet clone() { - ArrayList keyframes = mKeyframes; - int numKeyframes = mKeyframes.size(); - FloatKeyframe[] newKeyframes = new FloatKeyframe[numKeyframes]; - for (int i = 0; i < numKeyframes; ++i) { - newKeyframes[i] = (FloatKeyframe) keyframes.get(i).clone(); - } - return new FloatKeyframeSet(newKeyframes); - } - - @SuppressWarnings("unchecked") - public float getFloatValue(float fraction) { - if (mNumKeyframes == 2) { - if (firstTime) { - firstTime = false; - firstValue = ((FloatKeyframe) mKeyframes.get(0)).getFloatValue(); - lastValue = ((FloatKeyframe) mKeyframes.get(1)).getFloatValue(); - deltaValue = lastValue - firstValue; - } - if (mInterpolator != null) { - fraction = mInterpolator.getInterpolation(fraction); - } - if (mEvaluator == null) { - return firstValue + fraction * deltaValue; - } else { - return ((Number)mEvaluator.evaluate(fraction, firstValue, lastValue)).floatValue(); - } - } - if (fraction <= 0f) { - final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0); - final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(1); - float prevValue = prevKeyframe.getFloatValue(); - float nextValue = nextKeyframe.getFloatValue(); - float prevFraction = prevKeyframe.getFraction(); - float nextFraction = nextKeyframe.getFraction(); - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); - return mEvaluator == null ? prevValue + intervalFraction * (nextValue - prevValue) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).floatValue(); - } else if (fraction >= 1f) { - final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 2); - final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 1); - float prevValue = prevKeyframe.getFloatValue(); - float nextValue = nextKeyframe.getFloatValue(); - float prevFraction = prevKeyframe.getFraction(); - float nextFraction = nextKeyframe.getFraction(); - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); - return mEvaluator == null ? prevValue + intervalFraction * (nextValue - prevValue) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).floatValue(); - } - FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0); - for (int i = 1; i < mNumKeyframes; ++i) { - FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(i); - if (fraction < nextKeyframe.getFraction()) { - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevKeyframe.getFraction()) / - (nextKeyframe.getFraction() - prevKeyframe.getFraction()); - float prevValue = prevKeyframe.getFloatValue(); - float nextValue = nextKeyframe.getFloatValue(); - return mEvaluator == null ? prevValue + intervalFraction * (nextValue - prevValue) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).floatValue(); - } - prevKeyframe = nextKeyframe; - } - return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).floatValue(); - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntKeyframeSet.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntKeyframeSet.java deleted file mode 100644 index 2224f2126..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntKeyframeSet.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.telegram.messenger.Animation; - -import android.view.animation.Interpolator; - -import org.telegram.messenger.Animation.Keyframe.IntKeyframe; - -import java.util.ArrayList; - -class IntKeyframeSet extends KeyframeSet { - private int firstValue; - private int lastValue; - private int deltaValue; - private boolean firstTime = true; - - public IntKeyframeSet(IntKeyframe... keyframes) { - super(keyframes); - } - - @Override - public Object getValue(float fraction) { - return getIntValue(fraction); - } - - @Override - public IntKeyframeSet clone() { - ArrayList keyframes = mKeyframes; - int numKeyframes = mKeyframes.size(); - IntKeyframe[] newKeyframes = new IntKeyframe[numKeyframes]; - for (int i = 0; i < numKeyframes; ++i) { - newKeyframes[i] = (IntKeyframe) keyframes.get(i).clone(); - } - return new IntKeyframeSet(newKeyframes); - } - - @SuppressWarnings("unchecked") - public int getIntValue(float fraction) { - if (mNumKeyframes == 2) { - if (firstTime) { - firstTime = false; - firstValue = ((IntKeyframe) mKeyframes.get(0)).getIntValue(); - lastValue = ((IntKeyframe) mKeyframes.get(1)).getIntValue(); - deltaValue = lastValue - firstValue; - } - if (mInterpolator != null) { - fraction = mInterpolator.getInterpolation(fraction); - } - if (mEvaluator == null) { - return firstValue + (int)(fraction * deltaValue); - } else { - return ((Number)mEvaluator.evaluate(fraction, firstValue, lastValue)).intValue(); - } - } - if (fraction <= 0f) { - final IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(0); - final IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(1); - int prevValue = prevKeyframe.getIntValue(); - int nextValue = nextKeyframe.getIntValue(); - float prevFraction = prevKeyframe.getFraction(); - float nextFraction = nextKeyframe.getFraction(); - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); - return mEvaluator == null ? prevValue + (int)(intervalFraction * (nextValue - prevValue)) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).intValue(); - } else if (fraction >= 1f) { - final IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(mNumKeyframes - 2); - final IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(mNumKeyframes - 1); - int prevValue = prevKeyframe.getIntValue(); - int nextValue = nextKeyframe.getIntValue(); - float prevFraction = prevKeyframe.getFraction(); - float nextFraction = nextKeyframe.getFraction(); - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); - return mEvaluator == null ? prevValue + (int)(intervalFraction * (nextValue - prevValue)) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).intValue(); - } - IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(0); - for (int i = 1; i < mNumKeyframes; ++i) { - IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(i); - if (fraction < nextKeyframe.getFraction()) { - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevKeyframe.getFraction()) / (nextKeyframe.getFraction() - prevKeyframe.getFraction()); - int prevValue = prevKeyframe.getIntValue(); - int nextValue = nextKeyframe.getIntValue(); - return mEvaluator == null ? prevValue + (int)(intervalFraction * (nextValue - prevValue)) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).intValue(); - } - prevKeyframe = nextKeyframe; - } - return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).intValue(); - } -} - diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Keyframe.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Keyframe.java deleted file mode 100644 index b65ddaf44..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Keyframe.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.telegram.messenger.Animation; - -import android.view.animation.Interpolator; - -public abstract class Keyframe implements Cloneable { - - float mFraction; - Class mValueType; - private Interpolator mInterpolator = null; - boolean mHasValue = false; - - public static Keyframe ofInt(float fraction, int value) { - return new IntKeyframe(fraction, value); - } - - public static Keyframe ofInt(float fraction) { - return new IntKeyframe(fraction); - } - - public static Keyframe ofFloat(float fraction, float value) { - return new FloatKeyframe(fraction, value); - } - - public static Keyframe ofFloat(float fraction) { - return new FloatKeyframe(fraction); - } - - public static Keyframe ofObject(float fraction, Object value) { - return new ObjectKeyframe(fraction, value); - } - - public static Keyframe ofObject(float fraction) { - return new ObjectKeyframe(fraction, null); - } - - public boolean hasValue() { - return mHasValue; - } - - public abstract Object getValue(); - public abstract void setValue(Object value); - - public float getFraction() { - return mFraction; - } - - public void setFraction(float fraction) { - mFraction = fraction; - } - - public Interpolator getInterpolator() { - return mInterpolator; - } - - public void setInterpolator(Interpolator interpolator) { - mInterpolator = interpolator; - } - - public Class getType() { - return mValueType; - } - - @Override - public abstract Keyframe clone(); - - static class ObjectKeyframe extends Keyframe { - - Object mValue; - - ObjectKeyframe(float fraction, Object value) { - mFraction = fraction; - mValue = value; - mHasValue = (value != null); - mValueType = mHasValue ? value.getClass() : Object.class; - } - - public Object getValue() { - return mValue; - } - - public void setValue(Object value) { - mValue = value; - mHasValue = (value != null); - } - - @Override - public ObjectKeyframe clone() { - ObjectKeyframe kfClone = new ObjectKeyframe(getFraction(), mHasValue ? mValue : null); - kfClone.setInterpolator(getInterpolator()); - return kfClone; - } - } - - static class IntKeyframe extends Keyframe { - - int mValue; - - IntKeyframe(float fraction, int value) { - mFraction = fraction; - mValue = value; - mValueType = int.class; - mHasValue = true; - } - - IntKeyframe(float fraction) { - mFraction = fraction; - mValueType = int.class; - } - - public int getIntValue() { - return mValue; - } - - public Object getValue() { - return mValue; - } - - public void setValue(Object value) { - if (value != null && value.getClass() == Integer.class) { - mValue = (Integer) value; - mHasValue = true; - } - } - - @Override - public IntKeyframe clone() { - IntKeyframe kfClone = mHasValue ? new IntKeyframe(getFraction(), mValue) : new IntKeyframe(getFraction()); - kfClone.setInterpolator(getInterpolator()); - return kfClone; - } - } - - static class FloatKeyframe extends Keyframe { - - float mValue; - - FloatKeyframe(float fraction, float value) { - mFraction = fraction; - mValue = value; - mValueType = float.class; - mHasValue = true; - } - - FloatKeyframe(float fraction) { - mFraction = fraction; - mValueType = float.class; - } - - public float getFloatValue() { - return mValue; - } - - public Object getValue() { - return mValue; - } - - public void setValue(Object value) { - if (value != null && value.getClass() == Float.class) { - mValue = (Float) value; - mHasValue = true; - } - } - - @Override - public FloatKeyframe clone() { - FloatKeyframe kfClone = mHasValue ? new FloatKeyframe(getFraction(), mValue) : new FloatKeyframe(getFraction()); - kfClone.setInterpolator(getInterpolator()); - return kfClone; - } - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/KeyframeSet.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/KeyframeSet.java deleted file mode 100644 index 2e86dc876..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/KeyframeSet.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.telegram.messenger.Animation; - -import java.util.ArrayList; -import java.util.Arrays; -import android.util.Log; -import android.view.animation.Interpolator; - -import org.telegram.messenger.Animation.Keyframe.IntKeyframe; -import org.telegram.messenger.Animation.Keyframe.FloatKeyframe; -import org.telegram.messenger.Animation.Keyframe.ObjectKeyframe; - -class KeyframeSet { - - int mNumKeyframes; - - Keyframe mFirstKeyframe; - Keyframe mLastKeyframe; - Interpolator mInterpolator; - ArrayList mKeyframes; - TypeEvaluator mEvaluator; - - public KeyframeSet(Keyframe... keyframes) { - mNumKeyframes = keyframes.length; - mKeyframes = new ArrayList(); - mKeyframes.addAll(Arrays.asList(keyframes)); - mFirstKeyframe = mKeyframes.get(0); - mLastKeyframe = mKeyframes.get(mNumKeyframes - 1); - mInterpolator = mLastKeyframe.getInterpolator(); - } - - public static KeyframeSet ofInt(int... values) { - int numKeyframes = values.length; - IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2)]; - if (numKeyframes == 1) { - keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f); - keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]); - } else { - keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f, values[0]); - for (int i = 1; i < numKeyframes; ++i) { - keyframes[i] = (IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]); - } - } - return new IntKeyframeSet(keyframes); - } - - public static KeyframeSet ofFloat(float... values) { - boolean badValue = false; - int numKeyframes = values.length; - FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)]; - if (numKeyframes == 1) { - keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f); - keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]); - if (Float.isNaN(values[0])) { - badValue = true; - } - } else { - keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]); - for (int i = 1; i < numKeyframes; ++i) { - keyframes[i] = (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]); - if (Float.isNaN(values[i])) { - badValue = true; - } - } - } - if (badValue) { - Log.w("Animator", "Bad value (NaN) in float animator"); - } - return new FloatKeyframeSet(keyframes); - } - - public static KeyframeSet ofKeyframe(Keyframe... keyframes) { - int numKeyframes = keyframes.length; - boolean hasFloat = false; - boolean hasInt = false; - boolean hasOther = false; - for (Keyframe keyframe : keyframes) { - if (keyframe instanceof FloatKeyframe) { - hasFloat = true; - } else if (keyframe instanceof IntKeyframe) { - hasInt = true; - } else { - hasOther = true; - } - } - if (hasFloat && !hasInt && !hasOther) { - FloatKeyframe floatKeyframes[] = new FloatKeyframe[numKeyframes]; - for (int i = 0; i < numKeyframes; ++i) { - floatKeyframes[i] = (FloatKeyframe) keyframes[i]; - } - return new FloatKeyframeSet(floatKeyframes); - } else if (hasInt && !hasFloat && !hasOther) { - IntKeyframe intKeyframes[] = new IntKeyframe[numKeyframes]; - for (int i = 0; i < numKeyframes; ++i) { - intKeyframes[i] = (IntKeyframe) keyframes[i]; - } - return new IntKeyframeSet(intKeyframes); - } else { - return new KeyframeSet(keyframes); - } - } - - public static KeyframeSet ofObject(Object... values) { - int numKeyframes = values.length; - ObjectKeyframe keyframes[] = new ObjectKeyframe[Math.max(numKeyframes,2)]; - if (numKeyframes == 1) { - keyframes[0] = (ObjectKeyframe) Keyframe.ofObject(0f); - keyframes[1] = (ObjectKeyframe) Keyframe.ofObject(1f, values[0]); - } else { - keyframes[0] = (ObjectKeyframe) Keyframe.ofObject(0f, values[0]); - for (int i = 1; i < numKeyframes; ++i) { - keyframes[i] = (ObjectKeyframe) Keyframe.ofObject((float) i / (numKeyframes - 1), values[i]); - } - } - return new KeyframeSet(keyframes); - } - - public void setEvaluator(TypeEvaluator evaluator) { - mEvaluator = evaluator; - } - - @Override - public KeyframeSet clone() { - ArrayList keyframes = mKeyframes; - int numKeyframes = mKeyframes.size(); - Keyframe[] newKeyframes = new Keyframe[numKeyframes]; - for (int i = 0; i < numKeyframes; ++i) { - newKeyframes[i] = keyframes.get(i).clone(); - } - return new KeyframeSet(newKeyframes); - } - - @SuppressWarnings("unchecked") - public Object getValue(float fraction) { - if (mNumKeyframes == 2) { - if (mInterpolator != null) { - fraction = mInterpolator.getInterpolation(fraction); - } - return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(), mLastKeyframe.getValue()); - } - if (fraction <= 0f) { - final Keyframe nextKeyframe = mKeyframes.get(1); - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - final float prevFraction = mFirstKeyframe.getFraction(); - float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction); - return mEvaluator.evaluate(intervalFraction, mFirstKeyframe.getValue(), nextKeyframe.getValue()); - } else if (fraction >= 1f) { - final Keyframe prevKeyframe = mKeyframes.get(mNumKeyframes - 2); - final Interpolator interpolator = mLastKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - final float prevFraction = prevKeyframe.getFraction(); - float intervalFraction = (fraction - prevFraction) / (mLastKeyframe.getFraction() - prevFraction); - return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), mLastKeyframe.getValue()); - } - Keyframe prevKeyframe = mFirstKeyframe; - for (int i = 1; i < mNumKeyframes; ++i) { - Keyframe nextKeyframe = mKeyframes.get(i); - if (fraction < nextKeyframe.getFraction()) { - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - final float prevFraction = prevKeyframe.getFraction(); - float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction); - return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), nextKeyframe.getValue()); - } - prevKeyframe = nextKeyframe; - } - return mLastKeyframe.getValue(); - } - - @Override - public String toString() { - String returnVal = " "; - for (int i = 0; i < mNumKeyframes; ++i) { - returnVal += mKeyframes.get(i).getValue() + " "; - } - return returnVal; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ObjectAnimator10.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ObjectAnimator10.java deleted file mode 100644 index fef5f1851..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ObjectAnimator10.java +++ /dev/null @@ -1,488 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.telegram.messenger.Animation; - -import android.view.View; - -import java.util.HashMap; - -public final class ObjectAnimator10 extends ValueAnimator { - - private static final HashMap PROXY_PROPERTIES = new HashMap(); - - static { - Property ALPHA = new FloatProperty10("alpha") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setAlpha(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getAlpha(); - } - }; - - Property PIVOT_X = new FloatProperty10("pivotX") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setPivotX(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getPivotX(); - } - }; - - Property PIVOT_Y = new FloatProperty10("pivotY") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setPivotY(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getPivotY(); - } - }; - - Property TRANSLATION_X = new FloatProperty10("translationX") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setTranslationX(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getTranslationX(); - } - }; - - Property TRANSLATION_Y = new FloatProperty10("translationY") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setTranslationY(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getTranslationY(); - } - }; - - Property ROTATION = new FloatProperty10("rotation") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setRotation(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getRotation(); - } - }; - - Property ROTATION_X = new FloatProperty10("rotationX") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setRotationX(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getRotationX(); - } - }; - - Property ROTATION_Y = new FloatProperty10("rotationY") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setRotationY(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getRotationY(); - } - }; - - Property SCALE_X = new FloatProperty10("scaleX") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setScaleX(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getScaleX(); - } - }; - - Property SCALE_Y = new FloatProperty10("scaleY") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setScaleY(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getScaleY(); - } - }; - - Property SCROLL_X = new IntProperty("scrollX") { - @Override - public void setValue(View object, int value) { - View10.wrap(object).setScrollX(value); - } - - @Override - public Integer get(View object) { - return View10.wrap(object).getScrollX(); - } - }; - - Property SCROLL_Y = new IntProperty("scrollY") { - @Override - public void setValue(View object, int value) { - View10.wrap(object).setScrollY(value); - } - - @Override - public Integer get(View object) { - return View10.wrap(object).getScrollY(); - } - }; - - Property X = new FloatProperty10("x") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setX(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getX(); - } - }; - - Property Y = new FloatProperty10("y") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setY(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getY(); - } - }; - - PROXY_PROPERTIES.put("alpha", ALPHA); - PROXY_PROPERTIES.put("pivotX", PIVOT_X); - PROXY_PROPERTIES.put("pivotY", PIVOT_Y); - PROXY_PROPERTIES.put("translationX", TRANSLATION_X); - PROXY_PROPERTIES.put("translationY", TRANSLATION_Y); - PROXY_PROPERTIES.put("rotation", ROTATION); - PROXY_PROPERTIES.put("rotationX", ROTATION_X); - PROXY_PROPERTIES.put("rotationY", ROTATION_Y); - PROXY_PROPERTIES.put("scaleX", SCALE_X); - PROXY_PROPERTIES.put("scaleY", SCALE_Y); - PROXY_PROPERTIES.put("scrollX", SCROLL_X); - PROXY_PROPERTIES.put("scrollY", SCROLL_Y); - PROXY_PROPERTIES.put("x", X); - PROXY_PROPERTIES.put("y", Y); - } - - private Object mTarget; - private String mPropertyName; - private Property mProperty; - private boolean mAutoCancel = false; - - public void setPropertyName(String propertyName) { - if (mValues != null) { - PropertyValuesHolder valuesHolder = mValues[0]; - String oldName = valuesHolder.getPropertyName(); - valuesHolder.setPropertyName(propertyName); - mValuesMap.remove(oldName); - mValuesMap.put(propertyName, valuesHolder); - } - mPropertyName = propertyName; - mInitialized = false; - } - - public void setProperty(Property property) { - if (mValues != null) { - PropertyValuesHolder valuesHolder = mValues[0]; - String oldName = valuesHolder.getPropertyName(); - valuesHolder.setProperty(property); - mValuesMap.remove(oldName); - mValuesMap.put(mPropertyName, valuesHolder); - } - if (mProperty != null) { - mPropertyName = property.getName(); - } - mProperty = property; - mInitialized = false; - } - - public String getPropertyName() { - String propertyName = null; - if (mPropertyName != null) { - propertyName = mPropertyName; - } else if (mProperty != null) { - propertyName = mProperty.getName(); - } else if (mValues != null && mValues.length > 0) { - for (int i = 0; i < mValues.length; ++i) { - if (i == 0) { - propertyName = ""; - } else { - propertyName += ","; - } - propertyName += mValues[i].getPropertyName(); - } - } - return propertyName; - } - - public ObjectAnimator10() { - - } - - private ObjectAnimator10(Object target, String propertyName) { - mTarget = target; - setPropertyName(propertyName); - } - - private ObjectAnimator10(T target, Property property) { - mTarget = target; - setProperty(property); - } - - public static ObjectAnimator10 ofInt(Object target, String propertyName, int... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, propertyName); - anim.setIntValues(values); - return anim; - } - - public static ObjectAnimator10 ofInt(T target, Property property, int... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, property); - anim.setIntValues(values); - return anim; - } - - public static ObjectAnimator10 ofFloat(Object target, String propertyName, float... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, propertyName); - anim.setFloatValues(values); - return anim; - } - - public static ObjectAnimator10 ofFloat(T target, Property property, float... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, property); - anim.setFloatValues(values); - return anim; - } - - public static ObjectAnimator10 ofObject(Object target, String propertyName, TypeEvaluator evaluator, Object... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, propertyName); - anim.setObjectValues(values); - anim.setEvaluator(evaluator); - return anim; - } - - public static ObjectAnimator10 ofObject(T target, Property property, TypeEvaluator evaluator, V... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, property); - anim.setObjectValues(values); - anim.setEvaluator(evaluator); - return anim; - } - - public static ObjectAnimator10 ofPropertyValuesHolder(Object target, PropertyValuesHolder... values) { - ObjectAnimator10 anim = new ObjectAnimator10(); - anim.mTarget = target; - anim.setValues(values); - return anim; - } - - @SuppressWarnings("unchecked") - @Override - public void setIntValues(int... values) { - if (mValues == null || mValues.length == 0) { - if (mProperty != null) { - setValues(PropertyValuesHolder.ofInt(mProperty, values)); - } else { - setValues(PropertyValuesHolder.ofInt(mPropertyName, values)); - } - } else { - super.setIntValues(values); - } - } - - @SuppressWarnings("unchecked") - @Override - public void setFloatValues(float... values) { - if (mValues == null || mValues.length == 0) { - if (mProperty != null) { - setValues(PropertyValuesHolder.ofFloat(mProperty, values)); - } else { - setValues(PropertyValuesHolder.ofFloat(mPropertyName, values)); - } - } else { - super.setFloatValues(values); - } - } - - @Override - public void setObjectValues(Object... values) { - if (mValues == null || mValues.length == 0) { - if (mProperty != null) { - setValues(PropertyValuesHolder.ofObject(mProperty, null, values)); - } else { - setValues(PropertyValuesHolder.ofObject(mPropertyName, null, values)); - } - } else { - super.setObjectValues(values); - } - } - - public void setAutoCancel(boolean cancel) { - mAutoCancel = cancel; - } - - private boolean hasSameTargetAndProperties(Animator10 anim) { - if (anim instanceof ObjectAnimator10) { - PropertyValuesHolder[] theirValues = ((ObjectAnimator10) anim).getValues(); - if (((ObjectAnimator10) anim).getTarget() == mTarget && - mValues.length == theirValues.length) { - for (int i = 0; i < mValues.length; ++i) { - PropertyValuesHolder pvhMine = mValues[i]; - PropertyValuesHolder pvhTheirs = theirValues[i]; - if (pvhMine.getPropertyName() == null || - !pvhMine.getPropertyName().equals(pvhTheirs.getPropertyName())) { - return false; - } - } - return true; - } - } - return false; - } - - @Override - public void start() { - AnimationHandler handler = sAnimationHandler.get(); - if (handler != null) { - int numAnims = handler.mAnimations.size(); - for (int i = numAnims - 1; i >= 0; i--) { - if (handler.mAnimations.get(i) instanceof ObjectAnimator10) { - ObjectAnimator10 anim = (ObjectAnimator10) handler.mAnimations.get(i); - if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { - anim.cancel(); - } - } - } - numAnims = handler.mPendingAnimations.size(); - for (int i = numAnims - 1; i >= 0; i--) { - if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator10) { - ObjectAnimator10 anim = (ObjectAnimator10) handler.mPendingAnimations.get(i); - if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { - anim.cancel(); - } - } - } - numAnims = handler.mDelayedAnims.size(); - for (int i = numAnims - 1; i >= 0; i--) { - if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator10) { - ObjectAnimator10 anim = (ObjectAnimator10) handler.mDelayedAnims.get(i); - if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { - anim.cancel(); - } - } - } - } - super.start(); - } - - @Override - void initAnimation() { - if (!mInitialized) { - if ((mProperty == null) && (mTarget instanceof View) && PROXY_PROPERTIES.containsKey(mPropertyName)) { - setProperty(PROXY_PROPERTIES.get(mPropertyName)); - } - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.setupSetterAndGetter(mTarget); - } - super.initAnimation(); - } - } - - @Override - public ObjectAnimator10 setDuration(long duration) { - super.setDuration(duration); - return this; - } - - public Object getTarget() { - return mTarget; - } - - @Override - public void setTarget(Object target) { - if (mTarget != target) { - final Object oldTarget = mTarget; - mTarget = target; - if (oldTarget != null && target != null && oldTarget.getClass() == target.getClass()) { - return; - } - mInitialized = false; - } - } - - @Override - public void setupStartValues() { - initAnimation(); - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.setupStartValue(mTarget); - } - } - - @Override - public void setupEndValues() { - initAnimation(); - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.setupEndValue(mTarget); - } - } - - @Override - void animateValue(float fraction) { - super.animateValue(fraction); - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.setAnimatedValue(mTarget); - } - } - - @Override - public ObjectAnimator10 clone() { - return (ObjectAnimator10) super.clone(); - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Property.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Property.java deleted file mode 100755 index 98983e162..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Property.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.telegram.messenger.Animation; - -public abstract class Property { - - private final String mName; - private final Class mType; - - public static Property of(Class hostType, Class valueType, String name) { - return new ReflectiveProperty(hostType, valueType, name); - } - - public Property(Class type, String name) { - mName = name; - mType = type; - } - - public boolean isReadOnly() { - return false; - } - - public void set(T object, V value) { - throw new UnsupportedOperationException("Property " + getName() +" is read-only"); - } - - public abstract V get(T object); - - public String getName() { - return mName; - } - - public Class getType() { - return mType; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/PropertyValuesHolder.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/PropertyValuesHolder.java deleted file mode 100644 index c4ab83b4c..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/PropertyValuesHolder.java +++ /dev/null @@ -1,545 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.telegram.messenger.Animation; - -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -public class PropertyValuesHolder implements Cloneable { - - String mPropertyName; - protected Property mProperty; - Method mSetter = null; - private Method mGetter = null; - Class mValueType; - KeyframeSet mKeyframeSet = null; - - private static final TypeEvaluator sIntEvaluator = new IntEvaluator(); - private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator(); - - private static Class[] FLOAT_VARIANTS = {float.class, Float.class, double.class, int.class, Double.class, Integer.class}; - private static Class[] INTEGER_VARIANTS = {int.class, Integer.class, float.class, double.class, Float.class, Double.class}; - private static Class[] DOUBLE_VARIANTS = {double.class, Double.class, float.class, int.class, Float.class, Integer.class}; - - private static final HashMap> sSetterPropertyMap = new HashMap>(); - private static final HashMap> sGetterPropertyMap = new HashMap>(); - - final ReentrantReadWriteLock mPropertyMapLock = new ReentrantReadWriteLock(); - final Object[] mTmpValueArray = new Object[1]; - - private TypeEvaluator mEvaluator; - - private Object mAnimatedValue; - - private PropertyValuesHolder(String propertyName) { - mPropertyName = propertyName; - } - - private PropertyValuesHolder(Property property) { - mProperty = property; - if (property != null) { - mPropertyName = property.getName(); - } - } - - public static PropertyValuesHolder ofInt(String propertyName, int... values) { - return new IntPropertyValuesHolder(propertyName, values); - } - - public static PropertyValuesHolder ofInt(Property property, int... values) { - return new IntPropertyValuesHolder(property, values); - } - - public static PropertyValuesHolder ofFloat(String propertyName, float... values) { - return new FloatPropertyValuesHolder(propertyName, values); - } - - public static PropertyValuesHolder ofFloat(Property property, float... values) { - return new FloatPropertyValuesHolder(property, values); - } - - public static PropertyValuesHolder ofObject(String propertyName, TypeEvaluator evaluator, - Object... values) { - PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName); - pvh.setObjectValues(values); - pvh.setEvaluator(evaluator); - return pvh; - } - - public static PropertyValuesHolder ofObject(Property property, - TypeEvaluator evaluator, V... values) { - PropertyValuesHolder pvh = new PropertyValuesHolder(property); - pvh.setObjectValues(values); - pvh.setEvaluator(evaluator); - return pvh; - } - - public static PropertyValuesHolder ofKeyframe(String propertyName, Keyframe... values) { - KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values); - if (keyframeSet instanceof IntKeyframeSet) { - return new IntPropertyValuesHolder(propertyName, (IntKeyframeSet) keyframeSet); - } else if (keyframeSet instanceof FloatKeyframeSet) { - return new FloatPropertyValuesHolder(propertyName, (FloatKeyframeSet) keyframeSet); - } else { - PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName); - pvh.mKeyframeSet = keyframeSet; - pvh.mValueType = values[0].getType(); - return pvh; - } - } - - public static PropertyValuesHolder ofKeyframe(Property property, Keyframe... values) { - KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values); - if (keyframeSet instanceof IntKeyframeSet) { - return new IntPropertyValuesHolder(property, (IntKeyframeSet) keyframeSet); - } else if (keyframeSet instanceof FloatKeyframeSet) { - return new FloatPropertyValuesHolder(property, (FloatKeyframeSet) keyframeSet); - } else { - PropertyValuesHolder pvh = new PropertyValuesHolder(property); - pvh.mKeyframeSet = keyframeSet; - pvh.mValueType = values[0].getType(); - return pvh; - } - } - - public void setIntValues(int... values) { - mValueType = int.class; - mKeyframeSet = KeyframeSet.ofInt(values); - } - - public void setFloatValues(float... values) { - mValueType = float.class; - mKeyframeSet = KeyframeSet.ofFloat(values); - } - - public void setKeyframes(Keyframe... values) { - int numKeyframes = values.length; - Keyframe keyframes[] = new Keyframe[Math.max(numKeyframes, 2)]; - mValueType = values[0].getType(); - System.arraycopy(values, 0, keyframes, 0, numKeyframes); - mKeyframeSet = new KeyframeSet(keyframes); - } - - public void setObjectValues(Object... values) { - mValueType = values[0].getClass(); - mKeyframeSet = KeyframeSet.ofObject(values); - } - - @SuppressWarnings("unchecked") - private Method getPropertyFunction(Class targetClass, String prefix, Class valueType) { - Method returnVal = null; - String methodName = getMethodName(prefix, mPropertyName); - Class args[] = null; - if (valueType == null) { - try { - returnVal = targetClass.getMethod(methodName); - } catch (Throwable e) { - try { - returnVal = targetClass.getDeclaredMethod(methodName); - returnVal.setAccessible(true); - } catch (Throwable e2) { - e2.printStackTrace(); - } - } - } else { - args = new Class[1]; - Class typeVariants[]; - if (mValueType.equals(Float.class)) { - typeVariants = FLOAT_VARIANTS; - } else if (mValueType.equals(Integer.class)) { - typeVariants = INTEGER_VARIANTS; - } else if (mValueType.equals(Double.class)) { - typeVariants = DOUBLE_VARIANTS; - } else { - typeVariants = new Class[1]; - typeVariants[0] = mValueType; - } - for (Class typeVariant : typeVariants) { - args[0] = typeVariant; - try { - returnVal = targetClass.getMethod(methodName, args); - mValueType = typeVariant; - return returnVal; - } catch (Throwable e) { - try { - returnVal = targetClass.getDeclaredMethod(methodName, args); - returnVal.setAccessible(true); - mValueType = typeVariant; - return returnVal; - } catch (Throwable e2) { - // Swallow the error and keep trying other variants - } - } - } - } - - return returnVal; - } - - private Method setupSetterOrGetter(Class targetClass, HashMap> propertyMapMap, String prefix, Class valueType) { - Method setterOrGetter = null; - try { - mPropertyMapLock.writeLock().lock(); - HashMap propertyMap = propertyMapMap.get(targetClass); - if (propertyMap != null) { - setterOrGetter = propertyMap.get(mPropertyName); - } - if (setterOrGetter == null) { - setterOrGetter = getPropertyFunction(targetClass, prefix, valueType); - if (propertyMap == null) { - propertyMap = new HashMap(); - propertyMapMap.put(targetClass, propertyMap); - } - propertyMap.put(mPropertyName, setterOrGetter); - } - } finally { - mPropertyMapLock.writeLock().unlock(); - } - return setterOrGetter; - } - - void setupSetter(Class targetClass) { - mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", mValueType); - } - - private void setupGetter(Class targetClass) { - mGetter = setupSetterOrGetter(targetClass, sGetterPropertyMap, "get", null); - } - - @SuppressWarnings("unchecked") - void setupSetterAndGetter(Object target) { - if (mProperty != null) { - try { - Object testValue = mProperty.get(target); - for (Keyframe kf : mKeyframeSet.mKeyframes) { - if (!kf.hasValue()) { - kf.setValue(mProperty.get(target)); - } - } - return; - } catch (Throwable e) { - mProperty = null; - } - } - Class targetClass = target.getClass(); - if (mSetter == null) { - setupSetter(targetClass); - } - for (Keyframe kf : mKeyframeSet.mKeyframes) { - if (!kf.hasValue()) { - if (mGetter == null) { - setupGetter(targetClass); - if (mGetter == null) { - return; - } - } - try { - kf.setValue(mGetter.invoke(target)); - } catch (Throwable e) { - e.printStackTrace(); - } - } - } - } - - @SuppressWarnings("unchecked") - private void setupValue(Object target, Keyframe kf) { - if (mProperty != null) { - kf.setValue(mProperty.get(target)); - } - try { - if (mGetter == null) { - Class targetClass = target.getClass(); - setupGetter(targetClass); - if (mGetter == null) { - return; - } - } - kf.setValue(mGetter.invoke(target)); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - void setupStartValue(Object target) { - setupValue(target, mKeyframeSet.mKeyframes.get(0)); - } - - void setupEndValue(Object target) { - setupValue(target, mKeyframeSet.mKeyframes.get(mKeyframeSet.mKeyframes.size() - 1)); - } - - @Override - public PropertyValuesHolder clone() { - try { - PropertyValuesHolder newPVH = (PropertyValuesHolder) super.clone(); - newPVH.mPropertyName = mPropertyName; - newPVH.mProperty = mProperty; - newPVH.mKeyframeSet = mKeyframeSet.clone(); - newPVH.mEvaluator = mEvaluator; - return newPVH; - } catch (CloneNotSupportedException e) { - return null; - } - } - - @SuppressWarnings("unchecked") - void setAnimatedValue(Object target) { - if (mProperty != null) { - mProperty.set(target, getAnimatedValue()); - } - if (mSetter != null) { - try { - mTmpValueArray[0] = getAnimatedValue(); - mSetter.invoke(target, mTmpValueArray); - } catch (Throwable e) { - e.printStackTrace(); - } - } - } - - void init() { - if (mEvaluator == null) { - mEvaluator = (mValueType == Integer.class) ? sIntEvaluator : (mValueType == Float.class) ? sFloatEvaluator : null; - } - if (mEvaluator != null) { - mKeyframeSet.setEvaluator(mEvaluator); - } - } - - public void setEvaluator(TypeEvaluator evaluator) { - mEvaluator = evaluator; - mKeyframeSet.setEvaluator(evaluator); - } - - void calculateValue(float fraction) { - mAnimatedValue = mKeyframeSet.getValue(fraction); - } - - public void setPropertyName(String propertyName) { - mPropertyName = propertyName; - } - - public void setProperty(Property property) { - mProperty = property; - } - - public String getPropertyName() { - return mPropertyName; - } - - Object getAnimatedValue() { - return mAnimatedValue; - } - - @Override - public String toString() { - return mPropertyName + ": " + mKeyframeSet.toString(); - } - - static String getMethodName(String prefix, String propertyName) { - if (propertyName == null || propertyName.length() == 0) { - return prefix; - } - char firstLetter = Character.toUpperCase(propertyName.charAt(0)); - String theRest = propertyName.substring(1); - return prefix + firstLetter + theRest; - } - - static class IntPropertyValuesHolder extends PropertyValuesHolder { - private static final HashMap> sJNISetterPropertyMap = new HashMap>(); - private IntProperty mIntProperty; - - IntKeyframeSet mIntKeyframeSet; - int mIntAnimatedValue; - - public IntPropertyValuesHolder(String propertyName, IntKeyframeSet keyframeSet) { - super(propertyName); - mValueType = int.class; - mKeyframeSet = keyframeSet; - mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet; - } - - public IntPropertyValuesHolder(Property property, IntKeyframeSet keyframeSet) { - super(property); - mValueType = int.class; - mKeyframeSet = keyframeSet; - mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet; - if (property instanceof IntProperty) { - mIntProperty = (IntProperty) mProperty; - } - } - - public IntPropertyValuesHolder(String propertyName, int... values) { - super(propertyName); - setIntValues(values); - } - - public IntPropertyValuesHolder(Property property, int... values) { - super(property); - setIntValues(values); - if (property instanceof IntProperty) { - mIntProperty = (IntProperty) mProperty; - } - } - - @Override - public void setIntValues(int... values) { - super.setIntValues(values); - mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet; - } - - @Override - void calculateValue(float fraction) { - mIntAnimatedValue = mIntKeyframeSet.getIntValue(fraction); - } - - @Override - Object getAnimatedValue() { - return mIntAnimatedValue; - } - - @Override - public IntPropertyValuesHolder clone() { - IntPropertyValuesHolder newPVH = (IntPropertyValuesHolder) super.clone(); - newPVH.mIntKeyframeSet = (IntKeyframeSet) newPVH.mKeyframeSet; - return newPVH; - } - - @SuppressWarnings("unchecked") - @Override - void setAnimatedValue(Object target) { - if (mIntProperty != null) { - mIntProperty.setValue(target, mIntAnimatedValue); - return; - } - if (mProperty != null) { - mProperty.set(target, mIntAnimatedValue); - return; - } - if (mSetter != null) { - try { - mTmpValueArray[0] = mIntAnimatedValue; - mSetter.invoke(target, mTmpValueArray); - } catch (Throwable e) { - e.printStackTrace(); - } - } - } - - @Override - void setupSetter(Class targetClass) { - if (mProperty != null) { - return; - } - - super.setupSetter(targetClass); - } - } - - static class FloatPropertyValuesHolder extends PropertyValuesHolder { - - private static final HashMap> sJNISetterPropertyMap = new HashMap>(); - private FloatProperty10 mFloatProperty; - - FloatKeyframeSet mFloatKeyframeSet; - float mFloatAnimatedValue; - - public FloatPropertyValuesHolder(String propertyName, FloatKeyframeSet keyframeSet) { - super(propertyName); - mValueType = float.class; - mKeyframeSet = keyframeSet; - mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet; - } - - public FloatPropertyValuesHolder(Property property, FloatKeyframeSet keyframeSet) { - super(property); - mValueType = float.class; - mKeyframeSet = keyframeSet; - mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet; - if (property instanceof FloatProperty10) { - mFloatProperty = (FloatProperty10) mProperty; - } - } - - public FloatPropertyValuesHolder(String propertyName, float... values) { - super(propertyName); - setFloatValues(values); - } - - public FloatPropertyValuesHolder(Property property, float... values) { - super(property); - setFloatValues(values); - if (property instanceof FloatProperty10) { - mFloatProperty = (FloatProperty10) mProperty; - } - } - - @Override - public void setFloatValues(float... values) { - super.setFloatValues(values); - mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet; - } - - @Override - void calculateValue(float fraction) { - mFloatAnimatedValue = mFloatKeyframeSet.getFloatValue(fraction); - } - - @Override - Object getAnimatedValue() { - return mFloatAnimatedValue; - } - - @Override - public FloatPropertyValuesHolder clone() { - FloatPropertyValuesHolder newPVH = (FloatPropertyValuesHolder) super.clone(); - newPVH.mFloatKeyframeSet = (FloatKeyframeSet) newPVH.mKeyframeSet; - return newPVH; - } - - @SuppressWarnings("unchecked") - @Override - void setAnimatedValue(Object target) { - if (mFloatProperty != null) { - mFloatProperty.setValue(target, mFloatAnimatedValue); - return; - } - if (mProperty != null) { - mProperty.set(target, mFloatAnimatedValue); - return; - } - if (mSetter != null) { - try { - mTmpValueArray[0] = mFloatAnimatedValue; - mSetter.invoke(target, mTmpValueArray); - } catch (Throwable e) { - e.printStackTrace(); - } - } - } - - @Override - void setupSetter(Class targetClass) { - if (mProperty != null) { - return; - } - super.setupSetter(targetClass); - } - } -} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ReflectiveProperty.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ReflectiveProperty.java deleted file mode 100755 index 47b65aac9..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ReflectiveProperty.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.telegram.messenger.Animation; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * Internal class to automatically generate a Property for a given class/name pair, given the - * specification of {@link Property#of(java.lang.Class, java.lang.Class, java.lang.String)} - */ -class ReflectiveProperty extends Property { - - private static final String PREFIX_GET = "get"; - private static final String PREFIX_IS = "is"; - private static final String PREFIX_SET = "set"; - private Method mSetter; - private Method mGetter; - private Field mField; - - /** - * For given property name 'name', look for getName/isName method or 'name' field. - * Also look for setName method (optional - could be readonly). Failing method getters and - * field results in throwing NoSuchPropertyException. - * - * @param propertyHolder The class on which the methods or field are found - * @param name The name of the property, where this name is capitalized and appended to - * "get" and "is to search for the appropriate methods. If the get/is methods are not found, - * the constructor will search for a field with that exact name. - */ - public ReflectiveProperty(Class propertyHolder, Class valueType, String name) { - // TODO: cache reflection info for each new class/name pair - super(valueType, name); - char firstLetter = Character.toUpperCase(name.charAt(0)); - String theRest = name.substring(1); - String capitalizedName = firstLetter + theRest; - String getterName = PREFIX_GET + capitalizedName; - try { - mGetter = propertyHolder.getMethod(getterName, (Class[]) null); - } catch (NoSuchMethodException e) { - try { - /* The native implementation uses JNI to do reflection, which allows access to private methods. - * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback. - */ - mGetter = propertyHolder.getDeclaredMethod(getterName, (Class[]) null); - mGetter.setAccessible(true); - } catch (NoSuchMethodException e2) { - // getName() not available - try isName() instead - getterName = PREFIX_IS + capitalizedName; - try { - mGetter = propertyHolder.getMethod(getterName, (Class[]) null); - } catch (NoSuchMethodException e3) { - try { - /* The native implementation uses JNI to do reflection, which allows access to private methods. - * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback. - */ - mGetter = propertyHolder.getDeclaredMethod(getterName, (Class[]) null); - mGetter.setAccessible(true); - } catch (NoSuchMethodException e4) { - // Try public field instead - try { - mField = propertyHolder.getField(name); - Class fieldType = mField.getType(); - if (!typesMatch(valueType, fieldType)) { - throw new NoSuchPropertyException("Underlying type (" + fieldType + ") " + - "does not match Property type (" + valueType + ")"); - } - return; - } catch (NoSuchFieldException e5) { - // no way to access property - throw appropriate exception - throw new NoSuchPropertyException("No accessor method or field found for" - + " property with name " + name); - } - } - } - } - } - Class getterType = mGetter.getReturnType(); - // Check to make sure our getter type matches our valueType - if (!typesMatch(valueType, getterType)) { - throw new NoSuchPropertyException("Underlying type (" + getterType + ") " + - "does not match Property type (" + valueType + ")"); - } - String setterName = PREFIX_SET + capitalizedName; - try { - // mSetter = propertyHolder.getMethod(setterName, getterType); - // The native implementation uses JNI to do reflection, which allows access to private methods. - mSetter = propertyHolder.getDeclaredMethod(setterName, getterType); - mSetter.setAccessible(true); - } catch (NoSuchMethodException ignored) { - // Okay to not have a setter - just a readonly property - } - } - - /** - * Utility method to check whether the type of the underlying field/method on the target - * object matches the type of the Property. The extra checks for primitive types are because - * generics will force the Property type to be a class, whereas the type of the underlying - * method/field will probably be a primitive type instead. Accept float as matching Float, - * etc. - */ - private boolean typesMatch(Class valueType, Class getterType) { - if (getterType != valueType) { - if (getterType.isPrimitive()) { - return (getterType == float.class && valueType == Float.class) || - (getterType == int.class && valueType == Integer.class) || - (getterType == boolean.class && valueType == Boolean.class) || - (getterType == long.class && valueType == Long.class) || - (getterType == double.class && valueType == Double.class) || - (getterType == short.class && valueType == Short.class) || - (getterType == byte.class && valueType == Byte.class) || - (getterType == char.class && valueType == Character.class); - } - return false; - } - return true; - } - - @Override - public void set(T object, V value) { - if (mSetter != null) { - try { - mSetter.invoke(object, value); - } catch (IllegalAccessException e) { - throw new AssertionError(); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); - } - } else if (mField != null) { - try { - mField.set(object, value); - } catch (IllegalAccessException e) { - throw new AssertionError(); - } - } else { - throw new UnsupportedOperationException("Property " + getName() +" is read-only"); - } - } - - @Override - public V get(T object) { - if (mGetter != null) { - try { - return (V) mGetter.invoke(object, (Object[])null); - } catch (IllegalAccessException e) { - throw new AssertionError(); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); - } - } else if (mField != null) { - try { - return (V) mField.get(object); - } catch (IllegalAccessException e) { - throw new AssertionError(); - } - } - // Should not get here: there should always be a non-null getter or field - throw new AssertionError(); - } - - /** - * Returns false if there is no setter or public field underlying this Property. - */ - @Override - public boolean isReadOnly() { - return (mSetter == null && mField == null); - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ValueAnimator.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ValueAnimator.java deleted file mode 100644 index ec4648883..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ValueAnimator.java +++ /dev/null @@ -1,675 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.telegram.messenger.Animation; - -import android.os.Looper; -import android.util.AndroidRuntimeException; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; -import android.view.animation.LinearInterpolator; - -import org.telegram.messenger.AndroidUtilities; - -import java.util.ArrayList; -import java.util.HashMap; - -public class ValueAnimator extends Animator10 { - - private static float sDurationScale = 1.0f; - static final int STOPPED = 0; - static final int RUNNING = 1; - static final int SEEKED = 2; - - long mStartTime; - long mSeekTime = -1; - private long mPauseTime; - private boolean mResumed = false; - protected static ThreadLocal sAnimationHandler = new ThreadLocal(); - private static final Interpolator sDefaultInterpolator = new AccelerateDecelerateInterpolator(); - private boolean mPlayingBackwards = false; - private int mCurrentIteration = 0; - private float mCurrentFraction = 0f; - private boolean mStartedDelay = false; - private long mDelayStartTime; - int mPlayingState = STOPPED; - private boolean mRunning = false; - private boolean mStarted = false; - private boolean mStartListenersCalled = false; - boolean mInitialized = false; - - private long mDuration = (long)(300 * sDurationScale); - private long mUnscaledDuration = 300; - private long mStartDelay = 0; - private long mUnscaledStartDelay = 0; - private int mRepeatCount = 0; - private int mRepeatMode = RESTART; - private Interpolator mInterpolator = sDefaultInterpolator; - private ArrayList mUpdateListeners = null; - PropertyValuesHolder[] mValues; - HashMap mValuesMap; - - public static final int RESTART = 1; - public static final int REVERSE = 2; - public static final int INFINITE = -1; - - public static void setDurationScale(float durationScale) { - sDurationScale = durationScale; - } - - public static float getDurationScale() { - return sDurationScale; - } - - public ValueAnimator() { - - } - - public static ValueAnimator ofInt(int... values) { - ValueAnimator anim = new ValueAnimator(); - anim.setIntValues(values); - return anim; - } - - public static ValueAnimator ofFloat(float... values) { - ValueAnimator anim = new ValueAnimator(); - anim.setFloatValues(values); - return anim; - } - - public static ValueAnimator ofPropertyValuesHolder(PropertyValuesHolder... values) { - ValueAnimator anim = new ValueAnimator(); - anim.setValues(values); - return anim; - } - - public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values) { - ValueAnimator anim = new ValueAnimator(); - anim.setObjectValues(values); - anim.setEvaluator(evaluator); - return anim; - } - - public void setIntValues(int... values) { - if (values == null || values.length == 0) { - return; - } - if (mValues == null || mValues.length == 0) { - setValues(PropertyValuesHolder.ofInt("", values)); - } else { - PropertyValuesHolder valuesHolder = mValues[0]; - valuesHolder.setIntValues(values); - } - mInitialized = false; - } - - public void setFloatValues(float... values) { - if (values == null || values.length == 0) { - return; - } - if (mValues == null || mValues.length == 0) { - setValues(PropertyValuesHolder.ofFloat("", values)); - } else { - PropertyValuesHolder valuesHolder = mValues[0]; - valuesHolder.setFloatValues(values); - } - mInitialized = false; - } - - public void setObjectValues(Object... values) { - if (values == null || values.length == 0) { - return; - } - if (mValues == null || mValues.length == 0) { - setValues(PropertyValuesHolder.ofObject("", null, values)); - } else { - PropertyValuesHolder valuesHolder = mValues[0]; - valuesHolder.setObjectValues(values); - } - mInitialized = false; - } - - public void setValues(PropertyValuesHolder... values) { - int numValues = values.length; - mValues = values; - mValuesMap = new HashMap(numValues); - for (PropertyValuesHolder valuesHolder : values) { - mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder); - } - mInitialized = false; - } - - public PropertyValuesHolder[] getValues() { - return mValues; - } - - void initAnimation() { - if (!mInitialized) { - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.init(); - } - mInitialized = true; - } - } - - public ValueAnimator setDuration(long duration) { - if (duration < 0) { - throw new IllegalArgumentException("Animators cannot have negative duration: " + duration); - } - mUnscaledDuration = duration; - mDuration = (long)(duration * sDurationScale); - return this; - } - - public long getDuration() { - return mUnscaledDuration; - } - - public void setCurrentPlayTime(long playTime) { - initAnimation(); - long currentTime = AnimationUtils.currentAnimationTimeMillis(); - if (mPlayingState != RUNNING) { - mSeekTime = playTime; - mPlayingState = SEEKED; - } - mStartTime = currentTime - playTime; - doAnimationFrame(currentTime); - } - - public long getCurrentPlayTime() { - if (!mInitialized || mPlayingState == STOPPED) { - return 0; - } - return AnimationUtils.currentAnimationTimeMillis() - mStartTime; - } - - @SuppressWarnings("unchecked") - protected static class AnimationHandler implements Runnable { - - protected final ArrayList mAnimations = new ArrayList(); - private final ArrayList mTmpAnimations = new ArrayList(); - protected final ArrayList mPendingAnimations = new ArrayList(); - protected final ArrayList mDelayedAnims = new ArrayList(); - private final ArrayList mEndingAnims = new ArrayList(); - private final ArrayList mReadyAnims = new ArrayList(); - - private boolean mAnimationScheduled; - - public void start() { - scheduleAnimation(); - } - - private void doAnimationFrame(long frameTime) { - while (mPendingAnimations.size() > 0) { - ArrayList pendingCopy = (ArrayList) mPendingAnimations.clone(); - mPendingAnimations.clear(); - int count = pendingCopy.size(); - for (ValueAnimator anim : pendingCopy) { - if (anim.mStartDelay == 0) { - anim.startAnimation(this); - } else { - mDelayedAnims.add(anim); - } - } - } - - int numDelayedAnims = mDelayedAnims.size(); - for (ValueAnimator anim : mDelayedAnims) { - if (anim.delayedAnimationFrame(frameTime)) { - mReadyAnims.add(anim); - } - } - int numReadyAnims = mReadyAnims.size(); - if (numReadyAnims > 0) { - for (ValueAnimator anim : mReadyAnims) { - anim.startAnimation(this); - anim.mRunning = true; - mDelayedAnims.remove(anim); - } - mReadyAnims.clear(); - } - - int numAnims = mAnimations.size(); - for (ValueAnimator mAnimation : mAnimations) { - mTmpAnimations.add(mAnimation); - } - for (int i = 0; i < numAnims; ++i) { - ValueAnimator anim = mTmpAnimations.get(i); - if (mAnimations.contains(anim) && anim.doAnimationFrame(frameTime)) { - mEndingAnims.add(anim); - } - } - mTmpAnimations.clear(); - if (mEndingAnims.size() > 0) { - for (ValueAnimator mEndingAnim : mEndingAnims) { - mEndingAnim.endAnimation(this); - } - mEndingAnims.clear(); - } - - if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) { - scheduleAnimation(); - } - } - - @Override - public void run() { - mAnimationScheduled = false; - doAnimationFrame(System.nanoTime() / 1000000); - } - - private void scheduleAnimation() { - if (!mAnimationScheduled) { - AndroidUtilities.runOnUIThread(this); - mAnimationScheduled = true; - } - } - } - - public long getStartDelay() { - return mUnscaledStartDelay; - } - - public void setStartDelay(long startDelay) { - this.mStartDelay = (long)(startDelay * sDurationScale); - mUnscaledStartDelay = startDelay; - } - - public Object getAnimatedValue() { - if (mValues != null && mValues.length > 0) { - return mValues[0].getAnimatedValue(); - } - return null; - } - - public Object getAnimatedValue(String propertyName) { - PropertyValuesHolder valuesHolder = mValuesMap.get(propertyName); - if (valuesHolder != null) { - return valuesHolder.getAnimatedValue(); - } else { - return null; - } - } - - public void setRepeatCount(int value) { - mRepeatCount = value; - } - - public int getRepeatCount() { - return mRepeatCount; - } - - public void setRepeatMode(int value) { - mRepeatMode = value; - } - - public int getRepeatMode() { - return mRepeatMode; - } - - public void addUpdateListener(AnimatorUpdateListener listener) { - if (mUpdateListeners == null) { - mUpdateListeners = new ArrayList(); - } - mUpdateListeners.add(listener); - } - - public void removeAllUpdateListeners() { - if (mUpdateListeners == null) { - return; - } - mUpdateListeners.clear(); - mUpdateListeners = null; - } - - public void removeUpdateListener(AnimatorUpdateListener listener) { - if (mUpdateListeners == null) { - return; - } - mUpdateListeners.remove(listener); - if (mUpdateListeners.size() == 0) { - mUpdateListeners = null; - } - } - - @Override - public void setInterpolator(Interpolator value) { - if (value != null) { - mInterpolator = value; - } else { - mInterpolator = new LinearInterpolator(); - } - } - - @Override - public Interpolator getInterpolator() { - return mInterpolator; - } - - public void setEvaluator(TypeEvaluator value) { - if (value != null && mValues != null && mValues.length > 0) { - mValues[0].setEvaluator(value); - } - } - - @SuppressWarnings("unchecked") - private void notifyStartListeners() { - if (mListeners != null && !mStartListenersCalled) { - ArrayList tmpListeners = (ArrayList) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorListener tmpListener : tmpListeners) { - tmpListener.onAnimationStart(this); - } - } - mStartListenersCalled = true; - } - - private void start(boolean playBackwards) { - if (Looper.myLooper() == null) { - throw new AndroidRuntimeException("Animators may only be run on Looper threads"); - } - mPlayingBackwards = playBackwards; - mCurrentIteration = 0; - mPlayingState = STOPPED; - mStarted = true; - mStartedDelay = false; - mPaused = false; - AnimationHandler animationHandler = getOrCreateAnimationHandler(); - animationHandler.mPendingAnimations.add(this); - if (mStartDelay == 0) { - setCurrentPlayTime(0); - mPlayingState = STOPPED; - mRunning = true; - notifyStartListeners(); - } - animationHandler.start(); - } - - @Override - public void start() { - start(false); - } - - @SuppressWarnings("unchecked") - @Override - public void cancel() { - AnimationHandler handler = getOrCreateAnimationHandler(); - if (mPlayingState != STOPPED || handler.mPendingAnimations.contains(this) || handler.mDelayedAnims.contains(this)) { - if ((mStarted || mRunning) && mListeners != null) { - if (!mRunning) { - notifyStartListeners(); - } - ArrayList tmpListeners = (ArrayList) mListeners.clone(); - for (AnimatorListener listener : tmpListeners) { - listener.onAnimationCancel(this); - } - } - endAnimation(handler); - } - } - - @Override - public void end() { - AnimationHandler handler = getOrCreateAnimationHandler(); - if (!handler.mAnimations.contains(this) && !handler.mPendingAnimations.contains(this)) { - mStartedDelay = false; - startAnimation(handler); - mStarted = true; - } else if (!mInitialized) { - initAnimation(); - } - animateValue(mPlayingBackwards ? 0f : 1f); - endAnimation(handler); - } - - @Override - public void resume() { - if (mPaused) { - mResumed = true; - } - super.resume(); - } - - @Override - public void pause() { - boolean previouslyPaused = mPaused; - super.pause(); - if (!previouslyPaused && mPaused) { - mPauseTime = -1; - mResumed = false; - } - } - - @Override - public boolean isRunning() { - return (mPlayingState == RUNNING || mRunning); - } - - @Override - public boolean isStarted() { - return mStarted; - } - - public void reverse() { - mPlayingBackwards = !mPlayingBackwards; - if (mPlayingState == RUNNING) { - long currentTime = AnimationUtils.currentAnimationTimeMillis(); - long currentPlayTime = currentTime - mStartTime; - long timeLeft = mDuration - currentPlayTime; - mStartTime = currentTime - timeLeft; - } else if (mStarted) { - end(); - } else { - start(true); - } - } - - @SuppressWarnings("unchecked") - private void endAnimation(AnimationHandler handler) { - handler.mAnimations.remove(this); - handler.mPendingAnimations.remove(this); - handler.mDelayedAnims.remove(this); - mPlayingState = STOPPED; - mPaused = false; - if ((mStarted || mRunning) && mListeners != null) { - if (!mRunning) { - notifyStartListeners(); - } - ArrayList tmpListeners = (ArrayList) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorListener tmpListener : tmpListeners) { - tmpListener.onAnimationEnd(this); - } - } - mRunning = false; - mStarted = false; - mStartListenersCalled = false; - mPlayingBackwards = false; - } - - private void startAnimation(AnimationHandler handler) { - initAnimation(); - handler.mAnimations.add(this); - if (mStartDelay > 0 && mListeners != null) { - notifyStartListeners(); - } - } - - private boolean delayedAnimationFrame(long currentTime) { - if (!mStartedDelay) { - mStartedDelay = true; - mDelayStartTime = currentTime; - } else { - if (mPaused) { - if (mPauseTime < 0) { - mPauseTime = currentTime; - } - return false; - } else if (mResumed) { - mResumed = false; - if (mPauseTime > 0) { - mDelayStartTime += (currentTime - mPauseTime); - } - } - long deltaTime = currentTime - mDelayStartTime; - if (deltaTime > mStartDelay) { - mStartTime = currentTime - (deltaTime - mStartDelay); - mPlayingState = RUNNING; - return true; - } - } - return false; - } - - boolean animationFrame(long currentTime) { - boolean done = false; - switch (mPlayingState) { - case RUNNING: - case SEEKED: - float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f; - if (fraction >= 1f) { - if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) { - if (mListeners != null) { - int numListeners = mListeners.size(); - for (AnimatorListener mListener : mListeners) { - mListener.onAnimationRepeat(this); - } - } - if (mRepeatMode == REVERSE) { - mPlayingBackwards = !mPlayingBackwards; - } - mCurrentIteration += (int)fraction; - fraction = fraction % 1f; - mStartTime += mDuration; - } else { - done = true; - fraction = Math.min(fraction, 1.0f); - } - } - if (mPlayingBackwards) { - fraction = 1f - fraction; - } - animateValue(fraction); - break; - } - - return done; - } - - final boolean doAnimationFrame(long frameTime) { - if (mPlayingState == STOPPED) { - mPlayingState = RUNNING; - if (mSeekTime < 0) { - mStartTime = frameTime; - } else { - mStartTime = frameTime - mSeekTime; - mSeekTime = -1; - } - } - if (mPaused) { - if (mPauseTime < 0) { - mPauseTime = frameTime; - } - return false; - } else if (mResumed) { - mResumed = false; - if (mPauseTime > 0) { - mStartTime += (frameTime - mPauseTime); - } - } - final long currentTime = Math.max(frameTime, mStartTime); - return animationFrame(currentTime); - } - - public float getAnimatedFraction() { - return mCurrentFraction; - } - - void animateValue(float fraction) { - fraction = mInterpolator.getInterpolation(fraction); - mCurrentFraction = fraction; - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.calculateValue(fraction); - } - if (mUpdateListeners != null) { - int numListeners = mUpdateListeners.size(); - for (AnimatorUpdateListener mUpdateListener : mUpdateListeners) { - mUpdateListener.onAnimationUpdate(this); - } - } - } - - @Override - public ValueAnimator clone() { - final ValueAnimator anim = (ValueAnimator) super.clone(); - if (mUpdateListeners != null) { - ArrayList oldListeners = mUpdateListeners; - anim.mUpdateListeners = new ArrayList(); - int numListeners = oldListeners.size(); - for (AnimatorUpdateListener oldListener : oldListeners) { - anim.mUpdateListeners.add(oldListener); - } - } - anim.mSeekTime = -1; - anim.mPlayingBackwards = false; - anim.mCurrentIteration = 0; - anim.mInitialized = false; - anim.mPlayingState = STOPPED; - anim.mStartedDelay = false; - PropertyValuesHolder[] oldValues = mValues; - if (oldValues != null) { - int numValues = oldValues.length; - anim.mValues = new PropertyValuesHolder[numValues]; - anim.mValuesMap = new HashMap(numValues); - for (int i = 0; i < numValues; ++i) { - PropertyValuesHolder newValuesHolder = oldValues[i].clone(); - anim.mValues[i] = newValuesHolder; - anim.mValuesMap.put(newValuesHolder.getPropertyName(), newValuesHolder); - } - } - return anim; - } - - public interface AnimatorUpdateListener { - void onAnimationUpdate(ValueAnimator animation); - } - - public static int getCurrentAnimationsCount() { - AnimationHandler handler = sAnimationHandler.get(); - return handler != null ? handler.mAnimations.size() : 0; - } - - public static void clearAllAnimations() { - AnimationHandler handler = sAnimationHandler.get(); - if (handler != null) { - handler.mAnimations.clear(); - handler.mPendingAnimations.clear(); - handler.mDelayedAnims.clear(); - } - } - - private static AnimationHandler getOrCreateAnimationHandler() { - AnimationHandler handler = sAnimationHandler.get(); - if (handler == null) { - handler = new AnimationHandler(); - sAnimationHandler.set(handler); - } - return handler; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/View10.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/View10.java deleted file mode 100644 index 540b61a95..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/View10.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - Copyright 2012 Jake Wharton - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - */ - -package org.telegram.messenger.Animation; - -import android.graphics.Camera; -import android.graphics.Matrix; -import android.graphics.RectF; -import android.os.Build; -import android.view.View; -import android.view.animation.Animation; -import android.view.animation.Transformation; - -import java.lang.ref.WeakReference; -import java.util.WeakHashMap; - -public class View10 extends Animation { - - public static boolean NEED_PROXY = Build.VERSION.SDK_INT < 11; - - private static final WeakHashMap PROXIES = new WeakHashMap<>(); - - public static View10 wrap(View view) { - View10 proxy = PROXIES.get(view); - Animation animation = view.getAnimation(); - if (proxy == null || proxy != animation && animation != null) { - proxy = new View10(view); - PROXIES.put(view, proxy); - } else if (animation == null) { - view.setAnimation(proxy); - } - return proxy; - } - - private final WeakReference mView; - private final Camera mCamera = new Camera(); - private boolean mHasPivot; - - private float mAlpha = 1; - private float mPivotX; - private float mPivotY; - private float mRotationX; - private float mRotationY; - private float mRotationZ; - private float mScaleX = 1; - private float mScaleY = 1; - private float mTranslationX; - private float mTranslationY; - - private final RectF mBefore = new RectF(); - private final RectF mAfter = new RectF(); - private final Matrix mTempMatrix = new Matrix(); - - private View10(View view) { - setDuration(0); - setFillAfter(true); - view.setAnimation(this); - mView = new WeakReference<>(view); - } - - public float getAlpha() { - return mAlpha; - } - - public void setAlpha(float alpha) { - if (mAlpha != alpha) { - mAlpha = alpha; - View view = mView.get(); - if (view != null) { - view.invalidate(); - } - } - } - - public float getPivotX() { - return mPivotX; - } - - public void setPivotX(float pivotX) { - if (!mHasPivot || mPivotX != pivotX) { - prepareForUpdate(); - mHasPivot = true; - mPivotX = pivotX; - invalidateAfterUpdate(); - } - } - - public float getPivotY() { - return mPivotY; - } - - public void setPivotY(float pivotY) { - if (!mHasPivot || mPivotY != pivotY) { - prepareForUpdate(); - mHasPivot = true; - mPivotY = pivotY; - invalidateAfterUpdate(); - } - } - - public float getRotation() { - return mRotationZ; - } - - public void setRotation(float rotation) { - if (mRotationZ != rotation) { - prepareForUpdate(); - mRotationZ = rotation; - invalidateAfterUpdate(); - } - } - - public float getRotationX() { - return mRotationX; - } - - public void setRotationX(float rotationX) { - if (mRotationX != rotationX) { - prepareForUpdate(); - mRotationX = rotationX; - invalidateAfterUpdate(); - } - } - - public float getRotationY() { - return mRotationY; - } - - public void setRotationY(float rotationY) { - if (mRotationY != rotationY) { - prepareForUpdate(); - mRotationY = rotationY; - invalidateAfterUpdate(); - } - } - - public float getScaleX() { - return mScaleX; - } - - public void setScaleX(float scaleX) { - if (mScaleX != scaleX) { - prepareForUpdate(); - mScaleX = scaleX; - invalidateAfterUpdate(); - } - } - - public float getScaleY() { - return mScaleY; - } - - public void setScaleY(float scaleY) { - if (mScaleY != scaleY) { - prepareForUpdate(); - mScaleY = scaleY; - invalidateAfterUpdate(); - } - } - - public int getScrollX() { - View view = mView.get(); - if (view == null) { - return 0; - } - return view.getScrollX(); - } - - public void setScrollX(int value) { - View view = mView.get(); - if (view != null) { - view.scrollTo(value, view.getScrollY()); - } - } - - public int getScrollY() { - View view = mView.get(); - if (view == null) { - return 0; - } - return view.getScrollY(); - } - - public void setScrollY(int value) { - View view = mView.get(); - if (view != null) { - view.scrollTo(view.getScrollX(), value); - } - } - - public float getTranslationX() { - return mTranslationX; - } - - public void setTranslationX(float translationX) { - if (mTranslationX != translationX) { - prepareForUpdate(); - mTranslationX = translationX; - invalidateAfterUpdate(); - } - } - - public float getTranslationY() { - return mTranslationY; - } - - public void setTranslationY(float translationY) { - if (mTranslationY != translationY) { - prepareForUpdate(); - mTranslationY = translationY; - invalidateAfterUpdate(); - } - } - - public float getX() { - View view = mView.get(); - if (view == null) { - return 0; - } - return view.getLeft() + mTranslationX; - } - - public void setX(float x) { - View view = mView.get(); - if (view != null) { - setTranslationX(x - view.getLeft()); - } - } - - public float getY() { - View view = mView.get(); - if (view == null) { - return 0; - } - return view.getTop() + mTranslationY; - } - - public void setY(float y) { - View view = mView.get(); - if (view != null) { - setTranslationY(y - view.getTop()); - } - } - - private void prepareForUpdate() { - View view = mView.get(); - if (view != null) { - computeRect(mBefore, view); - } - } - - private void invalidateAfterUpdate() { - View view = mView.get(); - if (view == null || view.getParent() == null) { - return; - } - - final RectF after = mAfter; - computeRect(after, view); - after.union(mBefore); - - ((View) view.getParent()).invalidate( - (int) Math.floor(after.left), - (int) Math.floor(after.top), - (int) Math.ceil(after.right), - (int) Math.ceil(after.bottom)); - } - - private void computeRect(final RectF r, View view) { - final float w = view.getWidth(); - final float h = view.getHeight(); - - r.set(0, 0, w, h); - - final Matrix m = mTempMatrix; - m.reset(); - transformMatrix(m, view); - mTempMatrix.mapRect(r); - - r.offset(view.getLeft(), view.getTop()); - - if (r.right < r.left) { - final float f = r.right; - r.right = r.left; - r.left = f; - } - if (r.bottom < r.top) { - final float f = r.top; - r.top = r.bottom; - r.bottom = f; - } - } - - private void transformMatrix(Matrix m, View view) { - final float w = view.getWidth(); - final float h = view.getHeight(); - final boolean hasPivot = mHasPivot; - final float pX = hasPivot ? mPivotX : w / 2f; - final float pY = hasPivot ? mPivotY : h / 2f; - - final float rX = mRotationX; - final float rY = mRotationY; - final float rZ = mRotationZ; - if ((rX != 0) || (rY != 0) || (rZ != 0)) { - final Camera camera = mCamera; - camera.save(); - camera.rotateX(rX); - camera.rotateY(rY); - camera.rotateZ(-rZ); - camera.getMatrix(m); - camera.restore(); - m.preTranslate(-pX, -pY); - m.postTranslate(pX, pY); - } - - final float sX = mScaleX; - final float sY = mScaleY; - if ((sX != 1.0f) || (sY != 1.0f)) { - m.postScale(sX, sY); - final float sPX = -(pX / w) * ((sX * w) - w); - final float sPY = -(pY / h) * ((sY * h) - h); - m.postTranslate(sPX, sPY); - } - - m.postTranslate(mTranslationX, mTranslationY); - } - - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - View view = mView.get(); - if (view != null) { - t.setAlpha(mAlpha); - transformMatrix(t.getMatrix(), view); - } - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorListenerAdapterProxy.java b/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorListenerAdapterProxy.java deleted file mode 100644 index 4a5c55a17..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorListenerAdapterProxy.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.x. - * It is licensed under GNU GPL v. 2 or later. - * You should have received a copy of the license in this archive (see LICENSE). - * - * Copyright Nikolai Kudashov, 2013-2016. - */ - -package org.telegram.messenger.AnimationCompat; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; - -import org.telegram.messenger.Animation.Animator10; -import org.telegram.messenger.Animation.AnimatorListenerAdapter10; -import org.telegram.messenger.Animation.View10; - -public class AnimatorListenerAdapterProxy { - protected Object animatorListenerAdapter; - - public AnimatorListenerAdapterProxy() { - if (View10.NEED_PROXY) { - animatorListenerAdapter = new AnimatorListenerAdapter10() { - @Override - public void onAnimationCancel(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationCancel(animation); - } - - @Override - public void onAnimationEnd(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationEnd(animation); - } - - @Override - public void onAnimationRepeat(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationRepeat(animation); - } - - @Override - public void onAnimationStart(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationStart(animation); - } - - @Override - public void onAnimationPause(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationPause(animation); - } - - @Override - public void onAnimationResume(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationResume(animation); - } - }; - } else { - animatorListenerAdapter = new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationCancel(animation); - } - - @Override - public void onAnimationEnd(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationEnd(animation); - } - - @Override - public void onAnimationRepeat(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationRepeat(animation); - } - - @Override - public void onAnimationStart(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationStart(animation); - } - - @Override - public void onAnimationPause(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationPause(animation); - } - - @Override - public void onAnimationResume(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationResume(animation); - } - }; - } - } - - public void onAnimationCancel(Object animation) { - - } - - public void onAnimationEnd(Object animation) { - - } - - public void onAnimationRepeat(Object animation) { - - } - - public void onAnimationStart(Object animation) { - - } - - public void onAnimationPause(Object animation) { - - } - - public void onAnimationResume(Object animation) { - - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorSetProxy.java b/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorSetProxy.java deleted file mode 100644 index e6cccb0a2..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorSetProxy.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.x. - * It is licensed under GNU GPL v. 2 or later. - * You should have received a copy of the license in this archive (see LICENSE). - * - * Copyright Nikolai Kudashov, 2013-2016. - */ - -package org.telegram.messenger.AnimationCompat; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.view.animation.Interpolator; - -import org.telegram.messenger.Animation.Animator10; -import org.telegram.messenger.Animation.AnimatorListenerAdapter10; -import org.telegram.messenger.Animation.AnimatorSet10; -import org.telegram.messenger.Animation.View10; - -import java.lang.reflect.Array; -import java.util.ArrayList; - -public class AnimatorSetProxy { - - private Object animatorSet; - - public static T[] copyOf(U[] original, int newLength, Class newType) { - return copyOfRange(original, 0, newLength, newType); - } - - @SuppressWarnings("unchecked") - public static T[] copyOfRange(U[] original, int start, int end, Class newType) { - if (start > end) { - throw new IllegalArgumentException(); - } - int originalLength = original.length; - if (start < 0 || start > originalLength) { - throw new ArrayIndexOutOfBoundsException(); - } - int resultLength = end - start; - int copyLength = Math.min(resultLength, originalLength - start); - T[] result = (T[]) Array.newInstance(newType.getComponentType(), resultLength); - System.arraycopy(original, start, result, 0, copyLength); - return result; - } - - public AnimatorSetProxy() { - if (View10.NEED_PROXY) { - animatorSet = new AnimatorSet10(); - } else { - animatorSet = new AnimatorSet(); - } - } - - @SuppressWarnings("unchecked") - public void playTogether(Object... items) { - if (View10.NEED_PROXY) { - Animator10[] animators = copyOf(items, items.length, Animator10[].class); - ((AnimatorSet10) animatorSet).playTogether(animators); - } else { - Animator[] animators = copyOf(items, items.length, Animator[].class); - ((AnimatorSet) animatorSet).playTogether(animators); - } - } - - public void playTogether(ArrayList items) { - if (View10.NEED_PROXY) { - ArrayList animators = new ArrayList<>(); - for (Object obj : items) { - animators.add((Animator10)obj); - } - ((AnimatorSet10) animatorSet).playTogether(animators); - } else { - ArrayList animators = new ArrayList<>(); - for (Object obj : items) { - animators.add((Animator)obj); - } - ((AnimatorSet) animatorSet).playTogether(animators); - } - } - - public AnimatorSetProxy setDuration(long duration) { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).setDuration(duration); - } else { - ((AnimatorSet) animatorSet).setDuration(duration); - } - return this; - } - - public AnimatorSetProxy setStartDelay(long delay) { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).setStartDelay(delay); - } else { - ((AnimatorSet) animatorSet).setStartDelay(delay); - } - return this; - } - - public void start() { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).start(); - } else { - ((AnimatorSet) animatorSet).start(); - } - } - - public void cancel() { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).cancel(); - } else { - ((AnimatorSet) animatorSet).cancel(); - } - } - - public void addListener(AnimatorListenerAdapterProxy listener) { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).addListener((AnimatorListenerAdapter10) listener.animatorListenerAdapter); - } else { - ((AnimatorSet) animatorSet).addListener((AnimatorListenerAdapter) listener.animatorListenerAdapter); - } - } - - public void setInterpolator(Interpolator interpolator) { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).setInterpolator(interpolator); - } else { - ((AnimatorSet) animatorSet).setInterpolator(interpolator); - } - } - - @Override - public boolean equals(Object o) { - return animatorSet == o; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ObjectAnimatorProxy.java b/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ObjectAnimatorProxy.java deleted file mode 100644 index 8bf69db6a..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ObjectAnimatorProxy.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.x. - * It is licensed under GNU GPL v. 2 or later. - * You should have received a copy of the license in this archive (see LICENSE). - * - * Copyright Nikolai Kudashov, 2013-2016. - */ - -package org.telegram.messenger.AnimationCompat; - -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.view.animation.Interpolator; - -import org.telegram.messenger.Animation.AnimatorListenerAdapter10; -import org.telegram.messenger.Animation.ObjectAnimator10; -import org.telegram.messenger.Animation.View10; - -public class ObjectAnimatorProxy { - - private Object objectAnimator; - - public ObjectAnimatorProxy(Object animator) { - objectAnimator = animator; - } - - public static Object ofFloat(Object target, String propertyName, float... values) { - if (View10.NEED_PROXY) { - return ObjectAnimator10.ofFloat(target, propertyName, values); - } else { - return ObjectAnimator.ofFloat(target, propertyName, values); - } - } - - public static Object ofInt(Object target, String propertyName, int... values) { - if (View10.NEED_PROXY) { - return ObjectAnimator10.ofInt(target, propertyName, values); - } else { - return ObjectAnimator.ofInt(target, propertyName, values); - } - } - - public static ObjectAnimatorProxy ofFloatProxy(Object target, String propertyName, float... values) { - if (View10.NEED_PROXY) { - return new ObjectAnimatorProxy(ObjectAnimator10.ofFloat(target, propertyName, values)); - } else { - return new ObjectAnimatorProxy(ObjectAnimator.ofFloat(target, propertyName, values)); - } - } - - public static ObjectAnimatorProxy ofIntProxy(Object target, String propertyName, int... values) { - if (View10.NEED_PROXY) { - return new ObjectAnimatorProxy(ObjectAnimator10.ofInt(target, propertyName, values)); - } else { - return new ObjectAnimatorProxy(ObjectAnimator.ofInt(target, propertyName, values)); - } - } - - public ObjectAnimatorProxy setDuration(long duration) { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).setDuration(duration); - } else { - ((ObjectAnimator) objectAnimator).setDuration(duration); - } - return this; - } - - public void setInterpolator(Interpolator value) { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).setInterpolator(value); - } else { - ((ObjectAnimator) objectAnimator).setInterpolator(value); - } - } - - public ObjectAnimatorProxy start() { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).start(); - } else { - ((ObjectAnimator) objectAnimator).start(); - } - return this; - } - - public void setAutoCancel(boolean cancel) { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).setAutoCancel(cancel); - } else { - ((ObjectAnimator) objectAnimator).setAutoCancel(cancel); - } - } - - public boolean isRunning() { - if (View10.NEED_PROXY) { - return ((ObjectAnimator10) objectAnimator).isRunning(); - } else { - return ((ObjectAnimator) objectAnimator).isRunning(); - } - } - - public void end() { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).end(); - } else { - ((ObjectAnimator) objectAnimator).end(); - } - } - - public void cancel() { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).cancel(); - } else { - ((ObjectAnimator) objectAnimator).cancel(); - } - } - - public ObjectAnimatorProxy addListener(AnimatorListenerAdapterProxy listener) { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).addListener((AnimatorListenerAdapter10) listener.animatorListenerAdapter); - } else { - ((ObjectAnimator) objectAnimator).addListener((AnimatorListenerAdapter) listener.animatorListenerAdapter); - } - return this; - } - - @Override - public boolean equals(Object o) { - return objectAnimator == o; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ViewProxy.java b/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ViewProxy.java deleted file mode 100644 index 50a2690fa..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ViewProxy.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.x. - * It is licensed under GNU GPL v. 2 or later. - * You should have received a copy of the license in this archive (see LICENSE). - * - * Copyright Nikolai Kudashov, 2013-2016. - */ - -package org.telegram.messenger.AnimationCompat; - -import android.view.View; - -import org.telegram.messenger.Animation.View10; - -public class ViewProxy { - - public static float getAlpha(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getAlpha(); - } else { - return view.getAlpha(); - } - } - - public static void setAlpha(View view, float alpha) { - if (View10.NEED_PROXY) { - View10.wrap(view).setAlpha(alpha); - } else { - view.setAlpha(alpha); - } - } - - public static float getPivotX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getPivotX(); - } else { - return view.getPivotX(); - } - } - - public static void setPivotX(View view, float pivotX) { - if (View10.NEED_PROXY) { - View10.wrap(view).setPivotX(pivotX); - } else { - view.setPivotX(pivotX); - } - } - - public static float getPivotY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getPivotY(); - } else { - return view.getPivotY(); - } - } - - public static void setPivotY(View view, float pivotY) { - if (View10.NEED_PROXY) { - View10.wrap(view).setPivotY(pivotY); - } else { - view.setPivotY(pivotY); - } - } - - public static float getRotation(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getRotation(); - } else { - return view.getRotation(); - } - } - - public static void setRotation(View view, float rotation) { - if (View10.NEED_PROXY) { - View10.wrap(view).setRotation(rotation); - } else { - view.setRotation(rotation); - } - } - - public static float getRotationX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getRotationX(); - } else { - return view.getRotationX(); - } - } - - public void setRotationX(View view, float rotationX) { - if (View10.NEED_PROXY) { - View10.wrap(view).setRotationX(rotationX); - } else { - view.setRotationX(rotationX); - } - } - - public static float getRotationY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getRotationY(); - } else { - return view.getRotationY(); - } - } - - public void setRotationY(View view, float rotationY) { - if (View10.NEED_PROXY) { - View10.wrap(view).setRotationY(rotationY); - } else { - view.setRotationY(rotationY); - } - } - - public static float getScaleX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getScaleX(); - } else { - return view.getScaleX(); - } - } - - public static void setScaleX(View view, float scaleX) { - if (View10.NEED_PROXY) { - View10.wrap(view).setScaleX(scaleX); - } else { - view.setScaleX(scaleX); - } - } - - public static float getScaleY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getScaleY(); - } else { - return view.getScaleY(); - } - } - - public static void setScaleY(View view, float scaleY) { - if (View10.NEED_PROXY) { - View10.wrap(view).setScaleY(scaleY); - } else { - view.setScaleY(scaleY); - } - } - - public static int getScrollX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getScrollX(); - } else { - return view.getScrollX(); - } - } - - public static void setScrollX(View view, int value) { - if (View10.NEED_PROXY) { - View10.wrap(view).setScrollX(value); - } else { - view.setScrollX(value); - } - } - - public static int getScrollY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getScrollY(); - } else { - return view.getScrollY(); - } - } - - public static void setScrollY(View view, int value) { - if (View10.NEED_PROXY) { - View10.wrap(view).setScrollY(value); - } else { - view.setScrollY(value); - } - } - - public static float getTranslationX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getTranslationX(); - } else { - return view.getTranslationX(); - } - } - - public static void setTranslationX(View view, float translationX) { - if (View10.NEED_PROXY) { - View10.wrap(view).setTranslationX(translationX); - } else { - view.setTranslationX(translationX); - } - } - - public static float getTranslationY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getTranslationY(); - } else { - return view.getTranslationY(); - } - } - - public static void setTranslationY(View view, float translationY) { - if (View10.NEED_PROXY) { - View10.wrap(view).setTranslationY(translationY); - } else { - view.setTranslationY(translationY); - } - } - - public static float getX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getX(); - } else { - return view.getX(); - } - } - - public static void setX(View view, float x) { - if (View10.NEED_PROXY) { - View10.wrap(view).setX(x); - } else { - view.setX(x); - } - } - - public static float getY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getY(); - } else { - return view.getY(); - } - } - - public static void setY(View view, float y) { - if (View10.NEED_PROXY) { - View10.wrap(view).setY(y); - } else { - view.setY(y); - } - } - - public static Object wrap(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view); - } else { - return view; - } - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AnimatorListenerAdapterProxy.java b/TMessagesProj/src/main/java/org/telegram/messenger/AnimatorListenerAdapterProxy.java new file mode 100644 index 000000000..855df9b85 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/AnimatorListenerAdapterProxy.java @@ -0,0 +1,45 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.messenger; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; + +public class AnimatorListenerAdapterProxy extends AnimatorListenerAdapter { + + @Override + public void onAnimationCancel(Animator animator) { + + } + + @Override + public void onAnimationEnd(Animator animator) { + + } + + @Override + public void onAnimationRepeat(Animator animator) { + + } + + @Override + public void onAnimationStart(Animator animator) { + + } + + @Override + public void onAnimationPause(Animator animator) { + + } + + @Override + public void onAnimationResume(Animator animator) { + + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java index 8e4b68c9e..fb47b2eb3 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java @@ -20,9 +20,6 @@ import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; @@ -39,7 +36,6 @@ import java.io.File; import java.io.RandomAccessFile; public class ApplicationLoader extends Application { - private static NetworkAlarm networkAlarm = null; private static PendingIntent pendingIntent; private static Drawable cachedWallpaper; @@ -288,55 +284,59 @@ public class ApplicationLoader extends Application { public void onCreate() { super.onCreate(); - if (Build.VERSION.SDK_INT < 11) { - java.lang.System.setProperty("java.net.preferIPv4Stack", "true"); - java.lang.System.setProperty("java.net.preferIPv6Addresses", "false"); - } - applicationContext = getApplicationContext(); NativeLoader.initNativeLibs(ApplicationLoader.applicationContext); ConnectionsManager.native_setJava(Build.VERSION.SDK_INT == 14 || Build.VERSION.SDK_INT == 15); - - if (Build.VERSION.SDK_INT >= 14) { - new ForegroundDetector(this); - } + new ForegroundDetector(this); applicationHandler = new Handler(applicationContext.getMainLooper()); startPushService(); } + /*public static void sendRegIdToBackend(final String token) { + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + UserConfig.pushString = token; + UserConfig.registeredForPush = false; + UserConfig.saveConfig(false); + if (UserConfig.getClientUserId() != 0) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().registerForPush(token); + } + }); + } + } + }); + }*/ + public static void startPushService() { SharedPreferences preferences = applicationContext.getSharedPreferences("Notifications", MODE_PRIVATE); + if (preferences.getBoolean("pushService", true)) { - networkAlarm = new NetworkAlarm(); - } else { AlarmManager am = (AlarmManager) applicationContext.getSystemService(Context.ALARM_SERVICE); Intent i = new Intent(applicationContext, ApplicationLoader.class); pendingIntent = PendingIntent.getBroadcast(applicationContext, 0, i, 0); am.cancel(pendingIntent); am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 60000, pendingIntent); + + applicationContext.startService(new Intent(applicationContext, NotificationsService.class)); + } else { + stopPushService(); } - applicationContext.startService(new Intent(applicationContext, NotificationsService.class)); } public static void stopPushService() { - if (networkAlarm != null) { - networkAlarm = null; - } else { - AlarmManager am = (AlarmManager) applicationContext.getSystemService(Context.ALARM_SERVICE); - am.cancel(pendingIntent); - } applicationContext.stopService(new Intent(applicationContext, NotificationsService.class)); - } - public static void setAlarm(int timeout) { - FileLog.d("tmessages", "setting alarm to wake us in " + String.valueOf(timeout) + "ms"); - - if (networkAlarm != null) { - networkAlarm.setAlarm(applicationContext, timeout); - } + PendingIntent pintent = PendingIntent.getService(applicationContext, 0, new Intent(applicationContext, NotificationsService.class), 0); + AlarmManager alarm = (AlarmManager)applicationContext.getSystemService(Context.ALARM_SERVICE); + alarm.cancel(pintent); + alarm.cancel(pendingIntent); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AutoMessageReplyReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/AutoMessageReplyReceiver.java index 1635bd37b..1f45a7273 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AutoMessageReplyReceiver.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/AutoMessageReplyReceiver.java @@ -32,7 +32,7 @@ public class AutoMessageReplyReceiver extends BroadcastReceiver { if (dialog_id == 0 || max_id == 0) { return; } - SendMessagesHelper.getInstance().sendMessage(text.toString(), dialog_id, null, null, true, false, null, null, null); + SendMessagesHelper.getInstance().sendMessage(text.toString(), dialog_id, null, null, true, null, null, null); MessagesController.getInstance().markDialogAsRead(dialog_id, max_id, max_id, 0, true, false); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java b/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java index fe3724626..77778f8d2 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java @@ -110,7 +110,7 @@ public class Bitmaps { public static Bitmap createBitmap(int width, int height, Bitmap.Config config) { Bitmap bitmap; - if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 21) { + if (Build.VERSION.SDK_INT < 21) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inDither = true; options.inPreferredConfig = config; @@ -212,9 +212,7 @@ public class Bitmaps { } } bitmap.setDensity(source.getDensity()); - if (Build.VERSION.SDK_INT >= 12) { - bitmap.setHasAlpha(source.hasAlpha()); - } + bitmap.setHasAlpha(source.hasAlpha()); if (Build.VERSION.SDK_INT >= 19) { bitmap.setPremultiplied(source.isPremultiplied()); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java b/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java index a7373fd48..2ee013694 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java @@ -10,8 +10,8 @@ package org.telegram.messenger; public class BuildVars { public static boolean DEBUG_VERSION = false; - public static int BUILD_VERSION = 787; - public static String BUILD_VERSION_STRING = "3.8"; + public static int BUILD_VERSION = 821; + public static String BUILD_VERSION_STRING = "3.10"; public static int APP_ID = 12834; public static String APP_HASH = "c84d9229db1d6be95c067b02b126352c"; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java b/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java index 2dc0444a1..f26f1811f 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java @@ -71,17 +71,17 @@ public class ContactsController { } private String[] projectionPhones = { - ContactsContract.CommonDataKinds.Phone.CONTACT_ID, - ContactsContract.CommonDataKinds.Phone.NUMBER, - ContactsContract.CommonDataKinds.Phone.TYPE, - ContactsContract.CommonDataKinds.Phone.LABEL + ContactsContract.CommonDataKinds.Phone.CONTACT_ID, + ContactsContract.CommonDataKinds.Phone.NUMBER, + ContactsContract.CommonDataKinds.Phone.TYPE, + ContactsContract.CommonDataKinds.Phone.LABEL }; private String[] projectionNames = { - ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID, - ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, - ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, - ContactsContract.Data.DISPLAY_NAME, - ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID, + ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, + ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, + ContactsContract.Data.DISPLAY_NAME, + ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME }; public HashMap contactsBook = new HashMap<>(); @@ -98,7 +98,10 @@ public class ContactsController { public HashMap contactsByPhone = new HashMap<>(); + private int completedRequestsCount; + private static volatile ContactsController Instance = null; + public static ContactsController getInstance() { ContactsController localInstance = Instance; if (localInstance == null) { @@ -163,6 +166,12 @@ public class ContactsController { deleteAccountTTL = 0; loadingLastSeenInfo = 0; loadingGroupInfo = 0; + Utilities.globalQueue.postRunnable(new Runnable() { + @Override + public void run() { + completedRequestsCount = 0; + } + }); privacyRules = null; } @@ -171,7 +180,7 @@ public class ContactsController { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); inviteText = preferences.getString("invitetext", null); int time = preferences.getInt("invitetexttime", 0); - if (!updatingInviteText && (inviteText == null || time + 86400 < (int)(System.currentTimeMillis() / 1000))) { + if (!updatingInviteText && (inviteText == null || time + 86400 < (int) (System.currentTimeMillis() / 1000))) { updatingInviteText = true; TLRPC.TL_help_getInviteText req = new TLRPC.TL_help_getInviteText(); req.lang_code = LocaleController.getLocaleStringIso639(); @@ -277,12 +286,21 @@ public class ContactsController { public void run() { if (checkContactsInternal()) { FileLog.e("tmessages", "detected contacts change"); - ContactsController.getInstance().performSyncPhoneBook(ContactsController.getInstance().getContactsCopy(ContactsController.getInstance().contactsBook), true, false, true); + ContactsController.getInstance().performSyncPhoneBook(ContactsController.getInstance().getContactsCopy(ContactsController.getInstance().contactsBook), true, false, true, false); } } }); } + public void forceImportContacts() { + Utilities.globalQueue.postRunnable(new Runnable() { + @Override + public void run() { + ContactsController.getInstance().performSyncPhoneBook(new HashMap(), true, true, true, true); + } + }); + } + private boolean checkContactsInternal() { boolean reload = false; try { @@ -446,7 +464,7 @@ public class ContactsController { } try { - pCur = cr.query(ContactsContract.RawContacts.CONTENT_URI, new String[] { "display_name", ContactsContract.RawContacts.SYNC1, ContactsContract.RawContacts.CONTACT_ID }, ContactsContract.RawContacts.ACCOUNT_TYPE + " = " + "'com.whatsapp'", null, null); + pCur = cr.query(ContactsContract.RawContacts.CONTENT_URI, new String[]{"display_name", ContactsContract.RawContacts.SYNC1, ContactsContract.RawContacts.CONTACT_ID}, ContactsContract.RawContacts.ACCOUNT_TYPE + " = " + "'com.whatsapp'", null, null); if (pCur != null) { while ((pCur.moveToNext())) { String phone = pCur.getString(1); @@ -468,8 +486,8 @@ public class ContactsController { } String name = pCur.getString(0); - if (name == null || name.length() == 0) { - name = PhoneFormat.getInstance().format(phone); + if (TextUtils.isEmpty(name)) { + continue; } Contact contact = new Contact(); @@ -530,7 +548,7 @@ public class ContactsController { return ret; } - public void performSyncPhoneBook(final HashMap contactHashMap, final boolean requ, final boolean first, final boolean schedule) { + protected void performSyncPhoneBook(final HashMap contactHashMap, final boolean request, final boolean first, final boolean schedule, final boolean force) { if (!first && !contactsBookLoaded) { return; } @@ -562,21 +580,11 @@ public class ContactsController { } }*/ - boolean request = requ; - if (request && first) { - if (UserConfig.importHash != null && UserConfig.importHash.length() != 0 || UserConfig.contactsVersion != 1) { - UserConfig.importHash = ""; - UserConfig.contactsVersion = 1; - UserConfig.saveConfig(false); - request = false; - } - } - HashMap contactShortHashMap = new HashMap<>(); for (HashMap.Entry entry : contactHashMap.entrySet()) { Contact c = entry.getValue(); - for (String sphone : c.shortPhones) { - contactShortHashMap.put(sphone, c); + for (int a = 0; a < c.shortPhones.size(); a++) { + contactShortHashMap.put(c.shortPhones.get(a), c); } } @@ -595,8 +603,8 @@ public class ContactsController { Contact value = pair.getValue(); Contact existing = contactHashMap.get(id); if (existing == null) { - for (String s : value.shortPhones) { - Contact c = contactShortHashMap.get(s); + for (int a = 0; a < value.shortPhones.size(); a++) { + Contact c = contactShortHashMap.get(value.shortPhones.get(a)); if (c != null) { existing = c; id = existing.id; @@ -605,7 +613,7 @@ public class ContactsController { } } - boolean nameChanged = existing != null && (value.first_name != null && value.first_name.length() != 0 && !existing.first_name.equals(value.first_name) || value.last_name != null && existing.last_name != null && !existing.last_name.equals(value.last_name)); + boolean nameChanged = existing != null && (TextUtils.isEmpty(value.first_name) && !existing.first_name.equals(value.first_name) || !TextUtils.isEmpty(value.last_name) && !existing.last_name.equals(value.last_name)); if (existing == null || nameChanged) { for (int a = 0; a < value.phones.size(); a++) { String sphone = value.shortPhones.get(a); @@ -627,6 +635,7 @@ public class ContactsController { TLRPC.TL_inputPhoneContact imp = new TLRPC.TL_inputPhoneContact(); imp.client_id = value.id; + imp.client_id |= ((long) a) << 32; imp.first_name = value.first_name; imp.last_name = value.last_name; imp.phone = value.phones.get(a); @@ -646,13 +655,14 @@ public class ContactsController { TLRPC.TL_contact contact = contactsByPhone.get(sphone); if (contact != null) { TLRPC.User user = MessagesController.getInstance().getUser(contact.user_id); - if (user == null || user.first_name != null && user.first_name.length() != 0 || user.last_name != null && user.last_name.length() != 0) { + if (user == null || !TextUtils.isEmpty(user.first_name) || !TextUtils.isEmpty(user.last_name) || TextUtils.isEmpty(value.first_name) && TextUtils.isEmpty(value.last_name)) { continue; } } TLRPC.TL_inputPhoneContact imp = new TLRPC.TL_inputPhoneContact(); imp.client_id = value.id; + imp.client_id |= ((long) a) << 32; imp.first_name = value.first_name; imp.last_name = value.last_name; imp.phone = value.phones.get(a); @@ -699,7 +709,8 @@ public class ContactsController { try { final HashMap contactsPhonesShort = new HashMap<>(); - for (TLRPC.TL_contact value : contacts) { + for (int a = 0; a < contacts.size(); a++) { + TLRPC.TL_contact value = contacts.get(a); TLRPC.User user = MessagesController.getInstance().getUser(value.user_id); if (user == null || user.phone == null || user.phone.length() == 0) { continue; @@ -741,16 +752,19 @@ public class ContactsController { Contact value = pair.getValue(); int id = pair.getKey(); for (int a = 0; a < value.phones.size(); a++) { - String phone = value.shortPhones.get(a); - TLRPC.TL_contact contact = contactsByPhone.get(phone); - if (contact != null) { - TLRPC.User user = MessagesController.getInstance().getUser(contact.user_id); - if (user == null || user.first_name != null && user.first_name.length() != 0 || user.last_name != null && user.last_name.length() != 0) { - continue; + if (!force) { + String phone = value.shortPhones.get(a); + TLRPC.TL_contact contact = contactsByPhone.get(phone); + if (contact != null) { + TLRPC.User user = MessagesController.getInstance().getUser(contact.user_id); + if (user == null || !TextUtils.isEmpty(user.first_name) || !TextUtils.isEmpty(user.last_name) || TextUtils.isEmpty(value.first_name) && TextUtils.isEmpty(value.last_name)) { + continue; + } } } TLRPC.TL_inputPhoneContact imp = new TLRPC.TL_inputPhoneContact(); imp.client_id = id; + imp.client_id |= ((long) a) << 32; imp.first_name = value.first_name; imp.last_name = value.last_name; imp.phone = value.phones.get(a); @@ -769,23 +783,34 @@ public class ContactsController { FileLog.e("tmessages", "add contact " + contact.first_name + " " + contact.last_name + " " + contact.phone); } }*/ - final int count = (int)Math.ceil(toImport.size() / 500.0f); + + final HashMap contactsMapToSave = new HashMap<>(contactsMap); + completedRequestsCount = 0; + final int count = (int) Math.ceil(toImport.size() / 500.0f); for (int a = 0; a < count; a++) { ArrayList finalToImport = new ArrayList<>(); finalToImport.addAll(toImport.subList(a * 500, Math.min((a + 1) * 500, toImport.size()))); TLRPC.TL_contacts_importContacts req = new TLRPC.TL_contacts_importContacts(); req.contacts = finalToImport; req.replace = false; - final boolean isLastQuery = a == count - 1; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { + completedRequestsCount++; if (error == null) { FileLog.e("tmessages", "contacts imported"); - if (isLastQuery && !contactsMap.isEmpty()) { - MessagesStorage.getInstance().putCachedPhoneBook(contactsMap); + TLRPC.TL_contacts_importedContacts res = (TLRPC.TL_contacts_importedContacts) response; + if (!res.retry_contacts.isEmpty()) { + for (int a = 0; a < res.retry_contacts.size(); a++) { + long id = res.retry_contacts.get(a); + contactsMapToSave.remove((int) id); + } } - TLRPC.TL_contacts_importedContacts res = (TLRPC.TL_contacts_importedContacts)response; + + if (completedRequestsCount == count && !contactsMapToSave.isEmpty()) { + MessagesStorage.getInstance().putCachedPhoneBook(contactsMapToSave); + } + /*if (BuildVars.DEBUG_VERSION) { for (TLRPC.User user : res.users) { FileLog.e("tmessages", "received user " + user.first_name + " " + user.last_name + " " + user.phone); @@ -793,16 +818,16 @@ public class ContactsController { }*/ MessagesStorage.getInstance().putUsersAndChats(res.users, null, true, true); ArrayList cArr = new ArrayList<>(); - for (TLRPC.TL_importedContact c : res.imported) { + for (int a = 0; a < res.imported.size(); a++) { TLRPC.TL_contact contact = new TLRPC.TL_contact(); - contact.user_id = c.user_id; + contact.user_id = res.imported.get(a).user_id; cArr.add(contact); } processLoadedContacts(cArr, res.users, 2); } else { FileLog.e("tmessages", "import contacts error " + error.text); } - if (isLastQuery) { + if (completedRequestsCount == count) { Utilities.stageQueue.postRunnable(new Runnable() { @Override public void run() { @@ -838,13 +863,13 @@ public class ContactsController { applyContactsUpdates(delayedContactsUpdate, null, null, null); delayedContactsUpdate.clear(); } - } - }); - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - updateUnregisteredContacts(contacts); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.contactsDidLoaded); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + updateUnregisteredContacts(contacts); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.contactsDidLoaded); + } + }); } }); } @@ -894,7 +919,7 @@ public class ContactsController { @Override public void run(TLObject response, TLRPC.TL_error error) { if (error == null) { - TLRPC.contacts_Contacts res = (TLRPC.contacts_Contacts)response; + TLRPC.contacts_Contacts res = (TLRPC.contacts_Contacts) response; if (res instanceof TLRPC.TL_contacts_contactsNotModified) { contactsLoaded = true; if (!delayedContactsUpdate.isEmpty() && contactsBookLoaded) { @@ -942,11 +967,10 @@ public class ContactsController { contactsArr.addAll(contacts); } - for (TLRPC.TL_contact contact : contactsArr) { - TLRPC.User user = MessagesController.getInstance().getUser(contact.user_id); + for (int a = 0; a < contactsArr.size(); a++) { + TLRPC.User user = MessagesController.getInstance().getUser(contactsArr.get(a).user_id); if (user != null) { usersDict.put(user.id, user); - //if (BuildVars.DEBUG_VERSION) { // FileLog.e("tmessages", "loaded user contact " + user.first_name + " " + user.last_name + " " + user.phone); //} @@ -957,7 +981,7 @@ public class ContactsController { @Override public void run() { FileLog.e("tmessages", "done loading contacts"); - if (from == 1 && (contactsArr.isEmpty() || UserConfig.lastContactsSyncTime < (int) (System.currentTimeMillis() / 1000) - 24 * 60 * 60)) { + if (from == 1 && (contactsArr.isEmpty() || Math.abs(System.currentTimeMillis() / 1000 - UserConfig.lastContactsSyncTime) >= 24 * 60 * 60)) { loadContacts(false, true); return; } @@ -1023,7 +1047,8 @@ public class ContactsController { final HashMap contactsByPhonesDictFinal = contactsByPhonesDict; - for (TLRPC.TL_contact value : contactsArr) { + for (int a = 0; a < contactsArr.size(); a++) { + TLRPC.TL_contact value = contactsArr.get(a); TLRPC.User user = usersDict.get(value.user_id); if (user == null) { continue; @@ -1356,7 +1381,8 @@ public class ContactsController { if (newC == null || contactsTD == null) { newC = new ArrayList<>(); contactsTD = new ArrayList<>(); - for (Integer uid : ids) { + for (int a = 0; a < ids.size(); a++) { + Integer uid = ids.get(a); if (uid > 0) { TLRPC.TL_contact contact = new TLRPC.TL_contact(); contact.user_id = uid; @@ -1372,7 +1398,8 @@ public class ContactsController { StringBuilder toDelete = new StringBuilder(); boolean reloadContacts = false; - for (TLRPC.TL_contact newContact : newC) { + for (int a = 0; a < newC.size(); a++) { + TLRPC.TL_contact newContact = newC.get(a); TLRPC.User user = null; if (userDict != null) { user = userDict.get(newContact.user_id); @@ -1400,7 +1427,8 @@ public class ContactsController { toAdd.append(user.phone); } - for (final Integer uid : contactsTD) { + for (int a = 0; a < contactsTD.size(); a++) { + final Integer uid = contactsTD.get(a); Utilities.phoneBookQueue.postRunnable(new Runnable() { @Override public void run() { @@ -1454,13 +1482,15 @@ public class ContactsController { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - for (TLRPC.TL_contact contact : newContacts) { + for (int a = 0; a < newContacts.size(); a++) { + TLRPC.TL_contact contact = newContacts.get(a); if (contactsDict.get(contact.user_id) == null) { contacts.add(contact); contactsDict.put(contact.user_id, contact); } } - for (Integer uid : contactsToDelete) { + for (int a = 0; a < contactsToDelete.size(); a++) { + Integer uid = contactsToDelete.get(a); TLRPC.TL_contact contact = contactsDict.get(uid); if (contact != null) { contacts.remove(contact); @@ -1471,7 +1501,7 @@ public class ContactsController { updateUnregisteredContacts(contacts); performWriteContactsToPhoneBook(); } - performSyncPhoneBook(getContactsCopy(contactsBook), false, false, false); + performSyncPhoneBook(getContactsCopy(contactsBook), false, false, false, false); buildContactsSectionsArrays(!newContacts.isEmpty()); NotificationCenter.getInstance().postNotificationName(NotificationCenter.contactsDidLoaded); } @@ -1645,7 +1675,7 @@ public class ContactsController { if (error != null) { return; } - final TLRPC.TL_contacts_importedContacts res = (TLRPC.TL_contacts_importedContacts)response; + final TLRPC.TL_contacts_importedContacts res = (TLRPC.TL_contacts_importedContacts) response; MessagesStorage.getInstance().putUsersAndChats(res.users, null, true, true); /*if (BuildVars.DEBUG_VERSION) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/DialogObject.java b/TMessagesProj/src/main/java/org/telegram/messenger/DialogObject.java new file mode 100644 index 000000000..a31ed060f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/DialogObject.java @@ -0,0 +1,18 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.messenger; + +import org.telegram.tgnet.TLRPC; + +public class DialogObject { + + public static boolean isChannel(TLRPC.TL_dialog dialog) { + return dialog != null && (dialog.flags & 1) != 0; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java b/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java index 2521e1d22..0fefae69e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java @@ -43,6 +43,7 @@ public class FileLoadOperation { private volatile int state = stateIdle; private int downloadedBytes; private int totalBytesCount; + private int bytesCountPadding; private FileLoadOperationDelegate delegate; private byte[] key; private byte[] iv; @@ -110,8 +111,13 @@ public class FileLoadOperation { location.access_hash = documentLocation.access_hash; datacenter_id = documentLocation.dc_id; } - if (totalBytesCount <= 0) { - totalBytesCount = documentLocation.size; + totalBytesCount = documentLocation.size; + if (key != null) { + int toAdd = 0; + if (totalBytesCount % 16 != 0) { + bytesCountPadding = 16 - totalBytesCount % 16; + totalBytesCount += bytesCountPadding; + } } ext = FileLoader.getDocumentFileName(documentLocation); int idx; @@ -405,8 +411,15 @@ public class FileLoadOperation { onFinishLoadingFile(); return; } + int currentBytesSize = requestInfo.response.bytes.limit(); + downloadedBytes += currentBytesSize; + boolean finishedDownloading = currentBytesSize != currentDownloadChunkSize || (totalBytesCount == downloadedBytes || downloadedBytes % currentDownloadChunkSize != 0) && (totalBytesCount <= 0 || totalBytesCount <= downloadedBytes); + if (key != null) { Utilities.aesIgeEncryption(requestInfo.response.bytes.buffer, key, iv, false, true, 0, requestInfo.response.bytes.limit()); + if (finishedDownloading && bytesCountPadding != 0) { + requestInfo.response.bytes.limit(requestInfo.response.bytes.limit() - bytesCountPadding); + } } if (fileOutputStream != null) { FileChannel channel = fileOutputStream.getChannel(); @@ -416,10 +429,8 @@ public class FileLoadOperation { fiv.seek(0); fiv.write(iv); } - int currentBytesSize = requestInfo.response.bytes.limit(); - downloadedBytes += currentBytesSize; if (totalBytesCount > 0 && state == stateDownloading) { - delegate.didChangedLoadProgress(FileLoadOperation.this, Math.min(1.0f, (float)downloadedBytes / (float)totalBytesCount)); + delegate.didChangedLoadProgress(FileLoadOperation.this, Math.min(1.0f, (float) downloadedBytes / (float) totalBytesCount)); } for (int a = 0; a < delayedRequestInfos.size(); a++) { @@ -433,14 +444,10 @@ public class FileLoadOperation { } } - if (currentBytesSize != currentDownloadChunkSize) { + if (finishedDownloading) { onFinishLoadingFile(); } else { - if (totalBytesCount != downloadedBytes && downloadedBytes % currentDownloadChunkSize == 0 || totalBytesCount > 0 && totalBytesCount > downloadedBytes) { - startDownloadRequest(); - } else { - onFinishLoadingFile(); - } + startDownloadRequest(); } } catch (Exception e) { cleanup(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java index 7f38c91a9..fe2b2e137 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java @@ -79,7 +79,6 @@ public class ImageLoader { private HashMap retryHttpsTasks = new HashMap<>(); private int currentHttpFileLoadTasksCount = 0; - public VMRuntimeHack runtimeHack = null; private String ignoreRemoval = null; private volatile long lastCacheOutTime = 0; @@ -631,7 +630,7 @@ public class ImageLoader { BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inSampleSize = 1; - if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 21) { + if (Build.VERSION.SDK_INT < 21) { opts.inPurgeable = true; } @@ -685,9 +684,6 @@ public class ImageLoader { } else if (blurType == 0 && opts.inPurgeable) { Utilities.pinBitmap(image); } - if (runtimeHack != null) { - runtimeHack.trackFree(image.getRowBytes() * image.getHeight()); - } } } catch (Throwable e) { FileLog.e("tmessages", e); @@ -715,9 +711,6 @@ public class ImageLoader { } int delay = 20; - if (runtimeHack != null) { - delay = 60; - } if (mediaId != null) { delay = 0; } @@ -782,7 +775,7 @@ public class ImageLoader { } else { opts.inPreferredConfig = Bitmap.Config.RGB_565; } - if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 21) { + if (Build.VERSION.SDK_INT < 21) { opts.inPurgeable = true; } @@ -850,9 +843,6 @@ public class ImageLoader { if (!blured && opts.inPurgeable) { Utilities.pinBitmap(image); } - if (runtimeHack != null && image != null) { - runtimeHack.trackFree(image.getRowBytes() * image.getHeight()); - } } } catch (Throwable e) { //don't promt @@ -877,9 +867,6 @@ public class ImageLoader { toSet = bitmapDrawable; } else { Bitmap image = bitmapDrawable.getBitmap(); - if (runtimeHack != null) { - runtimeHack.trackAlloc(image.getRowBytes() * image.getHeight()); - } image.recycle(); } } @@ -1098,19 +1085,10 @@ public class ImageLoader { int cacheSize = Math.min(15, ((ActivityManager) ApplicationLoader.applicationContext.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass() / 7) * 1024 * 1024; - if (Build.VERSION.SDK_INT < 11) { - runtimeHack = new VMRuntimeHack(); - cacheSize = 1024 * 1024 * 3; - } memCache = new LruCache(cacheSize) { @Override protected int sizeOf(String key, BitmapDrawable value) { - Bitmap b = value.getBitmap(); - if (Build.VERSION.SDK_INT < 12) { - return b.getRowBytes() * b.getHeight(); - } else { - return b.getByteCount(); - } + return value.getBitmap().getByteCount(); } @Override @@ -1121,9 +1099,6 @@ public class ImageLoader { final Integer count = bitmapUseCounts.get(key); if (count == null || count == 0) { Bitmap b = oldValue.getBitmap(); - if (runtimeHack != null) { - runtimeHack.trackAlloc(b.getRowBytes() * b.getHeight()); - } if (!b.isRecycled()) { b.recycle(); } @@ -1937,11 +1912,7 @@ public class ImageLoader { } while (currentHttpTasksCount < 4 && !httpTasks.isEmpty()) { HttpImageTask task = httpTasks.poll(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null); - } else { - task.execute(null, null, null); - } + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null); currentHttpTasksCount++; } } @@ -2006,11 +1977,7 @@ public class ImageLoader { } while (currentHttpFileLoadTasksCount < 2 && !httpFileLoadTasks.isEmpty()) { HttpFileTask task = httpFileLoadTasks.poll(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null); - } else { - task.execute(null, null, null); - } + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null); currentHttpFileLoadTasksCount++; } } @@ -2057,7 +2024,7 @@ public class ImageLoader { } bmOptions.inJustDecodeBounds = false; bmOptions.inSampleSize = (int) scaleFactor; - bmOptions.inPurgeable = Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 21; + bmOptions.inPurgeable = Build.VERSION.SDK_INT < 21; String exifPath = null; if (path != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java index b7d156e08..b12dc6099 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java @@ -481,16 +481,12 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg } } - if (bitmapDrawable instanceof AnimatedFileDrawable) { - drawRegion.set(imageX, imageY, imageX + imageW, imageY + imageH); + if (bitmapW / scaleH > imageW) { + bitmapW /= scaleH; + drawRegion.set(imageX - (bitmapW - imageW) / 2, imageY, imageX + (bitmapW + imageW) / 2, imageY + imageH); } else { - if (bitmapW / scaleH > imageW) { - bitmapW /= scaleH; - drawRegion.set(imageX - (bitmapW - imageW) / 2, imageY, imageX + (bitmapW + imageW) / 2, imageY + imageH); - } else { - bitmapH /= scaleW; - drawRegion.set(imageX, imageY - (bitmapH - imageH) / 2, imageX + imageW, imageY + (bitmapH + imageH) / 2); - } + bitmapH /= scaleW; + drawRegion.set(imageX, imageY - (bitmapH - imageH) / 2, imageX + imageW, imageY + (bitmapH + imageH) / 2); } if (orientation % 360 == 90 || orientation % 360 == 270) { int width = (drawRegion.right - drawRegion.left) / 2; @@ -656,6 +652,8 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg public Bitmap getBitmap() { if (currentImage instanceof AnimatedFileDrawable) { return ((AnimatedFileDrawable) currentImage).getAnimatedBitmap(); + } else if (staticThumb instanceof AnimatedFileDrawable) { + return ((AnimatedFileDrawable) staticThumb).getAnimatedBitmap(); } else if (currentImage instanceof BitmapDrawable) { return ((BitmapDrawable) currentImage).getBitmap(); } else if (currentThumb instanceof BitmapDrawable) { @@ -669,6 +667,8 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg public int getBitmapWidth() { if (currentImage instanceof AnimatedFileDrawable) { return orientation % 360 == 0 || orientation % 360 == 180 ? currentImage.getIntrinsicWidth() : currentImage.getIntrinsicHeight(); + } else if (staticThumb instanceof AnimatedFileDrawable) { + return orientation % 360 == 0 || orientation % 360 == 180 ? staticThumb.getIntrinsicWidth() : staticThumb.getIntrinsicHeight(); } Bitmap bitmap = getBitmap(); return orientation % 360 == 0 || orientation % 360 == 180 ? bitmap.getWidth() : bitmap.getHeight(); @@ -677,6 +677,8 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg public int getBitmapHeight() { if (currentImage instanceof AnimatedFileDrawable) { return orientation % 360 == 0 || orientation % 360 == 180 ? currentImage.getIntrinsicHeight() : currentImage.getIntrinsicWidth(); + } else if (staticThumb instanceof AnimatedFileDrawable) { + return orientation % 360 == 0 || orientation % 360 == 180 ? staticThumb.getIntrinsicHeight() : staticThumb.getIntrinsicWidth(); } Bitmap bitmap = getBitmap(); return orientation % 360 == 0 || orientation % 360 == 180 ? bitmap.getHeight() : bitmap.getWidth(); @@ -876,6 +878,10 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg return currentImage instanceof AnimatedFileDrawable && ((AnimatedFileDrawable) currentImage).isRunning(); } + public AnimatedFileDrawable getAnimation() { + return currentImage instanceof AnimatedFileDrawable ? (AnimatedFileDrawable) currentImage : null; + } + protected Integer getTag(boolean thumb) { if (thumb) { return thumbTag; @@ -994,15 +1000,11 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg if (key != null && (newKey == null || !newKey.equals(key)) && image != null) { if (image instanceof AnimatedFileDrawable) { AnimatedFileDrawable fileDrawable = (AnimatedFileDrawable) image; - fileDrawable.stop(); fileDrawable.recycle(); } else if (image instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDrawable) image).getBitmap(); boolean canDelete = ImageLoader.getInstance().decrementUseCount(key); if (!ImageLoader.getInstance().isInCache(key)) { - if (ImageLoader.getInstance().runtimeHack != null) { - ImageLoader.getInstance().runtimeHack.trackAlloc(bitmap.getRowBytes() * bitmap.getHeight()); - } if (canDelete) { bitmap.recycle(); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java b/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java index a34e0a076..05ed68d5b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java @@ -374,7 +374,7 @@ public class LocaleController { if (languageNameInEnglish.contains("&") || languageNameInEnglish.contains("|")) { return false; } - if (languageCode.contains("&") || languageCode.contains("|")) { + if (languageCode.contains("&") || languageCode.contains("|") || languageCode.contains("/") || languageCode.contains("\\")) { return false; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java b/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java index ef7b89218..44b725c54 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java @@ -239,6 +239,14 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, private int hasAudioFocus; private boolean callInProgress; + private int audioFocus = AUDIO_NO_FOCUS_NO_DUCK; + private boolean resumeAudioOnFocusGain; + + private static final float VOLUME_DUCK = 0.2f; + private static final float VOLUME_NORMAL = 1.0f; + private static final int AUDIO_NO_FOCUS_NO_DUCK = 0; + private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1; + private static final int AUDIO_FOCUSED = 2; private ArrayList videoConvertQueue = new ArrayList<>(); private final Object videoQueueSync = new Object(); @@ -303,6 +311,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, private ArrayList playlist = new ArrayList<>(); private ArrayList shuffledPlaylist = new ArrayList<>(); private int currentPlaylistNum; + private boolean forceLoopCurrentPlaylist; private boolean downloadingCurrentMessage; private boolean playMusicAgain; private AudioInfo audioInfo; @@ -314,7 +323,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, private long recordTimeCount; private long recordDialogId; private MessageObject recordReplyingMessageObject; - private boolean recordAsAdmin; private DispatchQueue fileDecodingQueue; private DispatchQueue playerQueue; private ArrayList usedPlayerBuffers = new ArrayList<>(); @@ -369,7 +377,9 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, sampleStep = (float) len / 2 / (float) newPart; for (int i = 0; i < len / 2; i++) { short peak = buffer.getShort(); - sum += peak * peak; + if (peak > 2500) { + sum += peak * peak; + } if (i == (int) nextNum && currentNum < recordSamples.length) { recordSamples[currentNum] = peak; nextNum += sampleStep; @@ -594,12 +604,12 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, fileDecodingQueue = new DispatchQueue("fileDecodingQueue"); SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - mobileDataDownloadMask = preferences.getInt("mobileDataDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | (Build.VERSION.SDK_INT >= 11 ? AUTODOWNLOAD_MASK_GIF : 0)); - wifiDownloadMask = preferences.getInt("wifiDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | (Build.VERSION.SDK_INT >= 11 ? AUTODOWNLOAD_MASK_GIF : 0)); + mobileDataDownloadMask = preferences.getInt("mobileDataDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | AUTODOWNLOAD_MASK_GIF); + wifiDownloadMask = preferences.getInt("wifiDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | AUTODOWNLOAD_MASK_GIF); roamingDownloadMask = preferences.getInt("roamingDownloadMask", 0); saveToGallery = preferences.getBoolean("save_gallery", false); - autoplayGifs = preferences.getBoolean("autoplay_gif", true) && Build.VERSION.SDK_INT >= 11; - raiseToSpeak = preferences.getBoolean("raise_to_speak", true) && Build.VERSION.SDK_INT >= 11; + autoplayGifs = preferences.getBoolean("autoplay_gif", true); + raiseToSpeak = preferences.getBoolean("raise_to_speak", true); customTabs = preferences.getBoolean("custom_tabs", true); directShare = preferences.getBoolean("direct_share", true); shuffleMusic = preferences.getBoolean("shuffleMusic", false); @@ -668,8 +678,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, @Override public void onCallStateChanged(int state, String incomingNumber) { if (state == TelephonyManager.CALL_STATE_RINGING) { - if (MediaController.getInstance().isPlayingAudio(MediaController.getInstance().getPlayingMessageObject()) && !MediaController.getInstance().isAudioPaused()) { - MediaController.getInstance().pauseAudio(MediaController.getInstance().getPlayingMessageObject()); + if (isPlayingAudio(getPlayingMessageObject()) && !isAudioPaused()) { + pauseAudio(getPlayingMessageObject()); } else if (recordStartRunnable != null || recordingAudio != null) { stopRecording(2); } @@ -693,12 +703,46 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, @Override public void onAudioFocusChange(int focusChange) { if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { - if (MediaController.getInstance().isPlayingAudio(MediaController.getInstance().getPlayingMessageObject()) && !MediaController.getInstance().isAudioPaused()) { - MediaController.getInstance().pauseAudio(MediaController.getInstance().getPlayingMessageObject()); + if (isPlayingAudio(getPlayingMessageObject()) && !isAudioPaused()) { + pauseAudio(getPlayingMessageObject()); } hasAudioFocus = 0; + audioFocus = AUDIO_NO_FOCUS_NO_DUCK; } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - //MediaController.getInstance().playAudio(MediaController.getInstance().getPlayingMessageObject()); + audioFocus = AUDIO_FOCUSED; + if (resumeAudioOnFocusGain) { + resumeAudioOnFocusGain = false; + if (isPlayingAudio(getPlayingMessageObject()) && isAudioPaused()) { + playAudio(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()); + resumeAudioOnFocusGain = true; + } + } + setPlayerVolume(); + } + + private void setPlayerVolume() { + try { + float volume; + if (audioFocus != AUDIO_NO_FOCUS_CAN_DUCK) { + volume = VOLUME_NORMAL; + } else { + volume = VOLUME_DUCK; + } + if (audioPlayer != null) { + audioPlayer.setVolume(volume, volume); + } else if (audioTrackPlayer != null) { + audioTrackPlayer.setStereoVolume(volume, volume); + } + } catch (Exception e) { + FileLog.e("tmessages", e); } } @@ -1040,9 +1084,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } public void startMediaObserver() { - if (android.os.Build.VERSION.SDK_INT < 14) { - return; - } ApplicationLoader.applicationHandler.removeCallbacks(stopMediaObserverRunnable); startObserverToken++; try { @@ -1062,9 +1103,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } public void stopMediaObserver() { - if (android.os.Build.VERSION.SDK_INT < 14) { - return; - } if (stopMediaObserverRunnable == null) { stopMediaObserverRunnable = new StopMediaObserverRunnable(); } @@ -1610,7 +1648,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if (!raiseChat.playFirstUnreadVoiceMessage()) { raiseToEarRecord = true; useFrontSpeaker = false; - startRecording(raiseChat.getDialogId(), null, false); + startRecording(raiseChat.getDialogId(), null); } ignoreOnPause = true; if (proximityHasDifferentValues && proximityWakeLock != null && !proximityWakeLock.isHeld()) { @@ -1675,7 +1713,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, return; } raiseToEarRecord = true; - startRecording(raiseChat.getDialogId(), null, false); + startRecording(raiseChat.getDialogId(), null); ignoreOnPause = true; } @@ -1936,6 +1974,10 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, return playingMessageObject; } + public int getPlayingMessageObjectNum() { + return currentPlaylistNum; + } + private void buildShuffledPlayList() { if (playlist.isEmpty()) { return; @@ -1956,9 +1998,14 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } public boolean setPlaylist(ArrayList messageObjects, MessageObject current) { + return setPlaylist(messageObjects, current, true); + } + + public boolean setPlaylist(ArrayList messageObjects, MessageObject current, boolean loadMusic) { if (playingMessageObject == current) { return playAudio(current); } + forceLoopCurrentPlaylist = !loadMusic; playMusicAgain = !playlist.isEmpty(); playlist.clear(); for (int a = messageObjects.size() - 1; a >= 0; a--) { @@ -1971,6 +2018,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if (currentPlaylistNum == -1) { playlist.clear(); shuffledPlaylist.clear(); + currentPlaylistNum = playlist.size(); playlist.add(current); } if (current.isMusic()) { @@ -1978,7 +2026,9 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, buildShuffledPlayList(); currentPlaylistNum = 0; } - SharedMediaQuery.loadMusic(current.getDialogId(), playlist.get(0).getId()); + if (loadMusic) { + SharedMediaQuery.loadMusic(current.getDialogId(), playlist.get(0).getId()); + } } return playAudio(current); } @@ -1987,10 +2037,19 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, playNextMessage(false); } + public void playMessageAtIndex(int index) { + if (currentPlaylistNum < 0 || currentPlaylistNum >= playlist.size()) { + return; + } + currentPlaylistNum = index; + playMusicAgain = true; + playAudio(playlist.get(currentPlaylistNum)); + } + private void playNextMessage(boolean byStop) { ArrayList currentPlayList = shuffleMusic ? shuffledPlaylist : playlist; - if (byStop && repeatMode == 2) { + if (byStop && repeatMode == 2 && !forceLoopCurrentPlaylist) { cleanupPlayer(false, false); playAudio(currentPlayList.get(currentPlaylistNum)); return; @@ -1998,7 +2057,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, currentPlaylistNum++; if (currentPlaylistNum >= currentPlayList.size()) { currentPlaylistNum = 0; - if (byStop && repeatMode == 0) { + if (byStop && repeatMode == 0 && !forceLoopCurrentPlaylist) { if (audioPlayer != null || audioTrackPlayer != null) { if (audioPlayer != null) { try { @@ -2118,10 +2177,14 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } if (hasAudioFocus != neededAudioFocus) { hasAudioFocus = neededAudioFocus; + int result; if (neededAudioFocus == 3) { - NotificationsController.getInstance().audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN); + result = NotificationsController.getInstance().audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN); } else { - NotificationsController.getInstance().audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, neededAudioFocus == 2 ? AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK : AudioManager.AUDIOFOCUS_GAIN); + result = NotificationsController.getInstance().audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, neededAudioFocus == 2 ? AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK : AudioManager.AUDIOFOCUS_GAIN); + } + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + audioFocus = AUDIO_FOCUSED; } } } @@ -2270,6 +2333,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } } checkAudioFocus(messageObject); + setPlayerVolume(); isPaused = false; lastProgress = 0; @@ -2470,7 +2534,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, return downloadingCurrentMessage; } - public void startRecording(final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public void startRecording(final long dialog_id, final MessageObject reply_to_msg) { boolean paused = false; if (playingMessageObject != null && isPlayingAudio(playingMessageObject) && !isAudioPaused()) { paused = true; @@ -2522,17 +2586,13 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, }); return; } - //if (Build.VERSION.SDK_INT >= 11) { - // audioRecorder = new AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, recordBufferSize * 10); - //} else { - audioRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, recordBufferSize * 10); - //} + + audioRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, recordBufferSize * 10); recordStartTime = System.currentTimeMillis(); recordTimeCount = 0; samplesCount = 0; recordDialogId = dialog_id; recordReplyingMessageObject = reply_to_msg; - recordAsAdmin = asAdmin; fileBuffer.rewind(); audioRecorder.startRecording(); @@ -2600,7 +2660,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } TLRPC.TL_messages_messages messagesRes = new TLRPC.TL_messages_messages(); messagesRes.messages.add(messageObject.messageOwner); - MessagesStorage.getInstance().putMessages(messagesRes, messageObject.getDialogId(), -1, 0, 0, false); + MessagesStorage.getInstance().putMessages(messagesRes, messageObject.getDialogId(), -1, 0, false); ArrayList arrayList = new ArrayList<>(); arrayList.add(messageObject); NotificationCenter.getInstance().postNotificationName(NotificationCenter.replaceMessagesObjects, messageObject.getDialogId(), arrayList); @@ -2635,7 +2695,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, audioToSend.attributes.add(attributeAudio); if (duration > 700) { if (send == 1) { - SendMessagesHelper.getInstance().sendMessage(audioToSend, null, recordingAudioFileToSend.getAbsolutePath(), recordDialogId, recordReplyingMessageObject, recordAsAdmin, null, null); + SendMessagesHelper.getInstance().sendMessage(audioToSend, null, recordingAudioFileToSend.getAbsolutePath(), recordDialogId, recordReplyingMessageObject, null, null); } NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioDidSent, send == 2 ? audioToSend : null, send == 2 ? recordingAudioFileToSend.getAbsolutePath() : null); } else { @@ -2796,10 +2856,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if (result) { if (type == 2) { - if (Build.VERSION.SDK_INT >= 12) { - DownloadManager downloadManager = (DownloadManager) ApplicationLoader.applicationContext.getSystemService(Context.DOWNLOAD_SERVICE); - downloadManager.addCompletedDownload(destFile.getName(), destFile.getName(), false, mime, destFile.getAbsolutePath(), destFile.length(), true); - } + DownloadManager downloadManager = (DownloadManager) ApplicationLoader.applicationContext.getSystemService(Context.DOWNLOAD_SERVICE); + downloadManager.addCompletedDownload(destFile.getName(), destFile.getName(), false, mime, destFile.getAbsolutePath(), destFile.length(), true); } else { AndroidUtilities.addMediaToGallery(Uri.fromFile(destFile)); } @@ -2880,8 +2938,9 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public static String getFileName(Uri uri) { String result = null; if (uri.getScheme().equals("content")) { - Cursor cursor = ApplicationLoader.applicationContext.getContentResolver().query(uri, null, null, null, null); + Cursor cursor = null; try { + cursor = ApplicationLoader.applicationContext.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null); if (cursor.moveToFirst()) { result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java index 0bff8975a..3f77fec4e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java @@ -29,6 +29,7 @@ import org.telegram.ui.Components.URLSpanBotCommand; import org.telegram.ui.Components.URLSpanNoUnderline; import org.telegram.ui.Components.URLSpanNoUnderlineBold; import org.telegram.ui.Components.URLSpanReplacement; +import org.telegram.ui.Components.URLSpanUserMention; import java.io.File; import java.util.AbstractMap; @@ -313,6 +314,8 @@ public class MessageObject { messageText = LocaleController.getString("ActionMigrateFromGroup", R.string.ActionMigrateFromGroup); } else if (message.action instanceof TLRPC.TL_messageActionPinMessage) { generatePinMessageText(fromUser, fromUser == null ? chats.get(message.to_id.channel_id) : null); + } else if (message.action instanceof TLRPC.TL_messageActionHistoryClear) { + messageText = LocaleController.getString("HistoryCleared", R.string.HistoryCleared); } } } else if (!isMediaEmpty()) { @@ -369,7 +372,9 @@ public class MessageObject { if (messageOwner.message != null && messageOwner.id < 0 && messageOwner.message.length() > 6 && isVideo()) { videoEditedInfo = new VideoEditedInfo(); - videoEditedInfo.parseString(messageOwner.message); + if (!videoEditedInfo.parseString(messageOwner.message)) { + videoEditedInfo = null; + } } generateCaption(); @@ -461,6 +466,7 @@ public class MessageObject { } public void setType() { + int oldType = type; if (messageOwner instanceof TLRPC.TL_message || messageOwner instanceof TLRPC.TL_messageForwarded_old2) { if (isMediaEmpty()) { type = 0; @@ -508,11 +514,17 @@ public class MessageObject { contentType = -1; type = -1; } + } else if (messageOwner.action instanceof TLRPC.TL_messageActionHistoryClear) { + contentType = -1; + type = -1; } else { contentType = 1; type = 10; } } + if (oldType != 1000 && oldType != type) { + generateThumbs(false); + } } public void checkLayout() { @@ -879,7 +891,20 @@ public class MessageObject { generateLinkDescription(); textLayoutBlocks = new ArrayList<>(); - boolean useManualParse = messageOwner.entities.isEmpty() && ( + boolean hasEntities; + if (messageOwner.send_state != MESSAGE_SEND_STATE_SENT) { + hasEntities = false; + for (int a = 0; a < messageOwner.entities.size(); a++) { + if (!(messageOwner.entities.get(a) instanceof TLRPC.TL_inputMessageEntityMentionName)) { + hasEntities = true; + break; + } + } + } else { + hasEntities = !messageOwner.entities.isEmpty(); + } + + boolean useManualParse = !hasEntities && ( messageOwner instanceof TLRPC.TL_message_old || messageOwner instanceof TLRPC.TL_message_old2 || messageOwner instanceof TLRPC.TL_message_old3 || @@ -932,6 +957,10 @@ public class MessageObject { spannable.setSpan(new TypefaceSpan(AndroidUtilities.getTypeface("fonts/ritalic.ttf")), entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } else if (entity instanceof TLRPC.TL_messageEntityCode || entity instanceof TLRPC.TL_messageEntityPre) { spannable.setSpan(new TypefaceSpan(Typeface.MONOSPACE, AndroidUtilities.dp(MessagesController.getInstance().fontSize - 1)), entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (entity instanceof TLRPC.TL_messageEntityMentionName) { + spannable.setSpan(new URLSpanUserMention("" + ((TLRPC.TL_messageEntityMentionName) entity).user_id), entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (entity instanceof TLRPC.TL_inputMessageEntityMentionName) { + spannable.setSpan(new URLSpanUserMention("" + ((TLRPC.TL_inputMessageEntityMentionName) entity).user_id.user_id), entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } else if (!useManualParse) { String url = messageOwner.message.substring(entity.offset, entity.offset + entity.length); if (entity instanceof TLRPC.TL_messageEntityBotCommand) { @@ -967,7 +996,7 @@ public class MessageObject { maxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) - AndroidUtilities.dp(80); } } - if (fromUser != null && fromUser.bot) { + if (fromUser != null && fromUser.bot || (isMegagroup() || messageOwner.fwd_from != null && messageOwner.fwd_from.channel_id != 0) && !isOut()) { maxWidth -= AndroidUtilities.dp(20); } @@ -1177,27 +1206,10 @@ public class MessageObject { return message.media_unread; } - public boolean isImportant() { - return isImportant(messageOwner); - } - public boolean isMegagroup() { return isMegagroup(messageOwner); } - public static boolean isImportant(TLRPC.Message message) { - if (isMegagroup(message)) { - return message.post; - } - if (message.to_id.channel_id != 0) { - if (message instanceof TLRPC.TL_message_layer47 || message instanceof TLRPC.TL_message_old7) { - return message.to_id.channel_id != 0 && (message.from_id <= 0 || message.mentioned || message.out || (message.flags & TLRPC.MESSAGE_FLAG_HAS_FROM_ID) == 0); - } - return message.post; - } - return false; - } - public static boolean isMegagroup(TLRPC.Message message) { return (message.flags & TLRPC.MESSAGE_FLAG_MEGAGROUP) != 0; } @@ -1467,6 +1479,16 @@ public class MessageObject { } } + public String getStickerEmoji() { + for (int a = 0; a < messageOwner.media.document.attributes.size(); a++) { + TLRPC.DocumentAttribute attribute = messageOwner.media.document.attributes.get(a); + if (attribute instanceof TLRPC.TL_documentAttributeSticker) { + return attribute.alt != null && attribute.alt.length() > 0 ? attribute.alt : null; + } + } + return null; + } + public boolean isSticker() { if (type != 1000) { return type == 13; @@ -1524,6 +1546,22 @@ public class MessageObject { return ""; } + public int getDuration() { + TLRPC.Document document; + if (type == 0) { + document = messageOwner.media.webpage.document; + } else { + document = messageOwner.media.document; + } + for (int a = 0; a < document.attributes.size(); a++) { + TLRPC.DocumentAttribute attribute = document.attributes.get(a); + if (attribute instanceof TLRPC.TL_documentAttributeAudio) { + return attribute.duration; + } + } + return 0; + } + public String getMusicAuthor() { TLRPC.Document document; if (type == 0) { @@ -1599,7 +1637,7 @@ public class MessageObject { } if (message.to_id.channel_id == 0) { return message.out && (message.media instanceof TLRPC.TL_messageMediaPhoto || - message.media instanceof TLRPC.TL_messageMediaDocument && (isVideoMessage(message) || isGifDocument(message.media.document)) || + message.media instanceof TLRPC.TL_messageMediaDocument && !isStickerMessage(message) || message.media instanceof TLRPC.TL_messageMediaEmpty || message.media instanceof TLRPC.TL_messageMediaWebPage || message.media == null); @@ -1610,9 +1648,9 @@ public class MessageObject { return false; } } - if (chat.megagroup && message.out || !chat.megagroup && (chat.creator || chat.editor && isOut(message)) && isImportant(message)) { + if (chat.megagroup && message.out || !chat.megagroup && (chat.creator || chat.editor && isOut(message)) && message.post) { if (message.media instanceof TLRPC.TL_messageMediaPhoto || - message.media instanceof TLRPC.TL_messageMediaDocument && (isVideoMessage(message) || isGifDocument(message.media.document)) || + message.media instanceof TLRPC.TL_messageMediaDocument && !isStickerMessage(message) || message.media instanceof TLRPC.TL_messageMediaEmpty || message.media instanceof TLRPC.TL_messageMediaWebPage || message.media == null) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java index 4c01cc31a..415689a47 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java @@ -14,8 +14,6 @@ import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.os.Build; import android.os.Bundle; import android.util.Base64; import android.util.SparseArray; @@ -24,9 +22,12 @@ import android.widget.Toast; import org.telegram.SQLite.SQLiteCursor; import org.telegram.messenger.query.BotQuery; +import org.telegram.messenger.query.DraftQuery; import org.telegram.messenger.query.MessagesQuery; +import org.telegram.messenger.query.SearchQuery; import org.telegram.messenger.query.StickersQuery; import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.NativeByteBuffer; import org.telegram.tgnet.RequestDelegate; import org.telegram.tgnet.SerializedData; import org.telegram.tgnet.TLObject; @@ -55,12 +56,13 @@ public class MessagesController implements NotificationCenter.NotificationCenter private HashMap exportedChats = new HashMap<>(); - public ArrayList dialogs = new ArrayList<>(); - public ArrayList dialogsServerOnly = new ArrayList<>(); - public ArrayList dialogsGroupsOnly = new ArrayList<>(); + public ArrayList dialogs = new ArrayList<>(); + public ArrayList dialogsServerOnly = new ArrayList<>(); + public ArrayList dialogsGroupsOnly = new ArrayList<>(); public int nextDialogsCacheOffset; public ConcurrentHashMap dialogs_read_inbox_max = new ConcurrentHashMap<>(100, 1.0f, 2); - public ConcurrentHashMap dialogs_dict = new ConcurrentHashMap<>(100, 1.0f, 2); + public ConcurrentHashMap dialogs_read_outbox_max = new ConcurrentHashMap<>(100, 1.0f, 2); + public ConcurrentHashMap dialogs_dict = new ConcurrentHashMap<>(100, 1.0f, 2); public HashMap dialogMessage = new HashMap<>(); public HashMap dialogMessagesByRandomIds = new HashMap<>(); public HashMap dialogMessagesByIds = new HashMap<>(); @@ -90,6 +92,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter private HashMap channelsPts = new HashMap<>(); private HashMap gettingDifferenceChannels = new HashMap<>(); + private HashMap gettingUnknownChannels = new HashMap<>(); + private HashMap checkingLastMessagesDialogs = new HashMap<>(); + private ArrayList updatesQueueSeq = new ArrayList<>(); private ArrayList updatesQueuePts = new ArrayList<>(); private ArrayList updatesQueueQts = new ArrayList<>(); @@ -138,6 +143,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter public int minGroupConvertSize = 200; public int maxEditTime = 172800; public int groupBigSize; + public int ratingDecay; private ArrayList disabledFeatures = new ArrayList<>(); private class UserActionUpdatesSeq extends TLRPC.Updates { @@ -173,6 +179,46 @@ public class MessagesController implements NotificationCenter.NotificationCenter private static volatile MessagesController Instance = null; + private final Comparator dialogComparator = new Comparator() { + @Override + public int compare(TLRPC.TL_dialog dialog1, TLRPC.TL_dialog dialog2) { + TLRPC.DraftMessage draftMessage = DraftQuery.getDraft(dialog1.id); + int date1 = draftMessage != null && draftMessage.date >= dialog1.last_message_date ? draftMessage.date : dialog1.last_message_date; + draftMessage = DraftQuery.getDraft(dialog2.id); + int date2 = draftMessage != null && draftMessage.date >= dialog2.last_message_date ? draftMessage.date : dialog2.last_message_date; + if (date1 < date2) { + return 1; + } else if (date1 > date2) { + return -1; + } + return 0; + } + }; + + private final Comparator updatesComparator = new Comparator() { + @Override + public int compare(TLRPC.Update lhs, TLRPC.Update rhs) { + int ltype = getUpdateType(lhs); + int rtype = getUpdateType(rhs); + if (ltype != rtype) { + return AndroidUtilities.compare(ltype, rtype); + } else if (ltype == 0) { + return AndroidUtilities.compare(lhs.pts, rhs.pts); + } else if (ltype == 1) { + return AndroidUtilities.compare(lhs.qts, rhs.qts); + } else if (ltype == 2) { + int lChannel = getUpdateChannelId(lhs); + int rChannel = getUpdateChannelId(rhs); + if (lChannel == rChannel) { + return AndroidUtilities.compare(lhs.pts, rhs.pts); + } else { + return AndroidUtilities.compare(lChannel, rChannel); + } + } + return 0; + } + }; + public static MessagesController getInstance() { MessagesController localInstance = Instance; if (localInstance == null) { @@ -204,6 +250,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter maxMegagroupCount = preferences.getInt("maxMegagroupCount", 1000); maxEditTime = preferences.getInt("maxEditTime", 3600); groupBigSize = preferences.getInt("groupBigSize", 10); + ratingDecay = preferences.getInt("ratingDecay", 2419200); fontSize = preferences.getInt("fons_size", AndroidUtilities.isTablet() ? 18 : 16); String disabledFeaturesString = preferences.getString("disabledFeatures", null); if (disabledFeaturesString != null && disabledFeaturesString.length() != 0) { @@ -235,6 +282,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter groupBigSize = config.chat_big_size; disabledFeatures = config.disabled_features; maxEditTime = config.edit_time_limit; + ratingDecay = config.rating_e_decay; SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); @@ -243,6 +291,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter editor.putInt("maxMegagroupCount", maxMegagroupCount); editor.putInt("groupBigSize", groupBigSize); editor.putInt("maxEditTime", maxEditTime); + editor.putInt("ratingDecay", ratingDecay); try { SerializedData data = new SerializedData(); data.writeInt32(disabledFeatures.size()); @@ -446,7 +495,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (obj != null && obj.getId() == msgId) { obj.messageOwner.id = newMsgId; obj.messageOwner.send_state = MessageObject.MESSAGE_SEND_STATE_SENT; - TLRPC.Dialog dialog = dialogs_dict.get(did); + TLRPC.TL_dialog dialog = dialogs_dict.get(did); if (dialog != null) { if (dialog.top_message == msgId) { dialog.top_message = newMsgId; @@ -461,18 +510,21 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } - public void cleanUp() { + public void cleanup() { ContactsController.getInstance().cleanup(); MediaController.getInstance().cleanup(); NotificationsController.getInstance().cleanup(); - SendMessagesHelper.getInstance().cleanUp(); - SecretChatHelper.getInstance().cleanUp(); + SendMessagesHelper.getInstance().cleanup(); + SecretChatHelper.getInstance().cleanup(); StickersQuery.cleanup(); + SearchQuery.cleanup(); + DraftQuery.cleanup(); reloadingWebpages.clear(); reloadingWebpagesPending.clear(); dialogs_dict.clear(); dialogs_read_inbox_max.clear(); + dialogs_read_outbox_max.clear(); exportedChats.clear(); fullUsersAbout.clear(); dialogs.clear(); @@ -500,6 +552,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter updatesQueueSeq.clear(); updatesQueuePts.clear(); updatesQueueQts.clear(); + gettingUnknownChannels.clear(); updatesStartWaitTimeSeq = 0; updatesStartWaitTimePts = 0; updatesStartWaitTimeQts = 0; @@ -551,6 +604,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } addSupportUser(); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); } public TLRPC.User getUser(Integer id) { @@ -807,6 +861,88 @@ public class MessagesController implements NotificationCenter.NotificationCenter loadedFullChats.clear(); } + private void reloadDialogsReadValue(ArrayList dialogs, long did) { + if (dialogs.isEmpty()) { + return; + } + TLRPC.TL_messages_getPeerDialogs req = new TLRPC.TL_messages_getPeerDialogs(); + if (dialogs != null) { + for (int a = 0; a < dialogs.size(); a++) { + req.peers.add(getInputPeer((int) dialogs.get(a).id)); + } + } else { + req.peers.add(getInputPeer((int) did)); + } + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (response != null) { + TLRPC.TL_messages_peerDialogs res = (TLRPC.TL_messages_peerDialogs) response; + ArrayList arrayList = new ArrayList<>(); + for (int a = 0; a < res.dialogs.size(); a++) { + TLRPC.TL_dialog dialog = res.dialogs.get(a); + if (dialog.read_inbox_max_id == 0) { + dialog.read_inbox_max_id = 1; + } + if (dialog.read_outbox_max_id == 0) { + dialog.read_outbox_max_id = 1; + } + if (dialog.id == 0 && dialog.peer != null) { + if (dialog.peer.user_id != 0) { + dialog.id = dialog.peer.user_id; + } else if (dialog.peer.chat_id != 0) { + dialog.id = -dialog.peer.chat_id; + } else if (dialog.peer.channel_id != 0) { + dialog.id = -dialog.peer.channel_id; + } + } + + Integer value = dialogs_read_inbox_max.get(dialog.id); + if (value == null) { + value = 0; + } + dialogs_read_inbox_max.put(dialog.id, Math.max(dialog.read_inbox_max_id, value)); + if (value == 0) { + if (dialog.peer.channel_id != 0) { + TLRPC.TL_updateReadChannelInbox update = new TLRPC.TL_updateReadChannelInbox(); + update.channel_id = dialog.peer.channel_id; + update.max_id = dialog.read_inbox_max_id; + arrayList.add(update); + } else { + TLRPC.TL_updateReadHistoryInbox update = new TLRPC.TL_updateReadHistoryInbox(); + update.peer = dialog.peer; + update.max_id = dialog.read_inbox_max_id; + arrayList.add(update); + } + } + + value = dialogs_read_outbox_max.get(dialog.id); + if (value == null) { + value = 0; + } + dialogs_read_outbox_max.put(dialog.id, Math.max(dialog.read_outbox_max_id, value)); + if (value == 0) { + if (dialog.peer.channel_id != 0) { + TLRPC.TL_updateReadChannelOutbox update = new TLRPC.TL_updateReadChannelOutbox(); + update.channel_id = dialog.peer.channel_id; + update.max_id = dialog.read_outbox_max_id; + arrayList.add(update); + } else { + TLRPC.TL_updateReadHistoryOutbox update = new TLRPC.TL_updateReadHistoryOutbox(); + update.peer = dialog.peer; + update.max_id = dialog.read_outbox_max_id; + arrayList.add(update); + } + } + } + if (!arrayList.isEmpty()) { + processUpdateArray(arrayList, null, null, false); + } + } + } + }); + } + public void loadFullChat(final int chat_id, final int classGuid, boolean force) { if (loadingFullChats.contains(chat_id) || !force && loadedFullChats.contains(chat_id)) { return; @@ -828,11 +964,41 @@ public class MessagesController implements NotificationCenter.NotificationCenter public void run(TLObject response, final TLRPC.TL_error error) { if (error == null) { final TLRPC.TL_messages_chatFull res = (TLRPC.TL_messages_chatFull) response; - if (chat != null && chat.megagroup) { - res.full_chat.unread_important_count = Math.max(res.full_chat.unread_important_count, res.full_chat.unread_count); - } MessagesStorage.getInstance().putUsersAndChats(res.users, res.chats, true, true); MessagesStorage.getInstance().updateChatInfo(res.full_chat, false); + + if (ChatObject.isChannel(chat)) { + long dialog_id = -chat_id; + Integer value = dialogs_read_inbox_max.get(dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + } + + dialogs_read_inbox_max.put(dialog_id, Math.max(res.full_chat.read_inbox_max_id, value)); + if (value == 0) { + ArrayList arrayList = new ArrayList<>(); + TLRPC.TL_updateReadChannelInbox update = new TLRPC.TL_updateReadChannelInbox(); + update.channel_id = chat_id; + update.max_id = res.full_chat.read_inbox_max_id; + arrayList.add(update); + processUpdateArray(arrayList, null, null, false); + } + + value = dialogs_read_outbox_max.get(dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + } + dialogs_read_outbox_max.put(dialog_id, Math.max(res.full_chat.read_outbox_max_id, value)); + if (value == 0) { + ArrayList arrayList = new ArrayList<>(); + TLRPC.TL_updateReadChannelOutbox update = new TLRPC.TL_updateReadChannelOutbox(); + update.channel_id = chat_id; + update.max_id = res.full_chat.read_outbox_max_id; + arrayList.add(update); + processUpdateArray(arrayList, null, null, false); + } + } + AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { @@ -973,6 +1139,18 @@ public class MessagesController implements NotificationCenter.NotificationCenter chatsLocal.put(c.id, c); } + Integer inboxValue = dialogs_read_inbox_max.get(dialog_id); + if (inboxValue == null) { + inboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_inbox_max.put(dialog_id, inboxValue); + } + + Integer outboxValue = dialogs_read_outbox_max.get(dialog_id); + if (outboxValue == null) { + outboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_outbox_max.put(dialog_id, outboxValue); + } + final ArrayList objects = new ArrayList<>(); for (int a = 0; a < messagesRes.messages.size(); a++) { TLRPC.Message message = messagesRes.messages.get(a); @@ -980,11 +1158,12 @@ public class MessagesController implements NotificationCenter.NotificationCenter message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; } message.dialog_id = dialog_id; + message.unread = (message.out ? outboxValue : inboxValue) < message.id; objects.add(new MessageObject(message, usersLocal, chatsLocal, true)); } ImageLoader.saveMessagesThumbs(messagesRes.messages); - MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, -1, 0, 0, false); + MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, -1, 0, false); AndroidUtilities.runOnUIThread(new Runnable() { @Override @@ -1133,44 +1312,45 @@ public class MessagesController implements NotificationCenter.NotificationCenter protected void processNewChannelDifferenceParams(int pts, int pts_count, int channelId) { FileLog.e("tmessages", "processNewChannelDifferenceParams pts = " + pts + " pts_count = " + pts_count + " channeldId = " + channelId); - TLRPC.Dialog dialog = dialogs_dict.get((long) -channelId); - if (dialog instanceof TLRPC.TL_dialogChannel) { - Integer channelPts = channelsPts.get(channelId); - if (channelPts == null) { - channelPts = MessagesStorage.getInstance().getChannelPtsSync(channelId); - if (channelPts == 0) { - channelPts = 1; - } - channelsPts.put(channelId, channelPts); + TLRPC.TL_dialog dialog = dialogs_dict.get((long) -channelId); + if (!DialogObject.isChannel(dialog)) { + return; + } + Integer channelPts = channelsPts.get(channelId); + if (channelPts == null) { + channelPts = MessagesStorage.getInstance().getChannelPtsSync(channelId); + if (channelPts == 0) { + channelPts = 1; } - if (channelPts + pts_count == pts) { - FileLog.e("tmessages", "APPLY CHANNEL PTS"); - channelsPts.put(channelId, pts); - MessagesStorage.getInstance().saveChannelPts(channelId, pts); - } else if (channelPts != pts) { - Long updatesStartWaitTime = updatesStartWaitTimeChannels.get(channelId); - Boolean gettingDifferenceChannel = gettingDifferenceChannels.get(channelId); - if (gettingDifferenceChannel == null) { - gettingDifferenceChannel = false; + channelsPts.put(channelId, channelPts); + } + if (channelPts + pts_count == pts) { + FileLog.e("tmessages", "APPLY CHANNEL PTS"); + channelsPts.put(channelId, pts); + MessagesStorage.getInstance().saveChannelPts(channelId, pts); + } else if (channelPts != pts) { + Long updatesStartWaitTime = updatesStartWaitTimeChannels.get(channelId); + Boolean gettingDifferenceChannel = gettingDifferenceChannels.get(channelId); + if (gettingDifferenceChannel == null) { + gettingDifferenceChannel = false; + } + if (gettingDifferenceChannel || updatesStartWaitTime == null || Math.abs(System.currentTimeMillis() - updatesStartWaitTime) <= 1500) { + FileLog.e("tmessages", "ADD CHANNEL UPDATE TO QUEUE pts = " + pts + " pts_count = " + pts_count); + if (updatesStartWaitTime == null) { + updatesStartWaitTimeChannels.put(channelId, System.currentTimeMillis()); } - if (gettingDifferenceChannel || updatesStartWaitTime == null || Math.abs(System.currentTimeMillis() - updatesStartWaitTime) <= 1500) { - FileLog.e("tmessages", "ADD CHANNEL UPDATE TO QUEUE pts = " + pts + " pts_count = " + pts_count); - if (updatesStartWaitTime == null) { - updatesStartWaitTimeChannels.put(channelId, System.currentTimeMillis()); - } - UserActionUpdatesPts updates = new UserActionUpdatesPts(); - updates.pts = pts; - updates.pts_count = pts_count; - updates.chat_id = channelId; - ArrayList arrayList = updatesQueueChannels.get(channelId); - if (arrayList == null) { - arrayList = new ArrayList<>(); - updatesQueueChannels.put(channelId, arrayList); - } - arrayList.add(updates); - } else { - getChannelDifference(channelId); + UserActionUpdatesPts updates = new UserActionUpdatesPts(); + updates.pts = pts; + updates.pts_count = pts_count; + updates.chat_id = channelId; + ArrayList arrayList = updatesQueueChannels.get(channelId); + if (arrayList == null) { + arrayList = new ArrayList<>(); + updatesQueueChannels.put(channelId, arrayList); } + arrayList.add(updates); + } else { + getChannelDifference(channelId); } } } @@ -1309,29 +1489,59 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); } - public void loadUserPhotos(final int uid, final int offset, final int count, final long max_id, final boolean fromCache, final int classGuid) { + public void loadDialogPhotos(final int did, final int offset, final int count, final long max_id, final boolean fromCache, final int classGuid) { if (fromCache) { - MessagesStorage.getInstance().getUserPhotos(uid, offset, count, max_id, classGuid); + MessagesStorage.getInstance().getDialogPhotos(did, offset, count, max_id, classGuid); } else { - TLRPC.User user = getUser(uid); - if (user == null) { - return; - } - TLRPC.TL_photos_getUserPhotos req = new TLRPC.TL_photos_getUserPhotos(); - req.limit = count; - req.offset = offset; - req.max_id = (int) max_id; - req.user_id = getInputUser(user); - int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(TLObject response, TLRPC.TL_error error) { - if (error == null) { - TLRPC.photos_Photos res = (TLRPC.photos_Photos) response; - processLoadedUserPhotos(res, uid, offset, count, max_id, false, classGuid); - } + if (did > 0) { + TLRPC.User user = getUser(did); + if (user == null) { + return; } - }); - ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid); + TLRPC.TL_photos_getUserPhotos req = new TLRPC.TL_photos_getUserPhotos(); + req.limit = count; + req.offset = offset; + req.max_id = (int) max_id; + req.user_id = getInputUser(user); + int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (error == null) { + TLRPC.photos_Photos res = (TLRPC.photos_Photos) response; + processLoadedUserPhotos(res, did, offset, count, max_id, false, classGuid); + } + } + }); + ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid); + } else if (did < 0) { + TLRPC.TL_messages_search req = new TLRPC.TL_messages_search(); + req.filter = new TLRPC.TL_inputMessagesFilterChatPhotos(); + req.limit = count; + req.offset = offset; + req.max_id = (int) max_id; + req.q = ""; + req.peer = MessagesController.getInputPeer(did); + int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (error == null) { + TLRPC.messages_Messages messages = (TLRPC.messages_Messages) response; + TLRPC.TL_photos_photos res = new TLRPC.TL_photos_photos(); + res.count = messages.count; + res.users.addAll(messages.users); + for (int a = 0; a < messages.messages.size(); a++) { + TLRPC.Message message = messages.messages.get(a); + if (message.action == null || message.action.photo == null) { + continue; + } + res.photos.add(message.action.photo); + } + processLoadedUserPhotos(res, did, offset, count, max_id, false, classGuid); + } + } + }); + ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid); + } } } @@ -1341,6 +1551,11 @@ public class MessagesController implements NotificationCenter.NotificationCenter return; } blockedUsers.add(user_id); + if (user.bot) { + SearchQuery.removeInline(user_id); + } else { + SearchQuery.removePeer(user_id); + } NotificationCenter.getInstance().postNotificationName(NotificationCenter.blockedUsersDidLoaded); TLRPC.TL_contacts_block req = new TLRPC.TL_contacts_block(); req.id = getInputUser(user); @@ -1483,19 +1698,19 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } - public void processLoadedUserPhotos(final TLRPC.photos_Photos res, final int uid, final int offset, final int count, final long max_id, final boolean fromCache, final int classGuid) { + public void processLoadedUserPhotos(final TLRPC.photos_Photos res, final int did, final int offset, final int count, final long max_id, final boolean fromCache, final int classGuid) { if (!fromCache) { MessagesStorage.getInstance().putUsersAndChats(res.users, null, true, true); - MessagesStorage.getInstance().putUserPhotos(uid, res); + MessagesStorage.getInstance().putDialogPhotos(did, res); } else if (res == null || res.photos.isEmpty()) { - loadUserPhotos(uid, offset, count, max_id, false, classGuid); + loadDialogPhotos(did, offset, count, max_id, false, classGuid); return; } AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { putUsers(res.users, fromCache); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.userPhotosLoaded, uid, offset, count, fromCache, classGuid, res.photos); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogPhotosLoaded, did, offset, count, fromCache, classGuid, res.photos); } }); } @@ -1592,10 +1807,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); } - public void deleteDialog(final long did, final int onlyHistory) { - deleteDialog(did, true, onlyHistory, 0); - } - public void deleteUserChannelHistory(final TLRPC.Chat chat, final TLRPC.User user, int offset) { if (offset == 0) { MessagesStorage.getInstance().deleteUserChannelHistory(chat.id, user.id); @@ -1617,6 +1828,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); } + public void deleteDialog(final long did, final int onlyHistory) { + deleteDialog(did, true, onlyHistory, 0); + } + private void deleteDialog(final long did, final boolean first, final int onlyHistory, final int max_id) { int lower_part = (int) did; int high_id = (int) (did >> 32); @@ -1626,40 +1841,80 @@ public class MessagesController implements NotificationCenter.NotificationCenter MessagesStorage.getInstance().deleteDialog(did, onlyHistory); return; } + if (onlyHistory == 0 || onlyHistory == 3) { + AndroidUtilities.uninstallShortcut(did); + } if (first) { - TLRPC.Dialog dialog = dialogs_dict.get(did); + MessagesStorage.getInstance().deleteDialog(did, onlyHistory); + TLRPC.TL_dialog dialog = dialogs_dict.get(did); if (dialog != null) { if (max_id_delete == 0) { max_id_delete = Math.max(0, dialog.top_message); } - if (onlyHistory == 0) { + if (onlyHistory == 0 || onlyHistory == 3) { dialogs.remove(dialog); - if (dialogsServerOnly.remove(dialog)) { - if (dialog instanceof TLRPC.TL_dialogChannel) { - Utilities.stageQueue.postRunnable(new Runnable() { - @Override - public void run() { - channelsPts.remove(-(int) did); - shortPollChannels.delete(-(int) did); - needShortPollChannels.delete(-(int) did); - } - }); - } + if (dialogsServerOnly.remove(dialog) && DialogObject.isChannel(dialog)) { + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + channelsPts.remove(-(int) did); + shortPollChannels.delete(-(int) did); + needShortPollChannels.delete(-(int) did); + } + }); } dialogsGroupsOnly.remove(dialog); dialogs_dict.remove(did); dialogs_read_inbox_max.remove(did); + dialogs_read_outbox_max.remove(did); nextDialogsCacheOffset--; } else { dialog.unread_count = 0; } - dialogMessage.remove(dialog.id); - MessageObject object = dialogMessagesByIds.remove(dialog.top_message); + int lastMessageId; + MessageObject object = dialogMessage.remove(dialog.id); + if (object != null) { + lastMessageId = object.getId(); + dialogMessagesByIds.remove(object.getId()); + } else { + lastMessageId = dialog.top_message; + object = dialogMessagesByIds.remove(dialog.top_message); + } if (object != null && object.messageOwner.random_id != 0) { dialogMessagesByRandomIds.remove(object.messageOwner.random_id); } - dialog.top_message = 0; + if (onlyHistory == 1 && lower_part != 0 && lastMessageId > 0) { + TLRPC.TL_messageService message = new TLRPC.TL_messageService(); + message.id = dialog.top_message; + message.out = false; + message.from_id = UserConfig.getClientUserId(); + message.flags |= 256; + message.action = new TLRPC.TL_messageActionHistoryClear(); + message.date = dialog.last_message_date; + if (lower_part > 0) { + message.to_id = new TLRPC.TL_peerUser(); + message.to_id.user_id = lower_part; + } else { + TLRPC.Chat chat = getChat(-lower_part); + if (ChatObject.isChannel(chat)) { + message.to_id = new TLRPC.TL_peerChannel(); + message.to_id.channel_id = -lower_part; + } else { + message.to_id = new TLRPC.TL_peerChat(); + message.to_id.chat_id = -lower_part; + } + } + final MessageObject obj = new MessageObject(message, null, createdDialogIds.contains(message.dialog_id)); + final ArrayList objArr = new ArrayList<>(); + objArr.add(obj); + ArrayList arr = new ArrayList<>(); + arr.add(message); + updateInterfaceWithMessages(did, objArr); + MessagesStorage.getInstance().putMessages(arr, false, true, false, 0); + } else { + dialog.top_message = 0; + } } NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); NotificationCenter.getInstance().postNotificationName(NotificationCenter.removeAllMessagesFromDialog, did, false); @@ -1674,11 +1929,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); } }); - - MessagesStorage.getInstance().deleteDialog(did, onlyHistory); } - if (high_id == 1) { + if (high_id == 1 || onlyHistory == 3) { return; } @@ -1690,6 +1943,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.TL_messages_deleteHistory req = new TLRPC.TL_messages_deleteHistory(); req.peer = peer; req.max_id = max_id_delete; + req.just_clear = onlyHistory != 0; final int max_id_delete_final = max_id_delete; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override @@ -2187,60 +2441,44 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } - public void loadMessages(final long dialog_id, final int count, final int max_id, boolean fromCache, int midDate, final int classGuid, final int load_type, final int last_message_id, final int important, final int loadIndex) { - loadMessages(dialog_id, count, max_id, fromCache, midDate, classGuid, load_type, last_message_id, important, loadIndex, 0, 0, 0, false); + public void loadMessages(final long dialog_id, final int count, final int max_id, boolean fromCache, int midDate, final int classGuid, final int load_type, final int last_message_id, final boolean isChannel, final int loadIndex) { + loadMessages(dialog_id, count, max_id, fromCache, midDate, classGuid, load_type, last_message_id, isChannel, loadIndex, 0, 0, 0, false); } - public void loadMessages(final long dialog_id, final int count, final int max_id, boolean fromCache, int midDate, final int classGuid, final int load_type, final int last_message_id, final int important, final int loadIndex, final int first_unread, final int unread_count, final int last_date, final boolean queryFromServer) { - FileLog.e("tmessages", "load messages in chat " + dialog_id + " count " + count + " max_id " + max_id + " cache " + fromCache + " mindate = " + midDate + " guid " + classGuid + " load_type " + load_type + " last_message_id " + last_message_id + " imp " + important + " index " + loadIndex + " firstUnread " + first_unread + " underad count " + unread_count + " last_date " + last_date + " queryFromServer " + queryFromServer); + public void loadMessages(final long dialog_id, final int count, final int max_id, boolean fromCache, int midDate, final int classGuid, final int load_type, final int last_message_id, final boolean isChannel, final int loadIndex, final int first_unread, final int unread_count, final int last_date, final boolean queryFromServer) { + FileLog.e("tmessages", "load messages in chat " + dialog_id + " count " + count + " max_id " + max_id + " cache " + fromCache + " mindate = " + midDate + " guid " + classGuid + " load_type " + load_type + " last_message_id " + last_message_id + " index " + loadIndex + " firstUnread " + first_unread + " underad count " + unread_count + " last_date " + last_date + " queryFromServer " + queryFromServer); int lower_part = (int) dialog_id; if (fromCache || lower_part == 0) { - MessagesStorage.getInstance().getMessages(dialog_id, count, max_id, midDate, classGuid, load_type, important, loadIndex); + MessagesStorage.getInstance().getMessages(dialog_id, count, max_id, midDate, classGuid, load_type, isChannel, loadIndex); } else { - TLObject request; - if (important == 2) { - TLRPC.TL_channels_getImportantHistory req = new TLRPC.TL_channels_getImportantHistory(); - req.channel = getInputChannel(-lower_part); - if (load_type == 3) { - req.add_offset = -count / 2; - } else if (load_type == 1) { - req.add_offset = -count - 1; - } else if (load_type == 2 && max_id != 0) { - req.add_offset = -count + 6; - } else { - if (max_id != 0) { + TLRPC.TL_messages_getHistory req = new TLRPC.TL_messages_getHistory(); + req.peer = getInputPeer(lower_part); + if (load_type == 3) { + req.add_offset = -count / 2; + } else if (load_type == 1) { + req.add_offset = -count - 1; + } else if (load_type == 2 && max_id != 0) { + req.add_offset = -count + 6; + } else { + if (lower_part < 0 && max_id != 0) { + TLRPC.Chat chat = getChat(-lower_part); + if (ChatObject.isChannel(chat)) { req.add_offset = -1; req.limit += 1; } } - req.limit += count; - req.offset_id = max_id; - request = req; - } else { - TLRPC.TL_messages_getHistory req = new TLRPC.TL_messages_getHistory(); - req.peer = getInputPeer(lower_part); - if (load_type == 3) { - req.add_offset = -count / 2; - } else if (load_type == 1) { - req.add_offset = -count - 1; - } else if (load_type == 2 && max_id != 0) { - req.add_offset = -count + 6; - } else { - req.add_offset = 0; - } - req.limit = count; - req.offset_id = max_id; - request = req; } - int reqId = ConnectionsManager.getInstance().sendRequest(request, new RequestDelegate() { + req.limit = count; + req.offset_id = max_id; + int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { - if (error == null) { + if (response != null) { final TLRPC.messages_Messages res = (TLRPC.messages_Messages) response; if (res.messages.size() > count) { res.messages.remove(0); } - processLoadedMessages(res, dialog_id, count, max_id, false, classGuid, first_unread, last_message_id, unread_count, last_date, load_type, important, false, loadIndex, queryFromServer); + processLoadedMessages(res, dialog_id, count, max_id, false, classGuid, first_unread, last_message_id, unread_count, last_date, load_type, isChannel, false, loadIndex, queryFromServer); } } }); @@ -2291,7 +2529,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } if (!messagesRes.messages.isEmpty()) { - MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, -2, 0, 0, false); + MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, -2, 0, false); NotificationCenter.getInstance().postNotificationName(NotificationCenter.replaceMessagesObjects, dialog_id, arrayList); } } @@ -2302,8 +2540,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter } public void processLoadedMessages(final TLRPC.messages_Messages messagesRes, final long dialog_id, final int count, final int max_id, final boolean isCache, final int classGuid, - final int first_unread, final int last_message_id, final int unread_count, final int last_date, final int load_type, final int important, final boolean isEnd, final int loadIndex, final boolean queryFromServer) { - FileLog.e("tmessages", "processLoadedMessages size " + messagesRes.messages.size() + " in chat " + dialog_id + " count " + count + " max_id " + max_id + " cache " + isCache + " guid " + classGuid + " load_type " + load_type + " last_message_id " + last_message_id + " imp " + important + " index " + loadIndex + " firstUnread " + first_unread + " underad count " + unread_count + " last_date " + last_date + " queryFromServer " + queryFromServer); + final int first_unread, final int last_message_id, final int unread_count, final int last_date, final int load_type, final boolean isChannel, final boolean isEnd, final int loadIndex, final boolean queryFromServer) { + FileLog.e("tmessages", "processLoadedMessages size " + messagesRes.messages.size() + " in chat " + dialog_id + " count " + count + " max_id " + max_id + " cache " + isCache + " guid " + classGuid + " load_type " + load_type + " last_message_id " + last_message_id + " isChannel " + isChannel + " index " + loadIndex + " firstUnread " + first_unread + " underad count " + unread_count + " last_date " + last_date + " queryFromServer " + queryFromServer); Utilities.stageQueue.postRunnable(new Runnable() { @Override public void run() { @@ -2318,7 +2556,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter channelsPts.put(channelId, messagesRes.pts); createDialog = true; if (needShortPollChannels.indexOfKey(channelId) >= 0 && shortPollChannels.indexOfKey(channelId) < 0) { - getChannelDifference(channelId, 2); + getChannelDifference(channelId, 2, 0); } else { getChannelDifference(channelId); } @@ -2341,7 +2579,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - loadMessages(dialog_id, count, load_type == 2 && queryFromServer ? first_unread : max_id, false, 0, classGuid, load_type, last_message_id, important, loadIndex, first_unread, unread_count, last_date, queryFromServer); + loadMessages(dialog_id, count, load_type == 2 && queryFromServer ? first_unread : max_id, false, 0, classGuid, load_type, last_message_id, isChannel, loadIndex, first_unread, unread_count, last_date, queryFromServer); } }); return; @@ -2358,6 +2596,18 @@ public class MessagesController implements NotificationCenter.NotificationCenter } int size = messagesRes.messages.size(); if (!isCache) { + Integer inboxValue = dialogs_read_inbox_max.get(dialog_id); + if (inboxValue == null) { + inboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_inbox_max.put(dialog_id, inboxValue); + } + + Integer outboxValue = dialogs_read_outbox_max.get(dialog_id); + if (outboxValue == null) { + outboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_outbox_max.put(dialog_id, outboxValue); + } + for (int a = 0; a < size; a++) { TLRPC.Message message = messagesRes.messages.get(a); if (!isCache && message.post && !message.out) { @@ -2366,17 +2616,21 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (isMegagroup) { message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; } + if (message.action instanceof TLRPC.TL_messageActionChatDeleteUser) { TLRPC.User user = usersDict.get(message.action.user_id); if (user != null && user.bot) { message.reply_markup = new TLRPC.TL_replyKeyboardHide(); } - } else if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { + } + if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { message.unread = false; message.media_unread = false; + } else { + message.unread = (message.out ? outboxValue : inboxValue) < message.id; } } - MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, load_type, max_id, important, createDialog); + MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, load_type, max_id, createDialog); } final ArrayList objects = new ArrayList<>(); final ArrayList messagesToReload = new ArrayList<>(); @@ -2423,7 +2677,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (first_unread_final == Integer.MAX_VALUE) { first_unread_final = first_unread; } - NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesDidLoaded, dialog_id, count, objects, isCache, first_unread_final, last_message_id, unread_count, last_date, load_type, messagesRes.collapsed, isEnd, classGuid, loadIndex); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesDidLoaded, dialog_id, count, objects, isCache, first_unread_final, last_message_id, unread_count, last_date, load_type, isEnd, classGuid, loadIndex); if (!messagesToReload.isEmpty()) { reloadMessages(messagesToReload, dialog_id); } @@ -2450,13 +2704,13 @@ public class MessagesController implements NotificationCenter.NotificationCenter req.limit = count; boolean found = false; for (int a = dialogs.size() - 1; a >= 0; a--) { - TLRPC.Dialog dialog = dialogs.get(a); + TLRPC.TL_dialog dialog = dialogs.get(a); int lower_id = (int) dialog.id; int high_id = (int) (dialog.id >> 32); if (lower_id != 0 && high_id != 1 && dialog.top_message > 0) { MessageObject message = dialogMessage.get(dialog.id); if (message != null && message.getId() > 0) { - req.offset_date = Math.max(dialog.last_message_date_i, message.messageOwner.date); + req.offset_date = message.messageOwner.date; req.offset_id = message.messageOwner.id; int id; if (message.messageOwner.to_id.channel_id != 0) { @@ -2480,7 +2734,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter public void run(TLObject response, TLRPC.TL_error error) { if (error == null) { final TLRPC.messages_Dialogs dialogsRes = (TLRPC.messages_Dialogs) response; - processLoadedDialogs(dialogsRes, null, 0, count, false, false, false); + processLoadedDialogs(dialogsRes, null, 0, count, 0, false, false); } } }); @@ -2571,9 +2825,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter } StringBuilder dids = new StringBuilder(dialogsRes.dialogs.size() * 12); - HashMap dialogHashMap = new HashMap<>(); + HashMap dialogHashMap = new HashMap<>(); for (int a = 0; a < dialogsRes.dialogs.size(); a++) { - TLRPC.Dialog dialog = dialogsRes.dialogs.get(a); + TLRPC.TL_dialog dialog = dialogsRes.dialogs.get(a); if (dialog.peer.channel_id != 0) { dialog.id = -dialog.peer.channel_id; } else if (dialog.peer.chat_id != 0) { @@ -2590,7 +2844,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT did FROM dialogs WHERE did IN (%s)", dids.toString())); while (cursor.next()) { long did = cursor.longValue(0); - TLRPC.Dialog dialog = dialogHashMap.remove(did); + TLRPC.TL_dialog dialog = dialogHashMap.remove(did); if (dialog != null) { dialogsRes.dialogs.remove(dialog); for (int a = 0; a < dialogsRes.messages.size(); a++) { @@ -2603,10 +2857,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (message.id == dialog.top_message) { dialog.top_message = 0; } - if (message.id == dialog.top_not_important_message) { - dialog.top_not_important_message = 0; - } - if (dialog.top_message == 0 && dialog.top_not_important_message == 0) { + if (dialog.top_message == 0) { break; } } @@ -2622,7 +2873,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter offsetId = -1; dialogsRes.messages.remove(a); a--; - TLRPC.Dialog dialog = dialogHashMap.remove(MessageObject.getDialogId(message)); + TLRPC.TL_dialog dialog = dialogHashMap.remove(MessageObject.getDialogId(message)); if (dialog != null) { dialogsRes.dialogs.remove(dialog); } @@ -2631,7 +2882,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } cursor.dispose(); - processLoadedDialogs(dialogsRes, null, offsetId, 0, false, false, true); + processLoadedDialogs(dialogsRes, null, offsetId, 0, 0, false, true); } catch (Exception e) { FileLog.e("tmessages", e); AndroidUtilities.runOnUIThread(new Runnable() { @@ -2653,15 +2904,14 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } }); - } - public void processLoadedDialogs(final TLRPC.messages_Dialogs dialogsRes, final ArrayList encChats, final int offset, final int count, final boolean isCache, final boolean resetEnd, final boolean migrate) { + public void processLoadedDialogs(final TLRPC.messages_Dialogs dialogsRes, final ArrayList encChats, final int offset, final int count, final int loadType, final boolean resetEnd, final boolean migrate) { Utilities.stageQueue.postRunnable(new Runnable() { @Override public void run() { - FileLog.e("tmessages", "loaded from " + isCache + " count " + dialogsRes.dialogs.size()); - if (isCache && dialogsRes.dialogs.size() == 0) { + FileLog.e("tmessages", "loaded loadType " + loadType + " count " + dialogsRes.dialogs.size()); + if (loadType == 1 && dialogsRes.dialogs.size() == 0) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { @@ -2677,9 +2927,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter return; } - final HashMap new_dialogs_dict = new HashMap<>(); + final HashMap new_dialogs_dict = new HashMap<>(); final HashMap new_dialogMessage = new HashMap<>(); - final HashMap notImportantDates = new HashMap<>(); final HashMap usersDict = new HashMap<>(); final HashMap chatsDict = new HashMap<>(); @@ -2691,18 +2940,15 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.Chat c = dialogsRes.chats.get(a); chatsDict.put(c.id, c); } - if (isCache) { + if (loadType == 1) { nextDialogsCacheOffset = offset + count; } for (int a = 0; a < dialogsRes.messages.size(); a++) { TLRPC.Message message = dialogsRes.messages.get(a); if (message.to_id.channel_id != 0) { - if (!MessageObject.isImportant(message)) { - notImportantDates.put(-message.to_id.channel_id, message.date); - } TLRPC.Chat chat = chatsDict.get(message.to_id.channel_id); - if (chat != null && chat.left/* && !chat.megagroup*/) { + if (chat != null && chat.left) { continue; } if (chat != null && chat.megagroup) { @@ -2714,18 +2960,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter continue; } } - if (!isCache && message.post && !message.out) { + if (loadType != 1 && message.post && !message.out) { message.media_unread = true; } MessageObject messageObject = new MessageObject(message, usersDict, chatsDict, false); - MessageObject currentMessage = new_dialogMessage.get(messageObject.getDialogId()); - if (currentMessage == null || messageObject.isMegagroup() || messageObject.isImportant()) { - new_dialogMessage.put(messageObject.getDialogId(), messageObject); - } + new_dialogMessage.put(messageObject.getDialogId(), messageObject); } + final ArrayList dialogsToReload = new ArrayList<>(); for (int a = 0; a < dialogsRes.dialogs.size(); a++) { - TLRPC.Dialog d = dialogsRes.dialogs.get(a); + TLRPC.TL_dialog d = dialogsRes.dialogs.get(a); if (d.id == 0 && d.peer != null) { if (d.peer.user_id != 0) { d.id = d.peer.user_id; @@ -2744,20 +2988,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter d.last_message_date = mess.messageOwner.date; } } - if (d.last_message_date_i == 0 && d.top_not_important_message != 0) { - Integer date = notImportantDates.get((int) d.id); - if (date != null) { - d.last_message_date_i = date; - } - } - if (d instanceof TLRPC.TL_dialogChannel) { + boolean allowCheck = true; + if (DialogObject.isChannel(d)) { TLRPC.Chat chat = chatsDict.get(-(int) d.id); - if (chat != null && chat.megagroup) { - d.top_message = Math.max(d.top_message, d.top_not_important_message); - d.unread_count = Math.max(d.unread_count, d.unread_not_important_count); - } - if (chat != null && chat.left/* && !chat.megagroup*/) { - continue; + if (chat != null) { + if (!chat.megagroup) { + allowCheck = false; + } + if (chat.left) { + continue; + } } channelsPts.put(-(int) d.id, d.pts); } else if ((int) d.id < 0) { @@ -2768,14 +3008,24 @@ public class MessagesController implements NotificationCenter.NotificationCenter } new_dialogs_dict.put(d.id, d); + if (allowCheck && loadType == 1 && (d.read_outbox_max_id == 0 || d.read_inbox_max_id == 0) && d.top_message != 0) { + dialogsToReload.add(d); + } + Integer value = dialogs_read_inbox_max.get(d.id); if (value == null) { value = 0; } dialogs_read_inbox_max.put(d.id, Math.max(value, d.read_inbox_max_id)); + + value = dialogs_read_outbox_max.get(d.id); + if (value == null) { + value = 0; + } + dialogs_read_outbox_max.put(d.id, Math.max(value, d.read_outbox_max_id)); } - if (!isCache) { + if (loadType != 1) { ImageLoader.saveMessagesThumbs(dialogsRes.messages); for (int a = 0; a < dialogsRes.messages.size(); a++) { @@ -2785,22 +3035,40 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (user != null && user.bot) { message.reply_markup = new TLRPC.TL_replyKeyboardHide(); } - } else if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { + } + + if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { message.unread = false; message.media_unread = false; + } else { + ConcurrentHashMap read_max = message.out ? dialogs_read_outbox_max : dialogs_read_inbox_max; + Integer value = read_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + read_max.put(message.dialog_id, value); + } + message.unread = value < message.id; } } MessagesStorage.getInstance().putDialogs(dialogsRes); } + if (loadType == 2) { + TLRPC.Chat chat = dialogsRes.chats.get(0); + getChannelDifference(chat.id); + checkChannelInviter(chat.id); + } AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - if (!isCache) { + if (loadType != 1) { applyDialogsNotificationsSettings(dialogsRes.dialogs); + if (!UserConfig.draftsLoaded) { + DraftQuery.loadDrafts(); + } } - putUsers(dialogsRes.users, isCache); - putChats(dialogsRes.chats, isCache); + putUsers(dialogsRes.users, loadType == 1); + putChats(dialogsRes.chats, loadType == 1); if (encChats != null) { for (int a = 0; a < encChats.size(); a++) { TLRPC.EncryptedChat encryptedChat = encChats.get(a); @@ -2816,13 +3084,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter boolean added = false; int lastDialogDate = migrate && !dialogs.isEmpty() ? dialogs.get(dialogs.size() - 1).last_message_date : 0; - for (HashMap.Entry pair : new_dialogs_dict.entrySet()) { + for (HashMap.Entry pair : new_dialogs_dict.entrySet()) { Long key = pair.getKey(); - TLRPC.Dialog value = pair.getValue(); + TLRPC.TL_dialog value = pair.getValue(); if (migrate && lastDialogDate != 0 && value.last_message_date < lastDialogDate) { continue; } - TLRPC.Dialog currentDialog = dialogs_dict.get(key); + TLRPC.TL_dialog currentDialog = dialogs_dict.get(key); + if (loadType != 1 && value.draft instanceof TLRPC.TL_draftMessage) { + DraftQuery.saveDraft(value.id, value.draft, null, false); + } if (currentDialog == null) { added = true; dialogs_dict.put(key, value); @@ -2835,7 +3106,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } else { - if (!isCache) { + if (loadType != 1) { currentDialog.notify_settings = value.notify_settings; } MessageObject oldMsg = dialogMessage.get(key); @@ -2878,52 +3149,14 @@ public class MessagesController implements NotificationCenter.NotificationCenter } dialogs.clear(); - dialogsServerOnly.clear(); - dialogsGroupsOnly.clear(); dialogs.addAll(dialogs_dict.values()); - Collections.sort(dialogs, new Comparator() { - @Override - public int compare(TLRPC.Dialog tl_dialog, TLRPC.Dialog tl_dialog2) { - if (tl_dialog.last_message_date == tl_dialog2.last_message_date) { - return 0; - } else if (tl_dialog.last_message_date < tl_dialog2.last_message_date) { - return 1; - } else { - return -1; - } - } - }); - for (int a = 0; a < dialogs.size(); a++) { - TLRPC.Dialog d = dialogs.get(a); + sortDialogs(migrate ? chatsDict : null); - int high_id = (int) (d.id >> 32); - if ((int) d.id != 0 && high_id != 1) { - if (d instanceof TLRPC.TL_dialog) { - if (d.id < 0) { - if (migrate) { - TLRPC.Chat chat = chatsDict.get(-(int) d.id); - if (chat != null && chat.migrated_to != null) { - dialogs.remove(a); - a--; - continue; - } - } - dialogsGroupsOnly.add(d); - } - } else if (d instanceof TLRPC.TL_dialogChannel) { - int lower_id = (int) d.id; - TLRPC.Chat chat = getChat(-lower_id); - if (chat != null && (chat.megagroup && chat.editor || chat.creator)) { - dialogsGroupsOnly.add(d); - } - } - dialogsServerOnly.add(d); + if (loadType != 2) { + if (!migrate) { + dialogsEndReached = (dialogsRes.dialogs.size() == 0 || dialogsRes.dialogs.size() != count) && loadType == 0; } } - - if (!migrate) { - dialogsEndReached = (dialogsRes.dialogs.size() == 0 || dialogsRes.dialogs.size() != count) && !isCache; - } NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); if (migrate) { @@ -2933,11 +3166,14 @@ public class MessagesController implements NotificationCenter.NotificationCenter NotificationCenter.getInstance().postNotificationName(NotificationCenter.needReloadRecentDialogsSearch); } else { generateUpdateMessage(); - if (!added && isCache) { + if (!added && loadType == 1) { loadDialogs(0, count, false); } } migrateDialogs(UserConfig.migrateOffsetId, UserConfig.migrateOffsetDate, UserConfig.migrateOffsetUserId, UserConfig.migrateOffsetChatId, UserConfig.migrateOffsetChannelId, UserConfig.migrateOffsetAccess); + if (!dialogsToReload.isEmpty()) { + reloadDialogsReadValue(dialogsToReload, 0); + } } }); } @@ -2950,7 +3186,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter int currentValue2 = preferences.getInt("notifyuntil_" + dialog_id, 0); SharedPreferences.Editor editor = preferences.edit(); boolean updated = false; - TLRPC.Dialog dialog = dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = notify_settings; } @@ -2994,10 +3230,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } - private void applyDialogsNotificationsSettings(ArrayList dialogs) { + private void applyDialogsNotificationsSettings(ArrayList dialogs) { SharedPreferences.Editor editor = null; for (int a = 0; a < dialogs.size(); a++) { - TLRPC.Dialog dialog = dialogs.get(a); + TLRPC.TL_dialog dialog = dialogs.get(a); if (dialog.peer != null && dialog.notify_settings instanceof TLRPC.TL_peerNotifySettings) { if (editor == null) { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE); @@ -3035,7 +3271,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter @Override public void run() { for (HashMap.Entry entry : dialogsToUpdate.entrySet()) { - TLRPC.Dialog currentDialog = dialogs_dict.get(entry.getKey()); + TLRPC.TL_dialog currentDialog = dialogs_dict.get(entry.getKey()); if (currentDialog != null) { currentDialog.unread_count = entry.getValue(); } @@ -3046,11 +3282,97 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); } + protected void checkLastDialogMessage(final TLRPC.TL_dialog dialog, final TLRPC.InputPeer peer, long taskId) { + final int lower_id = (int) dialog.id; + if (lower_id == 0 || checkingLastMessagesDialogs.containsKey(lower_id)) { + return; + } + TLRPC.TL_messages_getHistory req = new TLRPC.TL_messages_getHistory(); + req.peer = peer == null ? getInputPeer(lower_id) : peer; + if (req.peer == null) { + return; + } + req.limit = 1; + checkingLastMessagesDialogs.put(lower_id, true); + + final long newTaskId; + if (taskId == 0) { + NativeByteBuffer data = null; + try { + data = new NativeByteBuffer(40 + peer.getObjectSize()); + data.writeInt32(2); + data.writeInt64(dialog.id); + data.writeInt32(dialog.top_message); + data.writeInt32(dialog.read_inbox_max_id); + data.writeInt32(dialog.read_outbox_max_id); + data.writeInt32(dialog.unread_count); + data.writeInt32(dialog.last_message_date); + data.writeInt32(dialog.pts); + data.writeInt32(dialog.flags); + peer.serializeToStream(data); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + newTaskId = MessagesStorage.getInstance().createPendingTask(data); + } else { + newTaskId = taskId; + } + + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (response != null) { + TLRPC.messages_Messages res = (TLRPC.messages_Messages) response; + if (!res.messages.isEmpty()) { + TLRPC.TL_messages_dialogs dialogs = new TLRPC.TL_messages_dialogs(); + TLRPC.Message newMessage = res.messages.get(0); + TLRPC.TL_dialog newDialog = new TLRPC.TL_dialog(); + newDialog.flags = dialog.flags; + newDialog.top_message = newMessage.id; + newDialog.last_message_date = newMessage.date; + newDialog.notify_settings = dialog.notify_settings; + newDialog.pts = dialog.pts; + newDialog.unread_count = dialog.unread_count; + newDialog.read_inbox_max_id = dialog.read_inbox_max_id; + newDialog.read_outbox_max_id = dialog.read_outbox_max_id; + newMessage.dialog_id = newDialog.id = dialog.id; + dialogs.users.addAll(res.users); + dialogs.chats.addAll(res.chats); + dialogs.dialogs.add(newDialog); + dialogs.messages.addAll(res.messages); + dialogs.count = 1; + processDialogsUpdate(dialogs, null); + MessagesStorage.getInstance().putMessages(res.messages, true, true, false, MediaController.getInstance().getAutodownloadMask(), true); + } else { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + TLRPC.TL_dialog currentDialog = dialogs_dict.get(dialog.id); + if (currentDialog != null && currentDialog.top_message == 0) { + deleteDialog(dialog.id, 3); + } + } + }); + } + } + if (newTaskId != 0) { + MessagesStorage.getInstance().removePendingTask(newTaskId); + } + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + checkingLastMessagesDialogs.remove(lower_id); + } + }); + } + }); + } + public void processDialogsUpdate(final TLRPC.messages_Dialogs dialogsRes, ArrayList encChats) { Utilities.stageQueue.postRunnable(new Runnable() { @Override public void run() { - final HashMap new_dialogs_dict = new HashMap<>(); + final HashMap new_dialogs_dict = new HashMap<>(); final HashMap new_dialogMessage = new HashMap<>(); final HashMap usersDict = new HashMap<>(); final HashMap chatsDict = new HashMap<>(); @@ -3069,7 +3391,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.Message message = dialogsRes.messages.get(a); if (message.to_id.channel_id != 0) { TLRPC.Chat chat = chatsDict.get(message.to_id.channel_id); - if (chat != null && chat.left/* && !chat.megagroup*/) { + if (chat != null && chat.left) { continue; } } else if (message.to_id.chat_id != 0) { @@ -3082,7 +3404,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter new_dialogMessage.put(messageObject.getDialogId(), messageObject); } for (int a = 0; a < dialogsRes.dialogs.size(); a++) { - TLRPC.Dialog d = dialogsRes.dialogs.get(a); + TLRPC.TL_dialog d = dialogsRes.dialogs.get(a); if (d.id == 0) { if (d.peer.user_id != 0) { d.id = d.peer.user_id; @@ -3092,9 +3414,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter d.id = -d.peer.channel_id; } } - if (d instanceof TLRPC.TL_dialogChannel) { + if (DialogObject.isChannel(d)) { TLRPC.Chat chat = chatsDict.get(-(int) d.id); - if (chat != null && chat.left/* && !chat.megagroup*/) { + if (chat != null && chat.left) { continue; } } else if ((int) d.id < 0) { @@ -3117,6 +3439,12 @@ public class MessagesController implements NotificationCenter.NotificationCenter value = 0; } dialogs_read_inbox_max.put(d.id, Math.max(value, d.read_inbox_max_id)); + + value = dialogs_read_outbox_max.get(d.id); + if (value == null) { + value = 0; + } + dialogs_read_outbox_max.put(d.id, Math.max(value, d.read_outbox_max_id)); } AndroidUtilities.runOnUIThread(new Runnable() { @@ -3125,10 +3453,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter putUsers(dialogsRes.users, true); putChats(dialogsRes.chats, true); - for (HashMap.Entry pair : new_dialogs_dict.entrySet()) { + for (HashMap.Entry pair : new_dialogs_dict.entrySet()) { Long key = pair.getKey(); - TLRPC.Dialog value = pair.getValue(); - TLRPC.Dialog currentDialog = dialogs_dict.get(key); + TLRPC.TL_dialog value = pair.getValue(); + TLRPC.TL_dialog currentDialog = dialogs_dict.get(key); if (currentDialog == null) { nextDialogsCacheOffset++; dialogs_dict.put(key, value); @@ -3160,6 +3488,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter dialogMessagesByRandomIds.remove(oldMsg.messageOwner.random_id); } } + if (messageObject == null) { + checkLastDialogMessage(value, null, 0); + } } } else { MessageObject newMsg = new_dialogMessage.get(value.id); @@ -3182,39 +3513,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter } dialogs.clear(); - dialogsServerOnly.clear(); - dialogsGroupsOnly.clear(); dialogs.addAll(dialogs_dict.values()); - Collections.sort(dialogs, new Comparator() { - @Override - public int compare(TLRPC.Dialog dialog, TLRPC.Dialog dialog2) { - if (dialog.last_message_date == dialog2.last_message_date) { - return 0; - } else if (dialog.last_message_date < dialog2.last_message_date) { - return 1; - } else { - return -1; - } - } - }); - for (int a = 0; a < dialogs.size(); a++) { - TLRPC.Dialog d = dialogs.get(a); - int high_id = (int) (d.id >> 32); - if ((int) d.id != 0 && high_id != 1) { - dialogsServerOnly.add(d); - if (d instanceof TLRPC.TL_dialog) { - if (d.id < 0) { - dialogsGroupsOnly.add(d); - } - } else if (d instanceof TLRPC.TL_dialogChannel) { - int lower_id = (int) d.id; - TLRPC.Chat chat = getChat(-lower_id); - if (chat != null && (chat.megagroup && chat.editor || chat.creator)) { - dialogsGroupsOnly.add(d); - } - } - } - } + sortDialogs(null); NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); NotificationsController.getInstance().processDialogsUpdateRead(dialogsToUpdate); } @@ -3332,14 +3632,14 @@ public class MessagesController implements NotificationCenter.NotificationCenter } dialogs_read_inbox_max.put(dialog_id, Math.max(value, max_positive_id)); - MessagesStorage.getInstance().processPendingRead(dialog_id, messageId, max_date, false); + MessagesStorage.getInstance().processPendingRead(dialog_id, messageId, max_date); MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override public void run() { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - TLRPC.Dialog dialog = dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(dialog_id); if (dialog != null) { dialog.unread_count = 0; NotificationCenter.getInstance().postNotificationName(NotificationCenter.updateInterfaces, UPDATE_MASK_READ_DIALOG_MESSAGE); @@ -3361,12 +3661,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); if (max_positive_id != Integer.MAX_VALUE) { - final long messageIdFinal = messageId; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { if (error == null) { - MessagesStorage.getInstance().processPendingRead(dialog_id, messageIdFinal, max_date, true); if (response instanceof TLRPC.TL_messages_affectedMessages) { TLRPC.TL_messages_affectedMessages res = (TLRPC.TL_messages_affectedMessages) response; processNewDifferenceParams(-1, res.pts, -1, res.pts_count); @@ -3394,7 +3692,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } }); } - MessagesStorage.getInstance().processPendingRead(dialog_id, max_id, max_date, false); + MessagesStorage.getInstance().processPendingRead(dialog_id, max_id, max_date); MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override @@ -3403,7 +3701,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter @Override public void run() { NotificationsController.getInstance().processReadMessages(null, dialog_id, max_date, 0, popup); - TLRPC.Dialog dialog = dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(dialog_id); if (dialog != null) { dialog.unread_count = 0; NotificationCenter.getInstance().postNotificationName(NotificationCenter.updateInterfaces, UPDATE_MASK_READ_DIALOG_MESSAGE); @@ -3673,26 +3971,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter }, ConnectionsManager.RequestFlagInvokeAfter); } - public void toogleChannelComments(int chat_id, boolean enabled) { - TLRPC.TL_channels_toggleComments req = new TLRPC.TL_channels_toggleComments(); - req.channel = getInputChannel(chat_id); - req.enabled = enabled; - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(TLObject response, TLRPC.TL_error error) { - if (response != null) { - processUpdates((TLRPC.Updates) response, false); - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - NotificationCenter.getInstance().postNotificationName(NotificationCenter.updateInterfaces, UPDATE_MASK_CHANNEL); - } - }); - } - } - }, ConnectionsManager.RequestFlagInvokeAfter); - } - public void toogleChannelSignatures(int chat_id, boolean enabled) { TLRPC.TL_channels_toggleSignatures req = new TLRPC.TL_channels_toggleSignatures(); req.channel = getInputChannel(chat_id); @@ -4133,41 +4411,24 @@ public class MessagesController implements NotificationCenter.NotificationCenter ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { - ConnectionsManager.getInstance().cleanUp(); + ConnectionsManager.getInstance().cleanup(); } }); } else { - ConnectionsManager.getInstance().cleanUp(); + ConnectionsManager.getInstance().cleanup(); } UserConfig.clearConfig(); NotificationCenter.getInstance().postNotificationName(NotificationCenter.appDidLogout); - MessagesStorage.getInstance().cleanUp(false); - cleanUp(); + MessagesStorage.getInstance().cleanup(false); + cleanup(); ContactsController.getInstance().deleteAllAppAccounts(); } public void generateUpdateMessage() { - if (UserConfig.lastUpdateVersion == null || UserConfig.lastUpdateVersion.equals(BuildVars.BUILD_VERSION_STRING)) { + if (BuildVars.DEBUG_VERSION || UserConfig.lastUpdateVersion == null || UserConfig.lastUpdateVersion.equals(BuildVars.BUILD_VERSION_STRING)) { return; } TLRPC.TL_help_getAppChangelog req = new TLRPC.TL_help_getAppChangelog(); - req.app_version = BuildVars.BUILD_VERSION_STRING; - try { - req.lang_code = LocaleController.getLocaleStringIso639(); - req.device_model = Build.MANUFACTURER + Build.MODEL; - req.system_version = "SDK " + Build.VERSION.SDK_INT; - } catch (Exception e) { - FileLog.e("tmessages", e); - } - if (req.lang_code == null || req.lang_code.trim().length() == 0) { - req.lang_code = "en"; - } - if (req.device_model == null || req.device_model.trim().length() == 0) { - req.device_model = "Android unknown"; - } - if (req.system_version == null || req.system_version.trim().length() == 0) { - req.system_version = "SDK Unknown"; - } ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { @@ -4183,7 +4444,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter update.popup = false; ArrayList updates = new ArrayList<>(); updates.add(update); - processUpdateArray(updates, null, null); + processUpdateArray(updates, null, null, false); } } }); @@ -4200,37 +4461,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.TL_account_registerDevice req = new TLRPC.TL_account_registerDevice(); req.token_type = 2; req.token = regid; - req.app_sandbox = false; - try { - req.lang_code = LocaleController.getLocaleStringIso639(); - if (req.lang_code.length() == 0) { - req.lang_code = "en"; - } - req.device_model = Build.MANUFACTURER + Build.MODEL; - req.system_version = "SDK " + Build.VERSION.SDK_INT; - PackageInfo pInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0); - req.app_version = pInfo.versionName + " (" + pInfo.versionCode + ")"; - } catch (Exception e) { - FileLog.e("tmessages", e); - req.lang_code = "en"; - req.device_model = "Android unknown"; - req.system_version = "SDK " + Build.VERSION.SDK_INT; - req.app_version = "App version unknown"; - } - - if (req.lang_code == null || req.lang_code.trim().length() == 0) { - req.lang_code = "en"; - } - if (req.device_model == null || req.device_model.trim().length() == 0) { - req.device_model = "Android unknown"; - } - if (req.app_version == null || req.app_version.trim().length() == 0) { - req.app_version = "App version unknown"; - } - if (req.system_version == null || req.system_version.trim().length() == 0) { - req.system_version = "SDK Unknown"; - } - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { @@ -4470,6 +4700,53 @@ public class MessagesController implements NotificationCenter.NotificationCenter setUpdatesStartTime(type, 0); } + protected void loadUnknownChannel(final TLRPC.Chat channel, final long taskId) { + if (!(channel instanceof TLRPC.TL_channel) || gettingUnknownChannels.containsKey(channel.id)) { + return; + } + gettingUnknownChannels.put(channel.id, true); + TLRPC.TL_inputPeerChannel inputPeer = new TLRPC.TL_inputPeerChannel(); + inputPeer.channel_id = channel.id; + inputPeer.access_hash = channel.access_hash; + TLRPC.TL_messages_getPeerDialogs req = new TLRPC.TL_messages_getPeerDialogs(); + req.peers.add(inputPeer); + final long newTaskId; + if (taskId == 0) { + NativeByteBuffer data = null; + try { + data = new NativeByteBuffer(4 + channel.getObjectSize()); + data.writeInt32(0); + channel.serializeToStream(data); + + } catch (Exception e) { + FileLog.e("tmessages", e); + } + newTaskId = MessagesStorage.getInstance().createPendingTask(data); + } else { + newTaskId = taskId; + } + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (response != null) { + TLRPC.TL_messages_peerDialogs res = (TLRPC.TL_messages_peerDialogs) response; + if (!res.dialogs.isEmpty() && !res.chats.isEmpty()) { + TLRPC.TL_messages_dialogs dialogs = new TLRPC.TL_messages_dialogs(); + dialogs.dialogs.addAll(res.dialogs); + dialogs.messages.addAll(res.messages); + dialogs.users.addAll(res.users); + dialogs.chats.addAll(res.chats); + processLoadedDialogs(dialogs, null, 0, 1, 2, false, false); + } + } + if (newTaskId != 0) { + MessagesStorage.getInstance().removePendingTask(newTaskId); + } + gettingUnknownChannels.remove(channel.id); + } + }); + } + public void startShortPoll(final int channelId, final boolean stop) { Utilities.stageQueue.postRunnable(new Runnable() { @Override @@ -4479,18 +4756,18 @@ public class MessagesController implements NotificationCenter.NotificationCenter } else { needShortPollChannels.put(channelId, 0); if (shortPollChannels.indexOfKey(channelId) < 0) { - getChannelDifference(channelId, 2); + getChannelDifference(channelId, 2, 0); } } } }); } - public void getChannelDifference(final int channelId) { - getChannelDifference(channelId, 0); + private void getChannelDifference(final int channelId) { + getChannelDifference(channelId, 0, 0); } - public void getChannelDifference(final int channelId, final int newDialogType) { + protected void getChannelDifference(final int channelId, final int newDialogType, final long taskId) { Boolean gettingDifferenceChannel = gettingDifferenceChannels.get(channelId); if (gettingDifferenceChannel == null) { gettingDifferenceChannel = false; @@ -4522,6 +4799,23 @@ public class MessagesController implements NotificationCenter.NotificationCenter return; } } + + final long newTaskId; + if (taskId == 0) { + NativeByteBuffer data = null; + try { + data = new NativeByteBuffer(12); + data.writeInt32(1); + data.writeInt32(channelId); + data.writeInt32(newDialogType); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + newTaskId = MessagesStorage.getInstance().createPendingTask(data); + } else { + newTaskId = taskId; + } + gettingDifferenceChannels.put(channelId, true); TLRPC.TL_updates_getChannelDifference req = new TLRPC.TL_updates_getChannelDifference(); req.channel = getInputChannel(channelId); @@ -4532,7 +4826,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, final TLRPC.TL_error error) { - if (error == null) { final TLRPC.updates_ChannelDifference res = (TLRPC.updates_ChannelDifference) response; @@ -4609,35 +4902,28 @@ public class MessagesController implements NotificationCenter.NotificationCenter ImageLoader.saveMessagesThumbs(res.new_messages); final ArrayList pushMessages = new ArrayList<>(); + long dialog_id = -channelId; + Integer inboxValue = dialogs_read_inbox_max.get(dialog_id); + if (inboxValue == null) { + inboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_inbox_max.put(dialog_id, inboxValue); + } + + Integer outboxValue = dialogs_read_outbox_max.get(dialog_id); + if (outboxValue == null) { + outboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_outbox_max.put(dialog_id, outboxValue); + } + for (int a = 0; a < res.new_messages.size(); a++) { TLRPC.Message message = res.new_messages.get(a); - if (!message.out) { - message.unread = true; - if (message.post || channelFinal != null && channelFinal.megagroup) { - message.media_unread = true; - } - } - if (message.action instanceof TLRPC.TL_messageActionChannelCreate) { - message.unread = false; - message.media_unread = false; - } + message.unread = !(channelFinal != null && channelFinal.left || (message.out ? outboxValue : inboxValue) >= message.id || message.action instanceof TLRPC.TL_messageActionChannelCreate); if (channelFinal != null && channelFinal.megagroup) { message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; } - long dialog_id = -channelId; - Integer value = dialogs_read_inbox_max.get(dialog_id); - if (value == null) { - value = MessagesStorage.getInstance().getChannelReadInboxMax(channelId); - } - MessageObject obj = new MessageObject(message, usersDict, createdDialogIds.contains(dialog_id)); - if (channelFinal != null && channelFinal.left || value >= obj.getId()) { - obj.setIsRead(); - obj.setContentIsRead(); - } - - if (!obj.isOut() && obj.isContentUnread()) { + if (!obj.isOut() && obj.isUnread()) { pushMessages.add(obj); } @@ -4678,39 +4964,32 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (!res.other_updates.isEmpty()) { - processUpdateArray(res.other_updates, res.users, res.chats); + processUpdateArray(res.other_updates, res.users, res.chats, true); } processChannelsUpdatesQueue(channelId, 1); MessagesStorage.getInstance().saveChannelPts(channelId, res.pts); } else if (res instanceof TLRPC.TL_updates_channelDifferenceTooLong) { + long dialog_id = -channelId; + + Integer inboxValue = dialogs_read_inbox_max.get(dialog_id); + if (inboxValue == null) { + inboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_inbox_max.put(dialog_id, inboxValue); + } + + Integer outboxValue = dialogs_read_outbox_max.get(dialog_id); + if (outboxValue == null) { + outboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_outbox_max.put(dialog_id, outboxValue); + } + for (int a = 0; a < res.messages.size(); a++) { TLRPC.Message message = res.messages.get(a); message.dialog_id = -channelId; - if (!message.out) { - message.unread = true; - if (message.post || channelFinal != null && channelFinal.megagroup) { - message.media_unread = true; - } - } - if (message.action instanceof TLRPC.TL_messageActionChannelCreate) { - message.unread = false; - message.media_unread = false; - } + message.unread = !(message.action instanceof TLRPC.TL_messageActionChannelCreate || channelFinal != null && channelFinal.left || (message.out ? outboxValue : inboxValue) >= message.id); if (channelFinal != null && channelFinal.megagroup) { message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; } - - if (channelFinal != null && channelFinal.left || res.read_inbox_max_id >= message.id) { - message.unread = false; - message.media_unread = false; - } - if (channelFinal != null && channelFinal.megagroup) { - message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; - } - } - if (channelFinal != null && channelFinal.megagroup) { - res.unread_important_count = Math.max(res.unread_count, res.unread_important_count); - res.top_important_message = Math.max(res.top_important_message, res.top_message); } MessagesStorage.getInstance().overwriteChannel(channelId, (TLRPC.TL_updates_channelDifferenceTooLong) res, newDialogType); } @@ -4724,7 +5003,11 @@ public class MessagesController implements NotificationCenter.NotificationCenter getChannelDifference(channelId); } FileLog.e("tmessages", "received channel difference with pts = " + res.pts + " channelId = " + channelId); - FileLog.e("tmessages", "messages = " + res.new_messages.size() + " users = " + res.users.size() + " chats = " + res.chats.size() + " other updates = " + res.other_updates.size()); + FileLog.e("tmessages", "new_messages = " + res.new_messages.size() + " messages = " + res.messages.size() + " users = " + res.users.size() + " chats = " + res.chats.size() + " other updates = " + res.other_updates.size()); + + if (newTaskId != 0) { + MessagesStorage.getInstance().removePendingTask(newTaskId); + } } }); } @@ -4737,6 +5020,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter } }); gettingDifferenceChannels.remove(channelId); + if (newTaskId != 0) { + MessagesStorage.getInstance().removePendingTask(newTaskId); + } } } }); @@ -4878,35 +5164,42 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (user != null && user.bot) { message.reply_markup = new TLRPC.TL_replyKeyboardHide(); } - } else if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { - message.unread = false; - message.media_unread = false; } - long uid; - if (message.dialog_id != 0) { - uid = message.dialog_id; - } else { + if (message.dialog_id == 0) { if (message.to_id.chat_id != 0) { - uid = -message.to_id.chat_id; + message.dialog_id = -message.to_id.chat_id; } else { if (message.to_id.user_id == UserConfig.getClientUserId()) { message.to_id.user_id = message.from_id; } - uid = message.to_id.user_id; + message.dialog_id = message.to_id.user_id; } } - MessageObject obj = new MessageObject(message, usersDict, chatsDict, createdDialogIds.contains(uid)); + if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { + message.unread = false; + message.media_unread = false; + } else { + ConcurrentHashMap read_max = message.out ? dialogs_read_outbox_max : dialogs_read_inbox_max; + Integer value = read_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + read_max.put(message.dialog_id, value); + } + message.unread = value < message.id; + } + + MessageObject obj = new MessageObject(message, usersDict, chatsDict, createdDialogIds.contains(message.dialog_id)); if (!obj.isOut() && obj.isUnread()) { pushMessages.add(obj); } - ArrayList arr = messages.get(uid); + ArrayList arr = messages.get(message.dialog_id); if (arr == null) { arr = new ArrayList<>(); - messages.put(uid, arr); + messages.put(message.dialog_id, arr); } arr.add(obj); } @@ -4941,7 +5234,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (!res.other_updates.isEmpty()) { - processUpdateArray(res.other_updates, res.users, res.chats); + processUpdateArray(res.other_updates, res.users, res.chats, true); } if (res instanceof TLRPC.TL_updates_difference) { @@ -5135,6 +5428,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } + private int getUpdateChannelId(TLRPC.Update update) { + if (update instanceof TLRPC.TL_updateNewChannelMessage) { + return ((TLRPC.TL_updateNewChannelMessage) update).message.to_id.channel_id; + } else if (update instanceof TLRPC.TL_updateEditChannelMessage) { + return ((TLRPC.TL_updateEditChannelMessage) update).message.to_id.channel_id; + } else { + return update.channel_id; + } + } + public void processUpdates(final TLRPC.Updates updates, boolean fromQueue) { ArrayList needGetChannelsDiff = null; boolean needGetDiff = false; @@ -5143,7 +5446,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (updates instanceof TLRPC.TL_updateShort) { ArrayList arr = new ArrayList<>(); arr.add(updates.update); - processUpdateArray(arr, null, null); + processUpdateArray(arr, null, null, false); } else if (updates instanceof TLRPC.TL_updateShortChatMessage || updates instanceof TLRPC.TL_updateShortMessage) { final int user_id = updates instanceof TLRPC.TL_updateShortChatMessage ? updates.from_id : updates.user_id; TLRPC.User user = getUser(user_id); @@ -5151,7 +5454,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.User user3 = null; TLRPC.Chat channel = null; - if (user == null || user.min) { //TODO + if (user == null || user.min) { user = MessagesStorage.getInstance().getUserSync(user_id); if (user != null && user.min) { user = null; @@ -5200,6 +5503,26 @@ public class MessagesController implements NotificationCenter.NotificationCenter } missingData = chat == null || user == null || needFwdUser && user2 == null && channel == null || needBotUser && user3 == null; } + if (!missingData && !updates.entities.isEmpty()) { + for (int a = 0; a < updates.entities.size(); a++) { + TLRPC.MessageEntity entity = updates.entities.get(a); + if (entity instanceof TLRPC.TL_messageEntityMentionName) { + int uid = ((TLRPC.TL_messageEntityMentionName) entity).user_id; + TLRPC.User entityUser = getUser(uid); + if (entityUser == null || entityUser.min) { + entityUser = MessagesStorage.getInstance().getUserSync(uid); + if (entityUser != null && entityUser.min) { + entityUser = null; + } + if (entityUser == null) { + missingData = true; + break; + } + putUser(user, true); + } + } + } + } if (user != null && user.status != null && user.status.expires <= 0) { onlinePrivacy.put(user.id, ConnectionsManager.getInstance().getCurrentTime()); updateStatus = true; @@ -5226,10 +5549,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter message.to_id.chat_id = updates.chat_id; message.dialog_id = -updates.chat_id; } + message.fwd_from = updates.fwd_from; message.silent = updates.silent; message.out = updates.out; - message.unread = updates.unread; message.mentioned = updates.mentioned; message.media_unread = updates.media_unread; message.entities = updates.entities; @@ -5239,6 +5562,15 @@ public class MessagesController implements NotificationCenter.NotificationCenter message.flags = updates.flags | TLRPC.MESSAGE_FLAG_HAS_FROM_ID; message.reply_to_msg_id = updates.reply_to_msg_id; message.media = new TLRPC.TL_messageMediaEmpty(); + + ConcurrentHashMap read_max = message.out ? dialogs_read_outbox_max : dialogs_read_inbox_max; + Integer value = read_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + read_max.put(message.dialog_id, value); + } + message.unread = value < message.id; + MessagesStorage.lastPtsValue = updates.pts; final MessageObject obj = new MessageObject(message, null, createdDialogIds.contains(message.dialog_id)); final ArrayList objArr = new ArrayList<>(); @@ -5277,20 +5609,19 @@ public class MessagesController implements NotificationCenter.NotificationCenter } }); } - - MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { - @Override - public void run() { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - if (!obj.isOut()) { + if (!obj.isOut()) { + MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { + @Override + public void run() { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { NotificationsController.getInstance().processNewMessages(objArr, true); } - } - }); - } - }); + }); + } + }); + } MessagesStorage.getInstance().putMessages(arr, false, true, false, 0); } else if (MessagesStorage.lastPtsValue != updates.pts) { FileLog.e("tmessages", "need get diff short message, pts: " + MessagesStorage.lastPtsValue + " " + updates.pts + " count = " + updates.pts_count); @@ -5309,20 +5640,22 @@ public class MessagesController implements NotificationCenter.NotificationCenter HashMap minChannels = null; for (int a = 0; a < updates.chats.size(); a++) { TLRPC.Chat chat = updates.chats.get(a); - if (chat instanceof TLRPC.TL_channel && chat.min) { - TLRPC.Chat existChat = getChat(chat.id); - if (existChat == null || existChat.min) { - TLRPC.Chat cacheChat = MessagesStorage.getInstance().getChatSync(updates.chat_id); - if (existChat == null) { - putChat(cacheChat, true); + if (chat instanceof TLRPC.TL_channel) { + if (chat.min) { + TLRPC.Chat existChat = getChat(chat.id); + if (existChat == null || existChat.min) { + TLRPC.Chat cacheChat = MessagesStorage.getInstance().getChatSync(updates.chat_id); + if (existChat == null) { + putChat(cacheChat, true); + } + existChat = cacheChat; } - existChat = cacheChat; - } - if (existChat == null || existChat.min) { - if (minChannels == null) { - minChannels = new HashMap<>(); + if (existChat == null || existChat.min) { + if (minChannels == null) { + minChannels = new HashMap<>(); + } + minChannels.put(chat.id, chat); } - minChannels.put(chat.id, chat); } } } @@ -5341,21 +5674,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (!needGetDiff) { MessagesStorage.getInstance().putUsersAndChats(updates.users, updates.chats, true, true); - Collections.sort(updates.updates, new Comparator() { - @Override - public int compare(TLRPC.Update lhs, TLRPC.Update rhs) { - int ltype = getUpdateType(lhs); - int rtype = getUpdateType(rhs); - if (ltype != rtype) { - return AndroidUtilities.compare(ltype, rtype); - } else if (ltype == 0 || ltype == 2) { - return AndroidUtilities.compare(lhs.pts, rhs.pts); - } else if (ltype == 1) { - return AndroidUtilities.compare(lhs.qts, rhs.qts); - } - return 0; - } - }); + Collections.sort(updates.updates, updatesComparator); for (int a = 0; a < updates.updates.size(); a++) { TLRPC.Update update = updates.updates.get(a); if (getUpdateType(update) == 0) { @@ -5376,7 +5695,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } if (MessagesStorage.lastPtsValue + updatesNew.pts_count == updatesNew.pts) { - if (!processUpdateArray(updatesNew.updates, updates.users, updates.chats)) { + if (!processUpdateArray(updatesNew.updates, updates.users, updates.chats, false)) { FileLog.e("tmessages", "need get diff inner TL_updates, seq: " + MessagesStorage.lastSeqValue + " " + updates.seq); needGetDiff = true; } else { @@ -5410,7 +5729,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } if (MessagesStorage.lastQtsValue == 0 || MessagesStorage.lastQtsValue + updatesNew.updates.size() == updatesNew.pts) { - processUpdateArray(updatesNew.updates, updates.users, updates.chats); + processUpdateArray(updatesNew.updates, updates.users, updates.chats, false); MessagesStorage.lastQtsValue = updatesNew.pts; needReceivedQueue = true; } else if (MessagesStorage.lastPtsValue != updatesNew.pts) { @@ -5426,21 +5745,23 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } else if (getUpdateType(update) == 2) { - int channelId; - if (update instanceof TLRPC.TL_updateNewChannelMessage) { - channelId = ((TLRPC.TL_updateNewChannelMessage) update).message.to_id.channel_id; - } else if (update instanceof TLRPC.TL_updateEditChannelMessage) { - channelId = ((TLRPC.TL_updateEditChannelMessage) update).message.to_id.channel_id; - } else { - channelId = update.channel_id; - } + int channelId = getUpdateChannelId(update); + boolean skipUpdate = false; Integer channelPts = channelsPts.get(channelId); if (channelPts == null) { channelPts = MessagesStorage.getInstance().getChannelPtsSync(channelId); if (channelPts == 0) { - channelPts = update.pts - update.pts_count; + for (int c = 0; c < updates.chats.size(); c++) { + TLRPC.Chat chat = updates.chats.get(c); + if (chat.id == channelId) { + loadUnknownChannel(chat, 0); + skipUpdate = true; + break; + } + } + } else { + channelsPts.put(channelId, channelPts); } - channelsPts.put(channelId, channelPts); } TLRPC.TL_updates updatesNew = new TLRPC.TL_updates(); updatesNew.updates.add(update); @@ -5448,7 +5769,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter updatesNew.pts_count = update.pts_count; for (int b = a + 1; b < updates.updates.size(); b++) { TLRPC.Update update2 = updates.updates.get(b); - if (getUpdateType(update2) == 2 && updatesNew.pts + update2.pts_count == update2.pts) { + if (getUpdateType(update2) == 2 && channelId == getUpdateChannelId(update2) && updatesNew.pts + update2.pts_count == update2.pts) { updatesNew.updates.add(update2); updatesNew.pts = update2.pts; updatesNew.pts_count += update2.pts_count; @@ -5458,43 +5779,47 @@ public class MessagesController implements NotificationCenter.NotificationCenter break; } } - if (channelPts + updatesNew.pts_count == updatesNew.pts) { - if (!processUpdateArray(updatesNew.updates, updates.users, updates.chats)) { - FileLog.e("tmessages", "need get channel diff inner TL_updates, channel_id = " + channelId); - if (needGetChannelsDiff == null) { - needGetChannelsDiff = new ArrayList<>(); - } else if (!needGetChannelsDiff.contains(channelId)) { - needGetChannelsDiff.add(channelId); + if (!skipUpdate) { + if (channelPts + updatesNew.pts_count == updatesNew.pts) { + if (!processUpdateArray(updatesNew.updates, updates.users, updates.chats, false)) { + FileLog.e("tmessages", "need get channel diff inner TL_updates, channel_id = " + channelId); + if (needGetChannelsDiff == null) { + needGetChannelsDiff = new ArrayList<>(); + } else if (!needGetChannelsDiff.contains(channelId)) { + needGetChannelsDiff.add(channelId); + } + } else { + channelsPts.put(channelId, updatesNew.pts); + MessagesStorage.getInstance().saveChannelPts(channelId, updatesNew.pts); } - } else { - channelsPts.put(channelId, updatesNew.pts); - MessagesStorage.getInstance().saveChannelPts(channelId, updatesNew.pts); - } - } else if (channelPts != updatesNew.pts) { - FileLog.e("tmessages", update + " need get channel diff, pts: " + channelPts + " " + updatesNew.pts + " count = " + updatesNew.pts_count + " channelId = " + channelId); - Long updatesStartWaitTime = updatesStartWaitTimeChannels.get(channelId); - Boolean gettingDifferenceChannel = gettingDifferenceChannels.get(channelId); - if (gettingDifferenceChannel == null) { - gettingDifferenceChannel = false; - } - if (gettingDifferenceChannel || updatesStartWaitTime == null || Math.abs(System.currentTimeMillis() - updatesStartWaitTime) <= 1500) { - if (updatesStartWaitTime == null) { - updatesStartWaitTimeChannels.put(channelId, System.currentTimeMillis()); + } else if (channelPts != updatesNew.pts) { + FileLog.e("tmessages", update + " need get channel diff, pts: " + channelPts + " " + updatesNew.pts + " count = " + updatesNew.pts_count + " channelId = " + channelId); + Long updatesStartWaitTime = updatesStartWaitTimeChannels.get(channelId); + Boolean gettingDifferenceChannel = gettingDifferenceChannels.get(channelId); + if (gettingDifferenceChannel == null) { + gettingDifferenceChannel = false; } - FileLog.e("tmessages", "add to queue"); - ArrayList arrayList = updatesQueueChannels.get(channelId); - if (arrayList == null) { - arrayList = new ArrayList<>(); - updatesQueueChannels.put(channelId, arrayList); - } - arrayList.add(updatesNew); - } else { - if (needGetChannelsDiff == null) { - needGetChannelsDiff = new ArrayList<>(); - } else if (!needGetChannelsDiff.contains(channelId)) { - needGetChannelsDiff.add(channelId); + if (gettingDifferenceChannel || updatesStartWaitTime == null || Math.abs(System.currentTimeMillis() - updatesStartWaitTime) <= 1500) { + if (updatesStartWaitTime == null) { + updatesStartWaitTimeChannels.put(channelId, System.currentTimeMillis()); + } + FileLog.e("tmessages", "add to queue"); + ArrayList arrayList = updatesQueueChannels.get(channelId); + if (arrayList == null) { + arrayList = new ArrayList<>(); + updatesQueueChannels.put(channelId, arrayList); + } + arrayList.add(updatesNew); + } else { + if (needGetChannelsDiff == null) { + needGetChannelsDiff = new ArrayList<>(); + } else if (!needGetChannelsDiff.contains(channelId)) { + needGetChannelsDiff.add(channelId); + } } } + } else { + FileLog.e("tmessages", "need load unknown channel = " + channelId); } } else { break; @@ -5510,7 +5835,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter processUpdate = MessagesStorage.lastSeqValue + 1 == updates.seq || updates.seq == 0 || updates.seq == MessagesStorage.lastSeqValue; } if (processUpdate) { - processUpdateArray(updates.updates, updates.users, updates.chats); + processUpdateArray(updates.updates, updates.users, updates.chats, false); if (updates.date != 0) { MessagesStorage.lastDateValue = updates.date; } @@ -5588,7 +5913,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter MessagesStorage.getInstance().saveDiffParams(MessagesStorage.lastSeqValue, MessagesStorage.lastPtsValue, MessagesStorage.lastDateValue, MessagesStorage.lastQtsValue); } - public boolean processUpdateArray(ArrayList updates, final ArrayList usersArr, final ArrayList chatsArr) { + public boolean processUpdateArray(ArrayList updates, final ArrayList usersArr, final ArrayList chatsArr, boolean fromGetDifference) { if (updates.isEmpty()) { if (usersArr != null || chatsArr != null) { AndroidUtilities.runOnUIThread(new Runnable() { @@ -5609,9 +5934,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter final ArrayList messagesArr = new ArrayList<>(); final HashMap> editingMessages = new HashMap<>(); final SparseArray channelViews = new SparseArray<>(); - final SparseArray> channelsGroups = new SparseArray<>(); final SparseArray markAsReadMessagesInbox = new SparseArray<>(); - final SparseIntArray markAsReadMessagesOutbox = new SparseIntArray(); + final SparseArray markAsReadMessagesOutbox = new SparseArray<>(); final ArrayList markAsReadMessages = new ArrayList<>(); final HashMap markAsReadEncrypted = new HashMap<>(); final SparseArray> deletedMessages = new SparseArray<>(); @@ -5644,6 +5968,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter checkForUsers = false; chatsDict = chats; } + if (fromGetDifference) { + checkForUsers = false; + } if (usersArr != null || chatsArr != null) { AndroidUtilities.runOnUIThread(new Runnable() { @@ -5665,7 +5992,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter message = ((TLRPC.TL_updateNewMessage) update).message; } else { message = ((TLRPC.TL_updateNewChannelMessage) update).message; - if (!message.out && message.from_id == UserConfig.getClientUserId()) { //TODO remove later + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + message.to_id.channel_id); + } + if (!message.out && message.from_id == UserConfig.getClientUserId()) { message.out = true; } } @@ -5690,6 +6020,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter putChat(chat, true); } if (chat == null) { + FileLog.d("tmessages", "not found chat " + chat_id); return false; } if (chat.megagroup) { @@ -5697,23 +6028,36 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } - for (int a = 0; a < 3; a++) { + int count = 3 + message.entities.size(); + for (int a = 0; a < count; a++) { + boolean allowMin = false; if (a != 0) { - user_id = a == 1 ? message.from_id : (message.fwd_from != null ? message.fwd_from.from_id : 0); + if (a == 1) { + user_id = message.from_id; + if (message.post) { + allowMin = true; + } + } else if (a == 2) { + user_id = message.fwd_from != null ? message.fwd_from.from_id : 0; + } else { + TLRPC.MessageEntity entity = message.entities.get(a - 3); + user_id = entity instanceof TLRPC.TL_messageEntityMentionName ? ((TLRPC.TL_messageEntityMentionName) entity).user_id : 0; + } } if (user_id > 0) { TLRPC.User user = usersDict.get(user_id); - if (user == null || user.min) { + if (user == null || !allowMin && user.min) { user = getUser(user_id); } - if (user == null || user.min) { + if (user == null || !allowMin && user.min) { user = MessagesStorage.getInstance().getUserSync(user_id); - if (user != null && user.min) { + if (user != null && !allowMin && user.min) { user = null; } putUser(user, true); } if (user == null) { + FileLog.d("tmessages", "not found user " + user_id); return false; } if (a == 1 && user.status != null && user.status.expires <= 0) { @@ -5723,6 +6067,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } + if (message.action instanceof TLRPC.TL_messageActionChatDeleteUser) { TLRPC.User user = usersDict.get(message.action.user_id); if (user != null && user.bot) { @@ -5730,28 +6075,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter } else if (message.from_id == UserConfig.getClientUserId() && message.action.user_id == UserConfig.getClientUserId()) { continue; } - } else if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { - message.unread = false; - message.media_unread = false; } - if (update instanceof TLRPC.TL_updateNewChannelMessage) { - if (message.to_id.channel_id != 0 && !message.out) { - message.unread = true; - if (message.post || (message.flags & TLRPC.MESSAGE_FLAG_MEGAGROUP) != 0) { - message.media_unread = true; - } - } - long dialog_id = -update.channel_id; - Integer value = dialogs_read_inbox_max.get(dialog_id); - if (value == null) { - value = MessagesStorage.getInstance().getChannelReadInboxMax(update.channel_id); - } - if (value >= message.id || ChatObject.isNotInChat(chat)) { - message.unread = false; - message.media_unread = false; - } - } messagesArr.add(message); ImageLoader.saveMessageThumbs(message); if (message.to_id.chat_id != 0) { @@ -5764,6 +6089,15 @@ public class MessagesController implements NotificationCenter.NotificationCenter } message.dialog_id = message.to_id.user_id; } + + ConcurrentHashMap read_max = message.out ? dialogs_read_outbox_max : dialogs_read_inbox_max; + Integer value = read_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + read_max.put(message.dialog_id, value); + } + message.unread = !(value >= message.id || chat != null && ChatObject.isNotInChat(chat) || message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate); + MessageObject obj = new MessageObject(message, usersDict, chatsDict, createdDialogIds.contains(message.dialog_id)); if (obj.type == 11) { interfaceUpdateMask |= UPDATE_MASK_CHAT_AVATAR; @@ -5776,7 +6110,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter messages.put(message.dialog_id, arr); } arr.add(obj); - if (!obj.isOut() && (obj.isUnread() && message.to_id.channel_id == 0 || obj.isContentUnread())) { + if (!obj.isOut() && obj.isUnread()) { pushMessages.add(obj); } } else if (update instanceof TLRPC.TL_updateReadMessagesContents) { @@ -5784,20 +6118,35 @@ public class MessagesController implements NotificationCenter.NotificationCenter long id = update.messages.get(a); markAsReadMessages.add(id); } - } else if (update instanceof TLRPC.TL_updateReadHistoryInbox) { - TLRPC.Peer peer = ((TLRPC.TL_updateReadHistoryInbox) update).peer; - if (peer.chat_id != 0) { - markAsReadMessagesInbox.put(-peer.chat_id, (long) update.max_id); + } else if (update instanceof TLRPC.TL_updateReadHistoryInbox || update instanceof TLRPC.TL_updateReadHistoryOutbox) { + long dialog_id; + ConcurrentHashMap read_max; + if (update instanceof TLRPC.TL_updateReadHistoryInbox) { + TLRPC.Peer peer = ((TLRPC.TL_updateReadHistoryInbox) update).peer; + if (peer.chat_id != 0) { + markAsReadMessagesInbox.put(-peer.chat_id, (long) update.max_id); + dialog_id = -peer.chat_id; + } else { + markAsReadMessagesInbox.put(peer.user_id, (long) update.max_id); + dialog_id = peer.user_id; + } + read_max = dialogs_read_inbox_max; } else { - markAsReadMessagesInbox.put(peer.user_id, (long) update.max_id); + TLRPC.Peer peer = ((TLRPC.TL_updateReadHistoryOutbox) update).peer; + if (peer.chat_id != 0) { + markAsReadMessagesOutbox.put(-peer.chat_id, (long) update.max_id); + dialog_id = -peer.chat_id; + } else { + markAsReadMessagesOutbox.put(peer.user_id, (long) update.max_id); + dialog_id = peer.user_id; + } + read_max = dialogs_read_outbox_max; } - } else if (update instanceof TLRPC.TL_updateReadHistoryOutbox) { - TLRPC.Peer peer = ((TLRPC.TL_updateReadHistoryOutbox) update).peer; - if (peer.chat_id != 0) { - markAsReadMessagesOutbox.put(-peer.chat_id, update.max_id); - } else { - markAsReadMessagesOutbox.put(peer.user_id, update.max_id); + Integer value = read_max.get(dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(update instanceof TLRPC.TL_updateReadHistoryOutbox, dialog_id); } + read_max.put(dialog_id, Math.max(value, update.max_id)); } else if (update instanceof TLRPC.TL_updateDeleteMessages) { ArrayList arrayList = deletedMessages.get(0); if (arrayList == null) { @@ -5912,35 +6261,37 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } else if (update instanceof TLRPC.TL_updateNewAuthorization) { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - NotificationCenter.getInstance().postNotificationName(NotificationCenter.newSessionReceived); - } - }); - TLRPC.TL_messageService newMessage = new TLRPC.TL_messageService(); - newMessage.action = new TLRPC.TL_messageActionLoginUnknownLocation(); - newMessage.action.title = update.device; - newMessage.action.address = update.location; - newMessage.local_id = newMessage.id = UserConfig.getNewMessageId(); - UserConfig.saveConfig(false); - newMessage.unread = true; - newMessage.flags = TLRPC.MESSAGE_FLAG_HAS_FROM_ID; - newMessage.date = update.date; - newMessage.from_id = 777000; - newMessage.to_id = new TLRPC.TL_peerUser(); - newMessage.to_id.user_id = UserConfig.getClientUserId(); - newMessage.dialog_id = 777000; + if (!MessagesStorage.getInstance().hasAuthMessage(update.date)) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + NotificationCenter.getInstance().postNotificationName(NotificationCenter.newSessionReceived); + } + }); + TLRPC.TL_messageService newMessage = new TLRPC.TL_messageService(); + newMessage.action = new TLRPC.TL_messageActionLoginUnknownLocation(); + newMessage.action.title = update.device; + newMessage.action.address = update.location; + newMessage.local_id = newMessage.id = UserConfig.getNewMessageId(); + UserConfig.saveConfig(false); + newMessage.unread = true; + newMessage.flags = TLRPC.MESSAGE_FLAG_HAS_FROM_ID; + newMessage.date = update.date; + newMessage.from_id = 777000; + newMessage.to_id = new TLRPC.TL_peerUser(); + newMessage.to_id.user_id = UserConfig.getClientUserId(); + newMessage.dialog_id = 777000; - messagesArr.add(newMessage); - MessageObject obj = new MessageObject(newMessage, usersDict, chatsDict, createdDialogIds.contains(newMessage.dialog_id)); - ArrayList arr = messages.get(newMessage.dialog_id); - if (arr == null) { - arr = new ArrayList<>(); - messages.put(newMessage.dialog_id, arr); + messagesArr.add(newMessage); + MessageObject obj = new MessageObject(newMessage, usersDict, chatsDict, createdDialogIds.contains(newMessage.dialog_id)); + ArrayList arr = messages.get(newMessage.dialog_id); + if (arr == null) { + arr = new ArrayList<>(); + messages.put(newMessage.dialog_id, arr); + } + arr.add(obj); + pushMessages.add(obj); } - arr.add(obj); - pushMessages.add(obj); } else if (update instanceof TLRPC.TL_updateNewGeoChatMessage) { //DEPRECATED } else if (update instanceof TLRPC.TL_updateNewEncryptedMessage) { @@ -6064,40 +6415,60 @@ public class MessagesController implements NotificationCenter.NotificationCenter } else if (update instanceof TLRPC.TL_updateWebPage) { webPages.put(update.webpage.id, update.webpage); } else if (update instanceof TLRPC.TL_updateChannelTooLong) { - if ((update.flags & 1) != 0) { - Integer channelPts = channelsPts.get(update.channel_id); - if (channelPts == null) { - channelPts = MessagesStorage.getInstance().getChannelPtsSync(update.channel_id); - if (channelPts == 0) { - channelPts = 1; + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + update.channel_id); + } + Integer channelPts = channelsPts.get(update.channel_id); + if (channelPts == null) { + channelPts = MessagesStorage.getInstance().getChannelPtsSync(update.channel_id); + if (channelPts == 0) { + TLRPC.Chat chat = chatsDict.get(update.channel_id); + if (chat == null || chat.min) { + chat = getChat(update.channel_id); } + if (chat == null || chat.min) { + chat = MessagesStorage.getInstance().getChatSync(update.channel_id); + putChat(chat, true); + } + if (chat != null && !chat.min) { + loadUnknownChannel(chat, 0); + } + } else { channelsPts.put(update.channel_id, channelPts); } - if (update.pts > channelPts) { + } + if (channelPts != 0) { + if ((update.flags & 1) != 0) { + if (update.pts > channelPts) { + getChannelDifference(update.channel_id); + } + } else { getChannelDifference(update.channel_id); } - } else { - getChannelDifference(update.channel_id); } - } else if (update instanceof TLRPC.TL_updateChannelGroup) { - ArrayList arrayList = channelsGroups.get(update.channel_id); - if (arrayList == null) { - arrayList = new ArrayList<>(); - channelsGroups.put(update.channel_id, arrayList); - } - arrayList.add(update.group); - } else if (update instanceof TLRPC.TL_updateReadChannelInbox) { + } else if (update instanceof TLRPC.TL_updateReadChannelInbox || update instanceof TLRPC.TL_updateReadChannelOutbox) { long message_id = update.max_id; message_id |= ((long) update.channel_id) << 32; - markAsReadMessagesInbox.put(-update.channel_id, message_id); - long dialog_id = -update.channel_id; - Integer value = dialogs_read_inbox_max.get(dialog_id); - if (value == null) { - value = MessagesStorage.getInstance().getChannelReadInboxMax(update.channel_id); + + ConcurrentHashMap read_max; + if (update instanceof TLRPC.TL_updateReadChannelInbox) { + read_max = dialogs_read_inbox_max; + markAsReadMessagesInbox.put(-update.channel_id, message_id); + } else { + read_max = dialogs_read_outbox_max; + markAsReadMessagesOutbox.put(-update.channel_id, message_id); } - dialogs_read_inbox_max.put(dialog_id, Math.max(value, update.max_id)); + + Integer value = read_max.get(dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(update instanceof TLRPC.TL_updateReadChannelOutbox, dialog_id); + } + read_max.put(dialog_id, Math.max(value, update.max_id)); } else if (update instanceof TLRPC.TL_updateDeleteChannelMessages) { + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + update.channel_id); + } ArrayList arrayList = deletedMessages.get(update.channel_id); if (arrayList == null) { arrayList = new ArrayList<>(); @@ -6105,8 +6476,14 @@ public class MessagesController implements NotificationCenter.NotificationCenter } arrayList.addAll(update.messages); } else if (update instanceof TLRPC.TL_updateChannel) { + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + update.channel_id); + } updatesOnMainThread.add(update); } else if (update instanceof TLRPC.TL_updateChannelMessageViews) { + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + update.channel_id); + } TLRPC.TL_updateChannelMessageViews updateChannelMessageViews = (TLRPC.TL_updateChannelMessageViews) update; SparseIntArray array = channelViews.get(update.channel_id); if (array == null) { @@ -6124,36 +6501,59 @@ public class MessagesController implements NotificationCenter.NotificationCenter updatesOnMainThread.add(update); } else if (update instanceof TLRPC.TL_updateNewStickerSet) { updatesOnMainThread.add(update); + } else if (update instanceof TLRPC.TL_updateDraftMessage) { + updatesOnMainThread.add(update); } else if (update instanceof TLRPC.TL_updateSavedGifs) { updatesOnMainThread.add(update); } else if (update instanceof TLRPC.TL_updateEditChannelMessage || update instanceof TLRPC.TL_updateEditMessage) { TLRPC.Message message; if (update instanceof TLRPC.TL_updateEditChannelMessage) { message = ((TLRPC.TL_updateEditChannelMessage) update).message; + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + message.to_id.channel_id); + } + TLRPC.Chat chat = chatsDict.get(message.to_id.channel_id); + if (chat == null) { + chat = getChat(message.to_id.channel_id); + } + if (chat == null) { + chat = MessagesStorage.getInstance().getChatSync(message.to_id.channel_id); + putChat(chat, true); + } + if (chat != null && chat.megagroup) { + message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; + } } else { message = ((TLRPC.TL_updateEditMessage) update).message; } - if (message.to_id.channel_id != 0 && !message.out) { - message.unread = true; - if (message.post || (message.flags & TLRPC.MESSAGE_FLAG_MEGAGROUP) != 0) { - message.media_unread = true; + if (message.out && (message.message == null || message.message.length() == 0)) { + message.message = "-1"; + message.attachPath = ""; + } + if (!fromGetDifference) { + int count = message.entities.size(); + for (int a = 0; a < count; a++) { + TLRPC.MessageEntity entity = message.entities.get(a); + if (entity instanceof TLRPC.TL_messageEntityMentionName) { + int user_id = ((TLRPC.TL_messageEntityMentionName) entity).user_id; + TLRPC.User user = usersDict.get(user_id); + if (user == null || user.min) { + user = getUser(user_id); + } + if (user == null || user.min) { + user = MessagesStorage.getInstance().getUserSync(user_id); + if (user != null && user.min) { + user = null; + } + putUser(user, true); + } + if (user == null) { + return false; + } + } } } - if (update instanceof TLRPC.TL_updateEditChannelMessage) { - long dialog_id = -update.channel_id; - Integer value = dialogs_read_inbox_max.get(dialog_id); - if (value == null) { - value = MessagesStorage.getInstance().getChannelReadInboxMax(update.channel_id); - } - if (value >= message.id) { - message.unread = false; - message.media_unread = false; - } //TODO unread for updateEditMessage? - } - - ImageLoader.saveMessageThumbs(message); - if (message.to_id.chat_id != 0) { message.dialog_id = -message.to_id.chat_id; } else if (message.to_id.channel_id != 0) { @@ -6164,6 +6564,17 @@ public class MessagesController implements NotificationCenter.NotificationCenter } message.dialog_id = message.to_id.user_id; } + + ConcurrentHashMap read_max = message.out ? dialogs_read_outbox_max : dialogs_read_inbox_max; + Integer value = read_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + read_max.put(message.dialog_id, value); + } + message.unread = value < message.id; + + ImageLoader.saveMessageThumbs(message); + MessageObject obj = new MessageObject(message, usersDict, chatsDict, createdDialogIds.contains(message.dialog_id)); ArrayList arr = editingMessages.get(message.dialog_id); @@ -6173,6 +6584,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter } arr.add(obj); } else if (update instanceof TLRPC.TL_updateChannelPinnedMessage) { + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + update.channel_id); + } TLRPC.TL_updateChannelPinnedMessage updateChannelPinnedMessage = (TLRPC.TL_updateChannelPinnedMessage) update; MessagesStorage.getInstance().updateChannelPinnedMessage(update.channel_id, updateChannelPinnedMessage.id); } @@ -6222,21 +6636,19 @@ public class MessagesController implements NotificationCenter.NotificationCenter for (int a = 0; a < messageObjects.size(); a++) { messagesRes.messages.add(messageObjects.get(a).messageOwner); } - MessagesStorage.getInstance().putMessages(messagesRes, pair.getKey(), -2, 0, 0, false); + MessagesStorage.getInstance().putMessages(messagesRes, pair.getKey(), -2, 0, false); } } if (channelViews.size() != 0) { MessagesStorage.getInstance().putChannelViews(channelViews, true); } - if (channelsGroups.size() != 0) { - //MessagesStorage.getInstance().applyNewChannelsGroups(channelsGroups); - } AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { int updateMask = interfaceUpdateMaskFinal; + boolean hasDraftUpdates = false; if (!updatesOnMainThread.isEmpty()) { ArrayList dbUsers = new ArrayList<>(); @@ -6322,7 +6734,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter dialog_id = -updateNotifySettings.peer.peer.channel_id; } - TLRPC.Dialog dialog = dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = update.notify_settings; } @@ -6353,17 +6765,19 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } else if (update instanceof TLRPC.TL_updateChannel) { - TLRPC.Dialog dialog = dialogs_dict.get(-(long) update.channel_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(-(long) update.channel_id); TLRPC.Chat chat = getChat(update.channel_id); - if (dialog == null && chat instanceof TLRPC.TL_channel && !chat.left) { - Utilities.stageQueue.postRunnable(new Runnable() { - @Override - public void run() { - getChannelDifference(update.channel_id, 1); - } - }); - } else if (chat.left && dialog != null) { - deleteDialog(dialog.id, 0); + if (chat != null) { + if (dialog == null && chat instanceof TLRPC.TL_channel && !chat.left) { + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + getChannelDifference(update.channel_id, 1, 0); + } + }); + } else if (chat.left && dialog != null) { + deleteDialog(dialog.id, 0); + } } updateMask |= UPDATE_MASK_CHANNEL; loadFullChat(update.channel_id, 0, true); @@ -6378,6 +6792,18 @@ public class MessagesController implements NotificationCenter.NotificationCenter } else if (update instanceof TLRPC.TL_updateSavedGifs) { SharedPreferences.Editor editor2 = ApplicationLoader.applicationContext.getSharedPreferences("emoji", Activity.MODE_PRIVATE).edit(); editor2.putLong("lastGifLoadTime", 0).commit(); + } else if (update instanceof TLRPC.TL_updateDraftMessage) { + hasDraftUpdates = true; + long did; + TLRPC.Peer peer = ((TLRPC.TL_updateDraftMessage) update).peer; + if (peer.user_id != 0) { + did = peer.user_id; + } else if (peer.channel_id != 0) { + did = -peer.channel_id; + } else { + did = -peer.chat_id; + } + DraftQuery.saveDraft(did, update.draft, null, true); } } if (editor != null) { @@ -6417,6 +6843,17 @@ public class MessagesController implements NotificationCenter.NotificationCenter } boolean updateDialogs = false; + if (!messages.isEmpty()) { + for (HashMap.Entry> entry : messages.entrySet()) { + Long key = entry.getKey(); + ArrayList value = entry.getValue(); + updateInterfaceWithMessages(key, value); + } + updateDialogs = true; + } else if (hasDraftUpdates) { + sortDialogs(null); + updateDialogs = true; + } if (!editingMessages.isEmpty()) { for (HashMap.Entry> pair : editingMessages.entrySet()) { Long dialog_id = pair.getKey(); @@ -6427,6 +6864,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter MessageObject newMessage = arrayList.get(a); if (oldObject.getId() == newMessage.getId()) { dialogMessage.put(dialog_id, newMessage); + if (newMessage.messageOwner.to_id != null && newMessage.messageOwner.to_id.channel_id == 0) { + dialogMessagesByIds.put(newMessage.getId(), newMessage); + } updateDialogs = true; break; } @@ -6435,14 +6875,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter NotificationCenter.getInstance().postNotificationName(NotificationCenter.replaceMessagesObjects, dialog_id, arrayList); } } - if (!messages.isEmpty()) { - for (HashMap.Entry> entry : messages.entrySet()) { - Long key = entry.getKey(); - ArrayList value = entry.getValue(); - updateInterfaceWithMessages(key, value); - } - updateDialogs = true; - } if (updateDialogs) { NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); } @@ -6481,11 +6913,11 @@ public class MessagesController implements NotificationCenter.NotificationCenter NotificationsController.getInstance().processReadMessages(markAsReadMessagesInbox, 0, 0, 0, false); for (int b = 0; b < markAsReadMessagesInbox.size(); b++) { int key = markAsReadMessagesInbox.keyAt(b); - long messageId = markAsReadMessagesInbox.get(key); - TLRPC.Dialog dialog = dialogs_dict.get((long) key); - if (dialog != null && dialog.top_message <= (int) messageId) { + int messageId = (int) ((long) markAsReadMessagesInbox.get(key)); + TLRPC.TL_dialog dialog = dialogs_dict.get((long) key); + if (dialog != null && dialog.top_message <= messageId) { MessageObject obj = dialogMessage.get(dialog.id); - if (obj != null) { + if (obj != null && !obj.isOut()) { obj.setIsRead(); updateMask |= UPDATE_MASK_READ_DIALOG_MESSAGE; } @@ -6493,11 +6925,11 @@ public class MessagesController implements NotificationCenter.NotificationCenter } for (int b = 0; b < markAsReadMessagesOutbox.size(); b++) { int key = markAsReadMessagesOutbox.keyAt(b); - int messageId = markAsReadMessagesOutbox.get(key); - TLRPC.Dialog dialog = dialogs_dict.get((long) key); + int messageId = (int) ((long) markAsReadMessagesOutbox.get(key)); + TLRPC.TL_dialog dialog = dialogs_dict.get((long) key); if (dialog != null && dialog.top_message <= messageId) { MessageObject obj = dialogMessage.get(dialog.id); - if (obj != null) { + if (obj != null && obj.isOut()) { obj.setIsRead(); updateMask |= UPDATE_MASK_READ_DIALOG_MESSAGE; } @@ -6508,7 +6940,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter for (HashMap.Entry entry : markAsReadEncrypted.entrySet()) { NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesReadEncrypted, entry.getKey(), entry.getValue()); long dialog_id = (long) (entry.getKey()) << 32; - TLRPC.Dialog dialog = dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(dialog_id); if (dialog != null) { MessageObject message = dialogMessage.get(dialog_id); if (message != null && message.messageOwner.date <= entry.getValue()) { @@ -6564,7 +6996,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (markAsReadMessagesInbox.size() != 0 || markAsReadMessagesOutbox.size() != 0 || !markAsReadEncrypted.isEmpty()) { if (markAsReadMessagesInbox.size() != 0) { - MessagesStorage.getInstance().updateDialogsWithReadMessages(markAsReadMessagesInbox, true); + MessagesStorage.getInstance().updateDialogsWithReadMessages(markAsReadMessagesInbox, markAsReadMessagesOutbox, true); } MessagesStorage.getInstance().markMessagesAsRead(markAsReadMessagesInbox, markAsReadMessagesOutbox, markAsReadEncrypted, true); } @@ -6667,12 +7099,11 @@ public class MessagesController implements NotificationCenter.NotificationCenter boolean isEncryptedChat = ((int) uid) == 0; MessageObject lastMessage = null; int channelId = 0; + boolean updateRating = false; for (int a = 0; a < messages.size(); a++) { MessageObject message = messages.get(a); if (lastMessage == null || (!isEncryptedChat && message.getId() > lastMessage.getId() || (isEncryptedChat || message.getId() < 0 && lastMessage.getId() < 0) && message.getId() < lastMessage.getId()) || message.messageOwner.date > lastMessage.messageOwner.date) { - if (message.messageOwner.to_id.channel_id == 0 || message.isMegagroup() || message.isImportant()) { - lastMessage = message; - } + lastMessage = message; if (message.messageOwner.to_id.channel_id != 0) { channelId = message.messageOwner.to_id.channel_id; } @@ -6680,6 +7111,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (message.isOut() && message.isNewGif() && !message.isSending() && !message.isForwarded()) { addNewGifToRecent(message.messageOwner.media.document, message.messageOwner.date); } + if (message.isOut() && message.isSent()) { + updateRating = true; + } } MessagesQuery.loadReplyMessagesForMessages(messages, uid); NotificationCenter.getInstance().postNotificationName(NotificationCenter.didReceivedNewMessages, uid, messages); @@ -6687,7 +7121,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (lastMessage == null) { return; } - TLRPC.Dialog dialog = dialogs_dict.get(uid); + TLRPC.TL_dialog dialog = dialogs_dict.get(uid); if (lastMessage.messageOwner.action instanceof TLRPC.TL_messageActionChatMigrateTo) { if (dialog != null) { dialogs.remove(dialog); @@ -6695,6 +7129,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter dialogsGroupsOnly.remove(dialog); dialogs_dict.remove(dialog.id); dialogs_read_inbox_max.remove(dialog.id); + dialogs_read_outbox_max.remove(dialog.id); nextDialogsCacheOffset--; dialogMessage.remove(dialog.id); MessageObject object = dialogMessagesByIds.remove(dialog.top_message); @@ -6716,15 +7151,12 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (channelId != 0 && chat == null || chat != null && chat.left) { return; } - if (!ChatObject.isChannel(chat)) { - dialog = new TLRPC.TL_dialog(); - } else { - dialog = new TLRPC.TL_dialogChannel(); - } + dialog = new TLRPC.TL_dialog(); dialog.id = uid; dialog.unread_count = 0; dialog.top_message = lastMessage.getId(); dialog.last_message_date = lastMessage.messageOwner.date; + dialog.flags = ChatObject.isChannel(chat) ? 1 : 0; dialogs_dict.put(uid, dialog); dialogs.add(dialog); dialogMessage.put(uid, lastMessage); @@ -6761,36 +7193,39 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (changed) { - dialogsServerOnly.clear(); - dialogsGroupsOnly.clear(); - Collections.sort(dialogs, new Comparator() { - @Override - public int compare(TLRPC.Dialog tl_dialog, TLRPC.Dialog tl_dialog2) { - if (tl_dialog.last_message_date == tl_dialog2.last_message_date) { - return 0; - } else if (tl_dialog.last_message_date < tl_dialog2.last_message_date) { - return 1; - } else { - return -1; + sortDialogs(null); + } + + if (updateRating) { + SearchQuery.increasePeerRaiting(uid); + } + } + + public void sortDialogs(HashMap chatsDict) { + dialogsServerOnly.clear(); + dialogsGroupsOnly.clear(); + Collections.sort(dialogs, dialogComparator); + for (int a = 0; a < dialogs.size(); a++) { + TLRPC.TL_dialog d = dialogs.get(a); + int high_id = (int) (d.id >> 32); + int lower_id = (int) d.id; + if (lower_id != 0 && high_id != 1) { + dialogsServerOnly.add(d); + if (DialogObject.isChannel(d)) { + TLRPC.Chat chat = getChat(-lower_id); + if (chat != null && (chat.megagroup && chat.editor || chat.creator)) { + dialogsGroupsOnly.add(d); } - } - }); - for (int a = 0; a < dialogs.size(); a++) { - TLRPC.Dialog d = dialogs.get(a); - int high_id = (int) (d.id >> 32); - if ((int) d.id != 0 && high_id != 1) { - dialogsServerOnly.add(d); - if (d instanceof TLRPC.TL_dialog) { - if (d.id < 0) { - dialogsGroupsOnly.add(d); - } - } else if (d instanceof TLRPC.TL_dialogChannel) { - int lower_id = (int) d.id; - TLRPC.Chat chat = getChat(-lower_id); - if (chat != null && (chat.megagroup && chat.editor || chat.creator)) { - dialogsGroupsOnly.add(d); + } else if (lower_id < 0) { + if (chatsDict != null) { + TLRPC.Chat chat = chatsDict.get(-lower_id); + if (chat != null && chat.migrated_to != null) { + dialogs.remove(a); + a--; + continue; } } + dialogsGroupsOnly.add(d); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java index 11ec02096..4e21d0b40 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java @@ -34,11 +34,14 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicLong; public class MessagesStorage { private DispatchQueue storageQueue = new DispatchQueue("storageQueue"); private SQLiteDatabase database; private File cacheFile; + private AtomicLong lastTaskId = new AtomicLong(System.currentTimeMillis()); + public static int lastDateValue = 0; public static int lastPtsValue = 0; public static int lastQtsValue = 0; @@ -53,6 +56,7 @@ public class MessagesStorage { private int lastSavedQts = 0; private static volatile MessagesStorage Instance = null; + public static MessagesStorage getInstance() { MessagesStorage localInstance = Instance; if (localInstance == null) { @@ -92,12 +96,8 @@ public class MessagesStorage { database.executeFast("PRAGMA secure_delete = ON").stepThis().dispose(); database.executeFast("PRAGMA temp_store = 1").stepThis().dispose(); if (createTable) { - database.executeFast("CREATE TABLE channel_group(uid INTEGER, start INTEGER, end INTEGER, count INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); - database.executeFast("CREATE TABLE messages_holes(uid INTEGER, start INTEGER, end INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS uid_end_messages_holes ON messages_holes(uid, end);").stepThis().dispose(); - database.executeFast("CREATE TABLE messages_imp_holes(uid INTEGER, start INTEGER, end INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_end_messages_imp_holes ON messages_imp_holes(uid, end);").stepThis().dispose(); database.executeFast("CREATE TABLE media_holes_v2(uid INTEGER, type INTEGER, start INTEGER, end INTEGER, PRIMARY KEY(uid, type, start));").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS uid_end_media_holes_v2 ON media_holes_v2(uid, type, end);").stepThis().dispose(); @@ -105,8 +105,6 @@ public class MessagesStorage { database.executeFast("CREATE TABLE messages(mid INTEGER PRIMARY KEY, uid INTEGER, read_state INTEGER, send_state INTEGER, date INTEGER, data BLOB, out INTEGER, ttl INTEGER, media INTEGER, replydata BLOB, imp INTEGER)").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS uid_mid_idx_messages ON messages(uid, mid);").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS uid_date_mid_idx_messages ON messages(uid, date, mid);").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_mid_idx_imp_messages ON messages(uid, mid, imp) WHERE imp = 1;").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_date_mid_imp_idx_messages ON messages(uid, date, mid, imp) WHERE imp = 1;").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS mid_out_idx_messages ON messages(mid, out);").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS task_idx_messages ON messages(uid, out, read_state, ttl, date, send_state);").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS send_state_idx_messages ON messages(mid, send_state, date) WHERE mid < 0 AND send_state = 1;").stepThis().dispose(); @@ -148,19 +146,20 @@ public class MessagesStorage { database.executeFast("CREATE TABLE chat_pinned(uid INTEGER PRIMARY KEY, pinned INTEGER, data BLOB)").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS chat_pinned_mid_idx ON chat_pinned(uid, pinned) WHERE pinned != 0;").stepThis().dispose(); + database.executeFast("CREATE TABLE chat_hints(did INTEGER, type INTEGER, rating REAL, date INTEGER, PRIMARY KEY(did, type))").stepThis().dispose(); + database.executeFast("CREATE INDEX IF NOT EXISTS chat_hints_rating_idx ON chat_hints(rating);").stepThis().dispose(); + database.executeFast("CREATE TABLE users_data(uid INTEGER PRIMARY KEY, about TEXT)").stepThis().dispose(); database.executeFast("CREATE TABLE users(uid INTEGER PRIMARY KEY, name TEXT, status INTEGER, data BLOB)").stepThis().dispose(); database.executeFast("CREATE TABLE chats(uid INTEGER PRIMARY KEY, name TEXT, data BLOB)").stepThis().dispose(); database.executeFast("CREATE TABLE enc_chats(uid INTEGER PRIMARY KEY, user INTEGER, name TEXT, data BLOB, g BLOB, authkey BLOB, ttl INTEGER, layer INTEGER, seq_in INTEGER, seq_out INTEGER, use_count INTEGER, exchange_id INTEGER, key_date INTEGER, fprint INTEGER, fauthkey BLOB, khash BLOB)").stepThis().dispose(); database.executeFast("CREATE TABLE channel_users_v2(did INTEGER, uid INTEGER, date INTEGER, data BLOB, PRIMARY KEY(did, uid))").stepThis().dispose(); database.executeFast("CREATE TABLE contacts(uid INTEGER PRIMARY KEY, mutual INTEGER)").stepThis().dispose(); - database.executeFast("CREATE TABLE pending_read(uid INTEGER PRIMARY KEY, max_id INTEGER)").stepThis().dispose(); database.executeFast("CREATE TABLE wallpapers(uid INTEGER PRIMARY KEY, data BLOB)").stepThis().dispose(); database.executeFast("CREATE TABLE user_photos(uid INTEGER, id INTEGER, data BLOB, PRIMARY KEY (uid, id))").stepThis().dispose(); database.executeFast("CREATE TABLE blocked_users(uid INTEGER PRIMARY KEY)").stepThis().dispose(); database.executeFast("CREATE TABLE dialog_settings(did INTEGER PRIMARY KEY, flags INTEGER);").stepThis().dispose(); database.executeFast("CREATE TABLE web_recent_v3(id TEXT, type INTEGER, image_url TEXT, thumb_url TEXT, local_url TEXT, width INTEGER, height INTEGER, size INTEGER, date INTEGER, document BLOB, PRIMARY KEY (id, type));").stepThis().dispose(); - database.executeFast("CREATE TABLE bot_recent(id INTEGER PRIMARY KEY, date INTEGER);").stepThis().dispose(); database.executeFast("CREATE TABLE stickers_v2(id INTEGER PRIMARY KEY, data BLOB, date INTEGER, hash TEXT);").stepThis().dispose(); database.executeFast("CREATE TABLE hashtag_recent_v2(id TEXT PRIMARY KEY, date INTEGER);").stepThis().dispose(); database.executeFast("CREATE TABLE webpage_pending(id INTEGER, mid INTEGER, PRIMARY KEY (id, mid));").stepThis().dispose(); @@ -170,9 +169,10 @@ public class MessagesStorage { database.executeFast("CREATE TABLE media_counts_v2(uid INTEGER, type INTEGER, count INTEGER, PRIMARY KEY(uid, type))").stepThis().dispose(); database.executeFast("CREATE TABLE keyvalue(id TEXT PRIMARY KEY, value TEXT)").stepThis().dispose(); database.executeFast("CREATE TABLE bot_info(uid INTEGER PRIMARY KEY, info BLOB)").stepThis().dispose(); + database.executeFast("CREATE TABLE pending_tasks(id INTEGER PRIMARY KEY, data BLOB);").stepThis().dispose(); //version - database.executeFast("PRAGMA user_version = 31").stepThis().dispose(); + database.executeFast("PRAGMA user_version = 34").stepThis().dispose(); //database.executeFast("CREATE TABLE secret_holes(uid INTEGER, seq_in INTEGER, seq_out INTEGER, data BLOB, PRIMARY KEY (uid, seq_in, seq_out));").stepThis().dispose(); //database.executeFast("CREATE TABLE attach_data(uid INTEGER, id INTEGER, data BLOB, PRIMARY KEY (uid, id))").stepThis().dispose(); @@ -206,7 +206,7 @@ public class MessagesStorage { } } int version = database.executeInt("PRAGMA user_version"); - if (version < 31) { + if (version < 34) { updateDbToLastVersion(version); } } @@ -214,6 +214,7 @@ public class MessagesStorage { FileLog.e("tmessages", e); } loadUnreadMessages(); + loadPendingTasks(); } public void updateDbToLastVersion(final int currentVersion) { @@ -301,17 +302,17 @@ public class MessagesStorage { SQLitePreparedStatement state = database.executeFast("REPLACE INTO enc_tasks_v2 VALUES(?, ?)"); if (cursor.next()) { int date = cursor.intValue(0); - int length; - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(1)); - if ((length = cursor.byteBufferValue(1, data)) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(1); + if (data != null) { + int length = data.limit(); for (int a = 0; a < length / 4; a++) { state.requery(); state.bindInteger(1, data.readInt32(false)); state.bindInteger(2, date); state.step(); } + data.reuse(); } - data.reuse(); } state.dispose(); cursor.dispose(); @@ -422,9 +423,10 @@ public class MessagesStorage { SQLitePreparedStatement state = database.executeFast("REPLACE INTO chat_settings_v2 VALUES(?, ?)"); while (cursor.next()) { int chat_id = cursor.intValue(0); - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(1)); - if (cursor.byteBufferValue(1, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(1); + if (data != null) { TLRPC.ChatParticipants participants = TLRPC.ChatParticipants.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (participants != null) { TLRPC.TL_chatFull chatFull = new TLRPC.TL_chatFull(); chatFull.id = chat_id; @@ -441,7 +443,6 @@ public class MessagesStorage { data2.reuse(); } } - data.reuse(); } state.dispose(); cursor.dispose(); @@ -454,13 +455,8 @@ public class MessagesStorage { database.executeFast("CREATE INDEX IF NOT EXISTS last_mid_i_idx_dialogs ON dialogs(last_mid_i);").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS unread_count_i_idx_dialogs ON dialogs(unread_count_i);").stepThis().dispose(); database.executeFast("ALTER TABLE messages ADD COLUMN imp INTEGER default 0").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_mid_idx_imp_messages ON messages(uid, mid, imp) WHERE imp = 1;").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_date_mid_imp_idx_messages ON messages(uid, date, mid, imp) WHERE imp = 1;").stepThis().dispose(); - database.executeFast("CREATE TABLE IF NOT EXISTS channel_group(uid INTEGER, start INTEGER, end INTEGER, count INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); database.executeFast("CREATE TABLE IF NOT EXISTS messages_holes(uid INTEGER, start INTEGER, end INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS uid_end_messages_holes ON messages_holes(uid, end);").stepThis().dispose(); - database.executeFast("CREATE TABLE IF NOT EXISTS messages_imp_holes(uid INTEGER, start INTEGER, end INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_end_messages_imp_holes ON messages_imp_holes(uid, end);").stepThis().dispose(); database.executeFast("PRAGMA user_version = 22").stepThis().dispose(); version = 22; } @@ -486,7 +482,6 @@ public class MessagesStorage { version = 28; } if (version == 28) { - database.executeFast("CREATE TABLE IF NOT EXISTS bot_recent(id INTEGER PRIMARY KEY, date INTEGER);").stepThis().dispose(); database.executeFast("PRAGMA user_version = 29").stepThis().dispose(); version = 29; } @@ -503,7 +498,25 @@ public class MessagesStorage { database.executeFast("CREATE INDEX IF NOT EXISTS chat_pinned_mid_idx ON chat_pinned(uid, pinned) WHERE pinned != 0;").stepThis().dispose(); database.executeFast("CREATE TABLE IF NOT EXISTS users_data(uid INTEGER PRIMARY KEY, about TEXT)").stepThis().dispose(); database.executeFast("PRAGMA user_version = 31").stepThis().dispose(); - //version = 31; + version = 31; + } + if (version == 31) { + database.executeFast("DROP TABLE IF EXISTS bot_recent;").stepThis().dispose(); + database.executeFast("CREATE TABLE IF NOT EXISTS chat_hints(did INTEGER, type INTEGER, rating REAL, date INTEGER, PRIMARY KEY(did, type))").stepThis().dispose(); + database.executeFast("CREATE INDEX IF NOT EXISTS chat_hints_rating_idx ON chat_hints(rating);").stepThis().dispose(); + database.executeFast("PRAGMA user_version = 32").stepThis().dispose(); + version = 32; + } + if (version == 32) { + database.executeFast("DROP INDEX IF EXISTS uid_mid_idx_imp_messages;").stepThis().dispose(); + database.executeFast("DROP INDEX IF EXISTS uid_date_mid_imp_idx_messages;").stepThis().dispose(); + database.executeFast("PRAGMA user_version = 33").stepThis().dispose(); + version = 33; + } + if (version == 33) { + database.executeFast("CREATE TABLE IF NOT EXISTS pending_tasks(id INTEGER PRIMARY KEY, data BLOB);").stepThis().dispose(); + database.executeFast("PRAGMA user_version = 34").stepThis().dispose(); + //version = 34; } } catch (Exception e) { FileLog.e("tmessages", e); @@ -512,7 +525,7 @@ public class MessagesStorage { }); } - public void cleanUp(final boolean isLogin) { + public void cleanup(final boolean isLogin) { storageQueue.cleanupQueue(); storageQueue.postRunnable(new Runnable() { @Override @@ -574,6 +587,106 @@ public class MessagesStorage { }); } + public long createPendingTask(final NativeByteBuffer data) { + if (data == null) { + return 0; + } + final long id = lastTaskId.getAndAdd(1); + storageQueue.postRunnable(new Runnable() { + @Override + public void run() { + try { + SQLitePreparedStatement state = database.executeFast("REPLACE INTO pending_tasks VALUES(?, ?)"); + state.bindLong(1, id); + state.bindByteBuffer(2, data); + state.step(); + state.dispose(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } finally { + data.reuse(); + } + } + }); + return id; + } + + public void removePendingTask(final long id) { + storageQueue.postRunnable(new Runnable() { + @Override + public void run() { + try { + database.executeFast("DELETE FROM pending_tasks WHERE id = " + id).stepThis().dispose(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + }); + } + + private void loadPendingTasks() { + storageQueue.postRunnable(new Runnable() { + @Override + public void run() { + try { + SQLiteCursor cursor = database.queryFinalized("SELECT id, data FROM pending_tasks WHERE 1"); + while (cursor.next()) { + final long taskId = cursor.longValue(0); + NativeByteBuffer data = cursor.byteBufferValue(1); + if (data != null) { + int type = data.readInt32(false); + switch (type) { + case 0: + final TLRPC.Chat chat = TLRPC.Chat.TLdeserialize(data, data.readInt32(false), false); + if (chat != null) { + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().loadUnknownChannel(chat, taskId); + } + }); + } + break; + case 1: + final int channelId = data.readInt32(false); + final int newDialogType = data.readInt32(false); + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().getChannelDifference(channelId, newDialogType, taskId); + } + }); + break; + case 2: + final TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); + dialog.id = data.readInt64(false); + dialog.top_message = data.readInt32(false); + dialog.read_inbox_max_id = data.readInt32(false); + dialog.read_outbox_max_id = data.readInt32(false); + dialog.unread_count = data.readInt32(false); + dialog.last_message_date = data.readInt32(false); + dialog.pts = data.readInt32(false); + dialog.flags = data.readInt32(false); + final TLRPC.InputPeer peer = TLRPC.InputPeer.TLdeserialize(data, data.readInt32(false), false); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().checkLastDialogMessage(dialog, peer, taskId); + } + }); + break; + } + data.reuse(); + } + } + cursor.dispose(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + }); + } + public void saveChannelPts(final int channelId, final int pts) { storageQueue.postRunnable(new Runnable() { @Override @@ -635,6 +748,10 @@ public class MessagesStorage { @Override public void run() { try { + ArrayList usersToLoad = new ArrayList<>(); + ArrayList chatsToLoad = new ArrayList<>(); + ArrayList encryptedChatIds = new ArrayList<>(); + final HashMap pushDialogs = new HashMap<>(); SQLiteCursor cursor = database.queryFinalized("SELECT d.did, d.unread_count, s.flags FROM dialogs as d LEFT JOIN dialog_settings as s ON d.did = s.did WHERE d.unread_count != 0"); StringBuilder ids = new StringBuilder(); @@ -647,6 +764,23 @@ public class MessagesStorage { ids.append(","); } ids.append(did); + int lower_id = (int) did; + int high_id = (int) (did >> 32); + if (lower_id != 0) { + if (lower_id < 0) { + if (!chatsToLoad.contains(-lower_id)) { + chatsToLoad.add(-lower_id); + } + } else { + if (!usersToLoad.contains(lower_id)) { + usersToLoad.add(lower_id); + } + } + } else { + if (!encryptedChatIds.contains(high_id)) { + encryptedChatIds.add(high_id); + } + } } } cursor.dispose(); @@ -656,49 +790,28 @@ public class MessagesStorage { final ArrayList chats = new ArrayList<>(); final ArrayList encryptedChats = new ArrayList<>(); if (ids.length() > 0) { - ArrayList usersToLoad = new ArrayList<>(); - ArrayList chatsToLoad = new ArrayList<>(); - ArrayList encryptedChatIds = new ArrayList<>(); - cursor = database.queryFinalized("SELECT read_state, data, send_state, mid, date, uid FROM messages WHERE uid IN (" + ids.toString() + ") AND out = 0 AND read_state IN(0,2) ORDER BY date DESC LIMIT 50"); while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(1)); - if (cursor.byteBufferValue(1, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(1); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); MessageObject.setUnreadFlags(message, cursor.intValue(0)); message.id = cursor.intValue(3); message.date = cursor.intValue(4); message.dialog_id = cursor.longValue(5); messages.add(message); - int lower_id = (int)message.dialog_id; - int high_id = (int)(message.dialog_id >> 32); - - if (lower_id != 0) { - if (lower_id < 0) { - if (!chatsToLoad.contains(-lower_id)) { - chatsToLoad.add(-lower_id); - } - } else { - if (!usersToLoad.contains(lower_id)) { - usersToLoad.add(lower_id); - } - } - } else { - if (!encryptedChatIds.contains(high_id)) { - encryptedChatIds.add(high_id); - } - } + int lower_id = (int) message.dialog_id; addUsersAndChatsFromMessage(message, usersToLoad, chatsToLoad); message.send_state = cursor.intValue(2); - if (message.to_id.channel_id == 0 && !MessageObject.isUnread(message) && lower_id != 0 || message.id > 0) { + if (message.to_id.channel_id == 0 && !MessageObject.isUnread(message) && lower_id != 0 || message.id > 0) { //TODO check message.send_state = 0; } if (lower_id == 0 && !cursor.isNull(5)) { message.random_id = cursor.longValue(5); } } - data.reuse(); } cursor.dispose(); @@ -792,11 +905,11 @@ public class MessagesStorage { searchImage.size = cursor.intValue(6); searchImage.date = cursor.intValue(7); if (!cursor.isNull(8)) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(8)); - if (cursor.byteBufferValue(8, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(8); + if (data != null) { searchImage.document = TLRPC.Document.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); } - data.reuse(); } searchImage.type = type; arrayList.add(searchImage); @@ -936,12 +1049,12 @@ public class MessagesStorage { SQLiteCursor cursor = database.queryFinalized("SELECT data FROM wallpapers WHERE 1"); final ArrayList wallPapers = new ArrayList<>(); while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.WallPaper wallPaper = TLRPC.WallPaper.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); wallPapers.add(wallPaper); } - data.reuse(); } cursor.dispose(); AndroidUtilities.runOnUIThread(new Runnable() { @@ -1039,9 +1152,10 @@ public class MessagesStorage { ArrayList filesToDelete = new ArrayList<>(); try { while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (message != null && message.from_id == uid && message.id != 1) { mids.add(message.id); if (message.media instanceof TLRPC.TL_messageMediaPhoto) { @@ -1063,7 +1177,6 @@ public class MessagesStorage { } } } - data.reuse(); } } catch (Exception e) { FileLog.e("tmessages", e); @@ -1098,14 +1211,26 @@ public class MessagesStorage { @Override public void run() { try { + if (messagesOnly == 3) { + int lastMid = -1; + SQLiteCursor cursor = database.queryFinalized("SELECT last_mid FROM dialogs WHERE did = " + did); + if (cursor.next()) { + lastMid = cursor.intValue(0); + } + cursor.dispose(); + if (lastMid != 0) { + return; + } + } if ((int) did == 0 || messagesOnly == 2) { SQLiteCursor cursor = database.queryFinalized("SELECT data FROM messages WHERE uid = " + did); ArrayList filesToDelete = new ArrayList<>(); try { while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (message != null && message.media != null) { if (message.media instanceof TLRPC.TL_messageMediaPhoto) { for (TLRPC.PhotoSize photoSize : message.media.photo.sizes) { @@ -1126,7 +1251,6 @@ public class MessagesStorage { } } } - data.reuse(); } } catch (Exception e) { FileLog.e("tmessages", e); @@ -1135,14 +1259,14 @@ public class MessagesStorage { FileLoader.getInstance().deleteFiles(filesToDelete, messagesOnly); } - if (messagesOnly == 0) { + if (messagesOnly == 0 || messagesOnly == 3) { database.executeFast("DELETE FROM dialogs WHERE did = " + did).stepThis().dispose(); database.executeFast("DELETE FROM chat_settings_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM chat_pinned WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM channel_users_v2 WHERE did = " + did).stepThis().dispose(); database.executeFast("DELETE FROM search_recent WHERE did = " + did).stepThis().dispose(); - int lower_id = (int)did; - int high_id = (int)(did >> 32); + int lower_id = (int) did; + int high_id = (int) (did >> 32); if (lower_id != 0) { if (high_id == 1) { database.executeFast("DELETE FROM chats WHERE uid = " + lower_id).stepThis().dispose(); @@ -1155,21 +1279,21 @@ public class MessagesStorage { } } else if (messagesOnly == 2) { SQLiteCursor cursor = database.queryFinalized("SELECT last_mid_i, last_mid FROM dialogs WHERE did = " + did); - ArrayList arrayList = new ArrayList<>(); + int messageId = -1; if (cursor.next()) { long last_mid_i = cursor.longValue(0); long last_mid = cursor.longValue(1); SQLiteCursor cursor2 = database.queryFinalized("SELECT data FROM messages WHERE uid = " + did + " AND mid IN (" + last_mid_i + "," + last_mid + ")"); try { while (cursor2.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor2.byteArrayLength(0)); - if (cursor2.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor2.byteBufferValue(0); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (message != null) { - arrayList.add(message); + messageId = message.id; } } - data.reuse(); } } catch (Exception e) { FileLog.e("tmessages", e); @@ -1177,9 +1301,7 @@ public class MessagesStorage { cursor2.dispose(); database.executeFast("DELETE FROM messages WHERE uid = " + did + " AND mid != " + last_mid_i + " AND mid != " + last_mid).stepThis().dispose(); - database.executeFast("DELETE FROM channel_group WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM messages_holes WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM messages_imp_holes WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM bot_keyboard WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_counts_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_v2 WHERE uid = " + did).stepThis().dispose(); @@ -1188,13 +1310,11 @@ public class MessagesStorage { SQLitePreparedStatement state5 = database.executeFast("REPLACE INTO messages_holes VALUES(?, ?, ?)"); SQLitePreparedStatement state6 = database.executeFast("REPLACE INTO media_holes_v2 VALUES(?, ?, ?, ?)"); - SQLitePreparedStatement state7 = database.executeFast("REPLACE INTO messages_imp_holes VALUES(?, ?, ?)"); - SQLitePreparedStatement state8 = database.executeFast("REPLACE INTO channel_group VALUES(?, ?, ?, ?)"); - createFirstHoles(did, state5, state6, state7, state8, arrayList); + if (messageId != -1) { + createFirstHoles(did, state5, state6, messageId); + } state5.dispose(); state6.dispose(); - state7.dispose(); - state8.dispose(); } cursor.dispose(); return; @@ -1202,12 +1322,10 @@ public class MessagesStorage { database.executeFast("UPDATE dialogs SET unread_count = 0, unread_count_i = 0 WHERE did = " + did).stepThis().dispose(); database.executeFast("DELETE FROM messages WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM channel_group WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM bot_keyboard WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_counts_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM messages_holes WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM messages_imp_holes WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_holes_v2 WHERE uid = " + did).stepThis().dispose(); BotQuery.clearBotKeyboard(did, null); AndroidUtilities.runOnUIThread(new Runnable() { @@ -1223,7 +1341,7 @@ public class MessagesStorage { }); } - public void getUserPhotos(final int uid, final int offset, final int count, final long max_id, final int classGuid) { + public void getDialogPhotos(final int did, final int offset, final int count, final long max_id, final int classGuid) { storageQueue.postRunnable(new Runnable() { @Override public void run() { @@ -1231,27 +1349,27 @@ public class MessagesStorage { SQLiteCursor cursor; if (max_id != 0) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM user_photos WHERE uid = %d AND id < %d ORDER BY id DESC LIMIT %d", uid, max_id, count)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM user_photos WHERE uid = %d AND id < %d ORDER BY id DESC LIMIT %d", did, max_id, count)); } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM user_photos WHERE uid = %d ORDER BY id DESC LIMIT %d,%d", uid, offset, count)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM user_photos WHERE uid = %d ORDER BY id DESC LIMIT %d,%d", did, offset, count)); } final TLRPC.photos_Photos res = new TLRPC.photos_Photos(); while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Photo photo = TLRPC.Photo.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); res.photos.add(photo); } - data.reuse(); } cursor.dispose(); Utilities.stageQueue.postRunnable(new Runnable() { @Override public void run() { - MessagesController.getInstance().processLoadedUserPhotos(res, uid, offset, count, max_id, true, classGuid); + MessagesController.getInstance().processLoadedUserPhotos(res, did, offset, count, max_id, true, classGuid); } }); } catch (Exception e) { @@ -1287,7 +1405,7 @@ public class MessagesStorage { }); } - public void putUserPhotos(final int uid, final TLRPC.photos_Photos photos) { + public void putDialogPhotos(final int did, final TLRPC.photos_Photos photos) { if (photos == null || photos.photos.isEmpty()) { return; } @@ -1303,7 +1421,7 @@ public class MessagesStorage { state.requery(); NativeByteBuffer data = new NativeByteBuffer(photo.getObjectSize()); photo.serializeToStream(data); - state.bindInteger(1, uid); + state.bindInteger(1, did); state.bindLong(2, photo.id); state.bindByteBuffer(3, data); state.step(); @@ -1421,7 +1539,7 @@ public class MessagesStorage { }); } - private void updateDialogsWithReadMessagesInternal(final ArrayList messages, final SparseArray inbox) { + private void updateDialogsWithReadMessagesInternal(final ArrayList messages, final SparseArray inbox, final SparseArray outbox) { try { HashMap dialogsToUpdate = new HashMap<>(); @@ -1446,24 +1564,39 @@ public class MessagesStorage { } } cursor.dispose(); - } else if (inbox != null && inbox.size() != 0) { - for (int b = 0; b < inbox.size(); b++) { - int key = inbox.keyAt(b); - long messageId = inbox.get(key); - SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(mid) FROM messages WHERE uid = %d AND mid > %d AND read_state IN(0,2) AND out = 0", key, messageId)); - if (cursor.next()) { - int count = cursor.intValue(0); - dialogsToUpdate.put((long) key, count); - } - cursor.dispose(); + } else { + if (inbox != null && inbox.size() != 0) { + for (int b = 0; b < inbox.size(); b++) { + int key = inbox.keyAt(b); + long messageId = inbox.get(key); + SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(mid) FROM messages WHERE uid = %d AND mid > %d AND read_state IN(0,2) AND out = 0", key, messageId)); + if (cursor.next()) { + int count = cursor.intValue(0); + dialogsToUpdate.put((long) key, count); + } + cursor.dispose(); - SQLitePreparedStatement state = database.executeFast("UPDATE dialogs SET inbox_max = max((SELECT inbox_max FROM dialogs WHERE did = ?), ?) WHERE did = ?"); - state.requery(); - state.bindLong(1, key); - state.bindInteger(2, (int) messageId); - state.bindLong(3, key); - state.step(); - state.dispose(); + SQLitePreparedStatement state = database.executeFast("UPDATE dialogs SET inbox_max = max((SELECT inbox_max FROM dialogs WHERE did = ?), ?) WHERE did = ?"); + state.requery(); + state.bindLong(1, key); + state.bindInteger(2, (int) messageId); + state.bindLong(3, key); + state.step(); + state.dispose(); + } + } + if (outbox != null && outbox.size() != 0) { + for (int b = 0; b < outbox.size(); b++) { + int key = outbox.keyAt(b); + long messageId = outbox.get(key); + SQLitePreparedStatement state = database.executeFast("UPDATE dialogs SET outbox_max = max((SELECT outbox_max FROM dialogs WHERE did = ?), ?) WHERE did = ?"); + state.requery(); + state.bindLong(1, key); + state.bindInteger(2, (int) messageId); + state.bindLong(3, key); + state.step(); + state.dispose(); + } } } @@ -1488,7 +1621,7 @@ public class MessagesStorage { } } - public void updateDialogsWithReadMessages(final SparseArray inbox, boolean useQueue) { + public void updateDialogsWithReadMessages(final SparseArray inbox, final SparseArray outbox, boolean useQueue) { if (inbox.size() == 0) { return; } @@ -1496,15 +1629,18 @@ public class MessagesStorage { storageQueue.postRunnable(new Runnable() { @Override public void run() { - updateDialogsWithReadMessagesInternal(null, inbox); + updateDialogsWithReadMessagesInternal(null, inbox, outbox); } }); } else { - updateDialogsWithReadMessagesInternal(null, inbox); + updateDialogsWithReadMessagesInternal(null, inbox, outbox); } } public void updateChatParticipants(final TLRPC.ChatParticipants participants) { + if (participants == null) { + return; + } storageQueue.postRunnable(new Runnable() { @Override public void run() { @@ -1513,12 +1649,12 @@ public class MessagesStorage { TLRPC.ChatFull info = null; ArrayList loadedUsers = new ArrayList<>(); if (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { info = TLRPC.ChatFull.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); info.pinned_msg_id = cursor.intValue(1); } - data.reuse(); } cursor.dispose(); if (info instanceof TLRPC.TL_chatFull) { @@ -1588,13 +1724,10 @@ public class MessagesStorage { public void run() { try { if (ifExist) { - boolean dontExist = true; SQLiteCursor cursor = database.queryFinalized("SELECT uid FROM chat_settings_v2 WHERE uid = " + info.id); - if (cursor.next()) { - dontExist = false; - } + boolean exist = cursor.next(); cursor.dispose(); - if (dontExist) { + if (!exist) { return; } } @@ -1609,27 +1742,33 @@ public class MessagesStorage { data.reuse(); if (info instanceof TLRPC.TL_channelFull) { - SQLiteCursor cursor = database.queryFinalized("SELECT date, last_mid_i, pts, date_i, last_mid FROM dialogs WHERE did = " + (-info.id)); + SQLiteCursor cursor = database.queryFinalized("SELECT date, pts, last_mid, inbox_max, outbox_max FROM dialogs WHERE did = " + (-info.id)); if (cursor.next()) { - int dialog_date = cursor.intValue(0); - long last_mid_i = cursor.longValue(1); - int pts = cursor.intValue(2); - int dialog_date_i = cursor.intValue(3); - long last_mid = cursor.longValue(4); + int inbox_max = cursor.intValue(3); + if (inbox_max <= info.read_inbox_max_id) { + int inbox_diff = info.read_inbox_max_id - inbox_max; + if (inbox_diff < info.unread_count) { + info.unread_count = inbox_diff; + } + int dialog_date = cursor.intValue(0); + int pts = cursor.intValue(1); + long last_mid = cursor.longValue(2); + int outbox_max = cursor.intValue(4); - state = database.executeFast("REPLACE INTO dialogs VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - state.bindLong(1, -info.id); - state.bindInteger(2, dialog_date); - state.bindInteger(3, info.unread_important_count); - state.bindLong(4, last_mid); - state.bindInteger(5, info.read_inbox_max_id); - state.bindInteger(6, 0); - state.bindLong(7, last_mid_i); - state.bindInteger(8, info.unread_count); - state.bindInteger(9, pts); - state.bindInteger(10, dialog_date_i); - state.step(); - state.dispose(); + state = database.executeFast("REPLACE INTO dialogs VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + state.bindLong(1, -info.id); + state.bindInteger(2, dialog_date); + state.bindInteger(3, info.unread_count); + state.bindLong(4, last_mid); + state.bindInteger(5, info.read_inbox_max_id); + state.bindInteger(6, Math.max(outbox_max, info.read_outbox_max_id)); + state.bindLong(7, 0); + state.bindInteger(8, 0); + state.bindInteger(9, pts); + state.bindInteger(10, 0); + state.step(); + state.dispose(); + } } cursor.dispose(); } @@ -1649,12 +1788,12 @@ public class MessagesStorage { TLRPC.ChatFull info = null; ArrayList loadedUsers = new ArrayList<>(); if (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { info = TLRPC.ChatFull.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); info.pinned_msg_id = cursor.intValue(1); } - data.reuse(); } cursor.dispose(); if (info instanceof TLRPC.TL_channelFull) { @@ -1695,12 +1834,12 @@ public class MessagesStorage { TLRPC.ChatFull info = null; ArrayList loadedUsers = new ArrayList<>(); if (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { info = TLRPC.ChatFull.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); info.pinned_msg_id = cursor.intValue(1); } - data.reuse(); } cursor.dispose(); if (info instanceof TLRPC.TL_chatFull) { @@ -1782,11 +1921,11 @@ public class MessagesStorage { TLRPC.ChatFull info = null; ArrayList loadedUsers = new ArrayList<>(); if (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { info = TLRPC.ChatFull.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); } - data.reuse(); } cursor.dispose(); result[0] = info instanceof TLRPC.TL_channelFull && info.migrated_from_chat_id != 0; @@ -1820,12 +1959,12 @@ public class MessagesStorage { try { SQLiteCursor cursor = database.queryFinalized("SELECT info, pinned FROM chat_settings_v2 WHERE uid = " + chat_id); if (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { info = TLRPC.ChatFull.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); info.pinned_msg_id = cursor.intValue(1); } - data.reuse(); } cursor.dispose(); @@ -1846,27 +1985,31 @@ public class MessagesStorage { info.participants = new TLRPC.TL_chatParticipants(); while (cursor.next()) { try { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - NativeByteBuffer data2 = new NativeByteBuffer(cursor.byteArrayLength(2)); - if (cursor.byteBufferValue(0, data) != 0 && cursor.byteBufferValue(2, data2) != 0) { - TLRPC.User user = TLRPC.User.TLdeserialize(data, data.readInt32(false), false); - TLRPC.ChannelParticipant participant = TLRPC.ChannelParticipant.TLdeserialize(data2, data2.readInt32(false), false); - if (user != null && participant != null) { - if (user.status != null) { - user.status.expires = cursor.intValue(1); - } - loadedUsers.add(user); - participant.date = cursor.intValue(3); - TLRPC.TL_chatChannelParticipant chatChannelParticipant = new TLRPC.TL_chatChannelParticipant(); - chatChannelParticipant.user_id = participant.user_id; - chatChannelParticipant.date = participant.date; - chatChannelParticipant.inviter_id = participant.inviter_id; - chatChannelParticipant.channelParticipant = participant; - info.participants.participants.add(chatChannelParticipant); - } + TLRPC.User user = null; + TLRPC.ChannelParticipant participant = null; + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { + user = TLRPC.User.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); + } + data = cursor.byteBufferValue(2); + if (data != null) { + participant = TLRPC.ChannelParticipant.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); + } + if (user != null && participant != null) { + if (user.status != null) { + user.status.expires = cursor.intValue(1); + } + loadedUsers.add(user); + participant.date = cursor.intValue(3); + TLRPC.TL_chatChannelParticipant chatChannelParticipant = new TLRPC.TL_chatChannelParticipant(); + chatChannelParticipant.user_id = participant.user_id; + chatChannelParticipant.date = participant.date; + chatChannelParticipant.inviter_id = participant.inviter_id; + chatChannelParticipant.channelParticipant = participant; + info.participants.participants.add(chatChannelParticipant); } - data.reuse(); - data2.reuse(); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -1902,57 +2045,48 @@ public class MessagesStorage { }); } - public void processPendingRead(final long dialog_id, final long max_id, final int max_date, final boolean delete) { + public void processPendingRead(final long dialog_id, final long max_id, final int max_date) { storageQueue.postRunnable(new Runnable() { @Override public void run() { try { - if (delete) { - //database.executeFast("DELETE FROM pending_read WHERE uid = " + dialog_id).stepThis().dispose(); - } else { - database.beginTransaction(); - SQLitePreparedStatement state;/* = database.executeFast("REPLACE INTO pending_read VALUES(?, ?)"); + database.beginTransaction(); + SQLitePreparedStatement state; + + int lower_id = (int) dialog_id; + + if (lower_id != 0) { + state = database.executeFast("UPDATE messages SET read_state = read_state | 1 WHERE uid = ? AND mid <= ? AND read_state IN(0,2) AND out = 0"); state.requery(); state.bindLong(1, dialog_id); - state.bindInteger(2, max_id); - state.step(); - state.dispose();*/ - - int lower_id = (int) dialog_id; - - if (lower_id != 0) { - state = database.executeFast("UPDATE messages SET read_state = read_state | 1 WHERE uid = ? AND mid <= ? AND read_state IN(0,2) AND out = 0"); - state.requery(); - state.bindLong(1, dialog_id); - state.bindLong(2, max_id); - state.step(); - state.dispose(); - } else { - state = database.executeFast("UPDATE messages SET read_state = read_state | 1 WHERE uid = ? AND date <= ? AND read_state IN(0,2) AND out = 0"); - state.requery(); - state.bindLong(1, dialog_id); - state.bindInteger(2, max_date); - state.step(); - state.dispose(); - } - - int currentMaxId = 0; - SQLiteCursor cursor = database.queryFinalized("SELECT inbox_max FROM dialogs WHERE did = " + dialog_id); - if (cursor.next()) { - currentMaxId = cursor.intValue(0); - } - cursor.dispose(); - currentMaxId = Math.max(currentMaxId, (int) max_id); - - state = database.executeFast("UPDATE dialogs SET unread_count = 0, unread_count_i = 0, inbox_max = ? WHERE did = ?"); - state.requery(); - state.bindInteger(1, currentMaxId); - state.bindLong(2, dialog_id); + state.bindLong(2, max_id); + state.step(); + state.dispose(); + } else { + state = database.executeFast("UPDATE messages SET read_state = read_state | 1 WHERE uid = ? AND date <= ? AND read_state IN(0,2) AND out = 0"); + state.requery(); + state.bindLong(1, dialog_id); + state.bindInteger(2, max_date); state.step(); state.dispose(); - - database.commitTransaction(); } + + int currentMaxId = 0; + SQLiteCursor cursor = database.queryFinalized("SELECT inbox_max FROM dialogs WHERE did = " + dialog_id); + if (cursor.next()) { + currentMaxId = cursor.intValue(0); + } + cursor.dispose(); + currentMaxId = Math.max(currentMaxId, (int) max_id); + + state = database.executeFast("UPDATE dialogs SET unread_count = 0, unread_count_i = 0, inbox_max = ? WHERE did = ?"); + state.requery(); + state.bindInteger(1, currentMaxId); + state.bindLong(2, dialog_id); + state.step(); + state.dispose(); + + database.commitTransaction(); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -2103,7 +2237,7 @@ public class MessagesStorage { contactHashMap.clear(); FileLog.e("tmessages", e); } - ContactsController.getInstance().performSyncPhoneBook(contactHashMap, true, true, false); + ContactsController.getInstance().performSyncPhoneBook(contactHashMap, true, true, false, false); } }); } @@ -2160,9 +2294,10 @@ public class MessagesStorage { ArrayList encryptedChatIds = new ArrayList<>(); SQLiteCursor cursor = database.queryFinalized("SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.uid, s.seq_in, s.seq_out, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid LEFT JOIN messages_seq as s ON m.mid = s.mid WHERE m.mid < 0 AND m.send_state = 1 ORDER BY m.mid DESC LIMIT " + count); while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(1)); - if (cursor.byteBufferValue(1, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(1); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (!messageHashMap.containsKey(message.id)) { MessageObject.setUnreadFlags(message, cursor.intValue(0)); message.id = cursor.intValue(3); @@ -2205,7 +2340,7 @@ public class MessagesStorage { addUsersAndChatsFromMessage(message, usersToLoad, chatsToLoad); message.send_state = cursor.intValue(2); - if (message.to_id.channel_id == 0 && !MessageObject.isUnread(message) && lower_id != 0 || message.id > 0) { + if (message.to_id.channel_id == 0 && !MessageObject.isUnread(message) && lower_id != 0 || message.id > 0) { //TODO check message.send_state = 0; } if (lower_id == 0 && !cursor.isNull(5)) { @@ -2213,7 +2348,6 @@ public class MessagesStorage { } } } - data.reuse(); } cursor.dispose(); @@ -2283,7 +2417,7 @@ public class MessagesStorage { return result[0]; } - public void getMessages(final long dialog_id, final int count, final int max_id, final int minDate, final int classGuid, final int load_type, final int important, final int loadIndex) { + public void getMessages(final long dialog_id, final int count, final int max_id, final int minDate, final int classGuid, final int load_type, final boolean isChannel, final int loadIndex) { storageQueue.postRunnable(new Runnable() { @Override public void run() { @@ -2298,7 +2432,7 @@ public class MessagesStorage { long messageMaxId = max_id; int max_id_query = max_id; int channelId = 0; - if (important != 0) { + if (isChannel) { channelId = -(int) dialog_id; } if (messageMaxId != 0 && channelId != 0) { @@ -2316,9 +2450,6 @@ public class MessagesStorage { SQLiteCursor cursor; int lower_id = (int) dialog_id; if (lower_id != 0) { - String imp = important == 2 ? " AND imp = 1 " : ""; - String holesTable = important == 2 ? "messages_imp_holes" : "messages_holes"; - if (load_type != 1 && load_type != 3 && minDate == 0) { if (load_type == 2) { cursor = database.queryFinalized("SELECT inbox_max, unread_count, date FROM dialogs WHERE did = " + dialog_id); @@ -2333,14 +2464,14 @@ public class MessagesStorage { } cursor.dispose(); if (!queryFromServer) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid), max(date) FROM messages WHERE uid = %d AND out = 0 AND read_state IN(0,2) AND mid > 0" + imp, dialog_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid), max(date) FROM messages WHERE uid = %d AND out = 0 AND read_state IN(0,2) AND mid > 0", dialog_id)); if (cursor.next()) { min_unread_id = cursor.intValue(0); max_unread_date = cursor.intValue(1); } cursor.dispose(); if (min_unread_id != 0) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(*) FROM messages WHERE uid = %d AND mid >= %d " + imp + "AND out = 0 AND read_state IN(0,2)", dialog_id, min_unread_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(*) FROM messages WHERE uid = %d AND mid >= %d AND out = 0 AND read_state IN(0,2)", dialog_id, min_unread_id)); if (cursor.next()) { count_unread = cursor.intValue(0); } @@ -2348,15 +2479,33 @@ public class MessagesStorage { } } else if (max_id_query == 0) { int existingUnreadCount = 0; - cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(*) FROM messages WHERE uid = %d AND mid > 0 " + imp + "AND out = 0 AND read_state IN(0,2)", dialog_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(*) FROM messages WHERE uid = %d AND mid > 0 AND out = 0 AND read_state IN(0,2)", dialog_id)); if (cursor.next()) { existingUnreadCount = cursor.intValue(0); } cursor.dispose(); if (existingUnreadCount == count_unread) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid) FROM messages WHERE uid = %d AND out = 0 AND read_state IN(0,2) AND mid > 0" + imp, dialog_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid) FROM messages WHERE uid = %d AND out = 0 AND read_state IN(0,2) AND mid > 0", dialog_id)); if (cursor.next()) { messageMaxId = max_id_query = min_unread_id = cursor.intValue(0); + if (messageMaxId != 0 && channelId != 0) { + messageMaxId |= ((long) channelId) << 32; + } + } + cursor.dispose(); + } + } else { + cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, end FROM messages_holes WHERE uid = %d AND start < %d AND end > %d", dialog_id, max_id_query, max_id_query)); + boolean containMessage = !cursor.next(); + cursor.dispose(); + + if (containMessage) { + cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid) FROM messages WHERE uid = %d AND out = 0 AND read_state IN(0,2) AND mid > %d", dialog_id, max_id_query)); + if (cursor.next()) { + messageMaxId = max_id_query = cursor.intValue(0); + if (messageMaxId != 0 && channelId != 0) { + messageMaxId |= ((long) channelId) << 32; + } } cursor.dispose(); } @@ -2378,7 +2527,7 @@ public class MessagesStorage { } } - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM " + holesTable + " WHERE uid = %d AND start IN (0, 1)", dialog_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM messages_holes WHERE uid = %d AND start IN (0, 1)", dialog_id)); if (cursor.next()) { isEnd = cursor.intValue(0) == 1; cursor.dispose(); @@ -2388,7 +2537,7 @@ public class MessagesStorage { if (cursor.next()) { int mid = cursor.intValue(0); if (mid != 0) { - SQLitePreparedStatement state = database.executeFast("REPLACE INTO " + holesTable + " VALUES(?, ?, ?)"); + SQLitePreparedStatement state = database.executeFast("REPLACE INTO messages_holes VALUES(?, ?, ?)"); state.requery(); state.bindLong(1, dialog_id); state.bindInteger(2, 0); @@ -2409,7 +2558,7 @@ public class MessagesStorage { boolean containMessage = max_id_query != 0; if (containMessage) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM " + holesTable + " WHERE uid = %d AND start < %d AND end > %d", dialog_id, max_id_query, max_id_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM messages_holes WHERE uid = %d AND start < %d AND end > %d", dialog_id, max_id_query, max_id_query)); if (cursor.next()) { containMessage = false; } @@ -2419,7 +2568,7 @@ public class MessagesStorage { if (containMessage) { long holeMessageMaxId = 0; long holeMessageMinId = 1; - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM " + holesTable + " WHERE uid = %d AND start >= %d ORDER BY start ASC LIMIT 1", dialog_id, max_id_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM messages_holes WHERE uid = %d AND start >= %d ORDER BY start ASC LIMIT 1", dialog_id, max_id_query)); if (cursor.next()) { holeMessageMaxId = cursor.intValue(0); if (channelId != 0) { @@ -2427,7 +2576,7 @@ public class MessagesStorage { } } cursor.dispose(); - cursor = database.queryFinalized(String.format(Locale.US, "SELECT end FROM " + holesTable + " WHERE uid = %d AND end <= %d ORDER BY end DESC LIMIT 1", dialog_id, max_id_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT end FROM messages_holes WHERE uid = %d AND end <= %d ORDER BY end DESC LIMIT 1", dialog_id, max_id_query)); if (cursor.next()) { holeMessageMinId = cursor.intValue(0); if (channelId != 0) { @@ -2446,18 +2595,18 @@ public class MessagesStorage { holeMessageMaxId |= ((long) channelId) << 32; } } - cursor = database.queryFinalized(String.format(Locale.US, "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid <= %d AND m.mid >= %d " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d) UNION " + - "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid > %d AND m.mid <= %d " + imp + "ORDER BY m.date ASC, m.mid ASC LIMIT %d)", dialog_id, messageMaxId, holeMessageMinId, count_query / 2, dialog_id, messageMaxId, holeMessageMaxId, count_query / 2)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid <= %d AND m.mid >= %d ORDER BY m.date DESC, m.mid DESC LIMIT %d) UNION " + + "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid > %d AND m.mid <= %d ORDER BY m.date ASC, m.mid ASC LIMIT %d)", dialog_id, messageMaxId, holeMessageMinId, count_query / 2, dialog_id, messageMaxId, holeMessageMaxId, count_query / 2)); } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid <= %d " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d) UNION " + - "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid > %d " + imp + "ORDER BY m.date ASC, m.mid ASC LIMIT %d)", dialog_id, messageMaxId, count_query / 2, dialog_id, messageMaxId, count_query / 2)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid <= %d ORDER BY m.date DESC, m.mid DESC LIMIT %d) UNION " + + "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid > %d ORDER BY m.date ASC, m.mid ASC LIMIT %d)", dialog_id, messageMaxId, count_query / 2, dialog_id, messageMaxId, count_query / 2)); } } else { cursor = null; } } else if (load_type == 1) { long holeMessageId = 0; - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, end FROM " + holesTable + " WHERE uid = %d AND start >= %d AND start != 1 AND end != 1 ORDER BY start ASC LIMIT 1", dialog_id, max_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, end FROM messages_holes WHERE uid = %d AND start >= %d AND start != 1 AND end != 1 ORDER BY start ASC LIMIT 1", dialog_id, max_id)); if (cursor.next()) { holeMessageId = cursor.intValue(0); if (channelId != 0) { @@ -2466,14 +2615,14 @@ public class MessagesStorage { } cursor.dispose(); if (holeMessageId != 0) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date >= %d AND m.mid > %d AND m.mid <= %d " + imp + "ORDER BY m.date ASC, m.mid ASC LIMIT %d", dialog_id, minDate, messageMaxId, holeMessageId, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date >= %d AND m.mid > %d AND m.mid <= %d ORDER BY m.date ASC, m.mid ASC LIMIT %d", dialog_id, minDate, messageMaxId, holeMessageId, count_query)); } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date >= %d AND m.mid > %d " + imp + "ORDER BY m.date ASC, m.mid ASC LIMIT %d", dialog_id, minDate, messageMaxId, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date >= %d AND m.mid > %d ORDER BY m.date ASC, m.mid ASC LIMIT %d", dialog_id, minDate, messageMaxId, count_query)); } } else if (minDate != 0) { if (messageMaxId != 0) { long holeMessageId = 0; - cursor = database.queryFinalized(String.format(Locale.US, "SELECT end FROM " + holesTable + " WHERE uid = %d AND end <= %d ORDER BY end DESC LIMIT 1", dialog_id, max_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT end FROM messages_holes WHERE uid = %d AND end <= %d ORDER BY end DESC LIMIT 1", dialog_id, max_id)); if (cursor.next()) { holeMessageId = cursor.intValue(0); if (channelId != 0) { @@ -2482,12 +2631,12 @@ public class MessagesStorage { } cursor.dispose(); if (holeMessageId != 0) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d AND m.mid < %d AND (m.mid >= %d OR m.mid < 0) " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d", dialog_id, minDate, messageMaxId, holeMessageId, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d AND m.mid < %d AND (m.mid >= %d OR m.mid < 0) ORDER BY m.date DESC, m.mid DESC LIMIT %d", dialog_id, minDate, messageMaxId, holeMessageId, count_query)); } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d AND m.mid < %d " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d", dialog_id, minDate, messageMaxId, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d AND m.mid < %d ORDER BY m.date DESC, m.mid DESC LIMIT %d", dialog_id, minDate, messageMaxId, count_query)); } } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, minDate, offset_query, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, minDate, offset_query, count_query)); } } else { cursor = database.queryFinalized(String.format(Locale.US, "SELECT max(mid) FROM messages WHERE uid = %d AND mid > 0", dialog_id)); @@ -2497,7 +2646,7 @@ public class MessagesStorage { cursor.dispose(); long holeMessageId = 0; - cursor = database.queryFinalized(String.format(Locale.US, "SELECT max(end) FROM " + holesTable + " WHERE uid = %d", dialog_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT max(end) FROM messages_holes WHERE uid = %d", dialog_id)); if (cursor.next()) { holeMessageId = cursor.intValue(0); if (channelId != 0) { @@ -2506,9 +2655,9 @@ public class MessagesStorage { } cursor.dispose(); if (holeMessageId != 0) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND (m.mid >= %d OR m.mid < 0) " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, holeMessageId, offset_query, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND (m.mid >= %d OR m.mid < 0) ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, holeMessageId, offset_query, count_query)); } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, offset_query, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, offset_query, count_query)); } } } else { @@ -2560,9 +2709,10 @@ public class MessagesStorage { } if (cursor != null) { while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(1)); - if (cursor.byteBufferValue(1, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(1); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); MessageObject.setUnreadFlags(message, cursor.intValue(0)); message.id = cursor.intValue(3); message.date = cursor.intValue(4); @@ -2570,21 +2720,23 @@ public class MessagesStorage { if ((message.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { message.views = cursor.intValue(7); } - message.ttl = cursor.intValue(8); + if (lower_id != 0) { + message.ttl = cursor.intValue(8); + } res.messages.add(message); addUsersAndChatsFromMessage(message, usersToLoad, chatsToLoad); if (message.reply_to_msg_id != 0 || message.reply_to_random_id != 0) { if (!cursor.isNull(6)) { - NativeByteBuffer data2 = new NativeByteBuffer(cursor.byteArrayLength(6)); - if (cursor.byteBufferValue(6, data2) != 0) { - message.replyMessage = TLRPC.Message.TLdeserialize(data2, data2.readInt32(false), false); + data = cursor.byteBufferValue(6); + if (data != null) { + message.replyMessage = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (message.replyMessage != null) { addUsersAndChatsFromMessage(message.replyMessage, usersToLoad, chatsToLoad); } } - data2.reuse(); } if (message.replyMessage == null) { if (message.reply_to_msg_id != 0) { @@ -2633,7 +2785,6 @@ public class MessagesStorage { } } } - data.reuse(); } cursor.dispose(); } @@ -2678,22 +2829,6 @@ public class MessagesStorage { res.messages.clear(); } - if (important == 2 && !res.messages.isEmpty()) { - if (max_id != 0) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, end, count FROM channel_group WHERE uid = %d AND ((start >= %d AND end <= %d) OR (start = %d))", dialog_id, res.messages.get(res.messages.size() - 1).id, res.messages.get(0).id, res.messages.get(0).id)); - } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, end, count FROM channel_group WHERE uid = %d AND start >= %d", dialog_id, res.messages.get(res.messages.size() - 1).id)); - } - while (cursor.next()) { - TLRPC.TL_messageGroup group = new TLRPC.TL_messageGroup(); - group.min_id = cursor.intValue(0); - group.max_id = cursor.intValue(1); - group.count = cursor.intValue(2); - res.collapsed.add(group); - } - cursor.dispose(); - } - if (!replyMessages.isEmpty()) { if (!replyMessageOwners.isEmpty()) { cursor = database.queryFinalized(String.format(Locale.US, "SELECT data, mid, date FROM messages WHERE mid IN(%s)", TextUtils.join(",", replyMessages))); @@ -2701,9 +2836,10 @@ public class MessagesStorage { cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.data, m.mid, m.date, r.random_id FROM randoms as r INNER JOIN messages as m ON r.mid = m.mid WHERE r.random_id IN(%s)", TextUtils.join(",", replyMessages))); } while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); message.id = cursor.intValue(1); message.date = cursor.intValue(2); message.dialog_id = dialog_id; @@ -2728,7 +2864,6 @@ public class MessagesStorage { } } } - data.reuse(); } cursor.dispose(); if (!replyMessageRandomOwners.isEmpty()) { @@ -2751,10 +2886,9 @@ public class MessagesStorage { res.messages.clear(); res.chats.clear(); res.users.clear(); - res.collapsed.clear(); FileLog.e("tmessages", e); } finally { - MessagesController.getInstance().processLoadedMessages(res, dialog_id, count_query, max_id, true, classGuid, min_unread_id, last_message_id, count_unread, max_unread_date, load_type, important, isEnd, loadIndex, queryFromServer); + MessagesController.getInstance().processLoadedMessages(res, dialog_id, count_query, max_id, true, classGuid, min_unread_id, last_message_id, count_unread, max_unread_date, load_type, isChannel, isEnd, loadIndex, queryFromServer); } } }); @@ -2816,16 +2950,16 @@ public class MessagesStorage { if (id != null) { SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM sent_files_v2 WHERE uid = '%s' AND type = %d", id, type)); if (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLObject file = TLRPC.MessageMedia.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (file instanceof TLRPC.TL_messageMediaDocument) { result.add(((TLRPC.TL_messageMediaDocument) file).document); } else if (file instanceof TLRPC.TL_messageMediaPhoto) { - result.add(((TLRPC.TL_messageMediaDocument) file).photo); + result.add(((TLRPC.TL_messageMediaPhoto) file).photo); } } - data.reuse(); } cursor.dispose(); } @@ -2901,7 +3035,7 @@ public class MessagesStorage { state = database.executeFast("UPDATE enc_chats SET seq_in = ?, seq_out = ?, use_count = ? WHERE uid = ?"); state.bindInteger(1, chat.seq_in); state.bindInteger(2, chat.seq_out); - state.bindInteger(3, (int)chat.key_use_count_in << 16 | chat.key_use_count_out); + state.bindInteger(3, (int) chat.key_use_count_in << 16 | chat.key_use_count_out); state.bindInteger(4, chat.id); state.step(); } catch (Exception e) { @@ -3052,6 +3186,31 @@ public class MessagesStorage { return result[0]; } + public boolean hasAuthMessage(final int date) { + final Semaphore semaphore = new Semaphore(0); + final boolean result[] = new boolean[1]; + storageQueue.postRunnable(new Runnable() { + @Override + public void run() { + try { + SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT mid FROM messages WHERE uid = 777000 AND date = %d AND mid < 0 LIMIT 1", date)); + result[0] = cursor.next(); + cursor.dispose(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } finally { + semaphore.release(); + } + } + }); + try { + semaphore.acquire(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + return result[0]; + } + public void getEncryptedChat(final int chat_id, final Semaphore semaphore, final ArrayList result) { if (semaphore == null || result == null) { return; @@ -3080,7 +3239,7 @@ public class MessagesStorage { }); } - public void putEncryptedChat(final TLRPC.EncryptedChat chat, final TLRPC.User user, final TLRPC.Dialog dialog) { + public void putEncryptedChat(final TLRPC.EncryptedChat chat, final TLRPC.User user, final TLRPC.TL_dialog dialog) { if (chat == null) { return; } @@ -3142,9 +3301,9 @@ public class MessagesStorage { state.bindInteger(3, dialog.unread_count); state.bindInteger(4, dialog.top_message); state.bindInteger(5, dialog.read_inbox_max_id); - state.bindInteger(6, 0); - state.bindInteger(7, dialog.top_not_important_message); - state.bindInteger(8, dialog.unread_not_important_count); + state.bindInteger(6, dialog.read_outbox_max_id); + state.bindInteger(7, 0); + state.bindInteger(8, 0); state.bindInteger(9, dialog.pts); state.bindInteger(10, 0); state.step(); @@ -3186,32 +3345,33 @@ public class MessagesStorage { SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM users WHERE uid = %d", user.id)); if (cursor.next()) { try { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.User oldUser = TLRPC.User.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (oldUser != null) { if (user.username != null) { oldUser.username = user.username; oldUser.flags |= 8; } else { oldUser.username = null; - oldUser.flags = oldUser.flags &~ 8; + oldUser.flags = oldUser.flags & ~8; } if (user.photo != null) { oldUser.photo = user.photo; oldUser.flags |= 32; } else { oldUser.photo = null; - oldUser.flags = oldUser.flags &~ 32; + oldUser.flags = oldUser.flags & ~32; } user = oldUser; } } - data.reuse(); } catch (Exception e) { FileLog.e("tmessages", e); } } + cursor.dispose(); } state.requery(); NativeByteBuffer data = new NativeByteBuffer(user.getObjectSize()); @@ -3248,9 +3408,10 @@ public class MessagesStorage { SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM chats WHERE uid = %d", chat.id)); if (cursor.next()) { try { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Chat oldChat = TLRPC.Chat.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (oldChat != null) { oldChat.title = chat.title; oldChat.photo = chat.photo; @@ -3263,16 +3424,16 @@ public class MessagesStorage { oldChat.flags |= 64; } else { oldChat.username = null; - oldChat.flags = oldChat.flags &~ 64; + oldChat.flags = oldChat.flags & ~64; } chat = oldChat; } } - data.reuse(); } catch (Exception e) { FileLog.e("tmessages", e); } } + cursor.dispose(); } state.requery(); NativeByteBuffer data = new NativeByteBuffer(chat.getObjectSize()); @@ -3298,9 +3459,10 @@ public class MessagesStorage { SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT data, status FROM users WHERE uid IN(%s)", usersToLoad)); while (cursor.next()) { try { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.User user = TLRPC.User.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (user != null) { if (user.status != null) { user.status.expires = cursor.intValue(1); @@ -3308,7 +3470,6 @@ public class MessagesStorage { result.add(user); } } - data.reuse(); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -3323,14 +3484,14 @@ public class MessagesStorage { SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM chats WHERE uid IN(%s)", chatsToLoad)); while (cursor.next()) { try { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Chat chat = TLRPC.Chat.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (chat != null) { result.add(chat); } } - data.reuse(); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -3345,9 +3506,10 @@ public class MessagesStorage { SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT data, user, g, authkey, ttl, layer, seq_in, seq_out, use_count, exchange_id, key_date, fprint, fauthkey, khash FROM enc_chats WHERE uid IN(%s)", chatsToLoad)); while (cursor.next()) { try { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.EncryptedChat chat = TLRPC.EncryptedChat.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (chat != null) { chat.user_id = cursor.intValue(1); if (usersToLoad != null && !usersToLoad.contains(chat.user_id)) { @@ -3360,8 +3522,8 @@ public class MessagesStorage { chat.seq_in = cursor.intValue(6); chat.seq_out = cursor.intValue(7); int use_count = cursor.intValue(8); - chat.key_use_count_in = (short)(use_count >> 16); - chat.key_use_count_out = (short)(use_count); + chat.key_use_count_in = (short) (use_count >> 16); + chat.key_use_count_out = (short) (use_count); chat.exchange_id = cursor.longValue(9); chat.key_create_date = cursor.intValue(10); chat.future_key_fingerprint = cursor.longValue(11); @@ -3370,7 +3532,6 @@ public class MessagesStorage { result.add(chat); } } - data.reuse(); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -3462,16 +3623,16 @@ public class MessagesStorage { DownloadObject downloadObject = new DownloadObject(); downloadObject.type = cursor.intValue(1); downloadObject.id = cursor.longValue(0); - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(2)); - if (cursor.byteBufferValue(2, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(2); + if (data != null) { TLRPC.MessageMedia messageMedia = TLRPC.MessageMedia.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (messageMedia.document != null) { downloadObject.object = messageMedia.document; } else if (messageMedia.photo != null) { downloadObject.object = FileLoader.getClosestPhotoSizeWithSize(messageMedia.photo.sizes, AndroidUtilities.getPhotoSize()); } } - data.reuse(); objects.add(downloadObject); } cursor.dispose(); @@ -3492,8 +3653,8 @@ public class MessagesStorage { private int getMessageMediaType(TLRPC.Message message) { if (message instanceof TLRPC.TL_message_secret && ( message.media instanceof TLRPC.TL_messageMediaPhoto && message.ttl > 0 && message.ttl <= 60 || - MessageObject.isVoiceMessage(message) || - MessageObject.isVideoMessage(message))) { + MessageObject.isVoiceMessage(message) || + MessageObject.isVideoMessage(message))) { return 1; } else if (message.media instanceof TLRPC.TL_messageMediaPhoto || MessageObject.isVideoMessage(message)) { return 0; @@ -3524,16 +3685,16 @@ public class MessagesStorage { cursor = database.queryFinalized(String.format(Locale.US, "SELECT mid, data FROM messages WHERE mid IN (%s)", TextUtils.join(",", mids))); while (cursor.next()) { int mid = cursor.intValue(0); - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(1)); - if (cursor.byteBufferValue(1, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(1); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (message.media instanceof TLRPC.TL_messageMediaWebPage) { message.id = mid; message.media.webpage = webPages.get(message.media.webpage.id); messages.add(message); } } - data.reuse(); } cursor.dispose(); @@ -3547,7 +3708,8 @@ public class MessagesStorage { SQLitePreparedStatement state = database.executeFast("UPDATE messages SET data = ? WHERE mid = ?"); SQLitePreparedStatement state2 = database.executeFast("UPDATE media_v2 SET data = ? WHERE mid = ?"); - for (TLRPC.Message message : messages) { + for (int a = 0; a < messages.size(); a++) { + TLRPC.Message message = messages.get(a); NativeByteBuffer data = new NativeByteBuffer(message.getObjectSize()); message.serializeToStream(data); @@ -3602,12 +3764,10 @@ public class MessagesStorage { } database.executeFast("DELETE FROM messages WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM channel_group WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM bot_keyboard WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_counts_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM messages_holes WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM messages_imp_holes WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_holes_v2 WHERE uid = " + did).stepThis().dispose(); BotQuery.clearBotKeyboard(did, null); @@ -3615,15 +3775,15 @@ public class MessagesStorage { dialogs.chats.addAll(difference.chats); dialogs.users.addAll(difference.users); dialogs.messages.addAll(difference.messages); - TLRPC.TL_dialogChannel dialog = new TLRPC.TL_dialogChannel(); + TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); dialog.id = did; + dialog.flags = 1; dialog.peer = new TLRPC.TL_peerChannel(); dialog.peer.channel_id = channel_id; - dialog.top_not_important_message = difference.top_message; - dialog.top_message = difference.top_important_message; + dialog.top_message = difference.top_message; dialog.read_inbox_max_id = difference.read_inbox_max_id; - dialog.unread_not_important_count = difference.unread_count; - dialog.unread_count = difference.unread_important_count; + dialog.read_outbox_max_id = difference.read_outbox_max_id; + dialog.unread_count = difference.unread_count; dialog.notify_settings = null; dialog.pts = difference.pts; dialogs.dialogs.add(dialog); @@ -3686,15 +3846,34 @@ public class MessagesStorage { return message.reply_markup != null && !(message.reply_markup instanceof TLRPC.TL_replyInlineMarkup) && (!message.reply_markup.selective || message.mentioned); } - private void putMessagesInternal(final ArrayList messages, final boolean withTransaction, final boolean doNotUpdateDialogDate, final int downloadMask) { + private void putMessagesInternal(final ArrayList messages, final boolean withTransaction, final boolean doNotUpdateDialogDate, final int downloadMask, boolean ifNoLastMessage) { try { + if (ifNoLastMessage) { + TLRPC.Message lastMessage = messages.get(0); + if (lastMessage.dialog_id == 0) { + if (lastMessage.to_id.user_id != 0) { + lastMessage.dialog_id = lastMessage.to_id.user_id; + } else if (lastMessage.to_id.chat_id != 0) { + lastMessage.dialog_id = -lastMessage.to_id.chat_id; + } else { + lastMessage.dialog_id = -lastMessage.to_id.channel_id; + } + } + int lastMid = -1; + SQLiteCursor cursor = database.queryFinalized("SELECT last_mid FROM dialogs WHERE did = " + lastMessage.dialog_id); + if (cursor.next()) { + lastMid = cursor.intValue(0); + } + cursor.dispose(); + if (lastMid != 0) { + return; + } + } if (withTransaction) { database.beginTransaction(); } HashMap messagesMap = new HashMap<>(); - HashMap messagesMapNotImportant = new HashMap<>(); HashMap messagesCounts = new HashMap<>(); - HashMap messagesCountsNotImportant = new HashMap<>(); HashMap> mediaCounts = null; HashMap botKeyboards = new HashMap<>(); @@ -3704,7 +3883,6 @@ public class MessagesStorage { StringBuilder messageIds = new StringBuilder(); HashMap dialogsReadMax = new HashMap<>(); HashMap messagesIdsMap = new HashMap<>(); - HashMap messagesIdsMapNotImportant = new HashMap<>(); SQLitePreparedStatement state = database.executeFast("REPLACE INTO messages VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)"); SQLitePreparedStatement state2 = null; @@ -3729,7 +3907,7 @@ public class MessagesStorage { messageId |= ((long) message.to_id.channel_id) << 32; } - if ((message.to_id.channel_id == 0 && MessageObject.isUnread(message) || MessageObject.isContentUnread(message)) && !MessageObject.isOut(message)) { + if (MessageObject.isUnread(message) && !MessageObject.isOut(message)) { Integer currentMaxId = dialogsReadMax.get(message.dialog_id); if (currentMaxId == null) { SQLiteCursor cursor = database.queryFinalized("SELECT inbox_max FROM dialogs WHERE did = " + message.dialog_id); @@ -3746,11 +3924,7 @@ public class MessagesStorage { messageIds.append(","); } messageIds.append(messageId); - if (message.to_id.channel_id == 0 || MessageObject.isMegagroup(message) || MessageObject.isImportant(message)) { - messagesIdsMap.put(messageId, message.dialog_id); - } else if (message.to_id.channel_id != 0) { - messagesIdsMapNotImportant.put(messageId, message.dialog_id); - } + messagesIdsMap.put(messageId, message.dialog_id); } } if (SharedMediaQuery.canAddMessageToMedia(message)) { @@ -3809,7 +3983,6 @@ public class MessagesStorage { SQLiteCursor cursor = database.queryFinalized("SELECT mid FROM messages WHERE mid IN(" + messageIds.toString() + ")"); while (cursor.next()) { messagesIdsMap.remove(cursor.longValue(0)); - messagesIdsMapNotImportant.remove(cursor.longValue(0)); } cursor.dispose(); for (Long dialog_id : messagesIdsMap.values()) { @@ -3820,14 +3993,6 @@ public class MessagesStorage { count++; messagesCounts.put(dialog_id, count); } - for (Long dialog_id : messagesIdsMapNotImportant.values()) { - Integer count = messagesCountsNotImportant.get(dialog_id); - if (count == null) { - count = 0; - } - count++; - messagesCountsNotImportant.put(dialog_id, count); - } } int downloadMediaMask = 0; @@ -3853,17 +4018,9 @@ public class MessagesStorage { } if (updateDialog) { - TLRPC.Message lastMessage; - if (message.to_id.channel_id == 0 || MessageObject.isMegagroup(message) || MessageObject.isImportant(message)) { - lastMessage = messagesMap.get(message.dialog_id); - if (lastMessage == null || message.date > lastMessage.date || message.id > 0 && lastMessage.id > 0 && message.id > lastMessage.id || message.id < 0 && lastMessage.id < 0 && message.id < lastMessage.id) { - messagesMap.put(message.dialog_id, message); - } - } else if (message.to_id.channel_id != 0) { - lastMessage = messagesMapNotImportant.get(message.dialog_id); - if (lastMessage == null || message.date > lastMessage.date || message.id > 0 && lastMessage.id > 0 && message.id > lastMessage.id || message.id < 0 && lastMessage.id < 0 && message.id < lastMessage.id) { - messagesMapNotImportant.put(message.dialog_id, message); - } + TLRPC.Message lastMessage = messagesMap.get(message.dialog_id); + if (lastMessage == null || message.date > lastMessage.date || message.id > 0 && lastMessage.id > 0 && message.id > lastMessage.id || message.id < 0 && lastMessage.id < 0 && message.id < lastMessage.id) { + messagesMap.put(message.dialog_id, message); } } @@ -3880,7 +4037,7 @@ public class MessagesStorage { } else { state.bindInteger(9, getMessageMediaType(message)); } - state.bindInteger(10, MessageObject.isImportant(message) ? 1 : 0); + state.bindInteger(10, 0); state.step(); if (message.random_id != 0) { @@ -3912,7 +4069,7 @@ public class MessagesStorage { data.reuse(); - if ((message.to_id.channel_id == 0 || MessageObject.isImportant(message)) && message.date >= ConnectionsManager.getInstance().getCurrentTime() - 60 * 60 && downloadMask != 0) { + if ((message.to_id.channel_id == 0 || message.post) && message.date >= ConnectionsManager.getInstance().getCurrentTime() - 60 * 60 && downloadMask != 0) { if (message.media instanceof TLRPC.TL_messageMediaPhoto || message.media instanceof TLRPC.TL_messageMediaDocument) { int type = 0; long id = 0; @@ -3979,7 +4136,6 @@ public class MessagesStorage { state = database.executeFast("REPLACE INTO dialogs VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); HashMap dids = new HashMap<>(); dids.putAll(messagesMap); - dids.putAll(messagesMapNotImportant); for (HashMap.Entry pair : dids.entrySet()) { Long key = pair.getKey(); @@ -3987,34 +4143,26 @@ public class MessagesStorage { continue; } TLRPC.Message message = messagesMap.get(key); - TLRPC.Message messageNotImportant = messagesMapNotImportant.get(key); int channelId = 0; if (message != null) { channelId = message.to_id.channel_id; } - if (messageNotImportant != null) { - channelId = messageNotImportant.to_id.channel_id; - } - SQLiteCursor cursor = database.queryFinalized("SELECT date, unread_count, last_mid_i, unread_count_i, pts, date_i, last_mid, inbox_max FROM dialogs WHERE did = " + key); + SQLiteCursor cursor = database.queryFinalized("SELECT date, unread_count, pts, last_mid, inbox_max, outbox_max FROM dialogs WHERE did = " + key); int dialog_date = 0; int last_mid = 0; int old_unread_count = 0; - int last_mid_i = 0; - int old_unread_count_i = 0; int pts = channelId != 0 ? 1 : 0; - int dialog_date_i = 0; int inbox_max = 0; + int outbox_max = 0; if (cursor.next()) { dialog_date = cursor.intValue(0); old_unread_count = cursor.intValue(1); - last_mid_i = cursor.intValue(2); - old_unread_count_i = cursor.intValue(3); - pts = cursor.intValue(4); - dialog_date_i = cursor.intValue(5); - last_mid = cursor.intValue(6); - inbox_max = cursor.intValue(7); + pts = cursor.intValue(2); + last_mid = cursor.intValue(3); + inbox_max = cursor.intValue(4); + outbox_max = cursor.intValue(5); } else if (channelId != 0) { MessagesController.getInstance().checkChannelInviter(channelId); } @@ -4026,28 +4174,15 @@ public class MessagesStorage { } else { messagesCounts.put(key, unread_count + old_unread_count); } - Integer unread_count_i = messagesCountsNotImportant.get(key); - if (unread_count_i == null) { - unread_count_i = 0; - } else { - messagesCountsNotImportant.put(key, unread_count_i + old_unread_count_i); - } long messageId = message != null ? message.id : last_mid; if (message != null) { if (message.local_id != 0) { messageId = message.local_id; } } - long messageIdNotImportant = messageNotImportant != null ? messageNotImportant.id : last_mid_i; - if (messageNotImportant != null) { - if (messageNotImportant.local_id != 0) { - messageIdNotImportant = messageNotImportant.local_id; - } - } if (channelId != 0) { messageId |= ((long) channelId) << 32; - messageIdNotImportant |= ((long) channelId) << 32; } state.requery(); @@ -4060,15 +4195,11 @@ public class MessagesStorage { state.bindInteger(3, old_unread_count + unread_count); state.bindLong(4, messageId); state.bindInteger(5, inbox_max); - state.bindInteger(6, 0); - state.bindLong(7, messageIdNotImportant); - state.bindInteger(8, unread_count_i + old_unread_count_i); + state.bindInteger(6, outbox_max); + state.bindLong(7, 0); + state.bindInteger(8, 0); state.bindInteger(9, pts); - if (messageNotImportant != null && (!doNotUpdateDialogDate || dialog_date == 0)) { - state.bindInteger(10, messageNotImportant.date); - } else { - state.bindInteger(10, dialog_date_i); - } + state.bindInteger(10, 0); state.step(); } state.dispose(); @@ -4118,6 +4249,10 @@ public class MessagesStorage { } public void putMessages(final ArrayList messages, final boolean withTransaction, boolean useQueue, final boolean doNotUpdateDialogDate, final int downloadMask) { + putMessages(messages, withTransaction, useQueue, doNotUpdateDialogDate, downloadMask, false); + } + + public void putMessages(final ArrayList messages, final boolean withTransaction, boolean useQueue, final boolean doNotUpdateDialogDate, final int downloadMask, final boolean ifNoLastMessage) { if (messages.size() == 0) { return; } @@ -4125,11 +4260,11 @@ public class MessagesStorage { storageQueue.postRunnable(new Runnable() { @Override public void run() { - putMessagesInternal(messages, withTransaction, doNotUpdateDialogDate, downloadMask); + putMessagesInternal(messages, withTransaction, doNotUpdateDialogDate, downloadMask, ifNoLastMessage); } }); } else { - putMessagesInternal(messages, withTransaction, doNotUpdateDialogDate, downloadMask); + putMessagesInternal(messages, withTransaction, doNotUpdateDialogDate, downloadMask, ifNoLastMessage); } } @@ -4283,7 +4418,7 @@ public class MessagesStorage { } } - return new long[] {did, newId}; + return new long[]{did, newId}; } else { SQLitePreparedStatement state = null; try { @@ -4336,7 +4471,7 @@ public class MessagesStorage { } } - return new long[] {did, _oldId}; + return new long[]{did, _oldId}; } } @@ -4438,7 +4573,7 @@ public class MessagesStorage { } } - private void markMessagesAsReadInternal(SparseArray inbox, SparseIntArray outbox, HashMap encryptedMessages) { + private void markMessagesAsReadInternal(SparseArray inbox, SparseArray outbox, HashMap encryptedMessages) { try { if (inbox != null) { for (int b = 0; b < inbox.size(); b++) { @@ -4450,13 +4585,13 @@ public class MessagesStorage { if (outbox != null) { for (int b = 0; b < outbox.size(); b++) { int key = outbox.keyAt(b); - int messageId = outbox.get(key); + long messageId = outbox.get(key); database.executeFast(String.format(Locale.US, "UPDATE messages SET read_state = read_state | 1 WHERE uid = %d AND mid > 0 AND mid <= %d AND read_state IN(0,2) AND out = 1", key, messageId)).stepThis().dispose(); } } if (encryptedMessages != null && !encryptedMessages.isEmpty()) { for (HashMap.Entry entry : encryptedMessages.entrySet()) { - long dialog_id = ((long)entry.getKey()) << 32; + long dialog_id = ((long) entry.getKey()) << 32; int max_date = entry.getValue(); SQLitePreparedStatement state = database.executeFast("UPDATE messages SET read_state = read_state | 1 WHERE uid = ? AND date <= ? AND read_state IN(0,2) AND out = 1"); state.requery(); @@ -4487,7 +4622,7 @@ public class MessagesStorage { }); } - public void markMessagesAsRead(final SparseArray inbox, final SparseIntArray outbox, final HashMap encryptedMessages, boolean useQueue) { + public void markMessagesAsRead(final SparseArray inbox, final SparseArray outbox, final HashMap encryptedMessages, boolean useQueue) { if (useQueue) { storageQueue.postRunnable(new Runnable() { @Override @@ -4522,7 +4657,7 @@ public class MessagesStorage { NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesDeleted, mids, 0); } }); - MessagesStorage.getInstance().updateDialogsWithReadMessagesInternal(mids, null); + MessagesStorage.getInstance().updateDialogsWithReadMessagesInternal(mids, null, null); MessagesStorage.getInstance().markMessagesAsDeletedInternal(mids, 0); MessagesStorage.getInstance().updateDialogsWithDeletedMessagesInternal(mids, 0); } @@ -4562,10 +4697,11 @@ public class MessagesStorage { if ((int) did != 0) { continue; } - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(1)); - if (cursor.byteBufferValue(1, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(1); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); - if (message != null && message.media != null) { + data.reuse(); + if (message != null) { if (message.media instanceof TLRPC.TL_messageMediaPhoto) { for (TLRPC.PhotoSize photoSize : message.media.photo.sizes) { File file = FileLoader.getPathToAttach(photoSize); @@ -4585,7 +4721,6 @@ public class MessagesStorage { } } } - data.reuse(); } } catch (Exception e) { FileLog.e("tmessages", e); @@ -4658,28 +4793,24 @@ public class MessagesStorage { ArrayList usersToLoad = new ArrayList<>(); ArrayList chatsToLoad = new ArrayList<>(); ArrayList encryptedToLoad = new ArrayList<>(); - SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT d.did, d.last_mid, d.unread_count, d.date, m.data, m.read_state, m.mid, m.send_state, m.date, d.last_mid_i, d.unread_count_i, d.pts, d.inbox_max FROM dialogs as d LEFT JOIN messages as m ON d.last_mid = m.mid WHERE d.did IN(%s)", ids)); + SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT d.did, d.last_mid, d.unread_count, d.date, m.data, m.read_state, m.mid, m.send_state, m.date, d.pts, d.inbox_max, d.outbox_max FROM dialogs as d LEFT JOIN messages as m ON d.last_mid = m.mid WHERE d.did IN(%s)", ids)); while (cursor.next()) { - TLRPC.Dialog dialog; - if (channelId == 0) { - dialog = new TLRPC.TL_dialog(); - } else { - dialog = new TLRPC.TL_dialogChannel(); - } + TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); dialog.id = cursor.longValue(0); dialog.top_message = cursor.intValue(1); - dialog.read_inbox_max_id = cursor.intValue(13); + dialog.read_inbox_max_id = cursor.intValue(10); + dialog.read_outbox_max_id = cursor.intValue(11); dialog.unread_count = cursor.intValue(2); dialog.last_message_date = cursor.intValue(3); - dialog.pts = cursor.intValue(11); - dialog.top_not_important_message = cursor.intValue(9); - dialog.unread_not_important_count = cursor.intValue(10); + dialog.pts = cursor.intValue(9); + dialog.flags = channelId == 0 ? 0 : 1; dialogs.dialogs.add(dialog); - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(4)); - if (cursor.byteBufferValue(4, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(4); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); MessageObject.setUnreadFlags(message, cursor.intValue(5)); message.id = cursor.intValue(6); message.send_state = cursor.intValue(7); @@ -4692,10 +4823,9 @@ public class MessagesStorage { addUsersAndChatsFromMessage(message, usersToLoad, chatsToLoad); } - data.reuse(); - int lower_id = (int)dialog.id; - int high_id = (int)(dialog.id >> 32); + int lower_id = (int) dialog.id; + int high_id = (int) (dialog.id >> 32); if (lower_id != 0) { if (high_id == 1) { if (!chatsToLoad.contains(lower_id)) { @@ -4985,78 +5115,33 @@ public class MessagesStorage { } } - public void putMessages(final TLRPC.messages_Messages messages, final long dialog_id, final int load_type, final int max_id, final int important, final boolean createDialog) { + public void putMessages(final TLRPC.messages_Messages messages, final long dialog_id, final int load_type, final int max_id, final boolean createDialog) { storageQueue.postRunnable(new Runnable() { @Override public void run() { try { if (messages.messages.isEmpty()) { if (load_type == 0) { - if (important != 2) { - doneHolesInTable("messages_holes", dialog_id, max_id); - doneHolesInMedia(dialog_id, max_id, -1); - } - if (important != 0) { - doneHolesInTable("messages_imp_holes", dialog_id, max_id); - } + doneHolesInTable("messages_holes", dialog_id, max_id); + doneHolesInMedia(dialog_id, max_id, -1); } return; } database.beginTransaction(); - if (!messages.collapsed.isEmpty() && important == 2) { - int maxId, minId; - int count = messages.collapsed.size(); - for (int a = 0; a < count; a++) { - TLRPC.TL_messageGroup group = messages.collapsed.get(a); - if (a < count - 1) { - minId = group.max_id; - maxId = messages.collapsed.get(a + 1).min_id; - closeHolesInTable("messages_holes", dialog_id, minId, maxId); - closeHolesInMedia(dialog_id, minId, maxId, -1); - } - if (a == 0) { - minId = messages.messages.get(messages.messages.size() - 1).id; - maxId = minId > group.min_id ? group.max_id : group.min_id; - closeHolesInTable("messages_holes", dialog_id, minId, maxId); - closeHolesInMedia(dialog_id, minId, maxId, -1); - } - if (a == count - 1) { - maxId = messages.messages.get(0).id; - minId = maxId < group.max_id ? group.min_id : group.max_id; - closeHolesInTable("messages_holes", dialog_id, minId, maxId); - closeHolesInMedia(dialog_id, minId, maxId, -1); - } - } - } if (load_type == 0) { int minId = messages.messages.get(messages.messages.size() - 1).id; - if (important != 2 || messages.collapsed.isEmpty()) { - closeHolesInTable("messages_holes", dialog_id, minId, max_id); - closeHolesInMedia(dialog_id, minId, max_id, -1); - } - if (important != 0) { - closeHolesInTable("messages_imp_holes", dialog_id, minId, max_id); - } + closeHolesInTable("messages_holes", dialog_id, minId, max_id); + closeHolesInMedia(dialog_id, minId, max_id, -1); } else if (load_type == 1) { int maxId = messages.messages.get(0).id; - if (important != 2 || messages.collapsed.isEmpty()) { - closeHolesInTable("messages_holes", dialog_id, max_id, maxId); - closeHolesInMedia(dialog_id, max_id, maxId, -1); - } - if (important != 0) { - closeHolesInTable("messages_imp_holes", dialog_id, max_id, maxId); - } + closeHolesInTable("messages_holes", dialog_id, max_id, maxId); + closeHolesInMedia(dialog_id, max_id, maxId, -1); } else if (load_type == 3 || load_type == 2) { int maxId = max_id == 0 ? Integer.MAX_VALUE : messages.messages.get(0).id; int minId = messages.messages.get(messages.messages.size() - 1).id; - if (important != 2 || messages.collapsed.isEmpty()) { - closeHolesInTable("messages_holes", dialog_id, minId, maxId); - closeHolesInMedia(dialog_id, minId, maxId, -1); - } - if (important != 0) { - closeHolesInTable("messages_imp_holes", dialog_id, minId, maxId); - } + closeHolesInTable("messages_holes", dialog_id, minId, maxId); + closeHolesInMedia(dialog_id, minId, maxId, -1); } int count = messages.messages.size(); @@ -5067,12 +5152,10 @@ public class MessagesStorage { SQLitePreparedStatement state = database.executeFast("REPLACE INTO messages VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)"); SQLitePreparedStatement state2 = database.executeFast("REPLACE INTO media_v2 VALUES(?, ?, ?, ?, ?)"); + SQLitePreparedStatement state5 = null; TLRPC.Message botKeyboard = null; - int countBeforeImportant = 0; - int countAfterImportant = 0; int minChannelMessageId = Integer.MAX_VALUE; int maxChannelMessageId = 0; - int lastChannelImportantId = -1; int channelId = 0; for (int a = 0; a < count; a++) { TLRPC.Message message = messages.messages.get(a); @@ -5086,8 +5169,18 @@ public class MessagesStorage { } if (load_type == -2) { - SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT mid FROM messages WHERE mid = %d", messageId)); - boolean exist = cursor.next(); + SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT mid, data FROM messages WHERE mid = %d", messageId)); + boolean exist; + if (exist = cursor.next()) { + NativeByteBuffer data = cursor.byteBufferValue(1); + if (data != null) { + TLRPC.Message oldMessage = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); + if (oldMessage != null) { + message.attachPath = oldMessage.attachPath; + } + } + } cursor.dispose(); if (!exist) { continue; @@ -5110,29 +5203,6 @@ public class MessagesStorage { state3.dispose(); } - boolean isImportant = MessageObject.isImportant(message); - if (load_type >= 0 && important == 1) { - if (isImportant) { - minChannelMessageId = Math.min(minChannelMessageId, message.id); - maxChannelMessageId = Math.max(maxChannelMessageId, message.id); - if (lastChannelImportantId == -1) { - countBeforeImportant = countAfterImportant; - } else { - if (countAfterImportant != 0) { - TLRPC.TL_messageGroup group = new TLRPC.TL_messageGroup(); - group.max_id = lastChannelImportantId; - group.min_id = message.id; - group.count = countAfterImportant; - messages.collapsed.add(group); - } - } - countAfterImportant = 0; - lastChannelImportantId = message.id; - } else { - countAfterImportant++; - } - } - fixUnsupportedMedia(message); state.requery(); NativeByteBuffer data = new NativeByteBuffer(message.getObjectSize()); @@ -5150,7 +5220,7 @@ public class MessagesStorage { } else { state.bindInteger(9, 0); } - state.bindInteger(10, isImportant ? 1 : 0); + state.bindInteger(10, 0); state.step(); if (SharedMediaQuery.canAddMessageToMedia(message)) { @@ -5163,6 +5233,15 @@ public class MessagesStorage { state2.step(); } data.reuse(); + if (message.media instanceof TLRPC.TL_messageMediaWebPage && message.media.webpage instanceof TLRPC.TL_webPagePending) { + if (state5 == null) { + state5 = database.executeFast("REPLACE INTO webpage_pending VALUES(?, ?)"); + } + state5.requery(); + state5.bindLong(1, message.media.webpage.id); + state5.bindLong(2, messageId); + state5.step(); + } if (load_type == 0 && isValidKeyboardToSave(message)) { if (botKeyboard == null || botKeyboard.id < message.id) { @@ -5172,62 +5251,13 @@ public class MessagesStorage { } state.dispose(); state2.dispose(); + if (state5 != null) { + state5.dispose(); + } if (botKeyboard != null) { BotQuery.putBotKeyboard(dialog_id, botKeyboard); } - if (load_type >= 0 && important != 0) { - /*if ((messages.flags & 1) == 0) { - if (countBeforeImportant != 0) { - if (load_type == 0) { - SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, count FROM channel_group WHERE uid = %d AND start <= %d ORDER BY start ASC LIMIT 1", dialog_id, maxChannelMessageId)); - if (cursor.next()) { - int currentStart = cursor.intValue(0); - int currentCount = cursor.intValue(1); - database.executeFast(String.format(Locale.US, "UPDATE channel_group SET start = %d, count = %d WHERE uid = %d AND start = %d", maxChannelMessageId, cursor.intValue(1) + countBeforeImportant, dialog_id, cursor.intValue(0))).stepThis().dispose(); - } else { - TLRPC.TL_messageGroup group = new TLRPC.TL_messageGroup(); - group.max_id = max_id != 0 ? max_id : Integer.MAX_VALUE; - group.min_id = maxChannelMessageId; - group.count = countBeforeImportant; - messages.collapsed.add(group); - } - cursor.dispose(); - } - } - if (countAfterImportant != 0) { - if (load_type == 0) { - TLRPC.TL_messageGroup group = new TLRPC.TL_messageGroup(); - group.max_id = minChannelMessageId; - group.min_id = 0; - group.count = countBeforeImportant; - messages.collapsed.add(group); - } - } - }*/ - if (!messages.collapsed.isEmpty()) { - state = database.executeFast("REPLACE INTO channel_group VALUES(?, ?, ?, ?)"); - for (int a = 0; a < messages.collapsed.size(); a++) { - TLRPC.TL_messageGroup group = messages.collapsed.get(a); - if (group.min_id > group.max_id) { - int temp = group.min_id; - group.min_id = group.max_id; - group.max_id = temp; - } - state.requery(); - state.bindLong(1, dialog_id); - state.bindInteger(2, group.min_id); - state.bindInteger(3, group.max_id); - state.bindInteger(4, group.count); - state.step(); - } - state.dispose(); - } - if (important == 1) { - messages.collapsed.clear(); - } - } - putUsersInternal(messages.users); putChatsInternal(messages.chats); @@ -5277,6 +5307,16 @@ public class MessagesStorage { } } } + if (!message.entities.isEmpty()) { + for (int a = 0; a < message.entities.size(); a++) { + TLRPC.MessageEntity entity = message.entities.get(a); + if (entity instanceof TLRPC.TL_messageEntityMentionName) { + usersToLoad.add(((TLRPC.TL_messageEntityMentionName) entity).user_id); + } else if (entity instanceof TLRPC.TL_inputMessageEntityMentionName) { + usersToLoad.add(((TLRPC.TL_inputMessageEntityMentionName) entity).user_id.user_id); + } + } + } if (message.media != null) { if (message.media.user_id != 0 && !usersToLoad.contains(message.media.user_id)) { usersToLoad.add(message.media.user_id); @@ -5314,39 +5354,32 @@ public class MessagesStorage { ArrayList encryptedToLoad = new ArrayList<>(); ArrayList replyMessages = new ArrayList<>(); HashMap replyMessageOwners = new HashMap<>(); - SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT d.did, d.last_mid, d.unread_count, d.date, m.data, m.read_state, m.mid, m.send_state, s.flags, m.date, d.last_mid_i, d.unread_count_i, d.pts, d.inbox_max, d.date_i, m.replydata FROM dialogs as d LEFT JOIN messages as m ON d.last_mid = m.mid LEFT JOIN dialog_settings as s ON d.did = s.did ORDER BY d.date DESC LIMIT %d,%d", offset, count)); + SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT d.did, d.last_mid, d.unread_count, d.date, m.data, m.read_state, m.mid, m.send_state, s.flags, m.date, d.pts, d.inbox_max, d.outbox_max, m.replydata FROM dialogs as d LEFT JOIN messages as m ON d.last_mid = m.mid LEFT JOIN dialog_settings as s ON d.did = s.did ORDER BY d.date DESC LIMIT %d,%d", offset, count)); while (cursor.next()) { - TLRPC.Dialog dialog; - int pts = cursor.intValue(12); - long id = cursor.longValue(0); - if (pts == 0 || (int) id > 0) { - dialog = new TLRPC.TL_dialog(); - } else { - dialog = new TLRPC.TL_dialogChannel(); - } - dialog.id = id; + TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); + dialog.id = cursor.longValue(0); dialog.top_message = cursor.intValue(1); dialog.unread_count = cursor.intValue(2); dialog.last_message_date = cursor.intValue(3); - dialog.pts = pts; - dialog.read_inbox_max_id = cursor.intValue(13); - dialog.last_message_date_i = cursor.intValue(14); - dialog.top_not_important_message = cursor.intValue(10); - dialog.unread_not_important_count = cursor.intValue(11); + dialog.pts = cursor.intValue(10); + dialog.flags = dialog.pts == 0 || (int) dialog.id > 0 ? 0 : 1; + dialog.read_inbox_max_id = cursor.intValue(11); + dialog.read_outbox_max_id = cursor.intValue(12); long flags = cursor.longValue(8); - int low_flags = (int)flags; + int low_flags = (int) flags; dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); if ((low_flags & 1) != 0) { - dialog.notify_settings.mute_until = (int)(flags >> 32); + dialog.notify_settings.mute_until = (int) (flags >> 32); if (dialog.notify_settings.mute_until == 0) { dialog.notify_settings.mute_until = Integer.MAX_VALUE; } } dialogs.dialogs.add(dialog); - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(4)); - if (cursor.byteBufferValue(4, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(4); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (message != null) { MessageObject.setUnreadFlags(message, cursor.intValue(5)); message.id = cursor.intValue(6); @@ -5362,15 +5395,15 @@ public class MessagesStorage { try { if (message.reply_to_msg_id != 0 && message.action instanceof TLRPC.TL_messageActionPinMessage) { - if (!cursor.isNull(15)) { - NativeByteBuffer data2 = new NativeByteBuffer(cursor.byteArrayLength(15)); - if (cursor.byteBufferValue(15, data2) != 0) { - message.replyMessage = TLRPC.Message.TLdeserialize(data2, data2.readInt32(false), false); + if (!cursor.isNull(13)) { + data = cursor.byteBufferValue(13); + if (data != null) { + message.replyMessage = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (message.replyMessage != null) { addUsersAndChatsFromMessage(message.replyMessage, usersToLoad, chatsToLoad); } } - data2.reuse(); } if (message.replyMessage == null) { long messageId = message.reply_to_msg_id; @@ -5388,10 +5421,9 @@ public class MessagesStorage { } } } - data.reuse(); - int lower_id = (int)dialog.id; - int high_id = (int)(dialog.id >> 32); + int lower_id = (int) dialog.id; + int high_id = (int) (dialog.id >> 32); if (lower_id != 0) { if (high_id == 1) { if (!chatsToLoad.contains(lower_id)) { @@ -5419,9 +5451,10 @@ public class MessagesStorage { if (!replyMessages.isEmpty()) { cursor = database.queryFinalized(String.format(Locale.US, "SELECT data, mid, date, uid FROM messages WHERE mid IN(%s)", TextUtils.join(",", replyMessages))); while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); message.id = cursor.intValue(1); message.date = cursor.intValue(2); message.dialog_id = cursor.longValue(3); @@ -5434,7 +5467,6 @@ public class MessagesStorage { message.dialog_id = owner.dialog_id; } } - data.reuse(); } cursor.dispose(); } @@ -5449,119 +5481,43 @@ public class MessagesStorage { if (!usersToLoad.isEmpty()) { getUsersInternal(TextUtils.join(",", usersToLoad), dialogs.users); } - MessagesController.getInstance().processLoadedDialogs(dialogs, encryptedChats, offset, count, true, false, false); + MessagesController.getInstance().processLoadedDialogs(dialogs, encryptedChats, offset, count, 1, false, false); } catch (Exception e) { dialogs.dialogs.clear(); dialogs.users.clear(); dialogs.chats.clear(); encryptedChats.clear(); FileLog.e("tmessages", e); - MessagesController.getInstance().processLoadedDialogs(dialogs, encryptedChats, 0, 100, true, true, false); + MessagesController.getInstance().processLoadedDialogs(dialogs, encryptedChats, 0, 100, 1, true, false); } } }); } - public static void createFirstHoles(long did, SQLitePreparedStatement state5, SQLitePreparedStatement state6, SQLitePreparedStatement state7, SQLitePreparedStatement state8, ArrayList arrayList) throws Exception { - int impMessageId = 0; - int notImpMessageId = 0; - for (int a = 0; a < arrayList.size(); a++) { - TLRPC.Message message = arrayList.get(a); + public static void createFirstHoles(long did, SQLitePreparedStatement state5, SQLitePreparedStatement state6, int messageId) throws Exception { + state5.requery(); + state5.bindLong(1, did); + state5.bindInteger(2, messageId == 1 ? 1 : 0); + state5.bindInteger(3, messageId); + state5.step(); - if (MessageObject.isImportant(message)) { - state7.requery(); - state7.bindLong(1, did); - state7.bindInteger(2, message.id == 1 ? 1 : 0); - state7.bindInteger(3, message.id); - state7.step(); - impMessageId = Math.max(message.id, impMessageId); - } else { - notImpMessageId = Math.max(message.id, notImpMessageId); - } - } - - if (impMessageId != 0 && notImpMessageId == 0) { - notImpMessageId = impMessageId; - impMessageId = 0; - } - - if (arrayList.size() == 1) { - int messageId = arrayList.get(0).id; - - state5.requery(); - state5.bindLong(1, did); - state5.bindInteger(2, messageId == 1 ? 1 : 0); - state5.bindInteger(3, messageId); - state5.step(); - - for (int b = 0; b < SharedMediaQuery.MEDIA_TYPES_COUNT; b++) { - state6.requery(); - state6.bindLong(1, did); - state6.bindInteger(2, b); - state6.bindInteger(3, messageId == 1 ? 1 : 0); - state6.bindInteger(4, messageId); - state6.step(); - } - } else if (arrayList.size() == 2) { - int firstId = arrayList.get(0).id; - int lastId = arrayList.get(1).id; - if (firstId > lastId) { - int temp = firstId; - firstId = lastId; - lastId = temp; - } - - state5.requery(); - state5.bindLong(1, did); - state5.bindInteger(2, firstId == 1 ? 1 : 0); - state5.bindInteger(3, firstId); - state5.step(); - - state5.requery(); - state5.bindLong(1, did); - state5.bindInteger(2, firstId); - state5.bindInteger(3, lastId); - state5.step(); - - for (int b = 0; b < SharedMediaQuery.MEDIA_TYPES_COUNT; b++) { - state6.requery(); - state6.bindLong(1, did); - state6.bindInteger(2, b); - state6.bindInteger(3, firstId == 1 ? 1 : 0); - state6.bindInteger(4, firstId); - state6.step(); - - state6.requery(); - state6.bindLong(1, did); - state6.bindInteger(2, b); - state6.bindInteger(3, firstId); - state6.bindInteger(4, lastId); - state6.step(); - } - - if (impMessageId != 0 && impMessageId < notImpMessageId) { - state8.requery(); - state8.bindLong(1, did); - state8.bindInteger(2, impMessageId); - state8.bindInteger(3, Integer.MAX_VALUE); - state8.bindInteger(4, notImpMessageId - impMessageId); - state8.step(); - } + for (int b = 0; b < SharedMediaQuery.MEDIA_TYPES_COUNT; b++) { + state6.requery(); + state6.bindLong(1, did); + state6.bindInteger(2, b); + state6.bindInteger(3, messageId == 1 ? 1 : 0); + state6.bindInteger(4, messageId); + state6.step(); } } private void putDialogsInternal(final TLRPC.messages_Dialogs dialogs) { try { database.beginTransaction(); - final HashMap> new_dialogMessage = new HashMap<>(); + final HashMap new_dialogMessage = new HashMap<>(); for (int a = 0; a < dialogs.messages.size(); a++) { TLRPC.Message message = dialogs.messages.get(a); - ArrayList arrayList = new_dialogMessage.get(message.dialog_id); - if (arrayList == null) { - arrayList = new ArrayList<>(); - new_dialogMessage.put(message.dialog_id, arrayList); - } - arrayList.add(message); + new_dialogMessage.put(message.dialog_id, message); } if (!dialogs.dialogs.isEmpty()) { @@ -5571,11 +5527,9 @@ public class MessagesStorage { SQLitePreparedStatement state4 = database.executeFast("REPLACE INTO dialog_settings VALUES(?, ?)"); SQLitePreparedStatement state5 = database.executeFast("REPLACE INTO messages_holes VALUES(?, ?, ?)"); SQLitePreparedStatement state6 = database.executeFast("REPLACE INTO media_holes_v2 VALUES(?, ?, ?, ?)"); - SQLitePreparedStatement state7 = database.executeFast("REPLACE INTO messages_imp_holes VALUES(?, ?, ?)"); - SQLitePreparedStatement state8 = database.executeFast("REPLACE INTO channel_group VALUES(?, ?, ?, ?)"); for (int a = 0; a < dialogs.dialogs.size(); a++) { - TLRPC.Dialog dialog = dialogs.dialogs.get(a); + TLRPC.TL_dialog dialog = dialogs.dialogs.get(a); if (dialog.id == 0) { if (dialog.peer.user_id != 0) { @@ -5587,74 +5541,54 @@ public class MessagesStorage { } } int messageDate = 0; - int messageDateI = 0; - boolean isMegagroup = false; - ArrayList arrayList = new_dialogMessage.get(dialog.id); - if (arrayList != null) { - for (int b = 0; b < arrayList.size(); b++) { - TLRPC.Message message = arrayList.get(b); - if (message.to_id.channel_id == 0 || MessageObject.isImportant(message)) { - messageDate = Math.max(message.date, messageDate); - } else { - messageDateI = Math.max(message.date, messageDateI); - } - isMegagroup = MessageObject.isMegagroup(message); + TLRPC.Message message = new_dialogMessage.get(dialog.id); + if (message != null) { + messageDate = Math.max(message.date, messageDate); - if (isValidKeyboardToSave(message)) { - BotQuery.putBotKeyboard(dialog.id, message); - } - - fixUnsupportedMedia(message); - NativeByteBuffer data = new NativeByteBuffer(message.getObjectSize()); - message.serializeToStream(data); - - long messageId = message.id; - if (message.to_id.channel_id != 0) { - messageId |= ((long) message.to_id.channel_id) << 32; - } - - state.requery(); - state.bindLong(1, messageId); - state.bindLong(2, dialog.id); - state.bindInteger(3, MessageObject.getUnreadFlags(message)); - state.bindInteger(4, message.send_state); - state.bindInteger(5, message.date); - state.bindByteBuffer(6, data); - state.bindInteger(7, (MessageObject.isOut(message) ? 1 : 0)); - state.bindInteger(8, 0); - if ((message.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { - state.bindInteger(9, message.views); - } else { - state.bindInteger(9, 0); - } - state.bindInteger(10, MessageObject.isImportant(message) ? 1 : 0); - state.step(); - - if (SharedMediaQuery.canAddMessageToMedia(message)) { - state3.requery(); - state3.bindLong(1, messageId); - state3.bindLong(2, dialog.id); - state3.bindInteger(3, message.date); - state3.bindInteger(4, SharedMediaQuery.getMediaType(message)); - state3.bindByteBuffer(5, data); - state3.step(); - } - data.reuse(); + if (isValidKeyboardToSave(message)) { + BotQuery.putBotKeyboard(dialog.id, message); } - createFirstHoles(dialog.id, state5, state6, state7, state8, arrayList); + fixUnsupportedMedia(message); + NativeByteBuffer data = new NativeByteBuffer(message.getObjectSize()); + message.serializeToStream(data); + + long messageId = message.id; + if (message.to_id.channel_id != 0) { + messageId |= ((long) message.to_id.channel_id) << 32; + } + + state.requery(); + state.bindLong(1, messageId); + state.bindLong(2, dialog.id); + state.bindInteger(3, MessageObject.getUnreadFlags(message)); + state.bindInteger(4, message.send_state); + state.bindInteger(5, message.date); + state.bindByteBuffer(6, data); + state.bindInteger(7, (MessageObject.isOut(message) ? 1 : 0)); + state.bindInteger(8, 0); + state.bindInteger(9, (message.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0 ? message.views : 0); + state.bindInteger(10, 0); + state.step(); + + if (SharedMediaQuery.canAddMessageToMedia(message)) { + state3.requery(); + state3.bindLong(1, messageId); + state3.bindLong(2, dialog.id); + state3.bindInteger(3, message.date); + state3.bindInteger(4, SharedMediaQuery.getMediaType(message)); + state3.bindByteBuffer(5, data); + state3.step(); + } + data.reuse(); + + createFirstHoles(dialog.id, state5, state6, message.id); } long topMessage = dialog.top_message; - long topMessageI = dialog.top_not_important_message; if (dialog.peer.channel_id != 0) { - if (isMegagroup) { - topMessage = topMessageI = Math.max(topMessage, topMessageI); - messageDate = messageDateI = Math.max(messageDate, messageDateI); - } topMessage |= ((long) dialog.peer.channel_id) << 32; - topMessageI |= ((long) dialog.peer.channel_id) << 32; } state2.requery(); @@ -5663,11 +5597,11 @@ public class MessagesStorage { state2.bindInteger(3, dialog.unread_count); state2.bindLong(4, topMessage); state2.bindInteger(5, dialog.read_inbox_max_id); - state2.bindInteger(6, 0); - state2.bindLong(7, topMessageI); - state2.bindInteger(8, dialog.unread_not_important_count); + state2.bindInteger(6, dialog.read_outbox_max_id); + state2.bindLong(7, 0); + state2.bindInteger(8, 0); state2.bindInteger(9, dialog.pts); - state2.bindInteger(10, messageDateI); + state2.bindInteger(10, 0); state2.step(); if (dialog.notify_settings != null) { @@ -5683,8 +5617,6 @@ public class MessagesStorage { state4.dispose(); state5.dispose(); state6.dispose(); - state7.dispose(); - state8.dispose(); } putUsersInternal(dialogs.users); @@ -5709,15 +5641,19 @@ public class MessagesStorage { }); } - public int getChannelReadInboxMax(final int channelId) { + public int getDialogReadMax(final boolean outbox, final long dialog_id) { final Semaphore semaphore = new Semaphore(0); - final Integer[] max = new Integer[] {0}; + final Integer[] max = new Integer[]{0}; MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override public void run() { SQLiteCursor cursor = null; try { - cursor = database.queryFinalized("SELECT inbox_max FROM dialogs WHERE did = " + (-channelId)); + if (outbox) { + cursor = database.queryFinalized("SELECT outbox_max FROM dialogs WHERE did = " + dialog_id); + } else { + cursor = database.queryFinalized("SELECT inbox_max FROM dialogs WHERE did = " + dialog_id); + } if (cursor.next()) { max[0] = cursor.intValue(0); } @@ -5741,7 +5677,7 @@ public class MessagesStorage { public int getChannelPtsSync(final int channelId) { final Semaphore semaphore = new Semaphore(0); - final Integer[] pts = new Integer[] {0}; + final Integer[] pts = new Integer[]{0}; MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override public void run() { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java b/TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java new file mode 100644 index 000000000..0c2163bee --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java @@ -0,0 +1,593 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.messenger; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Shader; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.browse.MediaBrowser; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Process; +import android.os.SystemClock; +import android.service.media.MediaBrowserService; +import android.text.TextUtils; + +import org.telegram.SQLite.SQLiteCursor; +import org.telegram.messenger.audioinfo.AudioInfo; +import org.telegram.messenger.query.SharedMediaQuery; +import org.telegram.tgnet.NativeByteBuffer; +import org.telegram.tgnet.TLRPC; +import org.telegram.ui.LaunchActivity; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class MusicBrowserService extends MediaBrowserService implements NotificationCenter.NotificationCenterDelegate { + + private static final String AUTO_APP_PACKAGE_NAME = "com.google.android.projection.gearhead"; + private static final String SLOT_RESERVATION_SKIP_TO_NEXT = "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT"; + private static final String SLOT_RESERVATION_SKIP_TO_PREV = "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS"; + private static final String SLOT_RESERVATION_QUEUE = "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE"; + + private MediaSession mediaSession; + private static final String MEDIA_ID_ROOT = "__ROOT__"; + + private boolean chatsLoaded; + private boolean loadingChats; + private ArrayList dialogs = new ArrayList<>(); + private HashMap users = new HashMap<>(); + private HashMap chats = new HashMap<>(); + private HashMap> musicObjects = new HashMap<>(); + private HashMap> musicQueues = new HashMap<>(); + + public static final String ACTION_CMD = "com.example.android.mediabrowserservice.ACTION_CMD"; + public static final String CMD_NAME = "CMD_NAME"; + public static final String CMD_PAUSE = "CMD_PAUSE"; + + private Paint roundPaint; + private RectF bitmapRect; + + private boolean serviceStarted; + + private int lastSelectedDialog; + + private static final int STOP_DELAY = 30000; + + private DelayedStopHandler delayedStopHandler = new DelayedStopHandler(this); + + @Override + public void onCreate() { + super.onCreate(); + ApplicationLoader.postInitApplication(); + + lastSelectedDialog = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE).getInt("auto_lastSelectedDialog", 0); + + mediaSession = new MediaSession(this, "MusicService"); + setSessionToken(mediaSession.getSessionToken()); + mediaSession.setCallback(new MediaSessionCallback()); + mediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + + Context context = getApplicationContext(); + Intent intent = new Intent(context, LaunchActivity.class); + PendingIntent pi = PendingIntent.getActivity(context, 99, intent, PendingIntent.FLAG_UPDATE_CURRENT); + mediaSession.setSessionActivity(pi); + + Bundle extras = new Bundle(); + extras.putBoolean(SLOT_RESERVATION_QUEUE, true); + extras.putBoolean(SLOT_RESERVATION_SKIP_TO_PREV, true); + extras.putBoolean(SLOT_RESERVATION_SKIP_TO_NEXT, true); + mediaSession.setExtras(extras); + + updatePlaybackState(null); + + NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioPlayStateChanged); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidStarted); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.audioDidReset); + } + + @Override + public int onStartCommand(Intent startIntent, int flags, int startId) { + /*if (startIntent != null) { + String action = startIntent.getAction(); + String command = startIntent.getStringExtra(CMD_NAME); + if (ACTION_CMD.equals(action)) { + if (CMD_PAUSE.equals(command)) { + if (mPlayback != null && mPlayback.isPlaying()) { + handlePauseRequest(); + } + } + } + }*/ + return START_STICKY; + } + + @Override + public void onDestroy() { + handleStopRequest(null); + delayedStopHandler.removeCallbacksAndMessages(null); + mediaSession.release(); + } + + @Override + public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { + if (clientPackageName == null || Process.SYSTEM_UID != clientUid && Process.myUid() != clientUid && !clientPackageName.equals("com.google.android.mediasimulator") && !clientPackageName.equals("com.google.android.projection.gearhead")) { + return null; + } + return new BrowserRoot(MEDIA_ID_ROOT, null); + } + + @Override + public void onLoadChildren(final String parentMediaId, final Result> result) { + if (!chatsLoaded) { + result.detach(); + if (loadingChats) { + return; + } + loadingChats = true; + MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { + @Override + public void run() { + try { + ArrayList usersToLoad = new ArrayList<>(); + ArrayList chatsToLoad = new ArrayList<>(); + SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT DISTINCT uid FROM media_v2 WHERE uid != 0 AND mid > 0 AND type = %d", SharedMediaQuery.MEDIA_MUSIC)); + while (cursor.next()) { + int lower_part = (int) cursor.longValue(0); + if (lower_part == 0) { + continue; + } + dialogs.add(lower_part); + if (lower_part > 0) { + usersToLoad.add(lower_part); + } else { + chatsToLoad.add(-lower_part); + } + } + cursor.dispose(); + if (!dialogs.isEmpty()) { + String ids = TextUtils.join(",", dialogs); + cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT uid, data, mid FROM media_v2 WHERE uid IN (%s) AND mid > 0 AND type = %d ORDER BY date DESC, mid DESC", ids, SharedMediaQuery.MEDIA_MUSIC)); + while (cursor.next()) { + NativeByteBuffer data = cursor.byteBufferValue(1); + if (data != null) { + TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); + if (MessageObject.isMusicMessage(message)) { + int did = cursor.intValue(0); + message.id = cursor.intValue(2); + message.dialog_id = did; + ArrayList arrayList = musicObjects.get(did); + ArrayList arrayList1 = musicQueues.get(did); + if (arrayList == null) { + arrayList = new ArrayList<>(); + musicObjects.put(did, arrayList); + arrayList1 = new ArrayList<>(); + musicQueues.put(did, arrayList1); + } + MessageObject messageObject = new MessageObject(message, null, false); + arrayList.add(0, messageObject); + MediaDescription.Builder builder = new MediaDescription.Builder().setMediaId(did + "_" + arrayList.size()); + builder.setTitle(messageObject.getMusicTitle()); + builder.setSubtitle(messageObject.getMusicAuthor()); + arrayList1.add(0, new MediaSession.QueueItem(builder.build(), arrayList1.size())); + } + } + } + cursor.dispose(); + if (!usersToLoad.isEmpty()) { + ArrayList usersArrayList = new ArrayList<>(); + MessagesStorage.getInstance().getUsersInternal(TextUtils.join(",", usersToLoad), usersArrayList); + for (int a = 0; a < usersArrayList.size(); a++) { + TLRPC.User user = usersArrayList.get(a); + users.put(user.id, user); + } + } + if (!chatsToLoad.isEmpty()) { + ArrayList chatsArrayList = new ArrayList<>(); + MessagesStorage.getInstance().getChatsInternal(TextUtils.join(",", chatsToLoad), chatsArrayList); + for (int a = 0; a < chatsArrayList.size(); a++) { + TLRPC.Chat chat = chatsArrayList.get(a); + chats.put(chat.id, chat); + } + } + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + chatsLoaded = true; + loadingChats = false; + loadChildrenImpl(parentMediaId, result); + if (lastSelectedDialog == 0 && !dialogs.isEmpty()) { + lastSelectedDialog = dialogs.get(0); + } + if (lastSelectedDialog != 0) { + ArrayList arrayList = musicObjects.get(lastSelectedDialog); + ArrayList arrayList1 = musicQueues.get(lastSelectedDialog); + if (arrayList != null && !arrayList.isEmpty()) { + mediaSession.setQueue(arrayList1); + if (lastSelectedDialog > 0) { + TLRPC.User user = users.get(lastSelectedDialog); + if (user != null) { + mediaSession.setQueueTitle(ContactsController.formatName(user.first_name, user.last_name)); + } else { + mediaSession.setQueueTitle("DELETED USER"); + } + } else { + TLRPC.Chat chat = chats.get(-lastSelectedDialog); + if (chat != null) { + mediaSession.setQueueTitle(chat.title); + } else { + mediaSession.setQueueTitle("DELETED CHAT"); + } + } + MessageObject messageObject = arrayList.get(0); + MediaMetadata.Builder builder = new MediaMetadata.Builder(); + builder.putLong(MediaMetadata.METADATA_KEY_DURATION, messageObject.getDuration() * 1000); + builder.putString(MediaMetadata.METADATA_KEY_ARTIST, messageObject.getMusicAuthor()); + builder.putString(MediaMetadata.METADATA_KEY_TITLE, messageObject.getMusicTitle()); + mediaSession.setMetadata(builder.build()); + } + } + updatePlaybackState(null); + } + }); + } + }); + } else { + loadChildrenImpl(parentMediaId, result); + } + } + + private void loadChildrenImpl(final String parentMediaId, final Result> result) { + List mediaItems = new ArrayList<>(); + + if (MEDIA_ID_ROOT.equals(parentMediaId)) { + for (int a = 0; a < dialogs.size(); a++) { + int did = dialogs.get(a); + MediaDescription.Builder builder = new MediaDescription.Builder().setMediaId("__CHAT_" + did); + TLRPC.FileLocation avatar = null; + if (did > 0) { + TLRPC.User user = users.get(did); + if (user != null) { + builder.setTitle(ContactsController.formatName(user.first_name, user.last_name)); + if (user.photo != null && user.photo.photo_small instanceof TLRPC.TL_fileLocation) { + avatar = user.photo.photo_small; + } + } else { + builder.setTitle("DELETED USER"); + } + } else { + TLRPC.Chat chat = chats.get(-did); + if (chat != null) { + builder.setTitle(chat.title); + if (chat.photo != null && chat.photo.photo_small instanceof TLRPC.TL_fileLocation) { + avatar = chat.photo.photo_small; + } + } else { + builder.setTitle("DELETED CHAT"); + } + } + Bitmap bitmap = null; + if (avatar != null) { + bitmap = createRoundBitmap(FileLoader.getPathToAttach(avatar, true)); + if (bitmap != null) { + builder.setIconBitmap(bitmap); + } + } + if (avatar == null || bitmap == null) { + builder.setIconUri(Uri.parse("android.resource://" + getApplicationContext().getPackageName() + "/drawable/contact_blue")); + } + mediaItems.add(new MediaBrowser.MediaItem(builder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + } else if (parentMediaId != null && parentMediaId.startsWith("__CHAT_")) { + int did = 0; + try { + did = Integer.parseInt(parentMediaId.replace("__CHAT_", "")); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + ArrayList arrayList = musicObjects.get(did); + if (arrayList != null) { + for (int a = 0; a < arrayList.size(); a++) { + MessageObject messageObject = arrayList.get(a); + MediaDescription.Builder builder = new MediaDescription.Builder().setMediaId(did + "_" + a); + builder.setTitle(messageObject.getMusicTitle()); + builder.setSubtitle(messageObject.getMusicAuthor()); + mediaItems.add(new MediaBrowser.MediaItem(builder.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE)); + } + } + } + result.sendResult(mediaItems); + } + + private Bitmap createRoundBitmap(File path) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = 2; + Bitmap bitmap = BitmapFactory.decodeFile(path.toString(), options); + if (bitmap != null) { + Bitmap result = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + result.eraseColor(Color.TRANSPARENT); + Canvas canvas = new Canvas(result); + BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + if (roundPaint == null) { + roundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + bitmapRect = new RectF(); + } + roundPaint.setShader(shader); + bitmapRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight()); + canvas.drawRoundRect(bitmapRect, bitmap.getWidth(), bitmap.getHeight(), roundPaint); + return result; + } + } catch (Throwable e) { + FileLog.e("tmessages", e); + } + return null; + } + + private final class MediaSessionCallback extends MediaSession.Callback { + @Override + public void onPlay() { + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject == null) { + onPlayFromMediaId(lastSelectedDialog + "_" + 0, null); + } else { + MediaController.getInstance().playAudio(messageObject); + } + } + + @Override + public void onSkipToQueueItem(long queueId) { + MediaController.getInstance().playMessageAtIndex((int) queueId); + handlePlayRequest(); + } + + @Override + public void onSeekTo(long position) { + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject != null) { + MediaController.getInstance().seekToProgress(messageObject, position / 1000 / (float) messageObject.getDuration()); + } + } + + @Override + public void onPlayFromMediaId(String mediaId, Bundle extras) { + String args[] = mediaId.split("_"); + if (args.length != 2) { + return; + } + try { + int did = Integer.parseInt(args[0]); + int id = Integer.parseInt(args[1]); + ArrayList arrayList = musicObjects.get(did); + ArrayList arrayList1 = musicQueues.get(did); + if (arrayList == null || id < 0 || id >= arrayList.size()) { + return; + } + lastSelectedDialog = did; + ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE).edit().putInt("auto_lastSelectedDialog", did).commit(); + MediaController.getInstance().setPlaylist(arrayList, arrayList.get(id), false); + mediaSession.setQueue(arrayList1); + if (did > 0) { + TLRPC.User user = users.get(did); + if (user != null) { + mediaSession.setQueueTitle(ContactsController.formatName(user.first_name, user.last_name)); + } else { + mediaSession.setQueueTitle("DELETED USER"); + } + } else { + TLRPC.Chat chat = chats.get(-did); + if (chat != null) { + mediaSession.setQueueTitle(chat.title); + } else { + mediaSession.setQueueTitle("DELETED CHAT"); + } + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + handlePlayRequest(); + } + + @Override + public void onPause() { + handlePauseRequest(); + } + + @Override + public void onStop() { + handleStopRequest(null); + } + + @Override + public void onSkipToNext() { + MediaController.getInstance().playNextMessage(); + } + + @Override + public void onSkipToPrevious() { + MediaController.getInstance().playPreviousMessage(); + } + + @Override + public void onPlayFromSearch(String query, Bundle extras) { + if (query == null || query.length() == 0) { + return; + } + query = query.toLowerCase(); + for (int a = 0; a < dialogs.size(); a++) { + int did = dialogs.get(a); + if (did > 0) { + TLRPC.User user = users.get(did); + if (user == null) { + continue; + } + if (user.first_name != null && user.first_name.startsWith(query) || user.last_name != null && user.last_name.startsWith(query)) { + onPlayFromMediaId(did + "_" + 0, null); + break; + } + } else { + TLRPC.Chat chat = chats.get(-did); + if (chat == null) { + continue; + } + if (chat.title != null && chat.title.toLowerCase().contains(query)) { + onPlayFromMediaId(did + "_" + 0, null); + break; + } + } + } + } + } + + private void updatePlaybackState(String error) { + long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN; + MessageObject playingMessageObject = MediaController.getInstance().getPlayingMessageObject(); + if (playingMessageObject != null) { + position = playingMessageObject.audioProgressSec * 1000; + } + + PlaybackState.Builder stateBuilder = new PlaybackState.Builder().setActions(getAvailableActions()); + int state; + if (playingMessageObject == null) { + state = PlaybackState.STATE_STOPPED; + } else { + if (MediaController.getInstance().isDownloadingCurrentMessage()) { + state = PlaybackState.STATE_BUFFERING; + } else { + state = MediaController.getInstance().isAudioPaused() ? PlaybackState.STATE_PAUSED : PlaybackState.STATE_PLAYING; + } + } + + if (error != null) { + stateBuilder.setErrorMessage(error); + state = PlaybackState.STATE_ERROR; + } + stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime()); + if (playingMessageObject != null) { + stateBuilder.setActiveQueueItemId(MediaController.getInstance().getPlayingMessageObjectNum()); + } else { + stateBuilder.setActiveQueueItemId(0); + } + + mediaSession.setPlaybackState(stateBuilder.build()); + } + + private long getAvailableActions() { + 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()) { + actions |= PlaybackState.ACTION_PAUSE; + } + actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; + actions |= PlaybackState.ACTION_SKIP_TO_NEXT; + } + return actions; + } + + private void handleStopRequest(String withError) { + delayedStopHandler.removeCallbacksAndMessages(null); + delayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY); + updatePlaybackState(withError); + stopSelf(); + serviceStarted = false; + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioPlayStateChanged); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidStarted); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.audioDidReset); + } + + private void handlePlayRequest() { + delayedStopHandler.removeCallbacksAndMessages(null); + if (!serviceStarted) { + startService(new Intent(getApplicationContext(), MusicBrowserService.class)); + serviceStarted = true; + } + + if (!mediaSession.isActive()) { + mediaSession.setActive(true); + } + + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject == null) { + return; + } + MediaMetadata.Builder builder = new MediaMetadata.Builder(); + builder.putLong(MediaMetadata.METADATA_KEY_DURATION, messageObject.getDuration() * 1000); + builder.putString(MediaMetadata.METADATA_KEY_ARTIST, messageObject.getMusicAuthor()); + builder.putString(MediaMetadata.METADATA_KEY_TITLE, messageObject.getMusicTitle()); + AudioInfo audioInfo = MediaController.getInstance().getAudioInfo(); + if (audioInfo != null) { + Bitmap bitmap = audioInfo.getCover(); + if (bitmap != null) { + builder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap); + } + } + mediaSession.setMetadata(builder.build()); + } + + private void handlePauseRequest() { + MediaController.getInstance().pauseAudio(MediaController.getInstance().getPlayingMessageObject()); + delayedStopHandler.removeCallbacksAndMessages(null); + delayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY); + } + + @Override + public void didReceivedNotification(int id, Object... args) { + updatePlaybackState(null); + handlePlayRequest(); + } + + private static class DelayedStopHandler extends Handler { + private final WeakReference mWeakReference; + + private DelayedStopHandler(MusicBrowserService service) { + mWeakReference = new WeakReference<>(service); + } + + @Override + public void handleMessage(Message msg) { + MusicBrowserService service = mWeakReference.get(); + if (service != null) { + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject != null && !MediaController.getInstance().isAudioPaused()) { + return; + } + service.stopSelf(); + service.serviceStarted = false; + } + } + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java b/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java index e1bbf9513..e161b016c 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java @@ -190,7 +190,11 @@ public class MusicPlayerService extends Service implements NotificationCenter.No metadataEditor.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, authorName); metadataEditor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, songName); if (audioInfo != null && audioInfo.getCover() != null) { - metadataEditor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, audioInfo.getCover()); + try { + metadataEditor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, audioInfo.getCover()); + } catch (Throwable e) { + FileLog.e("tmessages", e); + } } metadataEditor.apply(); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java index cafe110ea..cc76825dd 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java @@ -21,7 +21,7 @@ import java.util.zip.ZipFile; public class NativeLoader { - private final static int LIB_VERSION = 21; + private final static int LIB_VERSION = 22; 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"; @@ -75,11 +75,9 @@ public class NativeLoader { } out.close(); - if (Build.VERSION.SDK_INT >= 9) { - destLocalFile.setReadable(true, false); - destLocalFile.setExecutable(true, false); - destLocalFile.setWritable(true); - } + destLocalFile.setReadable(true, false); + destLocalFile.setExecutable(true, false); + destLocalFile.setWritable(true); try { System.load(destLocalFile.getAbsolutePath()); @@ -142,10 +140,12 @@ public class NativeLoader { folder = "x86"; } - if (Build.VERSION.SDK_INT == 8) { - File destFile = new File(context.getApplicationInfo().dataDir + "/lib", LIB_SO_NAME); + + File destFile = getNativeLibraryDir(context); + if (destFile != null) { + destFile = new File(destFile, LIB_SO_NAME); if (destFile.exists()) { - FileLog.d("tmessages", "Load normal lib"); + FileLog.d("tmessages", "load normal lib"); try { System.loadLibrary(LIB_NAME); init(crashDir, BuildVars.DEBUG_VERSION); @@ -154,31 +154,6 @@ public class NativeLoader { } catch (Error e) { FileLog.e("tmessages", e); } - } else { - try { - System.loadLibrary(LIB_NAME); - init(crashDir, BuildVars.DEBUG_VERSION); - nativeLoaded = true; - return; - } catch (Error e) { - FileLog.e("tmessages", e); - } - } - } else { - File destFile = getNativeLibraryDir(context); - if (destFile != null) { - destFile = new File(destFile, LIB_SO_NAME); - if (destFile.exists()) { - FileLog.d("tmessages", "load normal lib"); - try { - System.loadLibrary(LIB_NAME); - init(crashDir, BuildVars.DEBUG_VERSION); - nativeLoaded = true; - return; - } catch (Error e) { - FileLog.e("tmessages", e); - } - } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NetworkAlarm.java b/TMessagesProj/src/main/java/org/telegram/messenger/NetworkAlarm.java deleted file mode 100644 index 8f0f51d5f..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NetworkAlarm.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.telegram.messenger; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.PowerManager; - -public class NetworkAlarm extends BroadcastReceiver { - private static PowerManager pm; - private static PowerManager.WakeLock wl; - - @Override - public void onReceive(Context context, Intent intent) - { - FileLog.d("tmessages", "NetworkAlarm:onReceive, acquiring lock"); - if (wl == null) { - FileLog.d("tmessages", "NetworkAlarm:onReceive lock is null"); - } else { - if (!wl.isHeld()) { - wl.acquire(); - FileLog.d("tmessages", "NetworkAlarm:onReceive lock acquired"); - } else { - FileLog.d("tmessages", "NetworkAlarm:onReceive lock already held"); - } - } - } - - public void setAlarm(Context context, int timeout) - { - FileLog.d("tmessages", "NetworkAlarm:setAlarm entry"); - - if (pm == null) { - pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - } - - if (wl == null) { - wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, ""); - } - - if (!wl.isHeld()) { - wl.acquire(); - } - - AlarmManager am =( AlarmManager)context.getSystemService(Context.ALARM_SERVICE); - Intent i = new Intent(context, NetworkAlarm.class); - PendingIntent pi = PendingIntent.getBroadcast(context, 0, i, 0); - - am.cancel(pi); - if (android.os.Build.VERSION.SDK_INT >= 19) { - am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pi); - } else { - am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pi); - } - - FileLog.d("tmessages", "NetworkAlarm:setAlarm exit"); - wl.release(); - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java index 70c73aff6..618dec20e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java @@ -36,7 +36,7 @@ public class NotificationCenter { public static final int encryptedChatUpdated = totalEvents++; public static final int messagesReadEncrypted = totalEvents++; public static final int encryptedChatCreated = totalEvents++; - public static final int userPhotosLoaded = totalEvents++; + public static final int dialogPhotosLoaded = totalEvents++; public static final int removeAllMessagesFromDialog = totalEvents++; public static final int notificationsSettingsUpdated = totalEvents++; public static final int pushMessagesUpdated = totalEvents++; @@ -71,6 +71,9 @@ public class NotificationCenter { public static final int locationPermissionGranted = totalEvents++; public static final int peerSettingsDidLoaded = totalEvents++; public static final int wasUnableToFindCurrentLocation = totalEvents++; + public static final int reloadHints = totalEvents++; + public static final int reloadInlineHints = totalEvents++; + public static final int newDraftReceived = totalEvents++; public static final int httpFileDidLoaded = totalEvents++; public static final int httpFileDidFailedLoad = totalEvents++; @@ -162,6 +165,10 @@ public class NotificationCenter { } } + public boolean isAnimationInProgress() { + return animationInProgress; + } + public void postNotificationName(int id, Object... args) { boolean allowDuringAnimation = false; if (allowedNotifications != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationDismissReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationDismissReceiver.java new file mode 100644 index 000000000..a32a872f7 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationDismissReceiver.java @@ -0,0 +1,23 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.messenger; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +public class NotificationDismissReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Context.MODE_PRIVATE); + preferences.edit().putInt("dismissDate", intent.getIntExtra("messageDate", 0)).commit(); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java index d911f513c..36c54852e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java @@ -18,6 +18,8 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Point; import android.graphics.drawable.BitmapDrawable; import android.media.AudioManager; @@ -411,12 +413,15 @@ public class NotificationsController { Boolean value = settingsCache.get(dialog_id); boolean isChat = (int) dialog_id < 0; - popup = (int)dialog_id == 0 ? 0 : preferences.getInt(isChat ? "popupGroup" : "popupAll", 0); + popup = (int) dialog_id == 0 ? 0 : preferences.getInt(isChat ? "popupGroup" : "popupAll", 0); if (value == null) { int notifyOverride = getNotifyOverride(preferences, dialog_id); value = !(notifyOverride == 2 || (!preferences.getBoolean("EnableAll", true) || isChat && !preferences.getBoolean("EnableGroup", true)) && notifyOverride == 0); settingsCache.put(dialog_id, value); } + if (popup != 0 && messageObject.messageOwner.to_id.channel_id != 0 && !messageObject.isMegagroup()) { + popup = 0; + } if (value) { if (popup != 0) { popupArray.add(0, messageObject); @@ -769,7 +774,12 @@ public class NotificationsController { msg = LocaleController.formatString("NotificationMessageMap", R.string.NotificationMessageMap, name); } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaDocument) { if (messageObject.isSticker()) { - msg = LocaleController.formatString("NotificationMessageSticker", R.string.NotificationMessageSticker, name); + String emoji = messageObject.getStickerEmoji(); + if (emoji != null) { + msg = LocaleController.formatString("NotificationMessageStickerEmoji", R.string.NotificationMessageStickerEmoji, name, emoji); + } else { + msg = LocaleController.formatString("NotificationMessageSticker", R.string.NotificationMessageSticker, name); + } } else if (messageObject.isGif()) { msg = LocaleController.formatString("NotificationMessageGif", R.string.NotificationMessageGif, name); } else { @@ -889,10 +899,19 @@ public class NotificationsController { msg = LocaleController.formatString("NotificationActionPinnedVoiceChannel", R.string.NotificationActionPinnedVoiceChannel, chat.title); } } else if (object.isSticker()) { - if (!ChatObject.isChannel(chat) || chat.megagroup) { - msg = LocaleController.formatString("NotificationActionPinnedSticker", R.string.NotificationActionPinnedSticker, name, chat.title); + String emoji = messageObject.getStickerEmoji(); + if (emoji != null) { + if (!ChatObject.isChannel(chat) || chat.megagroup) { + msg = LocaleController.formatString("NotificationActionPinnedStickerEmoji", R.string.NotificationActionPinnedStickerEmoji, name, chat.title, emoji); + } else { + msg = LocaleController.formatString("NotificationActionPinnedStickerEmojiChannel", R.string.NotificationActionPinnedStickerEmojiChannel, chat.title, emoji); + } } else { - msg = LocaleController.formatString("NotificationActionPinnedStickerChannel", R.string.NotificationActionPinnedStickerChannel, chat.title); + if (!ChatObject.isChannel(chat) || chat.megagroup) { + msg = LocaleController.formatString("NotificationActionPinnedSticker", R.string.NotificationActionPinnedSticker, name, chat.title); + } else { + msg = LocaleController.formatString("NotificationActionPinnedStickerChannel", R.string.NotificationActionPinnedStickerChannel, chat.title); + } } } else if (object.messageOwner.media instanceof TLRPC.TL_messageMediaDocument) { if (!ChatObject.isChannel(chat) || chat.megagroup) { @@ -939,7 +958,7 @@ public class NotificationsController { } } else { if (ChatObject.isChannel(chat) && !chat.megagroup) { - if (messageObject.isImportant()) { + if (messageObject.messageOwner.post) { if (messageObject.isMediaEmpty()) { if (!shortMessage && messageObject.messageOwner.message != null && messageObject.messageOwner.message.length() != 0) { msg = LocaleController.formatString("NotificationMessageGroupText", R.string.NotificationMessageGroupText, name, chat.title, messageObject.messageOwner.message); @@ -960,7 +979,12 @@ public class NotificationsController { msg = LocaleController.formatString("ChannelMessageMap", R.string.ChannelMessageMap, name, chat.title); } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaDocument) { if (messageObject.isSticker()) { - msg = LocaleController.formatString("ChannelMessageSticker", R.string.ChannelMessageSticker, name, chat.title); + String emoji = messageObject.getStickerEmoji(); + if (emoji != null) { + msg = LocaleController.formatString("ChannelMessageStickerEmoji", R.string.ChannelMessageStickerEmoji, name, chat.title, emoji); + } else { + msg = LocaleController.formatString("ChannelMessageSticker", R.string.ChannelMessageSticker, name, chat.title); + } } else if (messageObject.isGif()) { msg = LocaleController.formatString("ChannelMessageGIF", R.string.ChannelMessageGIF, name, chat.title); } else { @@ -988,7 +1012,12 @@ public class NotificationsController { msg = LocaleController.formatString("ChannelMessageGroupMap", R.string.ChannelMessageGroupMap, name, chat.title); } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaDocument) { if (messageObject.isSticker()) { - msg = LocaleController.formatString("ChannelMessageGroupSticker", R.string.ChannelMessageGroupSticker, name, chat.title); + String emoji = messageObject.getStickerEmoji(); + if (emoji != null) { + msg = LocaleController.formatString("ChannelMessageGroupStickerEmoji", R.string.ChannelMessageGroupStickerEmoji, name, chat.title, emoji); + } else { + msg = LocaleController.formatString("ChannelMessageGroupSticker", R.string.ChannelMessageGroupSticker, name, chat.title); + } } else if (messageObject.isGif()) { msg = LocaleController.formatString("ChannelMessageGroupGif", R.string.ChannelMessageGroupGif, name, chat.title); } else { @@ -1017,7 +1046,12 @@ public class NotificationsController { msg = LocaleController.formatString("NotificationMessageGroupMap", R.string.NotificationMessageGroupMap, name, chat.title); } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaDocument) { if (messageObject.isSticker()) { - msg = LocaleController.formatString("NotificationMessageGroupSticker", R.string.NotificationMessageGroupSticker, name, chat.title); + String emoji = messageObject.getStickerEmoji(); + if (emoji != null) { + msg = LocaleController.formatString("NotificationMessageGroupStickerEmoji", R.string.NotificationMessageGroupStickerEmoji, name, chat.title, emoji); + } else { + msg = LocaleController.formatString("NotificationMessageGroupSticker", R.string.NotificationMessageGroupSticker, name, chat.title); + } } else if (messageObject.isGif()) { msg = LocaleController.formatString("NotificationMessageGroupGif", R.string.NotificationMessageGroupGif, name, chat.title); } else { @@ -1239,6 +1273,12 @@ public class NotificationsController { ConnectionsManager.getInstance().resumeNetworkMaybe(); MessageObject lastMessageObject = pushMessages.get(0); + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Context.MODE_PRIVATE); + int dismissDate = preferences.getInt("dismissDate", 0); + if (lastMessageObject.messageOwner.date <= dismissDate) { + dismissNotification(); + return; + } long dialog_id = lastMessageObject.getDialogId(); long override_dialog_id = dialog_id; @@ -1273,7 +1313,6 @@ public class NotificationsController { int priorityOverride; int vibrateOverride; - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Context.MODE_PRIVATE); int notifyOverride = getNotifyOverride(preferences, override_dialog_id); if (!notifyAboutLast || notifyOverride == 2 || (!preferences.getBoolean("EnableAll", true) || chat_id != 0 && !preferences.getBoolean("EnableGroup", true)) && notifyOverride == 0) { notifyDisabled = true; @@ -1443,6 +1482,7 @@ public class NotificationsController { int silent = 2; String lastMessage = null; + boolean hasNewMessages = false; if (pushMessages.size() == 1) { MessageObject messageObject = pushMessages.get(0); String message = lastMessage = getStringForMessage(messageObject, false); @@ -1467,7 +1507,7 @@ public class NotificationsController { for (int i = 0; i < count; i++) { MessageObject messageObject = pushMessages.get(i); String message = getStringForMessage(messageObject, false); - if (message == null) { + if (message == null || messageObject.messageOwner.date <= dismissDate) { continue; } if (silent == 2) { @@ -1489,10 +1529,26 @@ public class NotificationsController { mBuilder.setStyle(inboxStyle); } + Intent dismissIntent = new Intent(ApplicationLoader.applicationContext, NotificationDismissReceiver.class); + dismissIntent.putExtra("messageDate", lastMessageObject.messageOwner.date); + mBuilder.setDeleteIntent(PendingIntent.getBroadcast(ApplicationLoader.applicationContext, 1, dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT)); + if (photoPath != null) { BitmapDrawable img = ImageLoader.getInstance().getImageFromMemory(photoPath, null, "50_50"); if (img != null) { mBuilder.setLargeIcon(img.getBitmap()); + } else { + try { + float scaleFactor = 160.0f / AndroidUtilities.dp(50); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = scaleFactor < 1 ? 1 : (int) scaleFactor; + Bitmap bitmap = BitmapFactory.decodeFile(FileLoader.getPathToAttach(photoPath, true).toString(), options); + if (bitmap != null) { + mBuilder.setLargeIcon(bitmap); + } + } catch (Throwable e) { + //ignore + } } } @@ -1710,6 +1766,7 @@ public class NotificationsController { .setSmallIcon(R.drawable.notification) .setGroup("messages") .setContentText(text) + .setAutoCancel(true) .setColor(0xff2ca5e0) .setGroupSummary(false) .setContentIntent(contentIntent) diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/OpenChatReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/OpenChatReceiver.java new file mode 100644 index 000000000..2f50ed963 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/OpenChatReceiver.java @@ -0,0 +1,36 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.messenger; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import org.telegram.ui.LaunchActivity; + +public class OpenChatReceiver extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + if (intent == null) { + finish(); + } + if (intent.getAction() == null || !intent.getAction().startsWith("com.tmessages.openchat")) { + finish(); + return; + } + Intent intent2 = new Intent(this, LaunchActivity.class); + intent2.setAction(intent.getAction()); + intent2.putExtras(intent); + startActivity(intent2); + finish(); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java index 1420bf6aa..a9ad47e6e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java @@ -88,7 +88,7 @@ public class SecretChatHelper { return localInstance; } - public void cleanUp() { + public void cleanup() { sendingNotifyLayer.clear(); acceptingChats.clear(); secretHolesQueue.clear(); @@ -176,7 +176,7 @@ public class SecretChatHelper { } reqSend.random_id = message.random_id; - performSendEncryptedRequest(reqSend, message, encryptedChat, null, null); + performSendEncryptedRequest(reqSend, message, encryptedChat, null, null, null); } protected void processUpdateEncryption(TLRPC.TL_updateEncryption update, ConcurrentHashMap usersDict) { @@ -194,7 +194,7 @@ public class SecretChatHelper { user = usersDict.get(user_id); } newChat.user_id = user_id; - final TLRPC.Dialog dialog = new TLRPC.TL_dialog(); + final TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); dialog.id = dialog_id; dialog.unread_count = 0; dialog.top_message = 0; @@ -206,18 +206,7 @@ public class SecretChatHelper { MessagesController.getInstance().dialogs_dict.put(dialog.id, dialog); MessagesController.getInstance().dialogs.add(dialog); MessagesController.getInstance().putEncryptedChat(newChat, false); - Collections.sort(MessagesController.getInstance().dialogs, new Comparator() { - @Override - public int compare(TLRPC.Dialog tl_dialog, TLRPC.Dialog tl_dialog2) { - if (tl_dialog.last_message_date == tl_dialog2.last_message_date) { - return 0; - } else if (tl_dialog.last_message_date < tl_dialog2.last_message_date) { - return 1; - } else { - return -1; - } - } - }); + MessagesController.getInstance().sortDialogs(null); NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); } }); @@ -281,7 +270,7 @@ public class SecretChatHelper { } reqSend.random_id = message.random_id; - performSendEncryptedRequest(reqSend, message, encryptedChat, null, null); + performSendEncryptedRequest(reqSend, message, encryptedChat, null, null, null); } public void sendClearHistoryMessage(TLRPC.EncryptedChat encryptedChat, TLRPC.Message resendMessage) { @@ -308,7 +297,7 @@ public class SecretChatHelper { } reqSend.random_id = message.random_id; - performSendEncryptedRequest(reqSend, message, encryptedChat, null, null); + performSendEncryptedRequest(reqSend, message, encryptedChat, null, null, null); } public void sendNotifyLayerMessage(final TLRPC.EncryptedChat encryptedChat, TLRPC.Message resendMessage) { @@ -340,7 +329,7 @@ public class SecretChatHelper { } reqSend.random_id = message.random_id; - performSendEncryptedRequest(reqSend, message, encryptedChat, null, null); + performSendEncryptedRequest(reqSend, message, encryptedChat, null, null, null); } public void sendRequestKeyMessage(final TLRPC.EncryptedChat encryptedChat, TLRPC.Message resendMessage) { @@ -371,7 +360,7 @@ public class SecretChatHelper { } reqSend.random_id = message.random_id; - performSendEncryptedRequest(reqSend, message, encryptedChat, null, null); + performSendEncryptedRequest(reqSend, message, encryptedChat, null, null, null); } public void sendAcceptKeyMessage(final TLRPC.EncryptedChat encryptedChat, TLRPC.Message resendMessage) { @@ -403,7 +392,7 @@ public class SecretChatHelper { } reqSend.random_id = message.random_id; - performSendEncryptedRequest(reqSend, message, encryptedChat, null, null); + performSendEncryptedRequest(reqSend, message, encryptedChat, null, null, null); } public void sendCommitKeyMessage(final TLRPC.EncryptedChat encryptedChat, TLRPC.Message resendMessage) { @@ -434,7 +423,7 @@ public class SecretChatHelper { } reqSend.random_id = message.random_id; - performSendEncryptedRequest(reqSend, message, encryptedChat, null, null); + performSendEncryptedRequest(reqSend, message, encryptedChat, null, null, null); } public void sendAbortKeyMessage(final TLRPC.EncryptedChat encryptedChat, TLRPC.Message resendMessage, long excange_id) { @@ -464,7 +453,7 @@ public class SecretChatHelper { } reqSend.random_id = message.random_id; - performSendEncryptedRequest(reqSend, message, encryptedChat, null, null); + performSendEncryptedRequest(reqSend, message, encryptedChat, null, null, null); } public void sendNoopMessage(final TLRPC.EncryptedChat encryptedChat, TLRPC.Message resendMessage) { @@ -492,7 +481,7 @@ public class SecretChatHelper { } reqSend.random_id = message.random_id; - performSendEncryptedRequest(reqSend, message, encryptedChat, null, null); + performSendEncryptedRequest(reqSend, message, encryptedChat, null, null, null); } public void sendTTLMessage(TLRPC.EncryptedChat encryptedChat, TLRPC.Message resendMessage) { @@ -528,7 +517,7 @@ public class SecretChatHelper { } reqSend.random_id = message.random_id; - performSendEncryptedRequest(reqSend, message, encryptedChat, null, null); + performSendEncryptedRequest(reqSend, message, encryptedChat, null, null, null); } public void sendScreenshotMessage(TLRPC.EncryptedChat encryptedChat, ArrayList random_ids, TLRPC.Message resendMessage) { @@ -564,10 +553,11 @@ public class SecretChatHelper { } reqSend.random_id = message.random_id; - performSendEncryptedRequest(reqSend, message, encryptedChat, null, null); + performSendEncryptedRequest(reqSend, message, encryptedChat, null, null, null); } - private void processSentMessage(TLRPC.Message newMsg, TLRPC.EncryptedFile file, TLRPC.DecryptedMessage decryptedMessage, String originalPath) { + private void updateMediaPaths(MessageObject newMsgObj, TLRPC.EncryptedFile file, TLRPC.DecryptedMessage decryptedMessage, String originalPath) { + TLRPC.Message newMsg = newMsgObj.messageOwner; if (file != null) { if (newMsg.media instanceof TLRPC.TL_messageMediaPhoto && newMsg.media.photo != null) { TLRPC.PhotoSize size = newMsg.media.photo.sizes.get(newMsg.media.photo.sizes.size() - 1); @@ -608,6 +598,8 @@ public class SecretChatHelper { File cacheFile = new File(newMsg.attachPath); File cacheFile2 = FileLoader.getPathToAttach(newMsg.media.document); if (cacheFile.renameTo(cacheFile2)) { + newMsgObj.mediaExists = newMsgObj.attachPathExists; + newMsgObj.attachPathExists = false; newMsg.attachPath = ""; } } @@ -640,7 +632,7 @@ public class SecretChatHelper { return message.action instanceof TLRPC.TL_messageEncryptedAction && !(message.action.encryptedAction instanceof TLRPC.TL_decryptedMessageActionScreenshotMessages || message.action.encryptedAction instanceof TLRPC.TL_decryptedMessageActionSetMessageTTL); } - protected void performSendEncryptedRequest(final TLRPC.DecryptedMessage req, final TLRPC.Message newMsgObj, final TLRPC.EncryptedChat chat, final TLRPC.InputEncryptedFile encryptedFile, final String originalPath) { + protected void performSendEncryptedRequest(final TLRPC.DecryptedMessage req, final TLRPC.Message newMsgObj, final TLRPC.EncryptedChat chat, final TLRPC.InputEncryptedFile encryptedFile, final String originalPath, final MessageObject newMsg) { if (req == null || chat.auth_key == null || chat instanceof TLRPC.TL_encryptedChatRequested || chat instanceof TLRPC.TL_encryptedChatWaiting) { return; } @@ -800,8 +792,8 @@ public class SecretChatHelper { if (isSecretVisibleMessage(newMsgObj)) { newMsgObj.date = res.date; } - if (res.file instanceof TLRPC.TL_encryptedFile) { - processSentMessage(newMsgObj, res.file, req, originalPath); + if (newMsg != null && res.file instanceof TLRPC.TL_encryptedFile) { + updateMediaPaths(newMsg, res.file, req, originalPath); } MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override @@ -1042,7 +1034,7 @@ public class SecretChatHelper { newMessage.media.document.attributes = decryptedMessage.media.attributes; } newMessage.media.document.mime_type = decryptedMessage.media.mime_type; - newMessage.media.document.size = file.size; + newMessage.media.document.size = decryptedMessage.media.size != 0 ? Math.min(decryptedMessage.media.size, file.size) : file.size; newMessage.media.document.key = decryptedMessage.media.key; newMessage.media.document.iv = decryptedMessage.media.iv; if (newMessage.media.document.mime_type == null) { @@ -1150,7 +1142,7 @@ public class SecretChatHelper { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(did); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(did); if (dialog != null) { dialog.unread_count = 0; MessagesController.getInstance().dialogMessage.remove(dialog.id); @@ -1810,25 +1802,14 @@ public class SecretChatHelper { chat.seq_out = 1; chat.a_or_b = salt; MessagesController.getInstance().putEncryptedChat(chat, false); - TLRPC.Dialog dialog = new TLRPC.TL_dialog(); + TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); dialog.id = ((long) chat.id) << 32; dialog.unread_count = 0; dialog.top_message = 0; dialog.last_message_date = ConnectionsManager.getInstance().getCurrentTime(); MessagesController.getInstance().dialogs_dict.put(dialog.id, dialog); MessagesController.getInstance().dialogs.add(dialog); - Collections.sort(MessagesController.getInstance().dialogs, new Comparator() { - @Override - public int compare(TLRPC.Dialog tl_dialog, TLRPC.Dialog tl_dialog2) { - if (tl_dialog.last_message_date == tl_dialog2.last_message_date) { - return 0; - } else if (tl_dialog.last_message_date < tl_dialog2.last_message_date) { - return 1; - } else { - return -1; - } - } - }); + MessagesController.getInstance().sortDialogs(null); MessagesStorage.getInstance().putEncryptedChat(chat, user, dialog); NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); NotificationCenter.getInstance().postNotificationName(NotificationCenter.encryptedChatCreated, chat); @@ -1836,7 +1817,7 @@ public class SecretChatHelper { @Override public void run() { if (!delayedEncryptedChatUpdates.isEmpty()) { - MessagesController.getInstance().processUpdateArray(delayedEncryptedChatUpdates, null, null); + MessagesController.getInstance().processUpdateArray(delayedEncryptedChatUpdates, null, null, false); delayedEncryptedChatUpdates.clear(); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java index e50460313..9e8fe28c3 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java @@ -10,9 +10,7 @@ package org.telegram.messenger; import android.app.Activity; import android.app.AlertDialog; -import android.app.ProgressDialog; import android.content.Context; -import android.content.DialogInterface; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.location.Location; @@ -22,13 +20,14 @@ import android.media.MediaMetadataRetriever; import android.media.MediaPlayer; import android.media.ThumbnailUtils; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; import android.webkit.MimeTypeMap; import android.widget.Toast; import org.telegram.messenger.audioinfo.AudioInfo; +import org.telegram.messenger.query.DraftQuery; +import org.telegram.messenger.query.SearchQuery; import org.telegram.messenger.query.StickersQuery; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.QuickAckDelegate; @@ -234,7 +233,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter NotificationCenter.getInstance().addObserver(this, NotificationCenter.FileDidFailedLoad); } - public void cleanUp() { + public void cleanup() { delayedMessages.clear(); unsentMessages.clear(); sendingMessages.clear(); @@ -306,7 +305,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } message.sendEncryptedRequest.media.key = (byte[]) args[3]; message.sendEncryptedRequest.media.iv = (byte[]) args[4]; - SecretChatHelper.getInstance().performSendEncryptedRequest(message.sendEncryptedRequest, message.obj.messageOwner, message.encryptedChat, encryptedFile, message.originalPath); + SecretChatHelper.getInstance().performSendEncryptedRequest(message.sendEncryptedRequest, message.obj.messageOwner, message.encryptedChat, encryptedFile, message.originalPath, message.obj); arr.remove(a); a--; } @@ -588,7 +587,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter if (unsent) { unsentMessages.put(messageObject.getId(), messageObject); } - sendMessage(messageObject, messageObject.messageOwner.post); + sendMessage(messageObject); return true; } @@ -600,43 +599,43 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } - public void processForwardFromMyName(MessageObject messageObject, long did, boolean asAdmin) { + public void processForwardFromMyName(MessageObject messageObject, long did) { if (messageObject == null) { return; } if (messageObject.messageOwner.media != null && !(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaEmpty) && !(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage)) { if (messageObject.messageOwner.media.photo instanceof TLRPC.TL_photo) { - sendMessage((TLRPC.TL_photo) messageObject.messageOwner.media.photo, null, did, messageObject.replyMessageObject, asAdmin, null, null); + sendMessage((TLRPC.TL_photo) messageObject.messageOwner.media.photo, null, did, messageObject.replyMessageObject, null, null); } else if (messageObject.messageOwner.media.document instanceof TLRPC.TL_document) { - sendMessage((TLRPC.TL_document) messageObject.messageOwner.media.document, null, messageObject.messageOwner.attachPath, did, messageObject.replyMessageObject, asAdmin, null, null); + sendMessage((TLRPC.TL_document) messageObject.messageOwner.media.document, null, messageObject.messageOwner.attachPath, did, messageObject.replyMessageObject, null, null); } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaVenue || messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGeo) { - sendMessage(messageObject.messageOwner.media, did, messageObject.replyMessageObject, asAdmin, null, null); + sendMessage(messageObject.messageOwner.media, did, messageObject.replyMessageObject, null, null); } else if (messageObject.messageOwner.media.phone_number != null) { TLRPC.User user = new TLRPC.TL_userContact_old2(); user.phone = messageObject.messageOwner.media.phone_number; user.first_name = messageObject.messageOwner.media.first_name; user.last_name = messageObject.messageOwner.media.last_name; user.id = messageObject.messageOwner.media.user_id; - sendMessage(user, did, messageObject.replyMessageObject, asAdmin, null, null); + sendMessage(user, did, messageObject.replyMessageObject, null, null); } else { ArrayList arrayList = new ArrayList<>(); arrayList.add(messageObject); - sendMessage(arrayList, did, asAdmin); + sendMessage(arrayList, did); } } else if (messageObject.messageOwner.message != null) { TLRPC.WebPage webPage = null; if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { webPage = messageObject.messageOwner.media.webpage; } - sendMessage(messageObject.messageOwner.message, did, messageObject.replyMessageObject, webPage, true, asAdmin, messageObject.messageOwner.entities, null, null); + sendMessage(messageObject.messageOwner.message, did, messageObject.replyMessageObject, webPage, true, messageObject.messageOwner.entities, null, null); } else { ArrayList arrayList = new ArrayList<>(); arrayList.add(messageObject); - sendMessage(arrayList, did, asAdmin); + sendMessage(arrayList, did); } } - public void sendSticker(TLRPC.Document document, long peer, MessageObject replyingMessageObject, boolean asAdmin) { + public void sendSticker(TLRPC.Document document, long peer, MessageObject replyingMessageObject) { if (document == null) { return; } @@ -680,10 +679,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } } - SendMessagesHelper.getInstance().sendMessage((TLRPC.TL_document) document, null, null, peer, replyingMessageObject, asAdmin, null, null); + SendMessagesHelper.getInstance().sendMessage((TLRPC.TL_document) document, null, null, peer, replyingMessageObject, null, null); } - public void sendMessage(ArrayList messages, final long peer, boolean asAdmin) { + public void sendMessage(ArrayList messages, final long peer) { if ((int) peer == 0 || messages == null || messages.isEmpty()) { return; } @@ -763,7 +762,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } newMsg.local_id = newMsg.id = UserConfig.getNewMessageId(); newMsg.out = true; - if (asAdmin && to_id.channel_id != 0 && !isMegagroup) { + if (to_id.channel_id != 0 && !isMegagroup) { newMsg.from_id = isSignature ? UserConfig.getClientUserId() : -to_id.channel_id; newMsg.post = true; } else { @@ -778,9 +777,11 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter ids.add(newMsg.fwd_msg_id); newMsg.date = ConnectionsManager.getInstance().getCurrentTime(); if (inputPeer instanceof TLRPC.TL_inputPeerChannel) { - if (asAdmin && !isMegagroup) { + if (!isMegagroup) { newMsg.views = 1; newMsg.flags |= TLRPC.MESSAGE_FLAG_HAS_VIEWS; + } else { + newMsg.unread = true; } } else { if ((msgObj.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { @@ -833,10 +834,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter req.random_id = randomIds; req.id = ids; - if (asAdmin && req.to_peer.channel_id != 0 && !isMegagroup) { - req.broadcast = true; - } - final ArrayList newMsgObjArr = arr; final ArrayList newMsgArr = objArr; final HashMap messagesByRandomIdsFinal = messagesByRandomIds; @@ -856,6 +853,12 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter a--; } } + Integer value = MessagesController.getInstance().dialogs_read_outbox_max.get(peer); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(true, peer); + MessagesController.getInstance().dialogs_read_outbox_max.put(peer, value); + } + for (int a = 0; a < updates.updates.size(); a++) { TLRPC.Update update = updates.updates.get(a); if (update instanceof TLRPC.TL_updateNewMessage || update instanceof TLRPC.TL_updateNewChannelMessage) { @@ -870,6 +873,8 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; } } + message.unread = value < message.id; + Long random_id = newMessagesByIds.get(message.id); if (random_id != null) { final TLRPC.Message newMsgObj = messagesByRandomIdsFinal.get(random_id); @@ -892,6 +897,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter @Override public void run() { newMsgObj.send_state = MessageObject.MESSAGE_SEND_STATE_SENT; + SearchQuery.increasePeerRaiting(peer); NotificationCenter.getInstance().postNotificationName(NotificationCenter.messageReceivedByServer, oldId, newMsgObj.id, newMsgObj, peer); processSentMessage(oldId); removeFromSendingMessages(oldId); @@ -945,14 +951,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } - public void editMessage(MessageObject messageObject, String message, boolean searchLinks, final BaseFragment fragment) { - if (fragment == null || fragment.getParentActivity() == null) { - return; + public int editMessage(MessageObject messageObject, String message, boolean searchLinks, final BaseFragment fragment, ArrayList entities, final Runnable callback) { + if (fragment == null || fragment.getParentActivity() == null || callback == null) { + return 0; } - final ProgressDialog progressDialog = new ProgressDialog(fragment.getParentActivity()); - progressDialog.setMessage(LocaleController.getString("Loading", R.string.Loading)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.setCancelable(false); TLRPC.TL_messages_editMessage req = new TLRPC.TL_messages_editMessage(); req.peer = MessagesController.getInputPeer((int) messageObject.getDialogId()); @@ -960,19 +962,17 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter req.flags |= 2048; req.id = messageObject.getId(); req.no_webpage = !searchLinks; - final int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + if (entities != null) { + req.entities = entities; + req.flags |= 8; + } + return ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - try { - if (!fragment.getParentActivity().isFinishing()) { - progressDialog.dismiss(); - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } + callback.run(); } }); if (error == null) { @@ -993,22 +993,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } }); - progressDialog.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("tmessages", e); - } - } - }); - try { - progressDialog.show(); - } catch (Exception e) { - //don't promt - } } private void sendLocation(Location location) { @@ -1018,7 +1002,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter mediaGeo.geo._long = location.getLongitude(); for (HashMap.Entry entry : waitingForLocation.entrySet()) { MessageObject messageObject = entry.getValue(); - SendMessagesHelper.getInstance().sendMessage(mediaGeo, messageObject.getDialogId(), messageObject, false, null, null); + SendMessagesHelper.getInstance().sendMessage(mediaGeo, messageObject.getDialogId(), messageObject, null, null); } } @@ -1084,31 +1068,31 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter return !(messageObject == null || button == null) && waitingForCallback.containsKey(messageObject.getId() + "_" + Utilities.bytesToHex(button.data)); } - public void sendMessage(MessageObject retryMessageObject, boolean asAdmin) { - sendMessage(null, null, null, null, null, null, retryMessageObject.getDialogId(), retryMessageObject.messageOwner.attachPath, null, null, true, asAdmin, retryMessageObject, null, retryMessageObject.messageOwner.reply_markup, retryMessageObject.messageOwner.params); + public void sendMessage(MessageObject retryMessageObject) { + sendMessage(null, null, null, null, null, null, retryMessageObject.getDialogId(), retryMessageObject.messageOwner.attachPath, null, null, true, retryMessageObject, null, retryMessageObject.messageOwner.reply_markup, retryMessageObject.messageOwner.params); } - public void sendMessage(TLRPC.User user, long peer, MessageObject reply_to_msg, boolean asAdmin, TLRPC.ReplyMarkup replyMarkup, HashMap params) { - sendMessage(null, null, null, null, user, null, peer, null, reply_to_msg, null, true, asAdmin, null, null, replyMarkup, params); + public void sendMessage(TLRPC.User user, long peer, MessageObject reply_to_msg, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + sendMessage(null, null, null, null, user, null, peer, null, reply_to_msg, null, true, null, null, replyMarkup, params); } - public void sendMessage(TLRPC.TL_document document, VideoEditedInfo videoEditedInfo, String path, long peer, MessageObject reply_to_msg, boolean asAdmin, TLRPC.ReplyMarkup replyMarkup, HashMap params) { - sendMessage(null, null, null, videoEditedInfo, null, document, peer, path, reply_to_msg, null, true, asAdmin, null, null, replyMarkup, params); + public void sendMessage(TLRPC.TL_document document, VideoEditedInfo videoEditedInfo, String path, long peer, MessageObject reply_to_msg, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + sendMessage(null, null, null, videoEditedInfo, null, document, peer, path, reply_to_msg, null, true, null, null, replyMarkup, params); } - public void sendMessage(String message, long peer, MessageObject reply_to_msg, TLRPC.WebPage webPage, boolean searchLinks, boolean asAdmin, ArrayList entities, TLRPC.ReplyMarkup replyMarkup, HashMap params) { - sendMessage(message, null, null, null, null, null, peer, null, reply_to_msg, webPage, searchLinks, asAdmin, null, entities, replyMarkup, params); + public void sendMessage(String message, long peer, MessageObject reply_to_msg, TLRPC.WebPage webPage, boolean searchLinks, ArrayList entities, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + sendMessage(message, null, null, null, null, null, peer, null, reply_to_msg, webPage, searchLinks, null, entities, replyMarkup, params); } - public void sendMessage(TLRPC.MessageMedia location, long peer, MessageObject reply_to_msg, boolean asAdmin, TLRPC.ReplyMarkup replyMarkup, HashMap params) { - sendMessage(null, location, null, null, null, null, peer, null, reply_to_msg, null, true, asAdmin, null, null, replyMarkup, params); + public void sendMessage(TLRPC.MessageMedia location, long peer, MessageObject reply_to_msg, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + sendMessage(null, location, null, null, null, null, peer, null, reply_to_msg, null, true, null, null, replyMarkup, params); } - public void sendMessage(TLRPC.TL_photo photo, String path, long peer, MessageObject reply_to_msg, boolean asAdmin, TLRPC.ReplyMarkup replyMarkup, HashMap params) { - sendMessage(null, null, photo, null, null, null, peer, path, reply_to_msg, null, true, asAdmin, null, null, replyMarkup, params); + public void sendMessage(TLRPC.TL_photo photo, String path, long peer, MessageObject reply_to_msg, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + sendMessage(null, null, photo, null, null, null, peer, path, reply_to_msg, null, true, null, null, replyMarkup, params); } - private void sendMessage(String message, TLRPC.MessageMedia location, TLRPC.TL_photo photo, VideoEditedInfo videoEditedInfo, TLRPC.User user, TLRPC.TL_document document, long peer, String path, MessageObject reply_to_msg, TLRPC.WebPage webPage, boolean searchLinks, boolean asAdmin, MessageObject retryMessageObject, ArrayList entities, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + private void sendMessage(String message, TLRPC.MessageMedia location, TLRPC.TL_photo photo, VideoEditedInfo videoEditedInfo, TLRPC.User user, TLRPC.TL_document document, long peer, String path, MessageObject reply_to_msg, TLRPC.WebPage webPage, boolean searchLinks, MessageObject retryMessageObject, ArrayList entities, TLRPC.ReplyMarkup replyMarkup, HashMap params) { if (peer == 0) { return; } @@ -1123,6 +1107,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter int type = -1; int lower_id = (int) peer; int high_id = (int) (peer >> 32); + boolean isChannel = false; TLRPC.EncryptedChat encryptedChat = null; TLRPC.InputPeer sendToPeer = lower_id != 0 ? MessagesController.getInputPeer(lower_id) : null; ArrayList sendToPeers = null; @@ -1137,11 +1122,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } return; } - } else if (asAdmin && sendToPeer instanceof TLRPC.TL_inputPeerChannel) { + } else if (sendToPeer instanceof TLRPC.TL_inputPeerChannel) { TLRPC.Chat chat = MessagesController.getInstance().getChat(sendToPeer.channel_id); - if (chat.megagroup) { - asAdmin = false; - } + isChannel = chat != null && !chat.megagroup; } try { @@ -1326,7 +1309,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } newMsg.local_id = newMsg.id = UserConfig.getNewMessageId(); newMsg.out = true; - if (asAdmin && sendToPeer != null && sendToPeer.channel_id != 0) { + if (isChannel && sendToPeer != null) { newMsg.from_id = -sendToPeer.channel_id; } else { newMsg.from_id = UserConfig.getClientUserId(); @@ -1352,7 +1335,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter newMsg.date = ConnectionsManager.getInstance().getCurrentTime(); newMsg.flags |= TLRPC.MESSAGE_FLAG_HAS_MEDIA; if (sendToPeer instanceof TLRPC.TL_inputPeerChannel) { - if (asAdmin) { + if (isChannel) { newMsg.views = 1; newMsg.flags |= TLRPC.MESSAGE_FLAG_HAS_VIEWS; } @@ -1360,6 +1343,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter if (chat != null) { if (chat.megagroup) { newMsg.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; + newMsg.unread = true; } else { newMsg.post = true; if (chat.signatures) { @@ -1451,9 +1435,12 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter newMsg.media_unread = true; } + newMsg.send_state = MessageObject.MESSAGE_SEND_STATE_SENDING; newMsgObj = new MessageObject(newMsg, null, true); newMsgObj.replyMessageObject = reply_to_msg; - newMsgObj.messageOwner.send_state = MessageObject.MESSAGE_SEND_STATE_SENDING; + if (!newMsgObj.isForwarded() && newMsgObj.type == 3) { + newMsgObj.attachPathExists = true; + } ArrayList objArr = new ArrayList<>(); objArr.add(newMsgObj); @@ -1485,14 +1472,12 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } else { TLRPC.TL_messages_sendMessage reqSend = new TLRPC.TL_messages_sendMessage(); reqSend.message = message; + reqSend.clear_draft = retryMessageObject == null; if (newMsg.to_id instanceof TLRPC.TL_peerChannel) { reqSend.silent = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE).getBoolean("silent_" + peer, false); } reqSend.peer = sendToPeer; reqSend.random_id = newMsg.random_id; - if (asAdmin && sendToPeer instanceof TLRPC.TL_inputPeerChannel) { - reqSend.broadcast = true; - } if (reply_to_msg != null) { reqSend.flags |= 1; reqSend.reply_to_msg_id = reply_to_msg.getId(); @@ -1500,7 +1485,14 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter if (!searchLinks) { reqSend.no_webpage = true; } + if (entities != null && !entities.isEmpty()) { + reqSend.entities = entities; + reqSend.flags |= 8; + } performSendMessageRequest(reqSend, newMsgObj, null); + if (retryMessageObject == null) { + DraftQuery.cleanDraft(peer, false); + } } } else { TLRPC.TL_decryptedMessage reqSend; @@ -1536,7 +1528,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } else { reqSend.media = new TLRPC.TL_decryptedMessageMediaEmpty(); } - SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, null, null); + SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, null, null, newMsgObj); + if (retryMessageObject == null) { + DraftQuery.cleanDraft(peer, false); + } } } else if (type >= 1 && type <= 3 || type >= 5 && type <= 8 || type == 9 && encryptedChat != null) { if (encryptedChat == null) { @@ -1675,6 +1670,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter delayedMessage.sendRequest = request; } reqSend = request; + if (retryMessageObject == null) { + DraftQuery.cleanDraft(peer, false); + } } else { TLRPC.TL_messages_sendMedia request = new TLRPC.TL_messages_sendMedia(); request.peer = sendToPeer; @@ -1683,9 +1681,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } request.random_id = newMsg.random_id; request.media = inputMedia; - if (asAdmin && sendToPeer instanceof TLRPC.TL_inputPeerChannel) { - request.broadcast = true; - } if (reply_to_msg != null) { request.flags |= 1; request.reply_to_msg_id = reply_to_msg.getId(); @@ -1764,7 +1759,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } reqSend.media.lat = location.geo.lat; reqSend.media._long = location.geo._long; - SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, null, null); + SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, null, null, newMsgObj); } else if (type == 2 || type == 9 && photo != null) { TLRPC.PhotoSize small = photo.sizes.get(0); TLRPC.PhotoSize big = photo.sizes.get(photo.sizes.size() - 1); @@ -1809,7 +1804,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter encryptedFile.access_hash = big.location.secret; reqSend.media.key = big.location.key; reqSend.media.iv = big.location.iv; - SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, encryptedFile, null); + SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, encryptedFile, null, newMsgObj); } } else if (type == 3) { ImageLoader.fillPhotoSizeWithBytes(document.thumb); @@ -1865,7 +1860,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter encryptedFile.access_hash = document.access_hash; reqSend.media.key = document.key; reqSend.media.iv = document.iv; - SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, encryptedFile, null); + SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, encryptedFile, null, newMsgObj); } } else if (type == 6) { reqSend.media = new TLRPC.TL_decryptedMessageMediaContact(); @@ -1873,7 +1868,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter reqSend.media.first_name = user.first_name; reqSend.media.last_name = user.last_name; reqSend.media.user_id = user.id; - SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, null, null); + SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, null, null, newMsgObj); } else if (type == 7 || type == 9 && document != null) { if (MessageObject.isStickerDocument(document)) { reqSend.media = new TLRPC.TL_decryptedMessageMediaExternalDocument(); @@ -1890,7 +1885,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } else { ((TLRPC.TL_decryptedMessageMediaExternalDocument) reqSend.media).thumb = document.thumb; } - SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, null, null); + SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, null, null, newMsgObj); } else { ImageLoader.fillPhotoSizeWithBytes(document.thumb); if (AndroidUtilities.getPeerLayerVersion(encryptedChat.layer) >= 46) { @@ -1940,7 +1935,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter encryptedFile.access_hash = document.access_hash; reqSend.media.key = document.key; reqSend.media.iv = document.iv; - SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, encryptedFile, null); + SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, encryptedFile, null, newMsgObj); } } } else if (type == 8) { @@ -1986,6 +1981,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } performSendDelayedMessage(delayedMessage); } + if (retryMessageObject == null) { + DraftQuery.cleanDraft(peer, false); + } } } else if (type == 4) { TLRPC.TL_messages_forwardMessages reqSend = new TLRPC.TL_messages_forwardMessages(); @@ -2009,16 +2007,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } else { reqSend.id.add(retryMessageObject.messageOwner.fwd_msg_id); } - if (asAdmin && reqSend.to_peer.channel_id != 0) { - reqSend.broadcast = true; - } performSendMessageRequest(reqSend, newMsgObj, null); } else if (type == 9) { TLRPC.TL_messages_sendInlineBotResult reqSend = new TLRPC.TL_messages_sendInlineBotResult(); reqSend.peer = sendToPeer; - if (asAdmin && sendToPeer instanceof TLRPC.TL_inputPeerChannel) { - reqSend.broadcast = true; - } reqSend.random_id = newMsg.random_id; if (reply_to_msg != null) { reqSend.flags |= 1; @@ -2029,6 +2021,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } reqSend.query_id = Utilities.parseLong(params.get("query_id")); reqSend.id = params.get("id"); + if (retryMessageObject == null) { + reqSend.clear_draft = true; + DraftQuery.cleanDraft(peer, false); + } performSendMessageRequest(reqSend, newMsgObj, null); } } catch (Exception e) { @@ -2254,6 +2250,13 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } if (message != null) { + Integer value = MessagesController.getInstance().dialogs_read_outbox_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + MessagesController.getInstance().dialogs_read_outbox_max.put(message.dialog_id, value); + } + message.unread = value < message.id; + newMsgObj.id = message.id; updateMediaPaths(msgObj, message, originalPath, false); } else { @@ -2287,6 +2290,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); } + SearchQuery.increasePeerRaiting(newMsgObj.dialog_id); NotificationCenter.getInstance().postNotificationName(NotificationCenter.messageReceivedByServer, oldId, (isBroadcast ? oldId : newMsgObj.id), newMsgObj, newMsgObj.dialog_id); processSentMessage(oldId); removeFromSendingMessages(oldId); @@ -2402,7 +2406,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } else if (size2 != null && MessageObject.isStickerMessage(sentMessage) && size2.location != null) { size.location = size2.location; - } else if (size2 != null && size2.location instanceof TLRPC.TL_fileLocationUnavailable) { + } else if (size2 != null && size2.location instanceof TLRPC.TL_fileLocationUnavailable || size2 instanceof TLRPC.TL_photoSizeEmpty) { newMsg.media.document.thumb = sentMessage.media.document.thumb; } @@ -2414,6 +2418,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter TLRPC.DocumentAttribute attribute = newMsg.media.document.attributes.get(a); if (attribute instanceof TLRPC.TL_documentAttributeAudio) { oldWaveform = attribute.waveform; + break; } } newMsg.media.document.attributes = sentMessage.media.document.attributes; @@ -2439,12 +2444,16 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter if (!cacheFile.renameTo(cacheFile2)) { sentMessage.attachPath = newMsg.attachPath; sentMessage.message = newMsg.message; - } else if (!MessageObject.isVideoMessage(sentMessage)) { - newMsgObj.mediaExists = newMsgObj.attachPathExists; - newMsgObj.attachPathExists = false; - newMsg.attachPath = ""; - if (originalPath != null && originalPath.startsWith("http")) { - MessagesStorage.getInstance().addRecentLocalFile(originalPath, cacheFile2.toString(), newMsg.media.document); + } else { + if (MessageObject.isVideoMessage(sentMessage)) { + newMsgObj.attachPathExists = true; + } else { + newMsgObj.mediaExists = newMsgObj.attachPathExists; + newMsgObj.attachPathExists = false; + newMsg.attachPath = ""; + if (originalPath != null && originalPath.startsWith("http")) { + MessagesStorage.getInstance().addRecentLocalFile(originalPath, cacheFile2.toString(), newMsg.media.document); + } } } } else { @@ -2527,10 +2536,16 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } - private static boolean prepareSendingDocumentInternal(String path, String originalPath, Uri uri, String mime, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin, String caption) { + private static boolean prepareSendingDocumentInternal(String path, String originalPath, Uri uri, String mime, final long dialog_id, final MessageObject reply_to_msg, String caption) { if ((path == null || path.length() == 0) && uri == null) { return false; } + if (uri != null && AndroidUtilities.isInternalUri(uri)) { + return false; + } + if (path != null && AndroidUtilities.isInternalUri(Uri.fromFile(new File(path)))) { + return false; + } MimeTypeMap myMime = MimeTypeMap.getSingleton(); TLRPC.TL_documentAttributeAudio attributeAudio = null; if (uri != null) { @@ -2681,13 +2696,13 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(documentFinal, null, pathFinal, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(documentFinal, null, pathFinal, dialog_id, reply_to_msg, null, params); } }); return true; } - public static void prepareSendingDocument(String path, String originalPath, Uri uri, String mine, long dialog_id, MessageObject reply_to_msg, boolean asAdmin) { + public static void prepareSendingDocument(String path, String originalPath, Uri uri, String mine, long dialog_id, MessageObject reply_to_msg) { if ((path == null || originalPath == null) && uri == null) { return; } @@ -2699,10 +2714,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } paths.add(path); originalPaths.add(originalPath); - prepareSendingDocuments(paths, originalPaths, uris, mine, dialog_id, reply_to_msg, asAdmin); + prepareSendingDocuments(paths, originalPaths, uris, mine, dialog_id, reply_to_msg); } - public static void prepareSendingAudioDocuments(final ArrayList messageObjects, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public static void prepareSendingAudioDocuments(final ArrayList messageObjects, final long dialog_id, final MessageObject reply_to_msg) { new Thread(new Runnable() { @Override public void run() { @@ -2754,7 +2769,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(documentFinal, null, messageObject.messageOwner.attachPath, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(documentFinal, null, messageObject.messageOwner.attachPath, dialog_id, reply_to_msg, null, params); } }); } @@ -2762,7 +2777,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter }).start(); } - public static void prepareSendingDocuments(final ArrayList paths, final ArrayList originalPaths, final ArrayList uris, final String mime, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public static void prepareSendingDocuments(final ArrayList paths, final ArrayList originalPaths, final ArrayList uris, final String mime, final long dialog_id, final MessageObject reply_to_msg) { if (paths == null && originalPaths == null && uris == null || paths != null && originalPaths != null && paths.size() != originalPaths.size()) { return; } @@ -2772,14 +2787,14 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter boolean error = false; if (paths != null) { for (int a = 0; a < paths.size(); a++) { - if (!prepareSendingDocumentInternal(paths.get(a), originalPaths.get(a), null, mime, dialog_id, reply_to_msg, asAdmin, null)) { + if (!prepareSendingDocumentInternal(paths.get(a), originalPaths.get(a), null, mime, dialog_id, reply_to_msg, null)) { error = true; } } } if (uris != null) { for (int a = 0; a < uris.size(); a++) { - if (!prepareSendingDocumentInternal(null, null, uris.get(a), mime, dialog_id, reply_to_msg, asAdmin, null)) { + if (!prepareSendingDocumentInternal(null, null, uris.get(a), mime, dialog_id, reply_to_msg, null)) { error = true; } } @@ -2801,7 +2816,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter }).start(); } - public static void prepareSendingPhoto(String imageFilePath, Uri imageUri, long dialog_id, MessageObject reply_to_msg, CharSequence caption, boolean asAdmin) { + public static void prepareSendingPhoto(String imageFilePath, Uri imageUri, long dialog_id, MessageObject reply_to_msg, CharSequence caption) { ArrayList paths = null; ArrayList uris = null; ArrayList captions = null; @@ -2817,10 +2832,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter captions = new ArrayList<>(); captions.add(caption.toString()); } - prepareSendingPhotos(paths, uris, dialog_id, reply_to_msg, captions, asAdmin); + prepareSendingPhotos(paths, uris, dialog_id, reply_to_msg, captions); } - public static void prepareSendingBotContextResult(final TLRPC.BotInlineResult result, final HashMap params, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public static void prepareSendingBotContextResult(final TLRPC.BotInlineResult result, final HashMap params, final long dialog_id, final MessageObject reply_to_msg) { if (result == null) { return; } @@ -3017,17 +3032,17 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter public void run() { if (finalDocument != null) { finalDocument.caption = result.send_message.caption; - SendMessagesHelper.getInstance().sendMessage(finalDocument, null, finalPathFinal, dialog_id, reply_to_msg, asAdmin, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(finalDocument, null, finalPathFinal, dialog_id, reply_to_msg, result.send_message.reply_markup, params); } else if (finalPhoto != null) { finalPhoto.caption = result.send_message.caption; - SendMessagesHelper.getInstance().sendMessage(finalPhoto, result.content_url, dialog_id, reply_to_msg, asAdmin, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(finalPhoto, result.content_url, dialog_id, reply_to_msg, result.send_message.reply_markup, params); } } }); } }).run(); } else if (result.send_message instanceof TLRPC.TL_botInlineMessageText) { - SendMessagesHelper.getInstance().sendMessage(result.send_message.message, dialog_id, reply_to_msg, null, !result.send_message.no_webpage, asAdmin, result.send_message.entities, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(result.send_message.message, dialog_id, reply_to_msg, null, !result.send_message.no_webpage, result.send_message.entities, result.send_message.reply_markup, params); } else if (result.send_message instanceof TLRPC.TL_botInlineMessageMediaVenue) { TLRPC.TL_messageMediaVenue venue = new TLRPC.TL_messageMediaVenue(); venue.geo = result.send_message.geo; @@ -3035,21 +3050,21 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter venue.title = result.send_message.title; venue.provider = result.send_message.provider; venue.venue_id = result.send_message.venue_id; - SendMessagesHelper.getInstance().sendMessage(venue, dialog_id, reply_to_msg, asAdmin, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(venue, dialog_id, reply_to_msg, result.send_message.reply_markup, params); } else if (result.send_message instanceof TLRPC.TL_botInlineMessageMediaGeo) { TLRPC.TL_messageMediaGeo location = new TLRPC.TL_messageMediaGeo(); location.geo = result.send_message.geo; - SendMessagesHelper.getInstance().sendMessage(location, dialog_id, reply_to_msg, asAdmin, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(location, dialog_id, reply_to_msg, result.send_message.reply_markup, params); } else if (result.send_message instanceof TLRPC.TL_botInlineMessageMediaContact) { TLRPC.User user = new TLRPC.TL_user(); user.phone = result.send_message.phone_number; user.first_name = result.send_message.first_name; user.last_name = result.send_message.last_name; - SendMessagesHelper.getInstance().sendMessage(user, dialog_id, reply_to_msg, asAdmin, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(user, dialog_id, reply_to_msg, result.send_message.reply_markup, params); } } - public static void prepareSendingPhotosSearch(final ArrayList photos, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public static void prepareSendingPhotosSearch(final ArrayList photos, final long dialog_id, final MessageObject reply_to_msg) { if (photos == null || photos.isEmpty()) { return; } @@ -3145,7 +3160,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(documentFinal, null, pathFinal, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(documentFinal, null, pathFinal, dialog_id, reply_to_msg, null, params); } }); } else { @@ -3195,7 +3210,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(photoFinal, needDownloadHttpFinal ? searchImage.imageUrl : null, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(photoFinal, needDownloadHttpFinal ? searchImage.imageUrl : null, dialog_id, reply_to_msg, null, params); } }); } @@ -3219,7 +3234,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter return src; } - public static void prepareSendingText(final String text, final long dialog_id, final boolean asAdmin) { + public static void prepareSendingText(final String text, final long dialog_id) { MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override public void run() { @@ -3234,7 +3249,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter int count = (int) Math.ceil(textFinal.length() / 4096.0f); for (int a = 0; a < count; a++) { String mess = textFinal.substring(a * 4096, Math.min((a + 1) * 4096, textFinal.length())); - SendMessagesHelper.getInstance().sendMessage(mess, dialog_id, null, null, true, asAdmin, null, null, null); + SendMessagesHelper.getInstance().sendMessage(mess, dialog_id, null, null, true, null, null, null); } } } @@ -3245,28 +3260,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter }); } - public static void prepareSendingLocation(final TLRPC.MessageMedia location, final long dialog_id, final boolean asAdmin) { - MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { - @Override - public void run() { - Utilities.stageQueue.postRunnable(new Runnable() { - @Override - public void run() { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - if (location != null) { - SendMessagesHelper.getInstance().sendMessage(location, dialog_id, null, asAdmin, null, null); - } - } - }); - } - }); - } - }); - } - - public static void prepareSendingPhotos(ArrayList paths, ArrayList uris, final long dialog_id, final MessageObject reply_to_msg, final ArrayList captions, final boolean asAdmin) { + public static void prepareSendingPhotos(ArrayList paths, ArrayList uris, final long dialog_id, final MessageObject reply_to_msg, final ArrayList captions) { if (paths == null && uris == null || paths != null && paths.isEmpty() || uris != null && uris.isEmpty()) { return; } @@ -3364,7 +3358,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(photoFinal, null, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(photoFinal, null, dialog_id, reply_to_msg, null, params); } }); } @@ -3372,14 +3366,14 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } if (sendAsDocuments != null && !sendAsDocuments.isEmpty()) { for (int a = 0; a < sendAsDocuments.size(); a++) { - prepareSendingDocumentInternal(sendAsDocuments.get(a), sendAsDocumentsOriginal.get(a), null, extension, dialog_id, reply_to_msg, asAdmin, sendAsDocumentsCaptions.get(a)); + prepareSendingDocumentInternal(sendAsDocuments.get(a), sendAsDocumentsOriginal.get(a), null, extension, dialog_id, reply_to_msg, sendAsDocumentsCaptions.get(a)); } } } }).start(); } - public static void prepareSendingVideo(final String videoPath, final long estimatedSize, final long duration, final int width, final int height, final VideoEditedInfo videoEditedInfo, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public static void prepareSendingVideo(final String videoPath, final long estimatedSize, final long duration, final int width, final int height, final VideoEditedInfo videoEditedInfo, final long dialog_id, final MessageObject reply_to_msg) { if (videoPath == null || videoPath.length() == 0) { return; } @@ -3439,34 +3433,33 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter document.size = (int) temp.length(); } boolean infoObtained = false; - if (Build.VERSION.SDK_INT >= 14) { - MediaMetadataRetriever mediaMetadataRetriever = null; + + MediaMetadataRetriever mediaMetadataRetriever = null; + try { + mediaMetadataRetriever = new MediaMetadataRetriever(); + mediaMetadataRetriever.setDataSource(videoPath); + String width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); + if (width != null) { + attributeVideo.w = Integer.parseInt(width); + } + String height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); + if (height != null) { + attributeVideo.h = Integer.parseInt(height); + } + String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + if (duration != null) { + attributeVideo.duration = (int) Math.ceil(Long.parseLong(duration) / 1000.0f); + } + infoObtained = true; + } catch (Exception e) { + FileLog.e("tmessages", e); + } finally { try { - mediaMetadataRetriever = new MediaMetadataRetriever(); - mediaMetadataRetriever.setDataSource(videoPath); - String width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); - if (width != null) { - attributeVideo.w = Integer.parseInt(width); + if (mediaMetadataRetriever != null) { + mediaMetadataRetriever.release(); } - String height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); - if (height != null) { - attributeVideo.h = Integer.parseInt(height); - } - String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - if (duration != null) { - attributeVideo.duration = (int) Math.ceil(Long.parseLong(duration) / 1000.0f); - } - infoObtained = true; } catch (Exception e) { FileLog.e("tmessages", e); - } finally { - try { - if (mediaMetadataRetriever != null) { - mediaMetadataRetriever.release(); - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } } } if (!infoObtained) { @@ -3494,11 +3487,11 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(videoFinal, videoEditedInfo, finalPath, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(videoFinal, videoEditedInfo, finalPath, dialog_id, reply_to_msg, null, params); } }); } else { - prepareSendingDocumentInternal(videoPath, videoPath, null, null, dialog_id, reply_to_msg, asAdmin, null); + prepareSendingDocumentInternal(videoPath, videoPath, null, null, dialog_id, reply_to_msg, null); } } }).start(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/TgChooserTargetService.java b/TMessagesProj/src/main/java/org/telegram/messenger/TgChooserTargetService.java index 7f89fcb80..4e53ecf2b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/TgChooserTargetService.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/TgChooserTargetService.java @@ -180,5 +180,4 @@ public class TgChooserTargetService extends ChooserTargetService { } return null; } - } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java b/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java index ea10ee13e..a698626ee 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java @@ -20,27 +20,27 @@ import java.io.File; public class UserConfig { private static TLRPC.User currentUser; - public static boolean registeredForPush = false; + public static boolean registeredForPush; public static String pushString = ""; public static int lastSendMessageId = -210000; public static int lastLocalId = -210000; public static int lastBroadcastId = -1; public static String contactsHash = ""; - public static String importHash = ""; - public static boolean blockedUsersLoaded = false; + public static boolean blockedUsersLoaded; private final static Object sync = new Object(); - public static boolean saveIncomingPhotos = false; - public static int contactsVersion = 1; + public static boolean saveIncomingPhotos; public static String passcodeHash = ""; public static byte[] passcodeSalt = new byte[0]; - public static boolean appLocked = false; - public static int passcodeType = 0; + public static boolean appLocked; + public static int passcodeType; public static int autoLockIn = 60 * 60; - public static int lastPauseTime = 0; - public static boolean isWaitingForPasscodeEnter = false; + public static int lastPauseTime; + public static boolean isWaitingForPasscodeEnter; public static boolean useFingerprint = true; public static String lastUpdateVersion; public static int lastContactsSyncTime; + public static int lastHintsSyncTime; + public static boolean draftsLoaded; public static int migrateOffsetId = -1; public static int migrateOffsetDate = -1; @@ -72,9 +72,7 @@ public class UserConfig { editor.putInt("lastSendMessageId", lastSendMessageId); editor.putInt("lastLocalId", lastLocalId); editor.putString("contactsHash", contactsHash); - editor.putString("importHash", importHash); editor.putBoolean("saveIncomingPhotos", saveIncomingPhotos); - editor.putInt("contactsVersion", contactsVersion); editor.putInt("lastBroadcastId", lastBroadcastId); editor.putBoolean("blockedUsersLoaded", blockedUsersLoaded); editor.putString("passcodeHash1", passcodeHash); @@ -86,6 +84,8 @@ public class UserConfig { editor.putString("lastUpdateVersion2", lastUpdateVersion); editor.putInt("lastContactsSyncTime", lastContactsSyncTime); editor.putBoolean("useFingerprint", useFingerprint); + editor.putInt("lastHintsSyncTime", lastHintsSyncTime); + editor.putBoolean("draftsLoaded", draftsLoaded); editor.putInt("migrateOffsetId", migrateOffsetId); if (migrateOffsetId != -1) { @@ -160,9 +160,8 @@ public class UserConfig { lastSendMessageId = data.readInt32(false); lastLocalId = data.readInt32(false); contactsHash = data.readString(false); - importHash = data.readString(false); + data.readString(false); saveIncomingPhotos = data.readBool(false); - contactsVersion = 0; MessagesStorage.lastQtsValue = data.readInt32(false); MessagesStorage.lastSecretVersion = data.readInt32(false); int val = data.readInt32(false); @@ -186,9 +185,7 @@ public class UserConfig { lastSendMessageId = preferences.getInt("lastSendMessageId", -210000); lastLocalId = preferences.getInt("lastLocalId", -210000); contactsHash = preferences.getString("contactsHash", ""); - importHash = preferences.getString("importHash", ""); saveIncomingPhotos = preferences.getBoolean("saveIncomingPhotos", false); - contactsVersion = preferences.getInt("contactsVersion", 0); } if (lastLocalId > -210000) { lastLocalId = -210000; @@ -213,9 +210,7 @@ public class UserConfig { lastSendMessageId = preferences.getInt("lastSendMessageId", -210000); lastLocalId = preferences.getInt("lastLocalId", -210000); contactsHash = preferences.getString("contactsHash", ""); - importHash = preferences.getString("importHash", ""); saveIncomingPhotos = preferences.getBoolean("saveIncomingPhotos", false); - contactsVersion = preferences.getInt("contactsVersion", 0); lastBroadcastId = preferences.getInt("lastBroadcastId", -1); blockedUsersLoaded = preferences.getBoolean("blockedUsersLoaded", false); passcodeHash = preferences.getString("passcodeHash1", ""); @@ -226,6 +221,8 @@ public class UserConfig { useFingerprint = preferences.getBoolean("useFingerprint", true); lastUpdateVersion = preferences.getString("lastUpdateVersion2", "3.5"); lastContactsSyncTime = preferences.getInt("lastContactsSyncTime", (int) (System.currentTimeMillis() / 1000) - 23 * 60 * 60); + lastHintsSyncTime = preferences.getInt("lastHintsSyncTime", (int) (System.currentTimeMillis() / 1000) - 25 * 60 * 60); + draftsLoaded = preferences.getBoolean("draftsLoaded", false); migrateOffsetId = preferences.getInt("migrateOffsetId", 0); if (migrateOffsetId != -1) { @@ -294,9 +291,7 @@ public class UserConfig { currentUser = null; registeredForPush = false; contactsHash = ""; - importHash = ""; lastSendMessageId = -210000; - contactsVersion = 1; lastBroadcastId = -1; saveIncomingPhotos = false; blockedUsersLoaded = false; @@ -313,9 +308,11 @@ public class UserConfig { autoLockIn = 60 * 60; lastPauseTime = 0; useFingerprint = true; + draftsLoaded = true; isWaitingForPasscodeEnter = false; lastUpdateVersion = BuildVars.BUILD_VERSION_STRING; lastContactsSyncTime = (int) (System.currentTimeMillis() / 1000) - 23 * 60 * 60; + lastHintsSyncTime = (int) (System.currentTimeMillis() / 1000) - 25 * 60 * 60; saveConfig(true); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java b/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java index 15bf1bb2d..b11a5fc2b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java @@ -22,7 +22,7 @@ import java.util.regex.Pattern; public class Utilities { - public static Pattern pattern = Pattern.compile("[0-9]+"); + public static Pattern pattern = Pattern.compile("[\\-0-9]+"); public static SecureRandom random = new SecureRandom(); public static volatile DispatchQueue stageQueue = new DispatchQueue("stageQueue"); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java index 12c8666a7..e3629ec7d 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java @@ -25,27 +25,33 @@ public class VideoEditedInfo { return String.format(Locale.US, "-1_%d_%d_%d_%d_%d_%d_%d_%d_%s", startTime, endTime, rotationValue, originalWidth, originalHeight, bitrate, resultWidth, resultHeight, originalPath); } - public void parseString(String string) { + public boolean parseString(String string) { if (string.length() < 6) { - return; + return false; } - String args[] = string.split("_"); - if (args.length >= 10) { - startTime = Long.parseLong(args[1]); - endTime = Long.parseLong(args[2]); - rotationValue = Integer.parseInt(args[3]); - originalWidth = Integer.parseInt(args[4]); - originalHeight = Integer.parseInt(args[5]); - bitrate = Integer.parseInt(args[6]); - resultWidth = Integer.parseInt(args[7]); - resultHeight = Integer.parseInt(args[8]); - for (int a = 9; a < args.length; a++) { - if (originalPath == null) { - originalPath = args[a]; - } else { - originalPath += "_" + args[a]; + try { + String args[] = string.split("_"); + if (args.length >= 10) { + startTime = Long.parseLong(args[1]); + endTime = Long.parseLong(args[2]); + rotationValue = Integer.parseInt(args[3]); + originalWidth = Integer.parseInt(args[4]); + originalHeight = Integer.parseInt(args[5]); + bitrate = Integer.parseInt(args[6]); + resultWidth = Integer.parseInt(args[7]); + resultHeight = Integer.parseInt(args[8]); + for (int a = 9; a < args.length; a++) { + if (originalPath == null) { + originalPath = args[a]; + } else { + originalPath += "_" + args[a]; + } } } + return true; + } catch (Exception e) { + FileLog.e("tmessages", e); } + return false; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/WearReplyReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/WearReplyReceiver.java index 911daf078..6a490d6f1 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/WearReplyReceiver.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/WearReplyReceiver.java @@ -32,7 +32,7 @@ public class WearReplyReceiver extends BroadcastReceiver { if (dialog_id == 0 || max_id == 0) { return; } - SendMessagesHelper.getInstance().sendMessage(text.toString(), dialog_id, null, null, true, false, null, null, null); + SendMessagesHelper.getInstance().sendMessage(text.toString(), dialog_id, null, null, true, null, null, null); MessagesController.getInstance().markDialogAsRead(dialog_id, max_id, max_id, 0, true, false); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java b/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java index ae01ce9a2..9a3d56c2d 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java @@ -88,8 +88,14 @@ public class Browser { @Override public void onServiceConnected(CustomTabsClient client) { customTabsClient = client; - if (customTabsClient != null) { - customTabsClient.warmup(0); + if (MediaController.getInstance().canCustomTabs()) { + if (customTabsClient != null) { + try { + customTabsClient.warmup(0); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/AspectRatioFrameLayout.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/AspectRatioFrameLayout.java new file mode 100755 index 000000000..21cea8bb5 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/AspectRatioFrameLayout.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * A {@link FrameLayout} that resizes itself to match a specified aspect ratio. + */ +public final class AspectRatioFrameLayout extends FrameLayout { + + /** + * The {@link FrameLayout} will not resize itself if the fractional difference between its natural + * aspect ratio and the requested aspect ratio falls below this threshold. + *

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

    + * Counters should be written from the playback thread only. Counters may be read from any thread. + * To ensure that the counter values are correctly reflected between threads, users of this class + * should invoke {@link #ensureUpdated()} prior to reading and after writing. + */ +public final class CodecCounters { + + public int codecInitCount; + public int codecReleaseCount; + public int inputBufferCount; + public int outputFormatChangedCount; + public int outputBuffersChangedCount; + public int renderedOutputBufferCount; + public int skippedOutputBufferCount; + public int droppedOutputBufferCount; + public int maxConsecutiveDroppedOutputBufferCount; + + /** + * Should be invoked from the playback thread after the counters have been updated. Should also + * be invoked from any other thread that wishes to read the counters, before reading. These calls + * ensure that counter updates are made visible to the reading threads. + */ + public synchronized void ensureUpdated() { + // Do nothing. The use of synchronized ensures a memory barrier should another thread also + // call this method. + } + + public String getDebugString() { + ensureUpdated(); + StringBuilder builder = new StringBuilder(); + builder.append("cic:").append(codecInitCount); + builder.append(" crc:").append(codecReleaseCount); + builder.append(" ibc:").append(inputBufferCount); + builder.append(" ofc:").append(outputFormatChangedCount); + builder.append(" obc:").append(outputBuffersChangedCount); + builder.append(" ren:").append(renderedOutputBufferCount); + builder.append(" sob:").append(skippedOutputBufferCount); + builder.append(" dob:").append(droppedOutputBufferCount); + builder.append(" mcdob:").append(maxConsecutiveDroppedOutputBufferCount); + return builder.toString(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/CryptoInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/CryptoInfo.java new file mode 100755 index 000000000..e20623ac2 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/CryptoInfo.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaExtractor; + +/** + * Compatibility wrapper around {@link android.media.MediaCodec.CryptoInfo}. + */ +public final class CryptoInfo { + + /** + * @see android.media.MediaCodec.CryptoInfo#iv + */ + public byte[] iv; + /** + * @see android.media.MediaCodec.CryptoInfo#key + */ + public byte[] key; + /** + * @see android.media.MediaCodec.CryptoInfo#mode + */ + public int mode; + /** + * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData + */ + public int[] numBytesOfClearData; + /** + * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData + */ + public int[] numBytesOfEncryptedData; + /** + * @see android.media.MediaCodec.CryptoInfo#numSubSamples + */ + public int numSubSamples; + + private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo; + + public CryptoInfo() { + frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null; + } + + /** + * @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int) + */ + public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData, + byte[] key, byte[] iv, int mode) { + this.numSubSamples = numSubSamples; + this.numBytesOfClearData = numBytesOfClearData; + this.numBytesOfEncryptedData = numBytesOfEncryptedData; + this.key = key; + this.iv = iv; + this.mode = mode; + if (Util.SDK_INT >= 16) { + updateFrameworkCryptoInfoV16(); + } + } + + /** + * Equivalent to {@link MediaExtractor#getSampleCryptoInfo(android.media.MediaCodec.CryptoInfo)}. + * + * @param extractor The extractor from which to retrieve the crypto information. + */ + @TargetApi(16) + public void setFromExtractorV16(MediaExtractor extractor) { + extractor.getSampleCryptoInfo(frameworkCryptoInfo); + numSubSamples = frameworkCryptoInfo.numSubSamples; + numBytesOfClearData = frameworkCryptoInfo.numBytesOfClearData; + numBytesOfEncryptedData = frameworkCryptoInfo.numBytesOfEncryptedData; + key = frameworkCryptoInfo.key; + iv = frameworkCryptoInfo.iv; + mode = frameworkCryptoInfo.mode; + } + + /** + * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance. + *

    + * Successive calls to this method on a single {@link CryptoInfo} will return the same instance. + * Changes to the {@link CryptoInfo} will be reflected in the returned object. The return object + * should not be modified directly. + * + * @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance. + */ + @TargetApi(16) + public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() { + return frameworkCryptoInfo; + } + + @TargetApi(16) + private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() { + return new android.media.MediaCodec.CryptoInfo(); + } + + @TargetApi(16) + private void updateFrameworkCryptoInfoV16() { + frameworkCryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, key, iv, + mode); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DecoderInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DecoderInfo.java new file mode 100755 index 000000000..4e682242c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DecoderInfo.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaCodecInfo.CodecCapabilities; + +/** + * Contains information about a media decoder. + */ +@TargetApi(16) +public final class DecoderInfo { + + /** + * The name of the decoder. + *

    + * May be passed to {@link android.media.MediaCodec#createByCodecName(String)} to create an + * instance of the decoder. + */ + public final String name; + + /** + * {@link CodecCapabilities} for this decoder. + */ + public final CodecCapabilities capabilities; + + /** + * Whether the decoder supports seamless resolution switches. + * + * @see android.media.MediaCodecInfo.CodecCapabilities#isFeatureSupported(String) + * @see android.media.MediaCodecInfo.CodecCapabilities#FEATURE_AdaptivePlayback + */ + public final boolean adaptive; + + /** + * @param name The name of the decoder. + * @param capabilities {@link CodecCapabilities} of the decoder. + */ + /* package */ DecoderInfo(String name, CodecCapabilities capabilities) { + this.name = name; + this.capabilities = capabilities; + this.adaptive = isAdaptive(capabilities); + } + + private static boolean isAdaptive(CodecCapabilities capabilities) { + return capabilities != null && Util.SDK_INT >= 19 && isAdaptiveV19(capabilities); + } + + @TargetApi(19) + private static boolean isAdaptiveV19(CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DefaultLoadControl.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DefaultLoadControl.java new file mode 100755 index 000000000..43c4268d0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DefaultLoadControl.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.upstream.Allocator; +import org.telegram.messenger.exoplayer.upstream.NetworkLock; + +import android.os.Handler; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * A {@link LoadControl} implementation that allows loads to continue in a sequence that prevents + * any loader from getting too far ahead or behind any of the other loaders. + *

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

    + * This implementation of {@link LoadControl} integrates with {@link NetworkLock}, by registering + * itself as a task with priority {@link NetworkLock#STREAMING_PRIORITY} during loading periods, + * and unregistering itself during draining periods. + */ +public final class DefaultLoadControl implements LoadControl { + + /** + * Interface definition for a callback to be notified of {@link DefaultLoadControl} events. + */ + public interface EventListener { + + /** + * Invoked when the control transitions from a loading to a draining state, or vice versa. + * + * @param loading Whether the control is now in a loading state. + */ + void onLoadingChanged(boolean loading); + + } + + public static final int DEFAULT_LOW_WATERMARK_MS = 15000; + public static final int DEFAULT_HIGH_WATERMARK_MS = 30000; + public static final float DEFAULT_LOW_BUFFER_LOAD = 0.2f; + public static final float DEFAULT_HIGH_BUFFER_LOAD = 0.8f; + + private static final int ABOVE_HIGH_WATERMARK = 0; + private static final int BETWEEN_WATERMARKS = 1; + private static final int BELOW_LOW_WATERMARK = 2; + + private final Allocator allocator; + private final List loaders; + private final HashMap loaderStates; + private final Handler eventHandler; + private final EventListener eventListener; + + private final long lowWatermarkUs; + private final long highWatermarkUs; + private final float lowBufferLoad; + private final float highBufferLoad; + + private int targetBufferSize; + private long maxLoadStartPositionUs; + private int bufferState; + private boolean fillingBuffers; + private boolean streamingPrioritySet; + + /** + * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. + * + * @param allocator The {@link Allocator} used by the loader. + */ + public DefaultLoadControl(Allocator allocator) { + this(allocator, null, null); + } + + /** + * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. + * + * @param allocator The {@link Allocator} used by the loader. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public DefaultLoadControl(Allocator allocator, Handler eventHandler, + EventListener eventListener) { + this(allocator, eventHandler, eventListener, DEFAULT_LOW_WATERMARK_MS, + DEFAULT_HIGH_WATERMARK_MS, DEFAULT_LOW_BUFFER_LOAD, DEFAULT_HIGH_BUFFER_LOAD); + } + + /** + * Constructs a new instance. + * + * @param allocator The {@link Allocator} used by the loader. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param lowWatermarkMs The minimum duration of media that can be buffered for the control to + * be in the draining state. If less media is buffered, then the control will transition to + * the filling state. + * @param highWatermarkMs The minimum duration of media that can be buffered for the control to + * transition from filling to draining. + * @param lowBufferLoad The minimum fraction of the buffer that must be utilized for the control + * to be in the draining state. If the utilization is lower, then the control will transition + * to the filling state. + * @param highBufferLoad The minimum fraction of the buffer that must be utilized for the control + * to transition from the loading state to the draining state. + */ + public DefaultLoadControl(Allocator allocator, Handler eventHandler, EventListener eventListener, + int lowWatermarkMs, int highWatermarkMs, float lowBufferLoad, float highBufferLoad) { + this.allocator = allocator; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.loaders = new ArrayList<>(); + this.loaderStates = new HashMap<>(); + this.lowWatermarkUs = lowWatermarkMs * 1000L; + this.highWatermarkUs = highWatermarkMs * 1000L; + this.lowBufferLoad = lowBufferLoad; + this.highBufferLoad = highBufferLoad; + } + + @Override + public void register(Object loader, int bufferSizeContribution) { + loaders.add(loader); + loaderStates.put(loader, new LoaderState(bufferSizeContribution)); + targetBufferSize += bufferSizeContribution; + } + + @Override + public void unregister(Object loader) { + loaders.remove(loader); + LoaderState state = loaderStates.remove(loader); + targetBufferSize -= state.bufferSizeContribution; + updateControlState(); + } + + @Override + public void trimAllocator() { + allocator.trim(targetBufferSize); + } + + @Override + public Allocator getAllocator() { + return allocator; + } + + @Override + public boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs, + boolean loading) { + // Update the loader state. + int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs); + LoaderState loaderState = loaderStates.get(loader); + boolean loaderStateChanged = loaderState.bufferState != loaderBufferState + || loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading; + if (loaderStateChanged) { + loaderState.bufferState = loaderBufferState; + loaderState.nextLoadPositionUs = nextLoadPositionUs; + loaderState.loading = loading; + } + + // Update the buffer state. + int currentBufferSize = allocator.getTotalBytesAllocated(); + int bufferState = getBufferState(currentBufferSize); + boolean bufferStateChanged = this.bufferState != bufferState; + if (bufferStateChanged) { + this.bufferState = bufferState; + } + + // If either of the individual states have changed, update the shared control state. + if (loaderStateChanged || bufferStateChanged) { + updateControlState(); + } + + return currentBufferSize < targetBufferSize && nextLoadPositionUs != -1 + && nextLoadPositionUs <= maxLoadStartPositionUs; + } + + private int getLoaderBufferState(long playbackPositionUs, long nextLoadPositionUs) { + if (nextLoadPositionUs == -1) { + return ABOVE_HIGH_WATERMARK; + } else { + long timeUntilNextLoadPosition = nextLoadPositionUs - playbackPositionUs; + return timeUntilNextLoadPosition > highWatermarkUs ? ABOVE_HIGH_WATERMARK : + timeUntilNextLoadPosition < lowWatermarkUs ? BELOW_LOW_WATERMARK : + BETWEEN_WATERMARKS; + } + } + + private int getBufferState(int currentBufferSize) { + float bufferLoad = (float) currentBufferSize / targetBufferSize; + return bufferLoad > highBufferLoad ? ABOVE_HIGH_WATERMARK + : bufferLoad < lowBufferLoad ? BELOW_LOW_WATERMARK + : BETWEEN_WATERMARKS; + } + + private void updateControlState() { + boolean loading = false; + boolean haveNextLoadPosition = false; + int highestState = bufferState; + for (int i = 0; i < loaders.size(); i++) { + LoaderState loaderState = loaderStates.get(loaders.get(i)); + loading |= loaderState.loading; + haveNextLoadPosition |= loaderState.nextLoadPositionUs != -1; + highestState = Math.max(highestState, loaderState.bufferState); + } + + fillingBuffers = !loaders.isEmpty() && (loading || haveNextLoadPosition) + && (highestState == BELOW_LOW_WATERMARK + || (highestState == BETWEEN_WATERMARKS && fillingBuffers)); + if (fillingBuffers && !streamingPrioritySet) { + NetworkLock.instance.add(NetworkLock.STREAMING_PRIORITY); + streamingPrioritySet = true; + notifyLoadingChanged(true); + } else if (!fillingBuffers && streamingPrioritySet && !loading) { + NetworkLock.instance.remove(NetworkLock.STREAMING_PRIORITY); + streamingPrioritySet = false; + notifyLoadingChanged(false); + } + + maxLoadStartPositionUs = -1; + if (fillingBuffers) { + for (int i = 0; i < loaders.size(); i++) { + Object loader = loaders.get(i); + LoaderState loaderState = loaderStates.get(loader); + long loaderTime = loaderState.nextLoadPositionUs; + if (loaderTime != -1 + && (maxLoadStartPositionUs == -1 || loaderTime < maxLoadStartPositionUs)) { + maxLoadStartPositionUs = loaderTime; + } + } + } + } + + private void notifyLoadingChanged(final boolean loading) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadingChanged(loading); + } + }); + } + } + + private static class LoaderState { + + public final int bufferSizeContribution; + + public int bufferState; + public boolean loading; + public long nextLoadPositionUs; + + public LoaderState(int bufferSizeContribution) { + this.bufferSizeContribution = bufferSizeContribution; + bufferState = ABOVE_HIGH_WATERMARK; + loading = false; + nextLoadPositionUs = -1; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DummyTrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DummyTrackRenderer.java new file mode 100755 index 000000000..9afe11f0e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DummyTrackRenderer.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer; + +/** + * A {@link TrackRenderer} that does nothing. + *

    + * This renderer returns 0 from {@link #getTrackCount()} in order to request that it should be + * ignored. {@link IllegalStateException} is thrown from all other methods documented to indicate + * that they should not be invoked unless the renderer is prepared. + */ +public final class DummyTrackRenderer extends TrackRenderer { + + @Override + protected boolean doPrepare(long positionUs) throws ExoPlaybackException { + return true; + } + + @Override + protected int getTrackCount() { + return 0; + } + + @Override + protected MediaFormat getFormat(int track) { + throw new IllegalStateException(); + } + + @Override + protected boolean isEnded() { + throw new IllegalStateException(); + } + + @Override + protected boolean isReady() { + throw new IllegalStateException(); + } + + @Override + protected void seekTo(long positionUs) { + throw new IllegalStateException(); + } + + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) { + throw new IllegalStateException(); + } + + @Override + protected void maybeThrowError() { + throw new IllegalStateException(); + } + + @Override + protected long getDurationUs() { + throw new IllegalStateException(); + } + + @Override + protected long getBufferedPositionUs() { + throw new IllegalStateException(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlaybackException.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlaybackException.java new file mode 100755 index 000000000..9a1bd5cf7 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlaybackException.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer; + +/** + * Thrown when a non-recoverable playback failure occurs. + *

    + * Where possible, the cause returned by {@link #getCause()} will indicate the reason for failure. + */ +public final class ExoPlaybackException extends Exception { + + /** + * True if the cause (i.e. the {@link Throwable} returned by {@link #getCause()}) was only caught + * by a fail-safe at the top level of the player. False otherwise. + */ + public final boolean caughtAtTopLevel; + + public ExoPlaybackException(String message) { + super(message); + caughtAtTopLevel = false; + } + + public ExoPlaybackException(Throwable cause) { + super(cause); + caughtAtTopLevel = false; + } + + public ExoPlaybackException(String message, Throwable cause) { + super(message, cause); + caughtAtTopLevel = false; + } + + /* package */ ExoPlaybackException(Throwable cause, boolean caughtAtTopLevel) { + super(cause); + this.caughtAtTopLevel = caughtAtTopLevel; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayer.java new file mode 100755 index 000000000..d9887f3fe --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayer.java @@ -0,0 +1,415 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer; + +import android.os.Looper; + +/** + * An extensible media player exposing traditional high-level media player functionality, such as + * the ability to prepare, play, pause and seek. + * + *

    Topics covered here are: + *

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

    Assumptions and player construction

    + * + *

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

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

    Threading model

    + * + *

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

    + *

    MediaPlayer state diagram

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

    Player state

    + * + *

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

    + *

    ExoPlayer state

    + * + *

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

    + *

    ExoPlayer playback state transitions

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

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

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

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

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

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

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

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

    + * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the + * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding + * integer version 123045006 (123-045-006). + */ + public static final int VERSION_INT = 1005008; + + /** + * Whether the library was compiled with {@link org.telegram.messenger.exoplayer.util.Assertions} + * checks enabled. + */ + public static final boolean ASSERTIONS_ENABLED = true; + + /** + * Whether the library was compiled with {@link org.telegram.messenger.exoplayer.util.TraceUtil} + * trace enabled. + */ + public static final boolean TRACE_ENABLED = true; + + private ExoPlayerLibraryInfo() {} + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/FrameworkSampleSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/FrameworkSampleSource.java new file mode 100755 index 000000000..4206db6d4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/FrameworkSampleSource.java @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.SampleSource.SampleSourceReader; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.drm.DrmInitData.SchemeInitData; +import org.telegram.messenger.exoplayer.extractor.ExtractorSampleSource; +import org.telegram.messenger.exoplayer.extractor.mp4.PsshAtomUtil; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.media.MediaExtractor; +import android.net.Uri; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Map; +import java.util.UUID; + +/** + * Extracts samples from a stream using Android's {@link MediaExtractor}. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    + * If true is returned then we explicitly disable the feature. + * + * @return True if the device is known to enable frame-rate conversion logic that negatively + * impacts ExoPlayer. False otherwise. + */ + private static boolean deviceNeedsAutoFrcWorkaround() { + // nVidia Shield prior to M tries to adjust the playback rate to better map the frame-rate of + // content to the refresh rate of the display. For example playback of 23.976fps content is + // adjusted to play at 1.001x speed when the output display is 60Hz. Unfortunately the + // implementation causes ExoPlayer's reported playback position to drift out of sync. Captions + // also lose sync [Internal: b/26453592]. + return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecUtil.java new file mode 100755 index 000000000..e7de3a090 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecUtil.java @@ -0,0 +1,501 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaCodecInfo; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecInfo.CodecProfileLevel; +import android.media.MediaCodecList; +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A utility class for querying the available codecs. + */ +@TargetApi(16) +public final class MediaCodecUtil { + + /** + * Thrown when an error occurs querying the device for its underlying media capabilities. + *

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

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

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

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

    + * If the capacity of {@link #data} is sufficient this method does nothing. If the capacity is + * insufficient then an attempt is made to replace {@link #data} with a new {@link ByteBuffer} + * whose capacity is sufficient. Data up to the current position is copied to the new buffer. + * + * @param length The length of the write that must be accommodated, in bytes. + * @throws IllegalStateException If there is insufficient capacity to accommodate the write and + * the buffer replacement mode of the holder is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}. + */ + public void ensureSpaceForWrite(int length) throws IllegalStateException { + if (data == null) { + data = createReplacementBuffer(length); + return; + } + // Check whether the current buffer is sufficient. + int capacity = data.capacity(); + int position = data.position(); + int requiredCapacity = position + length; + if (capacity >= requiredCapacity) { + return; + } + // Instantiate a new buffer if possible. + ByteBuffer newData = createReplacementBuffer(requiredCapacity); + // Copy data up to the current position from the old buffer to the new one. + if (position > 0) { + data.position(0); + data.limit(position); + newData.put(data); + } + // Set the new buffer. + data = newData; + } + + /** + * Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_ENCRYPTED} set. + */ + public boolean isEncrypted() { + return (flags & C.SAMPLE_FLAG_ENCRYPTED) != 0; + } + + /** + * Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_DECODE_ONLY} set. + */ + public boolean isDecodeOnly() { + return (flags & C.SAMPLE_FLAG_DECODE_ONLY) != 0; + } + + /** + * Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_SYNC} set. + */ + public boolean isSyncFrame() { + return (flags & C.SAMPLE_FLAG_SYNC) != 0; + } + + /** + * Clears {@link #data}. Does nothing if {@link #data} is null. + */ + public void clearData() { + if (data != null) { + data.clear(); + } + } + + private ByteBuffer createReplacementBuffer(int requiredCapacity) { + if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_NORMAL) { + return ByteBuffer.allocate(requiredCapacity); + } else if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DIRECT) { + return ByteBuffer.allocateDirect(requiredCapacity); + } else { + int currentCapacity = data == null ? 0 : data.capacity(); + throw new IllegalStateException("Buffer too small (" + currentCapacity + " < " + + requiredCapacity + ")"); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSource.java new file mode 100755 index 000000000..255820c49 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSource.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer; + +import java.io.IOException; + +/** + * A source of media samples. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    + * This method should be called when access to the {@link SampleSource} is no longer required. + */ + public void release(); + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSourceTrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSourceTrackRenderer.java new file mode 100755 index 000000000..fbc5ebc10 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSourceTrackRenderer.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.MediaCodecUtil.DecoderQueryException; +import org.telegram.messenger.exoplayer.SampleSource.SampleSourceReader; + +import java.io.IOException; +import java.util.Arrays; + +/** + * Base class for {@link TrackRenderer} implementations that render samples obtained from a + * {@link SampleSource}. + */ +public abstract class SampleSourceTrackRenderer extends TrackRenderer { + + private final SampleSourceReader[] sources; + + private int[] handledSourceIndices; + private int[] handledSourceTrackIndices; + + private SampleSourceReader enabledSource; + private int enabledSourceTrackIndex; + + private long durationUs; + + /** + * @param sources One or more upstream sources from which the renderer can obtain samples. + */ + public SampleSourceTrackRenderer(SampleSource... sources) { + this.sources = new SampleSourceReader[sources.length]; + for (int i = 0; i < sources.length; i++) { + this.sources[i] = sources[i].register(); + } + } + + @Override + protected final boolean doPrepare(long positionUs) throws ExoPlaybackException { + boolean allSourcesPrepared = true; + for (int i = 0; i < sources.length; i++) { + allSourcesPrepared &= sources[i].prepare(positionUs); + } + if (!allSourcesPrepared) { + return false; + } + // The sources are all prepared. + int totalSourceTrackCount = 0; + for (int i = 0; i < sources.length; i++) { + totalSourceTrackCount += sources[i].getTrackCount(); + } + long durationUs = 0; + int handledTrackCount = 0; + int[] handledSourceIndices = new int[totalSourceTrackCount]; + int[] handledTrackIndices = new int[totalSourceTrackCount]; + int sourceCount = sources.length; + for (int sourceIndex = 0; sourceIndex < sourceCount; sourceIndex++) { + SampleSourceReader source = sources[sourceIndex]; + int sourceTrackCount = source.getTrackCount(); + for (int trackIndex = 0; trackIndex < sourceTrackCount; trackIndex++) { + MediaFormat format = source.getFormat(trackIndex); + boolean handlesTrack; + try { + handlesTrack = handlesTrack(format); + } catch (DecoderQueryException e) { + throw new ExoPlaybackException(e); + } + if (handlesTrack) { + handledSourceIndices[handledTrackCount] = sourceIndex; + handledTrackIndices[handledTrackCount] = trackIndex; + handledTrackCount++; + if (durationUs == TrackRenderer.UNKNOWN_TIME_US) { + // We've already encountered a track for which the duration is unknown, so the media + // duration is unknown regardless of the duration of this track. + } else { + long trackDurationUs = format.durationUs; + if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) { + durationUs = TrackRenderer.UNKNOWN_TIME_US; + } else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) { + // Do nothing. + } else { + durationUs = Math.max(durationUs, trackDurationUs); + } + } + } + } + } + this.durationUs = durationUs; + this.handledSourceIndices = Arrays.copyOf(handledSourceIndices, handledTrackCount); + this.handledSourceTrackIndices = Arrays.copyOf(handledTrackIndices, handledTrackCount); + return true; + } + + @Override + protected void onEnabled(int track, long positionUs, boolean joining) + throws ExoPlaybackException { + positionUs = shiftInputPosition(positionUs); + enabledSource = sources[handledSourceIndices[track]]; + enabledSourceTrackIndex = handledSourceTrackIndices[track]; + enabledSource.enable(enabledSourceTrackIndex, positionUs); + onDiscontinuity(positionUs); + } + + @Override + protected final void seekTo(long positionUs) throws ExoPlaybackException { + positionUs = shiftInputPosition(positionUs); + enabledSource.seekToUs(positionUs); + checkForDiscontinuity(positionUs); + } + + @Override + protected final void doSomeWork(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException { + positionUs = shiftInputPosition(positionUs); + boolean sourceIsReady = enabledSource.continueBuffering(enabledSourceTrackIndex, positionUs); + positionUs = checkForDiscontinuity(positionUs); + doSomeWork(positionUs, elapsedRealtimeUs, sourceIsReady); + } + + @Override + protected long getBufferedPositionUs() { + return enabledSource.getBufferedPositionUs(); + } + + @Override + protected long getDurationUs() { + return durationUs; + } + + @Override + protected void maybeThrowError() throws ExoPlaybackException { + if (enabledSource != null) { + maybeThrowError(enabledSource); + } else { + int sourceCount = sources.length; + for (int i = 0; i < sourceCount; i++) { + maybeThrowError(sources[i]); + } + } + } + + @Override + protected void onDisabled() throws ExoPlaybackException { + enabledSource.disable(enabledSourceTrackIndex); + enabledSource = null; + } + + @Override + protected void onReleased() throws ExoPlaybackException { + int sourceCount = sources.length; + for (int i = 0; i < sourceCount; i++) { + sources[i].release(); + } + } + + @Override + protected final int getTrackCount() { + return handledSourceTrackIndices.length; + } + + @Override + protected final MediaFormat getFormat(int track) { + SampleSourceReader source = sources[handledSourceIndices[track]]; + return source.getFormat(handledSourceTrackIndices[track]); + } + + /** + * Shifts positions passed to {@link #onEnabled(int, long, boolean)}, {@link #seekTo(long)} and + * {@link #doSomeWork(long, long)}. + *

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

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

    TrackRenderer state transitions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    + * See {@link #isMediaFormatFinal} for information about when this method is guaranteed to return + * correct data. + * + * @return The {@link DrmInitData} corresponding to this chunk. + */ + public abstract DrmInitData getDrmInitData(); + + /** + * Returns the output most recently passed to {@link #init(DefaultTrackOutput)}. + */ + protected final DefaultTrackOutput getOutput() { + return output; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Chunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Chunk.java new file mode 100755 index 000000000..e1c74410f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Chunk.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.upstream.Loader.Loadable; +import org.telegram.messenger.exoplayer.util.Assertions; + +/** + * An abstract base class for {@link Loadable} implementations that load chunks of data required + * for the playback of streams. + */ +public abstract class Chunk implements Loadable { + + /** + * Value of {@link #type} for chunks containing unspecified data. + */ + public static final int TYPE_UNSPECIFIED = 0; + /** + * Value of {@link #type} for chunks containing media data. + */ + public static final int TYPE_MEDIA = 1; + /** + * Value of {@link #type} for chunks containing media initialization data. + */ + public static final int TYPE_MEDIA_INITIALIZATION = 2; + /** + * Value of {@link #type} for chunks containing drm related data. + */ + public static final int TYPE_DRM = 3; + /** + * Value of {@link #type} for chunks containing manifest or playlist data. + */ + public static final int TYPE_MANIFEST = 4; + /** + * Implementations may define custom {@link #type} codes greater than or equal to this value. + */ + public static final int TYPE_CUSTOM_BASE = 10000; + + /** + * Value of {@link #trigger} for a load whose reason is unspecified. + */ + public static final int TRIGGER_UNSPECIFIED = 0; + /** + * Value of {@link #trigger} for a load triggered by an initial format selection. + */ + public static final int TRIGGER_INITIAL = 1; + /** + * Value of {@link #trigger} for a load triggered by a user initiated format selection. + */ + public static final int TRIGGER_MANUAL = 2; + /** + * Value of {@link #trigger} for a load triggered by an adaptive format selection. + */ + public static final int TRIGGER_ADAPTIVE = 3; + /** + * Value of {@link #trigger} for a load triggered whilst in a trick play mode. + */ + public static final int TRIGGER_TRICK_PLAY = 4; + /** + * Implementations may define custom {@link #trigger} codes greater than or equal to this value. + */ + public static final int TRIGGER_CUSTOM_BASE = 10000; + /** + * Value of {@link #parentId} if no parent id need be specified. + */ + public static final int NO_PARENT_ID = -1; + + /** + * The type of the chunk. For reporting only. + */ + public final int type; + /** + * The reason why the chunk was generated. For reporting only. + */ + public final int trigger; + /** + * The format associated with the data being loaded, or null if the data being loaded is not + * associated with a specific format. + */ + public final Format format; + /** + * The {@link DataSpec} that defines the data to be loaded. + */ + public final DataSpec dataSpec; + /** + * Optional identifier for a parent from which this chunk originates. + */ + public final int parentId; + + protected final DataSource dataSource; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed + * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then + * the length resolved by {@code dataSource.open(dataSpec)} must not exceed + * {@link Integer#MAX_VALUE}. + * @param type See {@link #type}. + * @param trigger See {@link #trigger}. + * @param format See {@link #format}. + * @param parentId See {@link #parentId}. + */ + public Chunk(DataSource dataSource, DataSpec dataSpec, int type, int trigger, Format format, + int parentId) { + this.dataSource = Assertions.checkNotNull(dataSource); + this.dataSpec = Assertions.checkNotNull(dataSpec); + this.type = type; + this.trigger = trigger; + this.format = format; + this.parentId = parentId; + } + + /** + * Gets the number of bytes that have been loaded. + * + * @return The number of bytes that have been loaded. + */ + public abstract long bytesLoaded(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkExtractorWrapper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkExtractorWrapper.java new file mode 100755 index 000000000..cb0f27691 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkExtractorWrapper.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * An {@link Extractor} wrapper for loading chunks containing a single track. + *

    + * The wrapper allows switching of the {@link SingleTrackOutput} that receives parsed data. + */ +public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput { + + /** + * Receives stream level data extracted by the wrapped {@link Extractor}. + */ + public interface SingleTrackOutput extends TrackOutput { + + /** + * @see ExtractorOutput#seekMap(SeekMap) + */ + void seekMap(SeekMap seekMap); + + /** + * @see ExtractorOutput#drmInitData(DrmInitData) + */ + void drmInitData(DrmInitData drmInitData); + + } + + private final Extractor extractor; + private boolean extractorInitialized; + private SingleTrackOutput output; + + // Accessed only on the loader thread. + private boolean seenTrack; + + /** + * @param extractor The extractor to wrap. + */ + public ChunkExtractorWrapper(Extractor extractor) { + this.extractor = extractor; + } + + /** + * Initializes the extractor to output to the provided {@link SingleTrackOutput}, and configures + * it to receive data from a new chunk. + * + * @param output The {@link SingleTrackOutput} that will receive the parsed data. + */ + public void init(SingleTrackOutput output) { + this.output = output; + if (!extractorInitialized) { + extractor.init(this); + extractorInitialized = true; + } else { + extractor.seek(); + } + } + + /** + * Reads from the provided {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}. + * @throws IOException If an error occurred reading from the source. + * @throws InterruptedException If the thread was interrupted. + */ + public int read(ExtractorInput input) throws IOException, InterruptedException { + int result = extractor.read(input, null); + Assertions.checkState(result != Extractor.RESULT_SEEK); + return result; + } + + // ExtractorOutput implementation. + + @Override + public TrackOutput track(int id) { + Assertions.checkState(!seenTrack); + seenTrack = true; + return this; + } + + @Override + public void endTracks() { + Assertions.checkState(seenTrack); + } + + @Override + public void seekMap(SeekMap seekMap) { + output.seekMap(seekMap); + } + + @Override + public void drmInitData(DrmInitData drmInitData) { + output.drmInitData(drmInitData); + } + + // TrackOutput implementation. + + @Override + public void format(MediaFormat format) { + output.format(format); + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return output.sampleData(input, length, allowEndOfInput); + } + + @Override + public void sampleData(ParsableByteArray data, int length) { + output.sampleData(data, length); + } + + @Override + public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { + output.sampleMetadata(timeUs, flags, size, offset, encryptionKey); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkOperationHolder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkOperationHolder.java new file mode 100755 index 000000000..fc5639f64 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkOperationHolder.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.chunk; + +/** + * Holds a chunk operation, which consists of a either: + *

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

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

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

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

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

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

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

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

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

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

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

    + * This method should be used for recycling the holder only, and not for reading the data. + * + * @return The array in which the data is held. + */ + public byte[] getDataHolder() { + return data; + } + + @Override + public long bytesLoaded() { + return limit; + } + + // Loadable implementation + + @Override + public final void cancelLoad() { + loadCanceled = true; + } + + @Override + public final boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public final void load() throws IOException, InterruptedException { + try { + dataSource.open(dataSpec); + limit = 0; + int bytesRead = 0; + while (bytesRead != -1 && !loadCanceled) { + maybeExpandData(); + bytesRead = dataSource.read(data, limit, READ_GRANULARITY); + if (bytesRead != -1) { + limit += bytesRead; + } + } + if (!loadCanceled) { + consume(data, limit); + } + } finally { + dataSource.close(); + } + } + + /** + * Invoked by {@link #load()}. Implementations should override this method to consume the loaded + * data. + * + * @param data An array containing the data. + * @param limit The limit of the data. + * @throws IOException If an error occurs consuming the loaded data. + */ + protected abstract void consume(byte[] data, int limit) throws IOException; + + private void maybeExpandData() { + if (data == null) { + data = new byte[READ_GRANULARITY]; + } else if (data.length < limit + READ_GRANULARITY) { + // The new length is calculated as (data.length + READ_GRANULARITY) rather than + // (limit + READ_GRANULARITY) in order to avoid small increments in the length. + data = Arrays.copyOf(data, data.length + READ_GRANULARITY); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Format.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Format.java new file mode 100755 index 000000000..cd0331233 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Format.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.util.Comparator; + +/** + * Defines the high level format of a media stream. + */ +public class Format { + + /** + * Sorts {@link Format} objects in order of decreasing bandwidth. + */ + public static final class DecreasingBandwidthComparator implements Comparator { + + @Override + public int compare(Format a, Format b) { + return b.bitrate - a.bitrate; + } + + } + + /** + * An identifier for the format. + */ + public final String id; + + /** + * The mime type of the format. + */ + public final String mimeType; + + /** + * The average bandwidth in bits per second. + */ + public final int bitrate; + + /** + * The width of the video in pixels, or -1 if unknown or not applicable. + */ + public final int width; + + /** + * The height of the video in pixels, or -1 if unknown or not applicable. + */ + public final int height; + + /** + * The video frame rate in frames per second, or -1 if unknown or not applicable. + */ + public final float frameRate; + + /** + * The number of audio channels, or -1 if unknown or not applicable. + */ + public final int audioChannels; + + /** + * The audio sampling rate in Hz, or -1 if unknown or not applicable. + */ + public final int audioSamplingRate; + + /** + * The codecs used to decode the format. Can be {@code null} if unknown. + */ + public final String codecs; + + /** + * The language of the format. Can be null if unknown. + *

    + * The language codes are two-letter lowercase ISO language codes (such as "en") as defined by + * ISO 639-1. + */ + public final String language; + + /** + * @param id The format identifier. + * @param mimeType The format mime type. + * @param width The width of the video in pixels, or -1 if unknown or not applicable. + * @param height The height of the video in pixels, or -1 if unknown or not applicable. + * @param frameRate The frame rate of the video in frames per second, or -1 if unknown or not + * applicable. + * @param numChannels The number of audio channels, or -1 if unknown or not applicable. + * @param audioSamplingRate The audio sampling rate in Hz, or -1 if unknown or not applicable. + * @param bitrate The average bandwidth of the format in bits per second. + */ + public Format(String id, String mimeType, int width, int height, float frameRate, int numChannels, + int audioSamplingRate, int bitrate) { + this(id, mimeType, width, height, frameRate, numChannels, audioSamplingRate, bitrate, null); + } + + /** + * @param id The format identifier. + * @param mimeType The format mime type. + * @param width The width of the video in pixels, or -1 if unknown or not applicable. + * @param height The height of the video in pixels, or -1 if unknown or not applicable. + * @param frameRate The frame rate of the video in frames per second, or -1 if unknown or not + * applicable. + * @param numChannels The number of audio channels, or -1 if unknown or not applicable. + * @param audioSamplingRate The audio sampling rate in Hz, or -1 if unknown or not applicable. + * @param bitrate The average bandwidth of the format in bits per second. + * @param language The language of the format. + */ + public Format(String id, String mimeType, int width, int height, float frameRate, int numChannels, + int audioSamplingRate, int bitrate, String language) { + this(id, mimeType, width, height, frameRate, numChannels, audioSamplingRate, bitrate, language, + null); + } + + /** + * @param id The format identifier. + * @param mimeType The format mime type. + * @param width The width of the video in pixels, or -1 if unknown or not applicable. + * @param height The height of the video in pixels, or -1 if unknown or not applicable. + * @param frameRate The frame rate of the video in frames per second, or -1 if unknown or not + * applicable. + * @param audioChannels The number of audio channels, or -1 if unknown or not applicable. + * @param audioSamplingRate The audio sampling rate in Hz, or -1 if unknown or not applicable. + * @param bitrate The average bandwidth of the format in bits per second. + * @param language The language of the format. + * @param codecs The codecs used to decode the format. + */ + public Format(String id, String mimeType, int width, int height, float frameRate, + int audioChannels, int audioSamplingRate, int bitrate, String language, String codecs) { + this.id = Assertions.checkNotNull(id); + this.mimeType = mimeType; + this.width = width; + this.height = height; + this.frameRate = frameRate; + this.audioChannels = audioChannels; + this.audioSamplingRate = audioSamplingRate; + this.bitrate = bitrate; + this.language = language; + this.codecs = codecs; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + /** + * Implements equality based on {@link #id} only. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Format other = (Format) obj; + return other.id.equals(id); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/FormatEvaluator.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/FormatEvaluator.java new file mode 100755 index 000000000..442e58bc0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/FormatEvaluator.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.upstream.BandwidthMeter; + +import java.util.List; +import java.util.Random; + +/** + * Selects from a number of available formats during playback. + */ +public interface FormatEvaluator { + + /** + * Enables the evaluator. + */ + void enable(); + + /** + * Disables the evaluator. + */ + void disable(); + + /** + * Update the supplied evaluation. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    + * This method may be called when the manager is in any state. + * + * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise. + */ + Exception getError(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/TypeEvaluator.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/KeysExpiredException.java old mode 100644 new mode 100755 similarity index 71% rename from TMessagesProj/src/main/java/org/telegram/messenger/Animation/TypeEvaluator.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/KeysExpiredException.java index 305f8ad8d..1b83c7eed --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/TypeEvaluator.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/KeysExpiredException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.telegram.messenger.exoplayer.drm; -package org.telegram.messenger.Animation; - -public interface TypeEvaluator { - T evaluate(float fraction, T startValue, T endValue); +/** + * Thrown when the drm keys loaded into an open session expire. + */ +public final class KeysExpiredException extends Exception { } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/MediaDrmCallback.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/MediaDrmCallback.java new file mode 100755 index 000000000..2fd695dc8 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/MediaDrmCallback.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.drm; + +import android.annotation.TargetApi; +import android.media.MediaDrm; + +import java.util.UUID; + +/** + * Performs {@link MediaDrm} key and provisioning requests. + */ +@TargetApi(18) +public interface MediaDrmCallback { + + /** + * Executes a provisioning request. + * + * @param uuid The UUID of the content protection scheme. + * @param request The request. + * @return The response data. + * @throws Exception If an error occurred executing the request. + */ + byte[] executeProvisionRequest(UUID uuid, MediaDrm.ProvisionRequest request) throws Exception; + + /** + * Executes a key request. + * + * @param uuid The UUID of the content protection scheme. + * @param request The request. + * @return The response data. + * @throws Exception If an error occurred executing the request. + */ + byte[] executeKeyRequest(UUID uuid, MediaDrm.KeyRequest request) throws Exception; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/StreamingDrmSessionManager.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/StreamingDrmSessionManager.java new file mode 100755 index 000000000..bc8be137a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/StreamingDrmSessionManager.java @@ -0,0 +1,518 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.drm; + +import org.telegram.messenger.exoplayer.drm.DrmInitData.SchemeInitData; +import org.telegram.messenger.exoplayer.extractor.mp4.PsshAtomUtil; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.media.DeniedByServerException; +import android.media.MediaCrypto; +import android.media.MediaDrm; +import android.media.MediaDrm.KeyRequest; +import android.media.MediaDrm.OnEventListener; +import android.media.MediaDrm.ProvisionRequest; +import android.media.NotProvisionedException; +import android.media.UnsupportedSchemeException; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.text.TextUtils; + +import java.util.HashMap; +import java.util.UUID; + +/** + * A base class for {@link DrmSessionManager} implementations that support streaming playbacks + * using {@link MediaDrm}. + */ +@TargetApi(18) +public class StreamingDrmSessionManager implements DrmSessionManager { + + /** + * Interface definition for a callback to be notified of {@link StreamingDrmSessionManager} + * events. + */ + public interface EventListener { + + /** + * Invoked each time keys are loaded. + */ + void onDrmKeysLoaded(); + + /** + * Invoked when a drm error occurs. + * + * @param e The corresponding exception. + */ + void onDrmSessionManagerError(Exception e); + + } + + /** + * UUID for the Widevine DRM scheme. + */ + public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); + + /** + * UUID for the PlayReady DRM scheme. + *

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

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

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

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

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

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

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

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

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

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

    + * Following a call to this method, the {@link ExtractorInput} passed to the next invocation of + * {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting from a + * random access position in the stream. Valid random access positions are the start of the + * stream and positions that can be obtained from any {@link SeekMap} passed to the + * {@link ExtractorOutput}. + */ + void seek(); + + /** + * Releases all kept resources. + */ + void release(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorInput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorInput.java new file mode 100755 index 000000000..e21c4abaa --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorInput.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.C; + +import java.io.EOFException; +import java.io.IOException; + +/** + * Provides data to be consumed by an {@link Extractor}. + */ +public interface ExtractorInput { + + /** + * Reads up to {@code length} bytes from the input and resets the peek position. + *

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

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

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

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

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

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

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

    + * If the end of the input is encountered before advancing the peek position, then behavior is + * dependent on {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is + * returned. Otherwise an {@link EOFException} is thrown. + * + * @param length The number of bytes by which to advance the peek position. + * @param allowEndOfInput True if encountering the end of the input before advancing is allowed, + * and should result in {@code false} being returned. False if it should be considered an + * error, causing an {@link EOFException} to be thrown. + * @return True if advancing the peek position was successful. False if the end of the input was + * encountered before the peek position could be advanced. + * @throws EOFException If the end of input was encountered having partially advanced (i.e. having + * advanced by at least one byte, but fewer than {@code length}), or if the end of input was + * encountered before advancing and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs advancing the peek position. + * @throws InterruptedException If the thread is interrupted. + */ + boolean advancePeekPosition(int length, boolean allowEndOfInput) + throws IOException, InterruptedException; + + /** + * Advances the peek position by {@code length} bytes. + * + * @param length The number of bytes to peek from the input. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void advancePeekPosition(int length) throws IOException, InterruptedException; + + /** + * Resets the peek position to equal the current read position. + */ + void resetPeekPosition(); + + /** + * Returns the current peek position (byte offset) in the stream. + * + * @return The peek position (byte offset) in the stream. + */ + long getPeekPosition(); + + /** + * Returns the current read position (byte offset) in the stream. + * + * @return The read position (byte offset) in the stream. + */ + long getPosition(); + + /** + * Returns the length of the source stream, or {@link C#LENGTH_UNBOUNDED} if it is unknown. + * + * @return The length of the source stream, or {@link C#LENGTH_UNBOUNDED}. + */ + long getLength(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorOutput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorOutput.java new file mode 100755 index 000000000..6b2ee095a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorOutput.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.drm.DrmInitData; + +/** + * Receives stream level data extracted by an {@link Extractor}. + */ +public interface ExtractorOutput { + + /** + * Invoked when the {@link Extractor} identifies the existence of a track in the stream. + *

    + * Returns a {@link TrackOutput} that will receive track level data belonging to the track. + * + * @param trackId A track identifier. + * @return The {@link TrackOutput} that should receive track level data belonging to the track. + */ + TrackOutput track(int trackId); + + /** + * Invoked when all tracks have been identified, meaning that {@link #track(int)} will not be + * invoked again. + */ + void endTracks(); + + /** + * Invoked when a {@link SeekMap} has been extracted from the stream. + * + * @param seekMap The extracted {@link SeekMap}. + */ + void seekMap(SeekMap seekMap); + + /** + * Invoked when {@link DrmInitData} has been extracted from the stream. + * + * @param drmInitData The extracted {@link DrmInitData}. + */ + void drmInitData(DrmInitData drmInitData); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorSampleSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorSampleSource.java new file mode 100755 index 000000000..bd1ea6c68 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorSampleSource.java @@ -0,0 +1,907 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.MediaFormatHolder; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.SampleSource; +import org.telegram.messenger.exoplayer.SampleSource.SampleSourceReader; +import org.telegram.messenger.exoplayer.TrackRenderer; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.upstream.Allocator; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.upstream.Loader; +import org.telegram.messenger.exoplayer.upstream.Loader.Loadable; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import android.net.Uri; +import android.os.Handler; +import android.os.SystemClock; +import android.util.SparseArray; + +import java.io.EOFException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link SampleSource} that extracts sample data using an {@link Extractor}. + * + *

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

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

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

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

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

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

    + * Populates {@link SampleHolder#size}, {@link SampleHolder#timeUs}, {@link SampleHolder#flags} + * and the {@code extrasHolder}. + * + * @param holder The holder into which the current sample information should be written. + * @param extrasHolder The holder into which extra sample information should be written. + * @return True if the holders were filled. False if there is no current sample. + */ + public synchronized boolean peekSample(SampleHolder holder, SampleExtrasHolder extrasHolder) { + if (queueSize == 0) { + return false; + } + holder.timeUs = timesUs[relativeReadIndex]; + holder.size = sizes[relativeReadIndex]; + holder.flags = flags[relativeReadIndex]; + extrasHolder.offset = offsets[relativeReadIndex]; + extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex]; + return true; + } + + /** + * Advances the read index to the next sample. + * + * @return The absolute position of the first byte in the rolling buffer that may still be + * required after advancing the index. Data prior to this position can be dropped. + */ + public synchronized long moveToNextSample() { + queueSize--; + int lastReadIndex = relativeReadIndex++; + absoluteReadIndex++; + if (relativeReadIndex == capacity) { + // Wrap around. + relativeReadIndex = 0; + } + return queueSize > 0 ? offsets[relativeReadIndex] + : (sizes[lastReadIndex] + offsets[lastReadIndex]); + } + + /** + * Attempts to locate the keyframe before the specified time, if it's present in the buffer. + * + * @param timeUs The seek time. + * @return The offset of the keyframe's data if the keyframe was present. -1 otherwise. + */ + public synchronized long skipToKeyframeBefore(long timeUs) { + if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) { + return -1; + } + + int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1; + long lastTimeUs = timesUs[lastWriteIndex]; + if (timeUs > lastTimeUs) { + return -1; + } + + // TODO: This can be optimized further using binary search, although the fact that the array + // is cyclic means we'd need to implement the binary search ourselves. + int sampleCount = 0; + int sampleCountToKeyframe = -1; + int searchIndex = relativeReadIndex; + while (searchIndex != relativeWriteIndex) { + if (timesUs[searchIndex] > timeUs) { + // We've gone too far. + break; + } else if ((flags[searchIndex] & C.SAMPLE_FLAG_SYNC) != 0) { + // We've found a keyframe, and we're still before the seek position. + sampleCountToKeyframe = sampleCount; + } + searchIndex = (searchIndex + 1) % capacity; + sampleCount++; + } + + if (sampleCountToKeyframe == -1) { + return -1; + } + + queueSize -= sampleCountToKeyframe; + relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity; + absoluteReadIndex += sampleCountToKeyframe; + return offsets[relativeReadIndex]; + } + + // Called by the loading thread. + + public synchronized void commitSample(long timeUs, int sampleFlags, long offset, int size, + byte[] encryptionKey) { + timesUs[relativeWriteIndex] = timeUs; + offsets[relativeWriteIndex] = offset; + sizes[relativeWriteIndex] = size; + flags[relativeWriteIndex] = sampleFlags; + encryptionKeys[relativeWriteIndex] = encryptionKey; + // Increment the write index. + queueSize++; + if (queueSize == capacity) { + // Increase the capacity. + int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; + long[] newOffsets = new long[newCapacity]; + long[] newTimesUs = new long[newCapacity]; + int[] newFlags = new int[newCapacity]; + int[] newSizes = new int[newCapacity]; + byte[][] newEncryptionKeys = new byte[newCapacity][]; + int beforeWrap = capacity - relativeReadIndex; + System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap); + System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap); + System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap); + System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap); + System.arraycopy(encryptionKeys, relativeReadIndex, newEncryptionKeys, 0, beforeWrap); + int afterWrap = relativeReadIndex; + System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); + System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); + System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); + System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); + System.arraycopy(encryptionKeys, 0, newEncryptionKeys, beforeWrap, afterWrap); + offsets = newOffsets; + timesUs = newTimesUs; + flags = newFlags; + sizes = newSizes; + encryptionKeys = newEncryptionKeys; + relativeReadIndex = 0; + relativeWriteIndex = capacity; + queueSize = capacity; + capacity = newCapacity; + } else { + relativeWriteIndex++; + if (relativeWriteIndex == capacity) { + // Wrap around. + relativeWriteIndex = 0; + } + } + } + + } + + /** + * Holds additional sample information not held by {@link SampleHolder}. + */ + private static final class SampleExtrasHolder { + + public long offset; + public byte[] encryptionKeyId; + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/SeekMap.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/SeekMap.java new file mode 100755 index 000000000..3a0df3cc0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/SeekMap.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.extractor; + +/** + * Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream. + */ +public interface SeekMap { + + /** + * A {@link SeekMap} that does not support seeking. + */ + public static final SeekMap UNSEEKABLE = new SeekMap() { + + @Override + public boolean isSeekable() { + return false; + } + + @Override + public long getPosition(long timeUs) { + return 0; + } + + }; + + /** + * Whether or not the seeking is supported. + *

    + * If seeking is not supported then the only valid seek position is the start of the file, and so + * {@link #getPosition(long)} will return 0 for all input values. + * + * @return True if seeking is supported. False otherwise. + */ + boolean isSeekable(); + + /** + * Maps a seek position in microseconds to a corresponding position (byte offset) in the stream + * from which data can be provided to the extractor. + * + * @param timeUs A seek position in microseconds. + * @return The corresponding position (byte offset) in the stream from which data can be provided + * to the extractor, or 0 if {@code #isSeekable()} returns false. + */ + long getPosition(long timeUs); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/TrackOutput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/TrackOutput.java new file mode 100755 index 000000000..893dd28c6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/TrackOutput.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.EOFException; +import java.io.IOException; + +/** + * Receives track level data extracted by an {@link Extractor}. + */ +public interface TrackOutput { + + /** + * Invoked when the {@link MediaFormat} of the track has been extracted from the stream. + * + * @param format The extracted {@link MediaFormat}. + */ + void format(MediaFormat format); + + /** + * Invoked to write sample data to the output. + * + * @param input An {@link ExtractorInput} from which to read the sample data. + * @param length The maximum length to read from the input. + * @param allowEndOfInput True if encountering the end of the input having read no data is + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @return The number of bytes appended. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException; + + /** + * Invoked to write sample data to the output. + * + * @param data A {@link ParsableByteArray} from which to read the sample data. + * @param length The number of bytes to read. + */ + void sampleData(ParsableByteArray data, int length); + + /** + * Invoked when metadata associated with a sample has been extracted from the stream. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    + * Subsequent calls to {@link #read(ExtractorInput)} will start reading a new EBML structure + * from scratch. + */ + public void reset(); + + /** + * Reads from an {@link ExtractorInput}, invoking an event callback if possible. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @return True if data can continue to be read. False if the end of the input was encountered. + * @throws ParserException If parsing fails. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + public boolean read(ExtractorInput input) throws ParserException, IOException, + InterruptedException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/EbmlReaderOutput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/EbmlReaderOutput.java new file mode 100755 index 000000000..cd1b8b873 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/EbmlReaderOutput.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.extractor.webm; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; + +import java.io.IOException; + +/** + * Defines EBML element IDs/types and reacts to events. + */ +/* package */ interface EbmlReaderOutput { + + /** + * Maps an element ID to a corresponding type. + *

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

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

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

    + * The element header (containing the element ID and content size) will already have been read. + * Implementations are required to consume the whole remainder of the element, which is + * {@code contentSize} bytes in length, before returning. Implementations are permitted to fail + * (by throwing an exception) having partially consumed the data, however if they do this, they + * must consume the remainder of the content when invoked again. + * + * @param id The element ID. + * @param contentsSize The element's content size. + * @param input The {@link ExtractorInput} from which data should be read. + * @throws ParserException If a parsing error occurs. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void binaryElement(int id, int contentsSize, ExtractorInput input) + throws ParserException, IOException, InterruptedException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/Sniffer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/Sniffer.java new file mode 100755 index 000000000..00391528e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/Sniffer.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.extractor.webm; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * Utility class that peeks from the input stream in order to determine whether it appears to be + * compatible input for this extractor. + */ +/* package */ final class Sniffer { + + /** + * The number of bytes to search for a valid header in {@link #sniff(ExtractorInput)}. + */ + private static final int SEARCH_LENGTH = 1024; + private static final int ID_EBML = 0x1A45DFA3; + + private final ParsableByteArray scratch; + private int peekLength; + + public Sniffer() { + scratch = new ParsableByteArray(8); + } + + /** + * @see Extractor#sniff + */ + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) (inputLength == C.LENGTH_UNBOUNDED || inputLength > SEARCH_LENGTH + ? SEARCH_LENGTH : inputLength); + // Find four bytes equal to ID_EBML near the start of the input. + input.peekFully(scratch.data, 0, 4); + long tag = scratch.readUnsignedInt(); + peekLength = 4; + while (tag != ID_EBML) { + if (++peekLength == bytesToSearch) { + return false; + } + input.peekFully(scratch.data, 0, 1); + tag = (tag << 8) & 0xFFFFFF00; + tag |= scratch.data[0] & 0xFF; + } + + // Read the size of the EBML header and make sure it is within the stream. + long headerSize = readUint(input); + long headerStart = peekLength; + if (headerSize == Long.MIN_VALUE + || (inputLength != C.LENGTH_UNBOUNDED && headerStart + headerSize >= inputLength)) { + return false; + } + + // Read the payload elements in the EBML header. + while (peekLength < headerStart + headerSize) { + long id = readUint(input); + if (id == Long.MIN_VALUE) { + return false; + } + long size = readUint(input); + if (size < 0 || size > Integer.MAX_VALUE) { + return false; + } + if (size != 0) { + input.advancePeekPosition((int) size); + peekLength += size; + } + } + return peekLength == headerStart + headerSize; + } + + /** + * Peeks a variable-length unsigned EBML integer from the input. + */ + private long readUint(ExtractorInput input) throws IOException, InterruptedException { + input.peekFully(scratch.data, 0, 1); + int value = scratch.data[0] & 0xFF; + if (value == 0) { + return Long.MIN_VALUE; + } + int mask = 0x80; + int length = 0; + while ((value & mask) == 0) { + mask >>= 1; + length++; + } + value &= ~mask; + input.peekFully(scratch.data, 1, length); + for (int i = 0; i < length; i++) { + value <<= 8; + value += scratch.data[i + 1] & 0xFF; + } + peekLength += length + 1; + return value; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/VarintReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/VarintReader.java new file mode 100755 index 000000000..ec24c9770 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/VarintReader.java @@ -0,0 +1,140 @@ +package org.telegram.messenger.exoplayer.extractor.webm; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; + +import java.io.EOFException; +import java.io.IOException; + +/** + * Reads EBML variable-length integers (varints) from an {@link ExtractorInput}. + */ +/* package */ final class VarintReader { + + private static final int STATE_BEGIN_READING = 0; + private static final int STATE_READ_CONTENTS = 1; + + /** + * The first byte of a variable-length integer (varint) will have one of these bit masks + * indicating the total length in bytes. + * + *

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

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

    + * If an {@link IOException} or {@link InterruptedException} is throw, the read can be resumed + * later by calling this method again, passing an {@link ExtractorInput} providing data starting + * where the previous one left off. + * + * @param input The {@link ExtractorInput} from which the integer should be read. + * @param allowEndOfInput True if encountering the end of the input having read no data is + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @param removeLengthMask Removes the variable-length integer length mask from the value. + * @param maximumAllowedLength Maximum allowed length of the variable integer to be read. + * @return The read value, or {@link C#RESULT_END_OF_INPUT} if {@code allowEndOfStream} is true + * and the end of the input was encountered, or {@link C#RESULT_MAX_LENGTH_EXCEEDED} if the + * length of the varint exceeded maximumAllowedLength. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + public long readUnsignedVarint(ExtractorInput input, boolean allowEndOfInput, + boolean removeLengthMask, int maximumAllowedLength) throws IOException, InterruptedException { + if (state == STATE_BEGIN_READING) { + // Read the first byte to establish the length. + if (!input.readFully(scratch, 0, 1, allowEndOfInput)) { + return C.RESULT_END_OF_INPUT; + } + int firstByte = scratch[0] & 0xFF; + length = parseUnsignedVarintLength(firstByte); + if (length == -1) { + throw new IllegalStateException("No valid varint length mask found"); + } + state = STATE_READ_CONTENTS; + } + + if (length > maximumAllowedLength) { + state = STATE_BEGIN_READING; + return C.RESULT_MAX_LENGTH_EXCEEDED; + } + + if (length != 1) { + // Read the remaining bytes. + input.readFully(scratch, 1, length - 1); + } + + state = STATE_BEGIN_READING; + return assembleVarint(scratch, length, removeLengthMask); + } + + /** + * Returns the number of bytes occupied by the most recently parsed varint. + */ + public int getLastLength() { + return length; + } + + /** + * Parses and the length of the varint given the first byte. + * + * @param firstByte First byte of the varint. + * @return Length of the varint beginning with the given byte if it was valid, -1 otherwise. + */ + public static int parseUnsignedVarintLength(int firstByte) { + int varIntLength = -1; + for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) { + if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) { + varIntLength = i + 1; + break; + } + } + return varIntLength; + } + + /** + * Assemble a varint from the given byte array. + * + * @param varintBytes Bytes that make up the varint. + * @param varintLength Length of the varint to assemble. + * @param removeLengthMask Removes the variable-length integer length mask from the value. + * @return Parsed and assembled varint. + */ + public static long assembleVarint(byte[] varintBytes, int varintLength, + boolean removeLengthMask) { + long varint = varintBytes[0] & 0xFFL; + if (removeLengthMask) { + varint &= ~VARINT_LENGTH_MASKS[varintLength - 1]; + } + for (int i = 1; i < varintLength; i++) { + varint = (varint << 8) | (varintBytes[i] & 0xFFL); + } + return varint; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/WebmExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/WebmExtractor.java new file mode 100755 index 000000000..da6495850 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/WebmExtractor.java @@ -0,0 +1,1560 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.extractor.webm; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.drm.DrmInitData.SchemeInitData; +import org.telegram.messenger.exoplayer.extractor.ChunkIndex; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.LongArray; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.NalUnitUtil; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import android.util.Pair; +import android.util.SparseArray; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.UUID; + +/** + * An extractor to facilitate data retrieval from the WebM container format. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      + *
    • {@link #EDGE_TYPE_NONE} + *
    • {@link #EDGE_TYPE_OUTLINE} + *
    • {@link #EDGE_TYPE_DROP_SHADOW} + *
    • {@link #EDGE_TYPE_RAISED} + *
    • {@link #EDGE_TYPE_DEPRESSED} + *
    + */ + public final int edgeType; + + /** + * The preferred edge color, if using an edge type other than {@link #EDGE_TYPE_NONE}. + */ + public final int edgeColor; + + /** + * The preferred typeface. + */ + public final Typeface typeface; + + /** + * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}. + * + * @param captionStyle A {@link CaptionStyle}. + * @return The equivalent {@link CaptionStyleCompat}. + */ + @TargetApi(19) + public static CaptionStyleCompat createFromCaptionStyle( + CaptioningManager.CaptionStyle captionStyle) { + if (Util.SDK_INT >= 21) { + return createFromCaptionStyleV21(captionStyle); + } else { + // Note - Any caller must be on at least API level 19 or greater (because CaptionStyle did + // not exist in earlier API levels). + return createFromCaptionStyleV19(captionStyle); + } + } + + /** + * @param foregroundColor See {@link #foregroundColor}. + * @param backgroundColor See {@link #backgroundColor}. + * @param windowColor See {@link #windowColor}. + * @param edgeType See {@link #edgeType}. + * @param edgeColor See {@link #edgeColor}. + * @param typeface See {@link #typeface}. + */ + public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor, int edgeType, + int edgeColor, Typeface typeface) { + this.foregroundColor = foregroundColor; + this.backgroundColor = backgroundColor; + this.windowColor = windowColor; + this.edgeType = edgeType; + this.edgeColor = edgeColor; + this.typeface = typeface; + } + + @TargetApi(19) + private static CaptionStyleCompat createFromCaptionStyleV19( + CaptioningManager.CaptionStyle captionStyle) { + return new CaptionStyleCompat( + captionStyle.foregroundColor, captionStyle.backgroundColor, Color.TRANSPARENT, + captionStyle.edgeType, captionStyle.edgeColor, captionStyle.getTypeface()); + } + + @TargetApi(21) + private static CaptionStyleCompat createFromCaptionStyleV21( + CaptioningManager.CaptionStyle captionStyle) { + return new CaptionStyleCompat( + captionStyle.hasForegroundColor() ? captionStyle.foregroundColor : DEFAULT.foregroundColor, + captionStyle.hasBackgroundColor() ? captionStyle.backgroundColor : DEFAULT.backgroundColor, + captionStyle.hasWindowColor() ? captionStyle.windowColor : DEFAULT.windowColor, + captionStyle.hasEdgeType() ? captionStyle.edgeType : DEFAULT.edgeType, + captionStyle.hasEdgeColor() ? captionStyle.edgeColor : DEFAULT.edgeColor, + captionStyle.getTypeface()); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Cue.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Cue.java new file mode 100755 index 000000000..a1a0e650f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Cue.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.text; + +import android.text.Layout.Alignment; + +/** + * Contains information about a specific cue, including textual content and formatting data. + */ +public class Cue { + + /** + * An unset position or width. + */ + public static final float DIMEN_UNSET = Float.MIN_VALUE; + /** + * An unset anchor or line type value. + */ + public static final int TYPE_UNSET = Integer.MIN_VALUE; + /** + * Anchors the left (for horizontal positions) or top (for vertical positions) edge of the cue + * box. + */ + public static final int ANCHOR_TYPE_START = 0; + /** + * Anchors the middle of the cue box. + */ + public static final int ANCHOR_TYPE_MIDDLE = 1; + /** + * Anchors the right (for horizontal positions) or bottom (for vertical positions) edge of the cue + * box. + */ + public static final int ANCHOR_TYPE_END = 2; + /** + * Value for {@link #lineType} when {@link #line} is a fractional position. + */ + public static final int LINE_TYPE_FRACTION = 0; + /** + * Value for {@link #lineType} when {@link #line} is a line number. + */ + public static final int LINE_TYPE_NUMBER = 1; + + /** + * The cue text. Note the {@link CharSequence} may be decorated with styling spans. + */ + public final CharSequence text; + /** + * The alignment of the cue text within the cue box. + */ + public final Alignment textAlignment; + /** + * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction + * orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of + * the value depends on the value of {@link #lineType}. + *

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

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

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

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

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

    + * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE} + * and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box + * respectively. + */ + public final int positionAnchor; + /** + * The size of the cue box in the writing direction specified as a fraction of the viewport size + * in that direction, or {@link #DIMEN_UNSET}. + */ + public final float size; + + public Cue() { + this(null); + } + + public Cue(CharSequence text) { + this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET); + } + + public Cue(CharSequence text, Alignment textAlignment, float line, int lineType, + int lineAnchor, float position, int positionAnchor, float size) { + this.text = text; + this.textAlignment = textAlignment; + this.line = line; + this.lineType = lineType; + this.lineAnchor = lineAnchor; + this.position = position; + this.positionAnchor = positionAnchor; + this.size = size; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/PlayableSubtitle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/PlayableSubtitle.java new file mode 100755 index 000000000..5c9d3f8a0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/PlayableSubtitle.java @@ -0,0 +1,75 @@ +package org.telegram.messenger.exoplayer.text; + +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.util.List; + +/** + * A subtitle that wraps another subtitle, making it playable by adjusting it to be correctly + * aligned with the playback timebase. + */ +/* package */ final class PlayableSubtitle implements Subtitle { + + /** + * The start time of the subtitle. + *

    + * May be less than {@code getEventTime(0)}, since a subtitle may begin prior to the time of the + * first event. + */ + public final long startTimeUs; + + private final Subtitle subtitle; + private final long offsetUs; + + /** + * @param subtitle The subtitle to wrap. + * @param isRelative True if the wrapped subtitle's timestamps are relative to the start time. + * False if they are absolute. + * @param startTimeUs The start time of the subtitle. + * @param offsetUs An offset to add to the subtitle timestamps. + */ + public PlayableSubtitle(Subtitle subtitle, boolean isRelative, long startTimeUs, long offsetUs) { + this.subtitle = subtitle; + this.startTimeUs = startTimeUs; + this.offsetUs = (isRelative ? startTimeUs : 0) + offsetUs; + } + + @Override + public int getEventTimeCount() { + return subtitle.getEventTimeCount(); + } + + @Override + public long getEventTime(int index) { + return subtitle.getEventTime(index) + offsetUs; + } + + @Override + public long getLastEventTime() { + return subtitle.getLastEventTime() + offsetUs; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + return subtitle.getNextEventTimeIndex(timeUs - offsetUs); + } + + @Override + public List getCues(long timeUs) { + return subtitle.getCues(timeUs - offsetUs); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Subtitle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Subtitle.java new file mode 100755 index 000000000..e4a697215 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Subtitle.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.text; + +import java.util.List; + +/** + * A subtitle that contains textual data associated with time indices. + */ +public interface Subtitle { + + /** + * Gets the index of the first event that occurs after a given time (exclusive). + * + * @param timeUs The time in microseconds. + * @return The index of the next event, or -1 if there are no events after the specified time. + */ + public int getNextEventTimeIndex(long timeUs); + + /** + * Gets the number of event times, where events are defined as points in time at which the cues + * returned by {@link #getCues(long)} changes. + * + * @return The number of event times. + */ + public int getEventTimeCount(); + + /** + * Gets the event time at a specified index. + * + * @param index The index of the event time to obtain. + * @return The event time in microseconds. + */ + public long getEventTime(int index); + + /** + * Convenience method for obtaining the last event time. + * + * @return The time of the last event in microseconds, or -1 if {@code getEventTimeCount() == 0}. + */ + public long getLastEventTime(); + + /** + * Retrieve the subtitle cues that should be displayed at a given time. + * + * @param timeUs The time in microseconds. + * @return A list of cues that should be displayed, possibly empty. + */ + public List getCues(long timeUs); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParser.java new file mode 100755 index 000000000..e7cec2ab8 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParser.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.text; + +import org.telegram.messenger.exoplayer.ParserException; + +/** + * Parses {@link Subtitle}s from a byte array. + */ +public interface SubtitleParser { + + /** + * Checks whether the parser supports a given subtitle mime type. + * + * @param mimeType A subtitle mime type. + * @return Whether the mime type is supported. + */ + public boolean canParse(String mimeType); + + /** + * Parses a {@link Subtitle} from the provided {@code byte[]}. + * + * @param bytes The array holding the subtitle data. + * @param offset The offset of the subtitle data in bytes. + * @param length The length of the subtitle data in bytes. + * @return A parsed representation of the subtitle. + * @throws ParserException If a problem occurred parsing the subtitle data. + */ + public Subtitle parse(byte[] bytes, int offset, int length) throws ParserException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParserHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParserHelper.java new file mode 100755 index 000000000..977107b15 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParserHelper.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.text; + +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import android.media.MediaCodec; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.io.IOException; + +/** + * Wraps a {@link SubtitleParser}, exposing an interface similar to {@link MediaCodec} for + * asynchronous parsing of subtitles. + */ +/* package */ final class SubtitleParserHelper implements Handler.Callback { + + private static final int MSG_FORMAT = 0; + private static final int MSG_SAMPLE = 1; + + private final SubtitleParser parser; + private final Handler handler; + + private SampleHolder sampleHolder; + private boolean parsing; + private PlayableSubtitle result; + private IOException error; + private RuntimeException runtimeError; + + private boolean subtitlesAreRelative; + private long subtitleOffsetUs; + + /** + * @param looper The {@link Looper} associated with the thread on which parsing should occur. + * @param parser The parser that should be used to parse the raw data. + */ + public SubtitleParserHelper(Looper looper, SubtitleParser parser) { + this.handler = new Handler(looper, this); + this.parser = parser; + flush(); + } + + /** + * Flushes the helper, canceling the current parsing operation, if there is one. + */ + public synchronized void flush() { + sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); + parsing = false; + result = null; + error = null; + runtimeError = null; + } + + /** + * Whether the helper is currently performing a parsing operation. + * + * @return True if the helper is currently performing a parsing operation. False otherwise. + */ + public synchronized boolean isParsing() { + return parsing; + } + + /** + * Gets the holder that should be populated with data to be parsed. + *

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

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

    + * The result is cleared as a result of calling this method, and so subsequent calls will return + * null until a subsequent parsing operation has finished. + * + * @return The result of the parsing operation, or null. + * @throws IOException If the parsing operation failed. + */ + public synchronized PlayableSubtitle getAndClearResult() throws IOException { + try { + if (error != null) { + throw error; + } else if (runtimeError != null) { + throw runtimeError; + } else { + return result; + } + } finally { + result = null; + error = null; + runtimeError = null; + } + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_FORMAT: + handleFormat((MediaFormat) msg.obj); + break; + case MSG_SAMPLE: + long sampleTimeUs = Util.getLong(msg.arg1, msg.arg2); + SampleHolder holder = (SampleHolder) msg.obj; + handleSample(sampleTimeUs, holder); + break; + } + return true; + } + + private void handleFormat(MediaFormat format) { + subtitlesAreRelative = format.subsampleOffsetUs == MediaFormat.OFFSET_SAMPLE_RELATIVE; + subtitleOffsetUs = subtitlesAreRelative ? 0 : format.subsampleOffsetUs; + } + + private void handleSample(long sampleTimeUs, SampleHolder holder) { + Subtitle parsedSubtitle = null; + ParserException error = null; + RuntimeException runtimeError = null; + try { + parsedSubtitle = parser.parse(holder.data.array(), 0, holder.size); + } catch (ParserException e) { + error = e; + } catch (RuntimeException e) { + runtimeError = e; + } + synchronized (this) { + if (sampleHolder != holder) { + // A flush has occurred since this holder was posted. Do nothing. + } else { + this.result = new PlayableSubtitle(parsedSubtitle, subtitlesAreRelative, sampleTimeUs, + subtitleOffsetUs); + this.error = error; + this.runtimeError = runtimeError; + this.parsing = false; + } + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextRenderer.java new file mode 100755 index 000000000..77a0682a5 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextRenderer.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.text; + +import java.util.List; + +/** + * An interface for components that render text. + */ +public interface TextRenderer { + + /** + * Invoked each time there is a change in the {@link Cue}s to be rendered. + * + * @param cues The {@link Cue}s to be rendered, or an empty list if no cues are to be rendered. + */ + void onCues(List cues); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextTrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextTrackRenderer.java new file mode 100755 index 000000000..ec4883f64 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextTrackRenderer.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.text; + +import org.telegram.messenger.exoplayer.ExoPlaybackException; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.MediaFormatHolder; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.SampleSource; +import org.telegram.messenger.exoplayer.SampleSourceTrackRenderer; +import org.telegram.messenger.exoplayer.TrackRenderer; +import org.telegram.messenger.exoplayer.util.Assertions; + +import android.annotation.TargetApi; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A {@link TrackRenderer} for subtitles. Text is parsed from sample data using a + * {@link SubtitleParser}. The actual rendering of each line of text is delegated to a + * {@link TextRenderer}. + *

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

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

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

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

    + * Supported features in this parser are: + *

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

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

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

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

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

    + * The allocation's length is obtained by calling {@link Allocator#getIndividualAllocationLength()} + * on the {@link Allocator} from which it was obtained. + */ +public final class Allocation { + + /** + * The array containing the allocated space. The allocated space may not be at the start of the + * array, and so {@link #translateOffset(int)} method must be used when indexing into it. + */ + public final byte[] data; + + private final int offset; + + /** + * @param data The array containing the allocated space. + * @param offset The offset of the allocated space within the array. + */ + public Allocation(byte[] data, int offset) { + this.data = data; + this.offset = offset; + } + + /** + * Translates a zero-based offset into the allocation to the corresponding {@link #data} offset. + * + * @param offset The zero-based offset to translate. + * @return The corresponding offset in {@link #data}. + */ + public int translateOffset(int offset) { + return this.offset + offset; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Allocator.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Allocator.java new file mode 100755 index 000000000..f6e919f92 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Allocator.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.upstream; + +/** + * A source of allocations. + */ +public interface Allocator { + + /** + * Obtain an {@link Allocation}. + *

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

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

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

    + * This method blocks until at least one byte of data can be read, the end of the opened range is + * detected, or an exception is thrown. + * + * @param buffer The buffer into which the read data should be stored. + * @param offset The start offset into {@code buffer} at which data should be written. + * @param readLength The maximum number of bytes to read. + * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened + * range is reached. + * @throws IOException If an error occurs reading from the source. + */ + int read(byte[] buffer, int offset, int readLength) throws IOException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSourceInputStream.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSourceInputStream.java new file mode 100755 index 000000000..620dc5ba0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSourceInputStream.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Allows data corresponding to a given {@link DataSpec} to be read from a {@link DataSource} and + * consumed as an {@link InputStream}. + */ +public final class DataSourceInputStream extends InputStream { + + private final DataSource dataSource; + private final DataSpec dataSpec; + private final byte[] singleByteArray; + + private boolean opened = false; + private boolean closed = false; + + /** + * @param dataSource The {@link DataSource} from which the data should be read. + * @param dataSpec The {@link DataSpec} defining the data to be read from {@code dataSource}. + */ + public DataSourceInputStream(DataSource dataSource, DataSpec dataSpec) { + this.dataSource = dataSource; + this.dataSpec = dataSpec; + singleByteArray = new byte[1]; + } + + /** + * Optional call to open the underlying {@link DataSource}. + *

    + * Calling this method does nothing if the {@link DataSource} is already open. Calling this + * method is optional, since the read and skip methods will automatically open the underlying + * {@link DataSource} if it's not open already. + * + * @throws IOException If an error occurs opening the {@link DataSource}. + */ + public void open() throws IOException { + checkOpened(); + } + + @Override + public int read() throws IOException { + int length = read(singleByteArray); + if (length == -1) { + return -1; + } + return singleByteArray[0] & 0xFF; + } + + @Override + public int read(byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + Assertions.checkState(!closed); + checkOpened(); + return dataSource.read(buffer, offset, length); + } + + @Override + public long skip(long byteCount) throws IOException { + Assertions.checkState(!closed); + checkOpened(); + return super.skip(byteCount); + } + + @Override + public void close() throws IOException { + if (!closed) { + dataSource.close(); + closed = true; + } + } + + private void checkOpened() throws IOException { + if (!opened) { + dataSource.open(dataSpec); + opened = true; + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSpec.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSpec.java new file mode 100755 index 000000000..fce9ecb22 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSpec.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.util.Assertions; + +import android.net.Uri; + +import java.util.Arrays; + +/** + * Defines a region of media data. + */ +public final class DataSpec { + + /** + * Permits an underlying network stack to request that the server use gzip compression. + *

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

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

    + * Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location + * of a subset of the underyling data. + */ + public final long position; + /** + * The length of the data. Greater than zero, or equal to {@link C#LENGTH_UNBOUNDED}. + */ + public final long length; + /** + * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the + * {@link DataSpec} is not intended to be used in conjunction with a cache. + */ + public final String key; + /** + * Request flags. Currently {@link #FLAG_ALLOW_GZIP} is the only supported flag. + */ + public final int flags; + + /** + * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. + * + * @param uri {@link #uri}. + */ + public DataSpec(Uri uri) { + this(uri, 0); + } + + /** + * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. + * + * @param uri {@link #uri}. + * @param flags {@link #flags}. + */ + public DataSpec(Uri uri, int flags) { + this(uri, 0, C.LENGTH_UNBOUNDED, null, flags); + } + + /** + * Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}. + * + * @param uri {@link #uri}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + */ + public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) { + this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0); + } + + /** + * Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}. + * + * @param uri {@link #uri}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, int flags) { + this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags); + } + + /** + * Construct a {@link DataSpec} where {@link #position} may differ from + * {@link #absoluteStreamPosition}. + * + * @param uri {@link #uri}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key, + int flags) { + this(uri, null, absoluteStreamPosition, position, length, key, flags); + } + + /** + * Construct a {@link DataSpec} where {@link #position} may differ from + * {@link #absoluteStreamPosition}. + * + * @param uri {@link #uri}. + * @param postBody {@link #postBody}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + public DataSpec(Uri uri, byte[] postBody, long absoluteStreamPosition, long position, long length, + String key, int flags) { + Assertions.checkArgument(absoluteStreamPosition >= 0); + Assertions.checkArgument(position >= 0); + Assertions.checkArgument(length > 0 || length == C.LENGTH_UNBOUNDED); + this.uri = uri; + this.postBody = postBody; + this.absoluteStreamPosition = absoluteStreamPosition; + this.position = position; + this.length = length; + this.key = key; + this.flags = flags; + } + + @Override + public String toString() { + return "DataSpec[" + uri + ", " + Arrays.toString(postBody) + ", " + absoluteStreamPosition + + ", " + position + ", " + length + ", " + key + ", " + flags + "]"; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultAllocator.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultAllocator.java new file mode 100755 index 000000000..6fe69ab7f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultAllocator.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import java.util.Arrays; + +/** + * Default implementation of {@link Allocator}. + */ +public final class DefaultAllocator implements Allocator { + + private static final int AVAILABLE_EXTRA_CAPACITY = 100; + + private final int individualAllocationSize; + private final byte[] initialAllocationBlock; + + private int allocatedCount; + private int availableCount; + private Allocation[] availableAllocations; + + /** + * Constructs an initially empty pool. + * + * @param individualAllocationSize The length of each individual allocation. + */ + public DefaultAllocator(int individualAllocationSize) { + this(individualAllocationSize, 0); + } + + /** + * Constructs a pool with some {@link Allocation}s created up front. + *

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

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

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

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

    + * This method blocks until at least one byte of data can be read, the end of the opened range is + * detected, or an exception is thrown. + * + * @param buffer The buffer into which the read data should be stored. + * @param offset The start offset into {@code buffer} at which data should be written. + * @param readLength The maximum number of bytes to read. + * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened + * range is reached. + * @throws IOException If an error occurs reading from the source. + */ + private int readInternal(byte[] buffer, int offset, int readLength) throws IOException { + readLength = bytesToRead == C.LENGTH_UNBOUNDED ? readLength + : (int) Math.min(readLength, bytesToRead - bytesRead); + if (readLength == 0) { + // We've read all of the requested data. + return C.RESULT_END_OF_INPUT; + } + + int read = inputStream.read(buffer, offset, readLength); + if (read == -1) { + if (bytesToRead != C.LENGTH_UNBOUNDED && bytesToRead != bytesRead) { + // The server closed the connection having not sent sufficient data. + throw new EOFException(); + } + return C.RESULT_END_OF_INPUT; + } + + bytesRead += read; + if (listener != null) { + listener.onBytesTransferred(read); + } + return read; + } + + /** + * Closes the current connection quietly, if there is one. + */ + private void closeConnectionQuietly() { + if (connection != null) { + try { + connection.disconnect(); + } catch (Exception e) { + Log.e(TAG, "Unexpected error while disconnecting", e); + } + connection = null; + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultUriDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultUriDataSource.java new file mode 100755 index 000000000..2c93ed98e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultUriDataSource.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import android.content.Context; + +import java.io.IOException; + +/** + * A {@link UriDataSource} that supports multiple URI schemes. The supported schemes are: + * + *

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

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

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

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

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

    + * This method should be called when the {@link Loader} is no longer required. + */ + public void release() { + if (loading) { + cancelLoading(); + } + downloadExecutorService.shutdown(); + } + + @SuppressLint("HandlerLeak") + private final class LoadTask extends Handler implements Runnable { + + private static final String TAG = "LoadTask"; + + private final Loadable loadable; + private final Loader.Callback callback; + + private volatile Thread executorThread; + + public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback) { + super(looper); + this.loadable = loadable; + this.callback = callback; + } + + public void quit() { + loadable.cancelLoad(); + if (executorThread != null) { + executorThread.interrupt(); + } + } + + @Override + public void run() { + try { + executorThread = Thread.currentThread(); + if (!loadable.isLoadCanceled()) { + TraceUtil.beginSection(loadable.getClass().getSimpleName() + ".load()"); + loadable.load(); + TraceUtil.endSection(); + } + sendEmptyMessage(MSG_END_OF_SOURCE); + } catch (IOException e) { + obtainMessage(MSG_IO_EXCEPTION, e).sendToTarget(); + } catch (InterruptedException e) { + // The load was canceled. + Assertions.checkState(loadable.isLoadCanceled()); + sendEmptyMessage(MSG_END_OF_SOURCE); + } catch (Exception e) { + // This should never happen, but handle it anyway. + Log.e(TAG, "Unexpected exception loading stream", e); + obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget(); + } catch (Error e) { + // We'd hope that the platform would kill the process if an Error is thrown here, but the + // executor may catch the error (b/20616433). Throw it here, but also pass and throw it from + // the handler thread so that the process dies even if the executor behaves in this way. + Log.e(TAG, "Unexpected error loading stream", e); + obtainMessage(MSG_FATAL_ERROR, e).sendToTarget(); + throw e; + } + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_FATAL_ERROR) { + throw (Error) msg.obj; + } + onFinished(); + if (loadable.isLoadCanceled()) { + callback.onLoadCanceled(loadable); + return; + } + switch (msg.what) { + case MSG_END_OF_SOURCE: + callback.onLoadCompleted(loadable); + break; + case MSG_IO_EXCEPTION: + callback.onLoadError(loadable, (IOException) msg.obj); + break; + } + } + + private void onFinished() { + loading = false; + currentTask = null; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/NetworkLock.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/NetworkLock.java new file mode 100755 index 000000000..83e310d83 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/NetworkLock.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.upstream; + +import java.io.IOException; +import java.util.PriorityQueue; + +/** + * A network task prioritization mechanism. + *

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

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

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

    + * If redirection occurred, the URI after redirection is the one returned. + * + * @return When the source is open, the URI from which data is being read. Null otherwise. + */ + String getUri(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UriLoadable.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UriLoadable.java new file mode 100755 index 000000000..7c169206c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UriLoadable.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.upstream.Loader.Loadable; + +import android.net.Uri; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A {@link Loadable} for loading an object from a URI. + * + * @param The type of the object being loaded. + */ +public final class UriLoadable implements Loadable { + + /** + * Parses an object from loaded data. + */ + public interface Parser { + + /** + * Parses an object from a response. + * + * @param connectionUrl The source of the response, after any redirection. + * @param inputStream An {@link InputStream} from which the response data can be read. + * @return The parsed object. + * @throws ParserException If an error occurs parsing the data. + * @throws IOException If an error occurs reading data from the stream. + */ + T parse(String connectionUrl, InputStream inputStream) throws ParserException, IOException; + + } + + private final DataSpec dataSpec; + private final UriDataSource uriDataSource; + private final Parser parser; + + private volatile T result; + private volatile boolean isCanceled; + + /** + * @param url The url from which the object should be loaded. + * @param uriDataSource A {@link UriDataSource} to use when loading the data. + * @param parser Parses the object from the response. + */ + public UriLoadable(String url, UriDataSource uriDataSource, Parser parser) { + this.uriDataSource = uriDataSource; + this.parser = parser; + dataSpec = new DataSpec(Uri.parse(url), DataSpec.FLAG_ALLOW_GZIP); + } + + /** + * Returns the loaded object, or null if an object has not been loaded. + */ + public final T getResult() { + return result; + } + + @Override + public final void cancelLoad() { + // We don't actually cancel anything, but we need to record the cancellation so that + // isLoadCanceled can return the correct value. + isCanceled = true; + } + + @Override + public final boolean isLoadCanceled() { + return isCanceled; + } + + @Override + public final void load() throws IOException, InterruptedException { + DataSourceInputStream inputStream = new DataSourceInputStream(uriDataSource, dataSpec); + try { + inputStream.open(); + result = parser.parse(uriDataSource.getUri(), inputStream); + } finally { + inputStream.close(); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/Cache.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/Cache.java new file mode 100755 index 000000000..0cf4b365e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/Cache.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.upstream.cache; + +import java.io.File; +import java.util.NavigableSet; +import java.util.Set; + +/** + * An interface for cache. + */ +public interface Cache { + + /** + * Interface definition for a callback to be notified of {@link Cache} events. + */ + public interface Listener { + + /** + * Invoked when a {@link CacheSpan} is added to the cache. + * + * @param cache The source of the event. + * @param span The added {@link CacheSpan}. + */ + void onSpanAdded(Cache cache, CacheSpan span); + + /** + * Invoked when a {@link CacheSpan} is removed from the cache. + * + * @param cache The source of the event. + * @param span The removed {@link CacheSpan}. + */ + void onSpanRemoved(Cache cache, CacheSpan span); + + /** + * Invoked when an existing {@link CacheSpan} is accessed, causing it to be replaced. The new + * {@link CacheSpan} is guaranteed to represent the same data as the one it replaces, however + * {@link CacheSpan#file} and {@link CacheSpan#lastAccessTimestamp} may have changed. + *

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

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

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

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

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

    + * If the input consists of NAL start code delimited units, then the returned array consists of + * the split NAL units, each of which is still prefixed with the NAL start code. For any other + * input, null is returned. + * + * @param data An array of data. + * @return The individual NAL units, or null if the input did not consist of NAL start code + * delimited units. + */ + public static byte[][] splitNalUnits(byte[] data) { + if (!isNalStartCode(data, 0)) { + // data does not consist of NAL start code delimited units. + return null; + } + List starts = new ArrayList<>(); + int nalUnitIndex = 0; + do { + starts.add(nalUnitIndex); + nalUnitIndex = findNalStartCode(data, nalUnitIndex + NAL_START_CODE.length); + } while (nalUnitIndex != -1); + byte[][] split = new byte[starts.size()][]; + for (int i = 0; i < starts.size(); i++) { + int startIndex = starts.get(i); + int endIndex = i < starts.size() - 1 ? starts.get(i + 1) : data.length; + byte[] nal = new byte[endIndex - startIndex]; + System.arraycopy(data, startIndex, nal, 0, nal.length); + split[i] = nal; + } + return split; + } + + /** + * Finds the next occurrence of the NAL start code from a given index. + * + * @param data The data in which to search. + * @param index The first index to test. + * @return The index of the first byte of the found start code, or -1. + */ + private static int findNalStartCode(byte[] data, int index) { + int endIndex = data.length - NAL_START_CODE.length; + for (int i = index; i <= endIndex; i++) { + if (isNalStartCode(data, i)) { + return i; + } + } + return -1; + } + + /** + * Tests whether there exists a NAL start code at a given index. + * + * @param data The data. + * @param index The index to test. + * @return Whether there exists a start code that begins at {@code index}. + */ + private static boolean isNalStartCode(byte[] data, int index) { + if (data.length - index <= NAL_START_CODE.length) { + return false; + } + for (int j = 0; j < NAL_START_CODE.length; j++) { + if (data[index + j] != NAL_START_CODE[j]) { + return false; + } + } + return true; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DebugTextViewHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DebugTextViewHelper.java new file mode 100755 index 000000000..ffe97f954 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DebugTextViewHelper.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.CodecCounters; +import org.telegram.messenger.exoplayer.chunk.Format; +import org.telegram.messenger.exoplayer.upstream.BandwidthMeter; + +import android.widget.TextView; + +/** + * A helper class for periodically updating debug information displayed by a {@link TextView}. + */ +public final class DebugTextViewHelper implements Runnable { + + /** + * Provides debug information about an ongoing playback. + */ + public interface Provider { + + /** + * Returns the current playback position, in milliseconds. + */ + long getCurrentPosition(); + + /** + * Returns a format whose information should be displayed, or null. + */ + Format getFormat(); + + /** + * Returns a {@link BandwidthMeter} whose estimate should be displayed, or null. + */ + BandwidthMeter getBandwidthMeter(); + + /** + * Returns a {@link CodecCounters} whose information should be displayed, or null. + */ + CodecCounters getCodecCounters(); + + } + + private static final int REFRESH_INTERVAL_MS = 1000; + + private final TextView textView; + private final Provider debuggable; + + /** + * @param debuggable The {@link Provider} from which debug information should be obtained. + * @param textView The {@link TextView} that should be updated to display the information. + */ + public DebugTextViewHelper(Provider debuggable, TextView textView) { + this.debuggable = debuggable; + this.textView = textView; + } + + /** + * Starts periodic updates of the {@link TextView}. + *

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

    + * Should be called from the application's main thread. + */ + public void stop() { + textView.removeCallbacks(this); + } + + @Override + public void run() { + textView.setText(getRenderString()); + textView.postDelayed(this, REFRESH_INTERVAL_MS); + } + + private String getRenderString() { + return getTimeString() + " " + getQualityString() + " " + getBandwidthString() + " " + + getVideoCodecCountersString(); + } + + private String getTimeString() { + return "ms(" + debuggable.getCurrentPosition() + ")"; + } + + private String getQualityString() { + Format format = debuggable.getFormat(); + return format == null ? "id:? br:? h:?" + : "id:" + format.id + " br:" + format.bitrate + " h:" + format.height; + } + + private String getBandwidthString() { + BandwidthMeter bandwidthMeter = debuggable.getBandwidthMeter(); + if (bandwidthMeter == null + || bandwidthMeter.getBitrateEstimate() == BandwidthMeter.NO_ESTIMATE) { + return "bw:?"; + } else { + return "bw:" + (bandwidthMeter.getBitrateEstimate() / 1000); + } + } + + private String getVideoCodecCountersString() { + CodecCounters codecCounters = debuggable.getCodecCounters(); + return codecCounters == null ? "" : codecCounters.getDebugString(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DtsUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DtsUtil.java new file mode 100755 index 000000000..49f1aa439 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DtsUtil.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.MediaFormat; + +import java.nio.ByteBuffer; + +/** + * Utility methods for parsing DTS frames. + */ +public final class DtsUtil { + + /** + * Maps AMODE to the number of channels. See ETSI TS 102 114 table 5.4. + */ + private static final int[] CHANNELS_BY_AMODE = new int[] {1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6, + 7, 8, 8}; + + /** + * Maps SFREQ to the sampling frequency in Hz. See ETSI TS 102 144 table 5.5. + */ + private static final int[] SAMPLE_RATE_BY_SFREQ = new int[] {-1, 8000, 16000, 32000, -1, -1, + 11025, 22050, 44100, -1, -1, 12000, 24000, 48000, -1, -1}; + + /** + * Maps RATE to 2 * bitrate in kbit/s. See ETSI TS 102 144 table 5.7. + */ + private static final int[] TWICE_BITRATE_KBPS_BY_RATE = new int[] {64, 112, 128, 192, 224, 256, + 384, 448, 512, 640, 768, 896, 1024, 1152, 1280, 1536, 1920, 2048, 2304, 2560, 2688, 2816, + 2823, 2944, 3072, 3840, 4096, 6144, 7680}; + + private static final ParsableBitArray SCRATCH_BITS = new ParsableBitArray(); + + /** + * Returns the DTS format given {@code data} containing the DTS frame according to ETSI TS 102 114 + * subsections 5.3/5.4. + *

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

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

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

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

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

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

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

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

    + * A line is considered to be terminated by any one of a carriage return ('\r'), a line feed + * ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The system's default + * charset (UTF-8) is used. + * + * @return A String containing the contents of the line, not including any line-termination + * characters, or null if the end of the stream has been reached. + */ + public String readLine() { + if (bytesLeft() == 0) { + return null; + } + int lineLimit = position; + while (lineLimit < limit && data[lineLimit] != '\n' && data[lineLimit] != '\r') { + lineLimit++; + } + if (lineLimit - position >= 3 && data[position] == (byte) 0xEF + && data[position + 1] == (byte) 0xBB && data[position + 2] == (byte) 0xBF) { + // There's a byte order mark at the start of the line. Discard it. + position += 3; + } + String line = new String(data, position, lineLimit - position); + position = lineLimit; + if (position == limit) { + return line; + } + if (data[position] == '\r') { + position++; + if (position == limit) { + return line; + } + } + if (data[position] == '\n') { + position++; + } + return line; + } + + /** + * Reads a long value encoded by UTF-8 encoding + * @throws NumberFormatException if there is a problem with decoding + * @return Decoded long value + */ + public long readUTF8EncodedLong() { + int length = 0; + long value = data[position]; + // find the high most 0 bit + for (int j = 7; j >= 0; j--) { + if ((value & (1 << j)) == 0) { + if (j < 6) { + value &= (1 << j) - 1; + length = 7 - j; + } else if (j == 7) { + length = 1; + } + break; + } + } + if (length == 0) { + throw new NumberFormatException("Invalid UTF-8 sequence first byte: " + value); + } + for (int i = 1; i < length; i++) { + int x = data[position + i]; + if ((x & 0xC0) != 0x80) { // if the high most 0 bit not 7th + throw new NumberFormatException("Invalid UTF-8 sequence continuation byte: " + value); + } + value = (value << 6) | (x & 0x3F); + } + position += length; + return value; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParserUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParserUtil.java new file mode 100755 index 000000000..88a477142 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParserUtil.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.util; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * Parser utility functions. + */ +public final class ParserUtil { + + private ParserUtil() {} + + public static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException { + return isEndTag(xpp) && xpp.getName().equals(name); + } + + public static boolean isEndTag(XmlPullParser xpp) throws XmlPullParserException { + return xpp.getEventType() == XmlPullParser.END_TAG; + } + + public static boolean isStartTag(XmlPullParser xpp, String name) + throws XmlPullParserException { + return isStartTag(xpp) && xpp.getName().equals(name); + } + + public static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException { + return xpp.getEventType() == XmlPullParser.START_TAG; + } + + public static String getAttributeValue(XmlPullParser xpp, String attributeName) { + int attributeCount = xpp.getAttributeCount(); + for (int i = 0; i < attributeCount; i++) { + if (attributeName.equals(xpp.getAttributeName(i))) { + return xpp.getAttributeValue(i); + } + } + return null; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PlayerControl.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PlayerControl.java new file mode 100755 index 000000000..bb3569ee6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PlayerControl.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.ExoPlayer; + +import android.widget.MediaController.MediaPlayerControl; + +/** + * An implementation of {@link MediaPlayerControl} for controlling an {@link ExoPlayer} instance. + *

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

    + * Application of audio effects is dependent on the audio renderer used. When using + * {@link org.telegram.messenger.exoplayer.MediaCodecAudioTrackRenderer}, the recommended approach is + * to extend the class and override + * {@link org.telegram.messenger.exoplayer.MediaCodecAudioTrackRenderer#onAudioSessionId}. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public int getAudioSessionId() { + throw new UnsupportedOperationException(); + } + + @Override + public int getBufferPercentage() { + return exoPlayer.getBufferedPercentage(); + } + + @Override + public int getCurrentPosition() { + return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : (int) exoPlayer.getCurrentPosition(); + } + + @Override + public int getDuration() { + return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : (int) exoPlayer.getDuration(); + } + + @Override + public boolean isPlaying() { + return exoPlayer.getPlayWhenReady(); + } + + @Override + public void start() { + exoPlayer.setPlayWhenReady(true); + } + + @Override + public void pause() { + exoPlayer.setPlayWhenReady(false); + } + + @Override + public void seekTo(int timeMillis) { + long seekPosition = exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : Math.min(Math.max(0, timeMillis), getDuration()); + exoPlayer.seekTo(seekPosition); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Predicate.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Predicate.java new file mode 100755 index 000000000..e54ccb6af --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Predicate.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.util; + +/** + * Determines a true of false value for a given input. + * + * @param The input type of the predicate. + */ +public interface Predicate { + + /** + * Evaluates an input. + * + * @param input The input to evaluate. + * @return The evaluated result. + */ + boolean evaluate(T input); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PriorityHandlerThread.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PriorityHandlerThread.java new file mode 100755 index 000000000..9bb3a80e4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PriorityHandlerThread.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.util; + +import android.os.HandlerThread; +import android.os.Process; + +/** + * A {@link HandlerThread} with a specified process priority. + */ +public final class PriorityHandlerThread extends HandlerThread { + + private final int priority; + + /** + * @param name The name of the thread. + * @param priority The priority level. See {@link Process#setThreadPriority(int)} for details. + */ + public PriorityHandlerThread(String name, int priority) { + super(name); + this.priority = priority; + } + + @Override + public void run() { + Process.setThreadPriority(priority); + super.run(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SlidingPercentile.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SlidingPercentile.java new file mode 100755 index 000000000..d66ba46e4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SlidingPercentile.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +/** + * Calculate any percentile over a sliding window of weighted values. A maximum total weight is + * configured. Once the maximum weight is reached, the oldest value is reduced in weight until it + * reaches zero and is removed. This maintains a constant total weight at steady state. + *

    + * SlidingPercentile can be used for bandwidth estimation based on a sliding window of past + * download rate observations. This is an alternative to sliding mean and exponential averaging + * which suffer from susceptibility to outliers and slow adaptation to step functions. + * + * @see Wiki: Moving average + * @see Wiki: Selection algorithm + */ +public final class SlidingPercentile { + + // Orderings. + private static final Comparator INDEX_COMPARATOR = new Comparator() { + @Override + public int compare(Sample a, Sample b) { + return a.index - b.index; + } + }; + + private static final Comparator VALUE_COMPARATOR = new Comparator() { + @Override + public int compare(Sample a, Sample b) { + return a.value < b.value ? -1 : b.value < a.value ? 1 : 0; + } + }; + + private static final int SORT_ORDER_NONE = -1; + private static final int SORT_ORDER_BY_VALUE = 0; + private static final int SORT_ORDER_BY_INDEX = 1; + + private static final int MAX_RECYCLED_SAMPLES = 5; + + private final int maxWeight; + private final ArrayList samples; + + private final Sample[] recycledSamples; + + private int currentSortOrder; + private int nextSampleIndex; + private int totalWeight; + private int recycledSampleCount; + + public SlidingPercentile(int maxWeight) { + this.maxWeight = maxWeight; + recycledSamples = new Sample[MAX_RECYCLED_SAMPLES]; + samples = new ArrayList<>(); + currentSortOrder = SORT_ORDER_NONE; + } + + /** + * Record a new observation. Respect the configured total weight by reducing in weight or + * removing the oldest observations as required. + * + * @param weight The weight of the new observation. + * @param value The value of the new observation. + */ + public void addSample(int weight, float value) { + ensureSortedByIndex(); + + Sample newSample = recycledSampleCount > 0 ? recycledSamples[--recycledSampleCount] + : new Sample(); + newSample.index = nextSampleIndex++; + newSample.weight = weight; + newSample.value = value; + samples.add(newSample); + totalWeight += weight; + + while (totalWeight > maxWeight) { + int excessWeight = totalWeight - maxWeight; + Sample oldestSample = samples.get(0); + if (oldestSample.weight <= excessWeight) { + totalWeight -= oldestSample.weight; + samples.remove(0); + if (recycledSampleCount < MAX_RECYCLED_SAMPLES) { + recycledSamples[recycledSampleCount++] = oldestSample; + } + } else { + oldestSample.weight -= excessWeight; + totalWeight -= excessWeight; + } + } + } + + /** + * Compute the percentile by integration. + * + * @param percentile The desired percentile, expressed as a fraction in the range (0,1]. + * @return The requested percentile value or Float.NaN. + */ + public float getPercentile(float percentile) { + ensureSortedByValue(); + float desiredWeight = percentile * totalWeight; + int accumulatedWeight = 0; + for (int i = 0; i < samples.size(); i++) { + Sample currentSample = samples.get(i); + accumulatedWeight += currentSample.weight; + if (accumulatedWeight >= desiredWeight) { + return currentSample.value; + } + } + // Clamp to maximum value or NaN if no values. + return samples.isEmpty() ? Float.NaN : samples.get(samples.size() - 1).value; + } + + /** + * Sort the samples by index, if not already. + */ + private void ensureSortedByIndex() { + if (currentSortOrder != SORT_ORDER_BY_INDEX) { + Collections.sort(samples, INDEX_COMPARATOR); + currentSortOrder = SORT_ORDER_BY_INDEX; + } + } + + /** + * Sort the samples by value, if not already. + */ + private void ensureSortedByValue() { + if (currentSortOrder != SORT_ORDER_BY_VALUE) { + Collections.sort(samples, VALUE_COMPARATOR); + currentSortOrder = SORT_ORDER_BY_VALUE; + } + } + + private static class Sample { + + public int index; + public int weight; + public float value; + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SystemClock.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SystemClock.java new file mode 100755 index 000000000..fcf045a07 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SystemClock.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.util; + +/** + * The standard implementation of {@link Clock}. + */ +public final class SystemClock implements Clock { + + @Override + public long elapsedRealtime() { + return android.os.SystemClock.elapsedRealtime(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/TraceUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/TraceUtil.java new file mode 100755 index 000000000..702fb204f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/TraceUtil.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.ExoPlayerLibraryInfo; + +import android.annotation.TargetApi; + +/** + * Calls through to {@link android.os.Trace} methods on supported API levels. + */ +public final class TraceUtil { + + private TraceUtil() {} + + /** + * Writes a trace message to indicate that a given section of code has begun. + * + * @see android.os.Trace#beginSection(String) + */ + public static void beginSection(String sectionName) { + if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) { + beginSectionV18(sectionName); + } + } + + /** + * Writes a trace message to indicate that a given section of code has ended. + * + * @see android.os.Trace#endSection() + */ + public static void endSection() { + if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) { + endSectionV18(); + } + } + + @TargetApi(18) + private static void beginSectionV18(String sectionName) { + android.os.Trace.beginSection(sectionName); + } + + @TargetApi(18) + private static void endSectionV18() { + android.os.Trace.endSection(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/UriUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/UriUtil.java new file mode 100755 index 000000000..8b1d950c6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/UriUtil.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.telegram.messenger.exoplayer.util; + +import android.net.Uri; +import android.text.TextUtils; + +/** + * Utility methods for manipulating URIs. + */ +public final class UriUtil { + + /** + * The length of arrays returned by {@link #getUriIndices(String)}. + */ + private static final int INDEX_COUNT = 4; + /** + * An index into an array returned by {@link #getUriIndices(String)}. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    + * Should only be called whilst synchronized on the lock object. + */ + private void maybeNotifyDecodeLoop() { + if (canDecodeBuffer()) { + lock.notify(); + } + } + + @Override + public final void run() { + try { + while (decode()) { + // Do nothing. + } + } catch (InterruptedException e) { + // Not expected. + throw new IllegalStateException(e); + } + } + + private boolean decode() throws InterruptedException { + I inputBuffer; + O outputBuffer; + boolean resetDecoder; + + // Wait until we have an input buffer to decode, and an output buffer to decode into. + synchronized (lock) { + while (!released && !canDecodeBuffer()) { + lock.wait(); + } + if (released) { + return false; + } + inputBuffer = queuedInputBuffers.removeFirst(); + outputBuffer = availableOutputBuffers[--availableOutputBufferCount]; + resetDecoder = flushed; + flushed = false; + } + + outputBuffer.reset(); + if (inputBuffer.getFlag(Buffer.FLAG_END_OF_STREAM)) { + outputBuffer.setFlag(Buffer.FLAG_END_OF_STREAM); + } else { + if (inputBuffer.getFlag(Buffer.FLAG_DECODE_ONLY)) { + outputBuffer.setFlag(Buffer.FLAG_DECODE_ONLY); + } + exception = decode(inputBuffer, outputBuffer, resetDecoder); + if (exception != null) { + // Memory barrier to ensure that the decoder exception is visible from the playback thread. + synchronized (lock) {} + return false; + } + } + + synchronized (lock) { + if (flushed || outputBuffer.getFlag(Buffer.FLAG_DECODE_ONLY)) { + // If a flush occurred while decoding or the buffer was only for decoding (not presentation) + // then make the output buffer available again rather than queueing it to be consumed. + availableOutputBuffers[availableOutputBufferCount++] = outputBuffer; + } else { + // Queue the decoded output buffer to be consumed. + queuedOutputBuffers.addLast(outputBuffer); + } + // Make the input buffer available again. + availableInputBuffers[availableInputBufferCount++] = inputBuffer; + } + + return true; + } + + private boolean canDecodeBuffer() { + return !queuedInputBuffers.isEmpty() && availableOutputBufferCount > 0; + } + + /** + * Creates a new input buffer. + */ + protected abstract I createInputBuffer(); + + /** + * Creates a new output buffer. + */ + protected abstract O createOutputBuffer(); + + /** + * Decodes the {@code inputBuffer} and stores any decoded output in {@code outputBuffer}. + * + * @param inputBuffer The buffer to decode. + * @param outputBuffer The output buffer to store decoded data. The flag + * {@link Buffer#FLAG_DECODE_ONLY} will be set if the same flag is set on {@code inputBuffer}, + * but the decoder may set/unset the flag if required. If the flag is set after this method + * returns, any output should not be presented. + * @param reset True if the decoder must be reset before decoding. + * @return A decoder exception if an error occurred, or null if decoding was successful. + */ + protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/BotQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/BotQuery.java index 03646523b..a3cf4adee 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/query/BotQuery.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/BotQuery.java @@ -29,6 +29,8 @@ public class BotQuery { public static void cleanup() { botInfos.clear(); + botKeyboards.clear(); + botKeyboardsByMids.clear(); } public static void clearBotKeyboard(final long did, final ArrayList messages) { @@ -68,11 +70,11 @@ public class BotQuery { NativeByteBuffer data; if (!cursor.isNull(0)) { - data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + data = cursor.byteBufferValue(0); + if (data != null) { botKeyboard = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); } - data.reuse(); } } cursor.dispose(); @@ -111,11 +113,11 @@ public class BotQuery { NativeByteBuffer data; if (!cursor.isNull(0)) { - data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + data = cursor.byteBufferValue(0); + if (data != null) { botInfo = TLRPC.BotInfo.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); } - data.reuse(); } } cursor.dispose(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/DraftQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/DraftQuery.java new file mode 100644 index 000000000..bfd6e574b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/DraftQuery.java @@ -0,0 +1,315 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.messenger.query; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.text.TextUtils; + +import org.telegram.SQLite.SQLiteCursor; +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.ChatObject; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.MessagesController; +import org.telegram.messenger.MessagesStorage; +import org.telegram.messenger.NotificationCenter; +import org.telegram.messenger.UserConfig; +import org.telegram.messenger.Utilities; +import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.NativeByteBuffer; +import org.telegram.tgnet.RequestDelegate; +import org.telegram.tgnet.SerializedData; +import org.telegram.tgnet.TLObject; +import org.telegram.tgnet.TLRPC; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public class DraftQuery { + + private static HashMap drafts = new HashMap<>(); + private static HashMap draftMessages = new HashMap<>(); + private static boolean inTransaction; + private static SharedPreferences preferences; + private static boolean loadingDrafts; + + static { + preferences = ApplicationLoader.applicationContext.getSharedPreferences("drafts", Activity.MODE_PRIVATE); + Map values = preferences.getAll(); + for (Map.Entry entry : values.entrySet()) { + try { + String key = entry.getKey(); + long did = Utilities.parseLong(key); + byte[] bytes = Utilities.hexToBytes((String) entry.getValue()); + SerializedData serializedData = new SerializedData(bytes); + if (key.startsWith("r_")) { + TLRPC.Message message = TLRPC.Message.TLdeserialize(serializedData, serializedData.readInt32(true), true); + if (message != null) { + draftMessages.put(did, message); + } + } else { + TLRPC.DraftMessage draftMessage = TLRPC.DraftMessage.TLdeserialize(serializedData, serializedData.readInt32(true), true); + if (draftMessage != null) { + drafts.put(did, draftMessage); + } + } + } catch (Exception e) { + //igonre + } + } + } + + public static void loadDrafts() { + if (UserConfig.draftsLoaded || loadingDrafts) { + return; + } + loadingDrafts = true; + TLRPC.TL_messages_getAllDrafts req = new TLRPC.TL_messages_getAllDrafts(); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (error != null) { + return; + } + MessagesController.getInstance().processUpdates((TLRPC.Updates) response, false); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + UserConfig.draftsLoaded = true; + loadingDrafts = false; + UserConfig.saveConfig(false); + } + }); + } + }); + } + + public static void cleanup() { + drafts.clear(); + draftMessages.clear(); + preferences.edit().clear().commit(); + } + + public static TLRPC.DraftMessage getDraft(long did) { + return drafts.get(did); + } + + public static TLRPC.Message getDraftMessage(long did) { + return draftMessages.get(did); + } + + public static void saveDraft(long did, CharSequence message, ArrayList entities, TLRPC.Message replyToMessage, boolean noWebpage) { + saveDraft(did, message, entities, replyToMessage, noWebpage, false); + } + + public static void saveDraft(long did, CharSequence message, ArrayList entities, TLRPC.Message replyToMessage, boolean noWebpage, boolean clean) { + TLRPC.DraftMessage draftMessage; + if (!TextUtils.isEmpty(message) || replyToMessage != null) { + draftMessage = new TLRPC.TL_draftMessage(); + } else { + draftMessage = new TLRPC.TL_draftMessageEmpty(); + } + draftMessage.date = (int) (System.currentTimeMillis() / 1000); + draftMessage.message = message == null ? "" : message.toString(); + draftMessage.no_webpage = noWebpage; + if (replyToMessage != null) { + draftMessage.reply_to_msg_id = replyToMessage.id; + draftMessage.flags |= 1; + } + if (entities != null && !entities.isEmpty()) { + draftMessage.entities = entities; + draftMessage.flags |= 8; + } + + TLRPC.DraftMessage currentDraft = drafts.get(did); + if (!clean) { + if (currentDraft != null && currentDraft.message.equals(draftMessage.message) && currentDraft.reply_to_msg_id == draftMessage.reply_to_msg_id && currentDraft.no_webpage == draftMessage.no_webpage || + currentDraft == null && TextUtils.isEmpty(draftMessage.message) && draftMessage.reply_to_msg_id == 0) { + return; + } + } + + saveDraft(did, draftMessage, replyToMessage, false); + int lower_id = (int) did; + if (lower_id != 0) { + TLRPC.TL_messages_saveDraft req = new TLRPC.TL_messages_saveDraft(); + req.peer = MessagesController.getInputPeer(lower_id); + if (req.peer == null) { + return; + } + req.message = draftMessage.message; + req.no_webpage = draftMessage.no_webpage; + req.reply_to_msg_id = draftMessage.reply_to_msg_id; + req.entities = draftMessage.entities; + req.flags = draftMessage.flags; + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + + } + }); + } + MessagesController.getInstance().sortDialogs(null); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); + } + + public static void saveDraft(final long did, TLRPC.DraftMessage draft, TLRPC.Message replyToMessage, boolean fromServer) { + SharedPreferences.Editor editor = preferences.edit(); + if (draft == null || draft instanceof TLRPC.TL_draftMessageEmpty) { + drafts.remove(did); + draftMessages.remove(did); + preferences.edit().remove("" + did).remove("r_" + did).commit(); + } else { + drafts.put(did, draft); + try { + SerializedData serializedData = new SerializedData(draft.getObjectSize()); + draft.serializeToStream(serializedData); + editor.putString("" + did, Utilities.bytesToHex(serializedData.toByteArray())); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + if (replyToMessage == null) { + draftMessages.remove(did); + editor.remove("r_" + did); + } else { + draftMessages.put(did, replyToMessage); + SerializedData serializedData = new SerializedData(replyToMessage.getObjectSize()); + replyToMessage.serializeToStream(serializedData); + editor.putString("r_" + did, Utilities.bytesToHex(serializedData.toByteArray())); + } + editor.commit(); + if (fromServer) { + if (draft.reply_to_msg_id != 0 && replyToMessage == null) { + int lower_id = (int) did; + TLRPC.User user = null; + TLRPC.Chat chat = null; + if (lower_id > 0) { + user = MessagesController.getInstance().getUser(lower_id); + } else { + chat = MessagesController.getInstance().getChat(-lower_id); + } + if (user != null || chat != null) { + long messageId = draft.reply_to_msg_id; + final int channelIdFinal; + if (ChatObject.isChannel(chat)) { + messageId |= ((long) chat.id) << 32; + channelIdFinal = chat.id; + } else { + channelIdFinal = 0; + } + final long messageIdFinal = messageId; + + MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { + @Override + public void run() { + try { + TLRPC.Message message = null; + SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data FROM messages WHERE mid = %d", messageIdFinal)); + if (cursor.next()) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { + message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); + } + } + cursor.dispose(); + if (message == null) { + if (channelIdFinal != 0) { + final TLRPC.TL_channels_getMessages req = new TLRPC.TL_channels_getMessages(); + req.channel = MessagesController.getInputChannel(channelIdFinal); + req.id.add((int) messageIdFinal); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (error == null) { + TLRPC.messages_Messages messagesRes = (TLRPC.messages_Messages) response; + if (!messagesRes.messages.isEmpty()) { + saveDraftReplyMessage(did, messagesRes.messages.get(0)); + } + } + } + }); + } else { + TLRPC.TL_messages_getMessages req = new TLRPC.TL_messages_getMessages(); + req.id.add((int) messageIdFinal); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (error == null) { + TLRPC.messages_Messages messagesRes = (TLRPC.messages_Messages) response; + if (!messagesRes.messages.isEmpty()) { + saveDraftReplyMessage(did, messagesRes.messages.get(0)); + } + } + } + }); + } + } else { + saveDraftReplyMessage(did, message); + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + }); + } + } + NotificationCenter.getInstance().postNotificationName(NotificationCenter.newDraftReceived, did); + } + } + + private static void saveDraftReplyMessage(final long did, final TLRPC.Message message) { + if (message == null) { + return; + } + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + TLRPC.DraftMessage draftMessage = drafts.get(did); + if (draftMessage != null && draftMessage.reply_to_msg_id == message.id) { + draftMessages.put(did, message); + SerializedData serializedData = new SerializedData(message.getObjectSize()); + message.serializeToStream(serializedData); + preferences.edit().putString("r_" + did, Utilities.bytesToHex(serializedData.toByteArray())).commit(); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.newDraftReceived, did); + } + } + }); + } + + public static void cleanDraft(long did, boolean replyOnly) { + TLRPC.DraftMessage draftMessage = drafts.get(did); + if (draftMessage == null) { + return; + } + if (!replyOnly) { + drafts.remove(did); + draftMessages.remove(did); + preferences.edit().remove("" + did).remove("r_" + did).commit(); + MessagesController.getInstance().sortDialogs(null); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); + } else if (draftMessage.reply_to_msg_id != 0) { + draftMessage.reply_to_msg_id = 0; + draftMessage.flags &= ~1; + saveDraft(did, draftMessage.message, draftMessage.entities, null, draftMessage.no_webpage, true); + } + } + + public static void beginTransaction() { + inTransaction = true; + } + + public static void endTransaction() { + inTransaction = false; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesQuery.java index d7e767ab3..6f0a230c7 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesQuery.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesQuery.java @@ -8,6 +8,7 @@ package org.telegram.messenger.query; +import android.text.Spannable; import android.text.TextUtils; import org.telegram.SQLite.SQLiteCursor; @@ -19,11 +20,13 @@ import org.telegram.messenger.MessagesController; import org.telegram.messenger.MessagesStorage; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.FileLog; +import org.telegram.messenger.Utilities; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.NativeByteBuffer; import org.telegram.tgnet.RequestDelegate; import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; +import org.telegram.ui.Components.URLSpanUserMention; import java.util.ArrayList; import java.util.HashMap; @@ -57,24 +60,25 @@ public class MessagesQuery { SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data, mid, date FROM messages WHERE mid = %d", messageId)); if (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { result = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); result.id = cursor.intValue(1); result.date = cursor.intValue(2); result.dialog_id = -channelId; MessagesStorage.addUsersAndChatsFromMessage(result, usersToLoad, chatsToLoad); } - data.reuse(); } cursor.dispose(); if (result == null) { cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data FROM chat_pinned WHERE uid = %d", channelId)); if (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { result = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (result.id != mid) { result = null; } else { @@ -82,7 +86,6 @@ public class MessagesQuery { MessagesStorage.addUsersAndChatsFromMessage(result, usersToLoad, chatsToLoad); } } - data.reuse(); } cursor.dispose(); } @@ -213,14 +216,14 @@ public class MessagesQuery { try { SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT m.data, m.mid, m.date, r.random_id FROM randoms as r INNER JOIN messages as m ON r.mid = m.mid WHERE r.random_id IN(%s)", TextUtils.join(",", replyMessages))); while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); message.id = cursor.intValue(1); message.date = cursor.intValue(2); message.dialog_id = dialogId; - ArrayList arrayList = replyMessageRandomOwners.remove(cursor.longValue(3)); if (arrayList != null) { MessageObject messageObject = new MessageObject(message, null, null, false); @@ -231,7 +234,6 @@ public class MessagesQuery { } } } - data.reuse(); } cursor.dispose(); if (!replyMessageRandomOwners.isEmpty()) { @@ -299,9 +301,10 @@ public class MessagesQuery { SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data, mid, date FROM messages WHERE mid IN(%s)", stringBuilder.toString())); while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); message.id = cursor.intValue(1); message.date = cursor.intValue(2); message.dialog_id = dialogId; @@ -309,7 +312,6 @@ public class MessagesQuery { result.add(message); replyMessages.remove((Integer) message.id); } - data.reuse(); } cursor.dispose(); @@ -437,4 +439,31 @@ public class MessagesQuery { } }); } + + public static ArrayList getEntities(CharSequence message) { + if (message == null) { + return null; + } + ArrayList entities = null; + if (message instanceof Spannable) { + Spannable spannable = (Spannable) message; + URLSpanUserMention spans[] = spannable.getSpans(0, message.length(), URLSpanUserMention.class); + if (spans != null && spans.length > 0) { + entities = new ArrayList<>(); + for (int b = 0; b < spans.length; b++) { + TLRPC.TL_inputMessageEntityMentionName entity = new TLRPC.TL_inputMessageEntityMentionName(); + entity.user_id = MessagesController.getInputUser(Utilities.parseInt(spans[b].getURL())); + if (entity.user_id != null) { + entity.offset = spannable.getSpanStart(spans[b]); + entity.length = Math.min(spannable.getSpanEnd(spans[b]), message.length()) - entity.offset; + if (message.charAt(entity.offset + entity.length - 1) == ' ') { + entity.length--; + } + entities.add(entity); + } + } + } + } + return entities; + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesSearchQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesSearchQuery.java index 551327785..33fb1ce6b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesSearchQuery.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesSearchQuery.java @@ -24,7 +24,10 @@ import java.util.ArrayList; public class MessagesSearchQuery { private static int reqId; + private static int mergeReqId; + private static long lastMergeDialogId; private static int lastReqId; + private static int messagesSearchCount[] = new int[] {0, 0}; private static boolean messagesSearchEndReached[] = new boolean[] {false, false}; private static ArrayList searchResultMessages = new ArrayList<>(); private static String lastSearchQuery; @@ -41,28 +44,38 @@ public class MessagesSearchQuery { return mask; } - public static void searchMessagesInChat(String query, final long dialog_id, final long mergeDialogId, final int guid, int direction) { + public static void searchMessagesInChat(String query, final long dialog_id, final long mergeDialogId, final int guid, final int direction) { + searchMessagesInChat(query, dialog_id, mergeDialogId, guid, direction, false); + } + + private static void searchMessagesInChat(String query, final long dialog_id, final long mergeDialogId, final int guid, final int direction, final boolean internal) { + int max_id = 0; + long queryWithDialog = dialog_id; + boolean firstQuery = !internal; if (reqId != 0) { ConnectionsManager.getInstance().cancelRequest(reqId, true); reqId = 0; } - int max_id = 0; - long queryWithDialog = dialog_id; + if (mergeReqId != 0) { + ConnectionsManager.getInstance().cancelRequest(mergeReqId, true); + mergeReqId = 0; + } if (query == null || query.length() == 0) { + if (searchResultMessages.isEmpty()) { + return; + } if (direction == 1) { lastReturnedNum++; if (lastReturnedNum < searchResultMessages.size()) { MessageObject messageObject = searchResultMessages.get(lastReturnedNum); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId()); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId(), lastReturnedNum, messagesSearchCount[0] + messagesSearchCount[1]); return; } else { if (messagesSearchEndReached[0] && mergeDialogId == 0 || messagesSearchEndReached[1]) { lastReturnedNum--; return; } - if (searchResultMessages.isEmpty()) { - return; - } + firstQuery = false; query = lastSearchQuery; MessageObject messageObject = searchResultMessages.get(searchResultMessages.size() - 1); if (messageObject.getDialogId() == dialog_id && !messagesSearchEndReached[0]) { @@ -86,7 +99,7 @@ public class MessagesSearchQuery { lastReturnedNum = searchResultMessages.size() - 1; } MessageObject messageObject = searchResultMessages.get(lastReturnedNum); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId()); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId(), lastReturnedNum, messagesSearchCount[0] + messagesSearchCount[1]); return; } else { return; @@ -95,13 +108,50 @@ public class MessagesSearchQuery { if (messagesSearchEndReached[0] && !messagesSearchEndReached[1] && mergeDialogId != 0) { queryWithDialog = mergeDialogId; } + if (queryWithDialog == dialog_id && firstQuery) { + if (mergeDialogId != 0) { + TLRPC.InputPeer inputPeer = MessagesController.getInputPeer((int) mergeDialogId); + if (inputPeer == null) { + return; + } + final TLRPC.TL_messages_search req = new TLRPC.TL_messages_search(); + req.peer = inputPeer; + lastMergeDialogId = mergeDialogId; + req.limit = 1; + req.q = query; + req.filter = new TLRPC.TL_inputMessagesFilterEmpty(); + mergeReqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (lastMergeDialogId == mergeDialogId) { + mergeReqId = 0; + if (response != null) { + TLRPC.messages_Messages res = (TLRPC.messages_Messages) response; + messagesSearchEndReached[1] = res.messages.isEmpty(); + messagesSearchCount[1] = res instanceof TLRPC.TL_messages_messagesSlice ? res.count : res.messages.size(); + searchMessagesInChat(req.q, dialog_id, mergeDialogId, guid, direction, true); + } + } + } + }); + } + }, ConnectionsManager.RequestFlagFailOnServerErrors); + return; + } else { + lastMergeDialogId = 0; + messagesSearchEndReached[1] = true; + messagesSearchCount[1] = 0; + } + } final TLRPC.TL_messages_search req = new TLRPC.TL_messages_search(); - req.limit = 21; - int lower_part = (int) queryWithDialog; - req.peer = MessagesController.getInputPeer(lower_part); + req.peer = MessagesController.getInputPeer((int) queryWithDialog); if (req.peer == null) { return; } + req.limit = 21; req.q = query; req.max_id = max_id; req.filter = new TLRPC.TL_inputMessagesFilterEmpty(); @@ -114,9 +164,9 @@ public class MessagesSearchQuery { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - reqId = 0; if (currentReqId == lastReqId) { - if (error == null) { + reqId = 0; + if (response != null) { TLRPC.messages_Messages res = (TLRPC.messages_Messages) response; MessagesStorage.getInstance().putUsersAndChats(res.users, res.chats, true, true); MessagesController.getInstance().putUsers(res.users, false); @@ -124,6 +174,7 @@ public class MessagesSearchQuery { if (req.max_id == 0 && queryWithDialogFinal == dialog_id) { lastReturnedNum = 0; searchResultMessages.clear(); + messagesSearchCount[0] = 0; } boolean added = false; for (int a = 0; a < Math.min(res.messages.size(), 20); a++) { @@ -132,23 +183,20 @@ public class MessagesSearchQuery { searchResultMessages.add(new MessageObject(message, null, false)); } messagesSearchEndReached[queryWithDialogFinal == dialog_id ? 0 : 1] = res.messages.size() != 21; - if (mergeDialogId == 0) { - messagesSearchEndReached[1] = messagesSearchEndReached[0]; - } + messagesSearchCount[queryWithDialogFinal == dialog_id ? 0 : 1] = res instanceof TLRPC.TL_messages_messagesSlice ? res.count : res.messages.size(); if (searchResultMessages.isEmpty()) { - NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, 0, getMask(), (long) 0); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, 0, getMask(), (long) 0, 0, 0); } else { if (added) { if (lastReturnedNum >= searchResultMessages.size()) { lastReturnedNum = searchResultMessages.size() - 1; } MessageObject messageObject = searchResultMessages.get(lastReturnedNum); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId()); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId(), lastReturnedNum, messagesSearchCount[0] + messagesSearchCount[1]); } } - if (queryWithDialogFinal == dialog_id && messagesSearchEndReached[0] && mergeDialogId != 0) { - messagesSearchEndReached[1] = false; - searchMessagesInChat(lastSearchQuery, dialog_id, mergeDialogId, guid, 0); + if (queryWithDialogFinal == dialog_id && messagesSearchEndReached[0] && mergeDialogId != 0 && !messagesSearchEndReached[1]) { + searchMessagesInChat(lastSearchQuery, dialog_id, mergeDialogId, guid, 0, true); } } } @@ -157,4 +205,8 @@ public class MessagesSearchQuery { } }, ConnectionsManager.RequestFlagFailOnServerErrors); } + + public static String getLastSearchQuery() { + return lastSearchQuery; + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/SearchQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/SearchQuery.java new file mode 100644 index 000000000..f4e050420 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/SearchQuery.java @@ -0,0 +1,410 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.messenger.query; + +import android.text.TextUtils; + +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.messenger.NotificationCenter; +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 java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Locale; + +public class SearchQuery { + + public static ArrayList hints = new ArrayList<>(); + public static ArrayList inlineBots = new ArrayList<>(); + private static HashMap inlineDates = new HashMap<>(); + private static boolean loaded; + private static boolean loading; + + public static void cleanup() { + loading = false; + loaded = false; + hints.clear(); + inlineBots.clear(); + inlineDates.clear(); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadHints); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadInlineHints); + } + + public static void loadHints(boolean cache) { + if (loading) { + return; + } + if (cache) { + if (loaded) { + return; + } + loading = true; + MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { + @Override + public void run() { + final ArrayList hintsNew = new ArrayList<>(); + final ArrayList inlineBotsNew = new ArrayList<>(); + final HashMap inlineDatesNew = new HashMap<>(); + final ArrayList users = new ArrayList<>(); + final ArrayList chats = new ArrayList<>(); + try { + ArrayList usersToLoad = new ArrayList<>(); + ArrayList chatsToLoad = new ArrayList<>(); + SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized("SELECT did, type, rating, date FROM chat_hints WHERE 1 ORDER BY rating DESC"); + while (cursor.next()) { + int did = cursor.intValue(0); + int type = cursor.intValue(1); + TLRPC.TL_topPeer peer = new TLRPC.TL_topPeer(); + peer.rating = cursor.doubleValue(2); + if (did > 0) { + peer.peer = new TLRPC.TL_peerUser(); + peer.peer.user_id = did; + usersToLoad.add(did); + } else { + peer.peer = new TLRPC.TL_peerChat(); + peer.peer.chat_id = -did; + chatsToLoad.add(-did); + } + if (type == 0) { + hintsNew.add(peer); + } else if (type == 1) { + inlineBotsNew.add(peer); + inlineDatesNew.put(did, cursor.intValue(3)); + } + } + cursor.dispose(); + if (!usersToLoad.isEmpty()) { + MessagesStorage.getInstance().getUsersInternal(TextUtils.join(",", usersToLoad), users); + } + + if (!chatsToLoad.isEmpty()) { + MessagesStorage.getInstance().getChatsInternal(TextUtils.join(",", chatsToLoad), chats); + } + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().putUsers(users, true); + MessagesController.getInstance().putChats(chats, true); + loading = false; + loaded = true; + hints = hintsNew; + inlineBots = inlineBotsNew; + inlineDates = inlineDatesNew; + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadHints); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadInlineHints); + if (Math.abs(UserConfig.lastHintsSyncTime - (int) (System.currentTimeMillis() / 1000)) >= 24 * 60 * 60) { + loadHints(false); + } + } + }); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + }); + loaded = true; + } else { + loading = true; + TLRPC.TL_contacts_getTopPeers req = new TLRPC.TL_contacts_getTopPeers(); + req.hash = 0; + req.bots_pm = false; + req.correspondents = true; + req.groups = false; + req.channels = false; + req.bots_inline = true; + req.offset = 0; + req.limit = 20; + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, TLRPC.TL_error error) { + if (response instanceof TLRPC.TL_contacts_topPeers) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + final TLRPC.TL_contacts_topPeers topPeers = (TLRPC.TL_contacts_topPeers) response; + MessagesController.getInstance().putUsers(topPeers.users, false); + MessagesController.getInstance().putChats(topPeers.chats, false); + for (int a = 0; a < topPeers.categories.size(); a++) { + TLRPC.TL_topPeerCategoryPeers category = topPeers.categories.get(a); + if (category.category instanceof TLRPC.TL_topPeerCategoryBotsInline) { + inlineBots = category.peers; + } else { + hints = category.peers; + } + } + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadHints); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadInlineHints); + final HashMap inlineDatesCopy = new HashMap<>(inlineDates); + MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { + @Override + public void run() { + try { + MessagesStorage.getInstance().getDatabase().executeFast("DELETE FROM chat_hints WHERE 1").stepThis().dispose(); + MessagesStorage.getInstance().getDatabase().beginTransaction(); + MessagesStorage.getInstance().putUsersAndChats(topPeers.users, topPeers.chats, false, false); + + SQLitePreparedStatement state = MessagesStorage.getInstance().getDatabase().executeFast("REPLACE INTO chat_hints VALUES(?, ?, ?, ?)"); + for (int a = 0; a < topPeers.categories.size(); a++) { + int type; + TLRPC.TL_topPeerCategoryPeers category = topPeers.categories.get(a); + if (category.category instanceof TLRPC.TL_topPeerCategoryBotsInline) { + type = 1; + } else { + type = 0; + } + for (int b = 0; b < category.peers.size(); b++) { + TLRPC.TL_topPeer peer = category.peers.get(b); + int did; + if (peer.peer instanceof TLRPC.TL_peerUser) { + did = peer.peer.user_id; + } else if (peer.peer instanceof TLRPC.TL_peerChat) { + did = -peer.peer.chat_id; + } else { + did = -peer.peer.channel_id; + } + Integer date = inlineDatesCopy.get(did); + state.requery(); + state.bindInteger(1, did); + state.bindInteger(2, type); + state.bindDouble(3, peer.rating); + state.bindInteger(4, date != null ? date : 0); + state.step(); + } + } + + state.dispose(); + + MessagesStorage.getInstance().getDatabase().commitTransaction(); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + UserConfig.lastHintsSyncTime = (int) (System.currentTimeMillis() / 1000); + UserConfig.saveConfig(false); + } + }); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + }); + } + }); + } + } + }); + } + } + + public static void increaseInlineRaiting(final int uid) { + Integer time = inlineDates.get(uid); + int dt; + if (time != null) { + dt = Math.max(1, ((int) (System.currentTimeMillis() / 1000)) - time); + } else { + dt = 60; + } + + TLRPC.TL_topPeer peer = null; + for (int a = 0; a < inlineBots.size(); a++) { + TLRPC.TL_topPeer p = inlineBots.get(a); + if (p.peer.user_id == uid) { + peer = p; + break; + } + } + if (peer == null) { + peer = new TLRPC.TL_topPeer(); + peer.peer = new TLRPC.TL_peerUser(); + peer.peer.user_id = uid; + inlineBots.add(peer); + } + peer.rating += Math.exp(dt / MessagesController.getInstance().ratingDecay); + Collections.sort(inlineBots, new Comparator() { + @Override + public int compare(TLRPC.TL_topPeer lhs, TLRPC.TL_topPeer rhs) { + if (lhs.rating > rhs.rating) { + return -1; + } else if (lhs.rating < rhs.rating) { + return 1; + } + return 0; + } + }); + if (inlineBots.size() > 20) { + inlineBots.remove(inlineBots.size() - 1); + } + savePeer(uid, 1, peer.rating); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadInlineHints); + } + + public static void removeInline(final int uid) { + TLRPC.TL_topPeerCategoryPeers category = null; + for (int a = 0; a < inlineBots.size(); a++) { + if (inlineBots.get(a).peer.user_id == uid) { + inlineBots.remove(a); + TLRPC.TL_contacts_resetTopPeerRating req = new TLRPC.TL_contacts_resetTopPeerRating(); + req.category = new TLRPC.TL_topPeerCategoryBotsInline(); + req.peer = MessagesController.getInputPeer(uid); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + + } + }); + deletePeer(uid, 1); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadInlineHints); + return; + } + } + } + + public static void removePeer(final int uid) { + TLRPC.TL_topPeerCategoryPeers category = null; + for (int a = 0; a < hints.size(); a++) { + if (hints.get(a).peer.user_id == uid) { + hints.remove(a); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadHints); + TLRPC.TL_contacts_resetTopPeerRating req = new TLRPC.TL_contacts_resetTopPeerRating(); + req.category = new TLRPC.TL_topPeerCategoryCorrespondents(); + req.peer = MessagesController.getInputPeer(uid); + deletePeer(uid, 0); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + + } + }); + return; + } + } + } + + public static void increasePeerRaiting(final long did) { + final int lower_id = (int) did; + if (lower_id <= 0) { + return; + } + //remove chats and bots for now + final TLRPC.User user = lower_id > 0 ? MessagesController.getInstance().getUser(lower_id) : null; + //final TLRPC.Chat chat = lower_id < 0 ? MessagesController.getInstance().getChat(-lower_id) : null; + if (user == null || user.bot/*&& chat == null || ChatObject.isChannel(chat) && !chat.megagroup*/) { + return; + } + MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { + @Override + public void run() { + double dt = 0; + try { + int lastTime = 0; + int lastMid = 0; + SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT MAX(mid), MAX(date) FROM messages WHERE uid = %d AND out = 1", did)); + if (cursor.next()) { + lastMid = cursor.intValue(0); + lastTime = cursor.intValue(1); + } + cursor.dispose(); + if (lastMid > 0) { + cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT date FROM messages WHERE uid = %d AND mid < %d AND out = 1 ORDER BY date DESC", did, lastMid)); + if (cursor.next()) { + dt = (lastTime - cursor.intValue(0)); + } + cursor.dispose(); + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + final double dtFinal = dt; + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + TLRPC.TL_topPeer peer = null; + for (int a = 0; a < hints.size(); a++) { + TLRPC.TL_topPeer p = hints.get(a); + if (lower_id < 0 && (p.peer.chat_id == -lower_id || p.peer.channel_id == -lower_id) || lower_id > 0 && p.peer.user_id == lower_id) { + peer = p; + break; + } + } + if (peer == null) { + peer = new TLRPC.TL_topPeer(); + if (lower_id > 0) { + peer.peer = new TLRPC.TL_peerUser(); + peer.peer.user_id = lower_id; + } else { + peer.peer = new TLRPC.TL_peerChat(); + peer.peer.chat_id = -lower_id; + } + hints.add(peer); + } + peer.rating += Math.exp(dtFinal / MessagesController.getInstance().ratingDecay); + Collections.sort(hints, new Comparator() { + @Override + public int compare(TLRPC.TL_topPeer lhs, TLRPC.TL_topPeer rhs) { + if (lhs.rating > rhs.rating) { + return -1; + } else if (lhs.rating < rhs.rating) { + return 1; + } + return 0; + } + }); + + savePeer((int) did, 0, peer.rating); + + NotificationCenter.getInstance().postNotificationName(NotificationCenter.reloadHints); + } + }); + } + }); + } + + private static void savePeer(final int did, final int type, final double rating) { + MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { + @Override + public void run() { + try { + SQLitePreparedStatement state = MessagesStorage.getInstance().getDatabase().executeFast("REPLACE INTO chat_hints VALUES(?, ?, ?, ?)"); + state.requery(); + state.bindInteger(1, did); + state.bindInteger(2, type); + state.bindDouble(3, rating); + state.bindInteger(4, (int) System.currentTimeMillis() / 1000); + state.step(); + state.dispose(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + }); + } + + private static void deletePeer(final int did, final int type) { + MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { + @Override + public void run() { + try { + MessagesStorage.getInstance().getDatabase().executeFast(String.format(Locale.US, "DELETE FROM chat_hints WHERE did = %d AND type = %d", did, type)).stepThis().dispose(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + }); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/SharedMediaQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/SharedMediaQuery.java index 2a1841784..8f2ccdc3e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/query/SharedMediaQuery.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/SharedMediaQuery.java @@ -375,9 +375,10 @@ public class SharedMediaQuery { } while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); message.id = cursor.intValue(1); message.dialog_id = uid; if ((int) uid == 0) { @@ -394,7 +395,6 @@ public class SharedMediaQuery { } } } - data.reuse(); } cursor.dispose(); @@ -481,16 +481,16 @@ public class SharedMediaQuery { SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data, mid FROM media_v2 WHERE uid = %d AND mid < %d AND type = %d ORDER BY date DESC, mid DESC LIMIT 1000", uid, max_id, MEDIA_MUSIC)); while (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (MessageObject.isMusicMessage(message)) { message.id = cursor.intValue(1); message.dialog_id = uid; arrayList.add(0, new MessageObject(message, null, false)); } } - data.reuse(); } cursor.dispose(); } catch (Exception e) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/StickersQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/StickersQuery.java index bbc794bb2..74fbcb5ad 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/query/StickersQuery.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/StickersQuery.java @@ -155,7 +155,10 @@ public class StickersQuery { if (!stickersByEmoji.containsKey(id)) { stickersByEmoji.put(id, stickerPack.emoticon); } - arrayList.add(stickersById.get(id)); + TLRPC.Document sticker = stickersById.get(id); + if (sticker != null) { + arrayList.add(sticker); + } } } loadHash = calcStickersHash(stickerSets); @@ -179,18 +182,18 @@ public class StickersQuery { try { cursor = MessagesStorage.getInstance().getDatabase().queryFinalized("SELECT data, date, hash FROM stickers_v2 WHERE 1"); if (cursor.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { newStickerArray = new ArrayList<>(); int count = data.readInt32(false); for (int a = 0; a < count; a++) { TLRPC.TL_messages_stickerSet stickerSet = TLRPC.TL_messages_stickerSet.TLdeserialize(data, data.readInt32(false), false); newStickerArray.add(stickerSet); } + data.reuse(); } date = cursor.intValue(1); hash = calcStickersHash(newStickerArray); - data.reuse(); } } catch (Throwable e) { FileLog.e("tmessages", e); @@ -404,7 +407,10 @@ public class StickersQuery { if (!stickersByEmojiNew.containsKey(id)) { stickersByEmojiNew.put(id, stickerPack.emoticon); } - arrayList.add(stickersByIdNew.get(id)); + TLRPC.Document sticker = stickersByIdNew.get(id); + if (sticker != null) { + arrayList.add(sticker); + } } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java index 5db54056b..3e46bc8a4 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java @@ -18,7 +18,6 @@ package org.telegram.messenger.support.widget; import android.content.Context; -import android.content.res.TypedArray; import android.database.Observable; import android.graphics.Canvas; import android.graphics.PointF; @@ -59,7 +58,6 @@ import android.view.FocusFinder; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; -import android.view.View.MeasureSpec; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; @@ -153,9 +151,6 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro private static final boolean DEBUG = false; - private static final int[] NESTED_SCROLLING_ATTRS - = {16843830 /* android.R.attr.nestedScrollingEnabled */}; - /** * On Kitkat and JB MR2, there is a bug which prevents DisplayList from being invalidated if * a View is two levels deep(wrt to ViewHolder.itemView). DisplayList can be invalidated by @@ -859,6 +854,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro */ public void setAdapter(Adapter adapter) { // bail out if layout is frozen + stopScroll(); setLayoutFrozen(false); setAdapterInternal(adapter, false, true); requestLayout(); @@ -4671,7 +4667,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " + "position " + position + "(offset:" + offsetPosition + ")." - + "state:" + mState.getItemCount()); + + "state:" + mState.getItemCount() + " tag " + getTag() + " adapter " + getAdapter()); } final int type = mAdapter.getItemViewType(offsetPosition); diff --git a/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java b/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java index e6a5f08b0..c0e032609 100644 --- a/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java +++ b/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java @@ -133,6 +133,7 @@ public class ConnectionsManager { TLRPC.TL_error error = null; if (response != 0) { NativeByteBuffer buff = NativeByteBuffer.wrap(response); + buff.reused = true; resp = object.deserializeResponse(buff, buff.readInt32(true), true); } else if (errorText != null) { error = new TLRPC.TL_error(); @@ -169,7 +170,7 @@ public class ConnectionsManager { native_cancelRequest(token, notifyServer); } - public void cleanUp() { + public void cleanup() { native_cleanUp(); } @@ -260,6 +261,7 @@ public class ConnectionsManager { public static void onUnparsedMessageReceived(int address) { try { NativeByteBuffer buff = NativeByteBuffer.wrap(address); + buff.reused = true; final TLObject message = TLClassStore.Instance().TLdeserialize(buff, buff.readInt32(true), true); if (message instanceof TLRPC.Updates) { FileLog.d("tmessages", "java received " + message); @@ -327,6 +329,7 @@ public class ConnectionsManager { public static void onUpdateConfig(int address) { try { NativeByteBuffer buff = NativeByteBuffer.wrap(address); + buff.reused = true; final TLRPC.TL_config message = TLRPC.TL_config.TLdeserialize(buff, buff.readInt32(true), true); if (message != null) { Utilities.stageQueue.postRunnable(new Runnable() { diff --git a/TMessagesProj/src/main/java/org/telegram/tgnet/NativeByteBuffer.java b/TMessagesProj/src/main/java/org/telegram/tgnet/NativeByteBuffer.java index eddea361a..91185ea06 100644 --- a/TMessagesProj/src/main/java/org/telegram/tgnet/NativeByteBuffer.java +++ b/TMessagesProj/src/main/java/org/telegram/tgnet/NativeByteBuffer.java @@ -9,8 +9,9 @@ public class NativeByteBuffer extends AbstractSerializedData { protected int address; public ByteBuffer buffer; - private boolean justCalc = false; - private int len = 0; + private boolean justCalc; + private int len; + public boolean reused = true; private static final ThreadLocal addressWrapper = new ThreadLocal() { @Override @@ -22,14 +23,16 @@ public class NativeByteBuffer extends AbstractSerializedData { public static NativeByteBuffer wrap(int address) { NativeByteBuffer result = addressWrapper.get(); if (address != 0) { + if (!result.reused) { + FileLog.e("tmessages", "forgot to reuse?"); + } result.address = address; + result.reused = false; result.buffer = native_getJavaByteBuffer(address); result.buffer.limit(native_limit(address)); int position = native_position(address); if (position <= result.buffer.limit()) { result.buffer.position(position); - } else { - FileLog.e("tmessages", "what with position " + position); } result.buffer.order(ByteOrder.LITTLE_ENDIAN); } @@ -494,6 +497,7 @@ public class NativeByteBuffer extends AbstractSerializedData { public void reuse() { if (address != 0) { + reused = true; native_reuse(address); } } diff --git a/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java b/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java index f7c6d0505..50c9eb3da 100644 --- a/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java +++ b/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java @@ -54,9 +54,98 @@ public class TLRPC { public static final int MESSAGE_FLAG_HAS_MEDIA = 0x00000200; public static final int MESSAGE_FLAG_HAS_VIEWS = 0x00000400; public static final int MESSAGE_FLAG_HAS_BOT_ID = 0x00000800; + public static final int MESSAGE_FLAG_EDITED = 0x00008000; public static final int MESSAGE_FLAG_MEGAGROUP = 0x80000000; - public static final int LAYER = 51; + public static final int LAYER = 53; + + public static class DraftMessage extends TLObject { + public int flags; + public boolean no_webpage; + public int reply_to_msg_id; + public String message; + public ArrayList entities = new ArrayList<>(); + public int date; + + public static DraftMessage TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + DraftMessage result = null; + switch(constructor) { + case 0xba4baec5: + result = new TL_draftMessageEmpty(); + break; + case 0xfd8e711f: + result = new TL_draftMessage(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in DraftMessage", constructor)); + } + if (result != null) { + result.readParams(stream, exception); + } + return result; + } + } + + public static class TL_draftMessageEmpty extends DraftMessage { + public static int constructor = 0xba4baec5; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + + public static class TL_draftMessage extends DraftMessage { + public static int constructor = 0xfd8e711f; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + no_webpage = (flags & 2) != 0; + if ((flags & 1) != 0) { + reply_to_msg_id = stream.readInt32(exception); + } + message = stream.readString(exception); + if ((flags & 8) != 0) { + int magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + int count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + MessageEntity object = MessageEntity.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + entities.add(object); + } + } + date = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = no_webpage ? (flags | 2) : (flags &~ 2); + stream.writeInt32(flags); + if ((flags & 1) != 0) { + stream.writeInt32(reply_to_msg_id); + } + stream.writeString(message); + if ((flags & 8) != 0) { + stream.writeInt32(0x1cb5c415); + int count = entities.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + entities.get(a).serializeToStream(stream); + } + } + stream.writeInt32(date); + } + } public static class ChatPhoto extends TLObject { public FileLocation photo_small; @@ -746,7 +835,6 @@ public class TLRPC { public int flags; public int pts; public int count; - public ArrayList collapsed = new ArrayList<>(); public static messages_Messages TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { messages_Messages result = null; @@ -754,7 +842,7 @@ public class TLRPC { case 0x8c718e87: result = new TL_messages_messages(); break; - case 0xbc0f17bc: + case 0x99262e37: result = new TL_messages_channelMessages(); break; case 0xb446ae3: @@ -846,111 +934,86 @@ public class TLRPC { } } - public static class TL_messages_channelMessages extends messages_Messages { - public static int constructor = 0xbc0f17bc; + public static class TL_messages_channelMessages extends messages_Messages { + public static int constructor = 0x99262e37; - public void readParams(AbstractSerializedData stream, boolean exception) { - flags = stream.readInt32(exception); - pts = stream.readInt32(exception); - count = stream.readInt32(exception); - int magic = stream.readInt32(exception); - if (magic != 0x1cb5c415) { - if (exception) { - throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); - } - return; - } - int count = stream.readInt32(exception); - for (int a = 0; a < count; a++) { - Message object = Message.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - messages.add(object); - } - if ((flags & 1) != 0) { - magic = stream.readInt32(exception); - if (magic != 0x1cb5c415) { - if (exception) { - throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); - } - return; - } - count = stream.readInt32(exception); - for (int a = 0; a < count; a++) { - TL_messageGroup object = TL_messageGroup.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - collapsed.add(object); - } - } - magic = stream.readInt32(exception); - if (magic != 0x1cb5c415) { - if (exception) { - throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); - } - return; - } - count = stream.readInt32(exception); - for (int a = 0; a < count; a++) { - Chat object = Chat.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - chats.add(object); - } - magic = stream.readInt32(exception); - if (magic != 0x1cb5c415) { - if (exception) { - throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); - } - return; - } - count = stream.readInt32(exception); - for (int a = 0; a < count; a++) { - User object = User.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - users.add(object); - } - } + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + pts = stream.readInt32(exception); + count = stream.readInt32(exception); + int magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + int count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + Message object = Message.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + messages.add(object); + } + magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + Chat object = Chat.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + chats.add(object); + } + magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + User object = User.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + users.add(object); + } + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(flags); - stream.writeInt32(pts); - stream.writeInt32(count); - stream.writeInt32(0x1cb5c415); - int count = messages.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - messages.get(a).serializeToStream(stream); - } - if ((flags & 1) != 0) { - stream.writeInt32(0x1cb5c415); - count = collapsed.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - collapsed.get(a).serializeToStream(stream); - } - } - stream.writeInt32(0x1cb5c415); - count = chats.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - chats.get(a).serializeToStream(stream); - } - stream.writeInt32(0x1cb5c415); - count = users.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - users.get(a).serializeToStream(stream); - } - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + stream.writeInt32(pts); + stream.writeInt32(count); + stream.writeInt32(0x1cb5c415); + int count = messages.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + messages.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = chats.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + chats.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = users.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + users.get(a).serializeToStream(stream); + } + } + } public static class TL_messages_messagesSlice extends messages_Messages { public static int constructor = 0xb446ae3; @@ -2766,8 +2829,8 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeInt32(flags); flags = phone_registered ? (flags | 1) : (flags &~ 1); + stream.writeInt32(flags); type.serializeToStream(stream); stream.writeString(phone_code_hash); if ((flags & 2) != 0) { @@ -4056,8 +4119,8 @@ public class TLRPC { public int admins_count; public int kicked_count; public int read_inbox_max_id; + public int read_outbox_max_id; public int unread_count; - public int unread_important_count; public Photo chat_photo; public PeerNotifySettings notify_settings; public ExportedChatInvite exported_invite; @@ -4070,9 +4133,12 @@ public class TLRPC { public static ChatFull TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { ChatFull result = null; switch(constructor) { - case 0x97bee562: + case 0xc3d5512f: result = new TL_channelFull(); break; + case 0x97bee562: + result = new TL_channelFull_layer52(); + break; case 0x2e02a614: result = new TL_chatFull(); break; @@ -4094,6 +4160,97 @@ public class TLRPC { } public static class TL_channelFull extends ChatFull { + public static int constructor = 0xc3d5512f; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + can_view_participants = (flags & 8) != 0; + can_set_username = (flags & 64) != 0; + id = stream.readInt32(exception); + about = stream.readString(exception); + if ((flags & 1) != 0) { + participants_count = stream.readInt32(exception); + } + if ((flags & 2) != 0) { + admins_count = stream.readInt32(exception); + } + if ((flags & 4) != 0) { + kicked_count = stream.readInt32(exception); + } + read_inbox_max_id = stream.readInt32(exception); + read_outbox_max_id = stream.readInt32(exception); + unread_count = stream.readInt32(exception); + chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); + notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); + exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); + int magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + int count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + BotInfo object = BotInfo.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + bot_info.add(object); + } + if ((flags & 16) != 0) { + migrated_from_chat_id = stream.readInt32(exception); + } + if ((flags & 16) != 0) { + migrated_from_max_id = stream.readInt32(exception); + } + if ((flags & 32) != 0) { + pinned_msg_id = stream.readInt32(exception); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = can_view_participants ? (flags | 8) : (flags &~ 8); + flags = can_set_username ? (flags | 64) : (flags &~ 64); + stream.writeInt32(flags); + stream.writeInt32(id); + stream.writeString(about); + if ((flags & 1) != 0) { + stream.writeInt32(participants_count); + } + if ((flags & 2) != 0) { + stream.writeInt32(admins_count); + } + if ((flags & 4) != 0) { + stream.writeInt32(kicked_count); + } + stream.writeInt32(read_inbox_max_id); + stream.writeInt32(read_outbox_max_id); + stream.writeInt32(unread_count); + chat_photo.serializeToStream(stream); + notify_settings.serializeToStream(stream); + exported_invite.serializeToStream(stream); + stream.writeInt32(0x1cb5c415); + int count = bot_info.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + bot_info.get(a).serializeToStream(stream); + } + if ((flags & 16) != 0) { + stream.writeInt32(migrated_from_chat_id); + } + if ((flags & 16) != 0) { + stream.writeInt32(migrated_from_max_id); + } + if ((flags & 32) != 0) { + stream.writeInt32(pinned_msg_id); + } + } + } + + public static class TL_channelFull_layer52 extends TL_channelFull { public static int constructor = 0x97bee562; @@ -4114,7 +4271,7 @@ public class TLRPC { } read_inbox_max_id = stream.readInt32(exception); unread_count = stream.readInt32(exception); - unread_important_count = stream.readInt32(exception); + stream.readInt32(exception); chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); @@ -4162,7 +4319,7 @@ public class TLRPC { } stream.writeInt32(read_inbox_max_id); stream.writeInt32(unread_count); - stream.writeInt32(unread_important_count); + stream.writeInt32(0); chat_photo.serializeToStream(stream); notify_settings.serializeToStream(stream); exported_invite.serializeToStream(stream); @@ -4247,7 +4404,7 @@ public class TLRPC { } read_inbox_max_id = stream.readInt32(exception); unread_count = stream.readInt32(exception); - unread_important_count = stream.readInt32(exception); + stream.readInt32(exception); chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); @@ -4291,7 +4448,7 @@ public class TLRPC { } stream.writeInt32(read_inbox_max_id); stream.writeInt32(unread_count); - stream.writeInt32(unread_important_count); + stream.writeInt32(0); chat_photo.serializeToStream(stream); notify_settings.serializeToStream(stream); exported_invite.serializeToStream(stream); @@ -4330,7 +4487,7 @@ public class TLRPC { } read_inbox_max_id = stream.readInt32(exception); unread_count = stream.readInt32(exception); - unread_important_count = stream.readInt32(exception); + stream.readInt32(exception); chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); @@ -4353,7 +4510,7 @@ public class TLRPC { } stream.writeInt32(read_inbox_max_id); stream.writeInt32(unread_count); - stream.writeInt32(unread_important_count); + stream.writeInt32(0); chat_photo.serializeToStream(stream); notify_settings.serializeToStream(stream); exported_invite.serializeToStream(stream); @@ -4422,6 +4579,59 @@ public class TLRPC { } } + public static class TL_topPeerCategoryPeers extends TLObject { + public static int constructor = 0xfb834291; + + public TopPeerCategory category; + public int count; + public ArrayList peers = new ArrayList<>(); + + public static TL_topPeerCategoryPeers TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_topPeerCategoryPeers.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_topPeerCategoryPeers", constructor)); + } else { + return null; + } + } + TL_topPeerCategoryPeers result = new TL_topPeerCategoryPeers(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + category = TopPeerCategory.TLdeserialize(stream, stream.readInt32(exception), exception); + count = stream.readInt32(exception); + int magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + int count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + TL_topPeer object = TL_topPeer.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + peers.add(object); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + category.serializeToStream(stream); + stream.writeInt32(count); + stream.writeInt32(0x1cb5c415); + int count = peers.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + peers.get(a).serializeToStream(stream); + } + } + } + public static class InputUser extends TLObject { public int user_id; public long access_hash; @@ -5213,9 +5423,9 @@ public class TLRPC { public void readParams(AbstractSerializedData stream, boolean exception) { email_pattern = stream.readString(exception); - } + } - public void serializeToStream(AbstractSerializedData stream) { + public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); stream.writeString(email_pattern); } @@ -6261,6 +6471,9 @@ public class TLRPC { case 0x51bdb021: result = new TL_messageActionChatMigrateTo(); break; + case 0x9fbab604: + result = new TL_messageActionHistoryClear(); + break; case 0x7fcb13a8: result = new TL_messageActionChatEditPhoto(); break; @@ -6398,6 +6611,15 @@ public class TLRPC { } } + public static class TL_messageActionHistoryClear extends MessageAction { + public static int constructor = 0x9fbab604; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + public static class TL_messageActionChatEditPhoto extends MessageAction { public static int constructor = 0x7fcb13a8; @@ -7246,6 +7468,82 @@ public class TLRPC { } } + public static class TopPeerCategory extends TLObject { + + public static TopPeerCategory TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + TopPeerCategory result = null; + switch(constructor) { + case 0x637b7ed: + result = new TL_topPeerCategoryCorrespondents(); + break; + case 0xbd17a14a: + result = new TL_topPeerCategoryGroups(); + break; + case 0x148677e2: + result = new TL_topPeerCategoryBotsInline(); + break; + case 0x161d9628: + result = new TL_topPeerCategoryChannels(); + break; + case 0xab661b5b: + result = new TL_topPeerCategoryBotsPM(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in TopPeerCategory", constructor)); + } + if (result != null) { + result.readParams(stream, exception); + } + return result; + } + } + + public static class TL_topPeerCategoryCorrespondents extends TopPeerCategory { + public static int constructor = 0x637b7ed; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + + public static class TL_topPeerCategoryGroups extends TopPeerCategory { + public static int constructor = 0xbd17a14a; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + + public static class TL_topPeerCategoryBotsInline extends TopPeerCategory { + public static int constructor = 0x148677e2; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + + public static class TL_topPeerCategoryChannels extends TopPeerCategory { + public static int constructor = 0x161d9628; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + + public static class TL_topPeerCategoryBotsPM extends TopPeerCategory { + public static int constructor = 0xab661b5b; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + public static class TL_contactBlocked extends TLObject { public static int constructor = 0x561bc879; @@ -8695,6 +8993,7 @@ public class TLRPC { public int inviter_id; public int date; public int version; + public DraftMessage draft; public int pts; public int pts_count; public long query_id; @@ -8727,7 +9026,6 @@ public class TLRPC { public TL_messages_stickerSet stickerset; public ContactLink my_link; public ContactLink foreign_link; - public TL_messageGroup group; public String first_name; public String last_name; public String username; @@ -8746,6 +9044,9 @@ public class TLRPC { case 0xea4b0e5c: result = new TL_updateChatParticipantAdd(); break; + case 0xee2bb969: + result = new TL_updateDraftMessage(); + break; case 0x43ae3dec: result = new TL_updateStickerSets(); break; @@ -8809,6 +9110,9 @@ public class TLRPC { case 0x2cbd95af: result = new TL_updateInlineBotCallbackQuery(); break; + case 0x25d6c9c7: + result = new TL_updateReadChannelOutbox(); + break; case 0x1710f156: result = new TL_updateEncryptedChatTyping(); break; @@ -8839,9 +9143,6 @@ public class TLRPC { case 0x9961fd5c: result = new TL_updateReadHistoryInbox(); break; - case 0xc36c1e3c: - result = new TL_updateChannelGroup(); - break; case 0xa7332b73: result = new TL_updateUserName(); break; @@ -8923,6 +9224,23 @@ public class TLRPC { } } + public static class TL_updateDraftMessage extends Update { + public static int constructor = 0xee2bb969; + + public Peer peer; + + public void readParams(AbstractSerializedData stream, boolean exception) { + peer = Peer.TLdeserialize(stream, stream.readInt32(exception), exception); + draft = DraftMessage.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + peer.serializeToStream(stream); + draft.serializeToStream(stream); + } + } + public static class TL_updateStickerSets extends Update { public static int constructor = 0x43ae3dec; @@ -9319,6 +9637,22 @@ public class TLRPC { } } + public static class TL_updateReadChannelOutbox extends Update { + public static int constructor = 0x25d6c9c7; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + channel_id = stream.readInt32(exception); + max_id = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(channel_id); + stream.writeInt32(max_id); + } + } + public static class TL_updateEncryptedChatTyping extends Update { public static int constructor = 0x1710f156; @@ -9499,22 +9833,6 @@ public class TLRPC { } } - public static class TL_updateChannelGroup extends Update { - public static int constructor = 0xc36c1e3c; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - channel_id = stream.readInt32(exception); - group = TL_messageGroup.TLdeserialize(stream, stream.readInt32(exception), exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(channel_id); - group.serializeToStream(stream); - } - } - public static class TL_updateUserName extends Update { public static int constructor = 0xa7332b73; @@ -10509,21 +10827,18 @@ public class TLRPC { public static class MessageEntity extends TLObject { public int offset; - public int length; - public String language; + public int length; public String url; + public String language; public static MessageEntity TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { - MessageEntity result = null; + MessageEntity result = null; switch(constructor) { - case 0x6ed02538: - result = new TL_messageEntityUrl(); + case 0x76a6d327: + result = new TL_messageEntityTextUrl(); break; - case 0xbd610bc9: - result = new TL_messageEntityBold(); - break; - case 0x826f8b60: - result = new TL_messageEntityItalic(); + case 0x6cef8ac7: + result = new TL_messageEntityBotCommand(); break; case 0x64e475c2: result = new TL_messageEntityEmail(); @@ -10531,24 +10846,33 @@ public class TLRPC { case 0x73924be0: result = new TL_messageEntityPre(); break; - case 0x76a6d327: - result = new TL_messageEntityTextUrl(); - break; case 0xbb92ba95: result = new TL_messageEntityUnknown(); break; + case 0x6ed02538: + result = new TL_messageEntityUrl(); + break; + case 0x826f8b60: + result = new TL_messageEntityItalic(); + break; + case 0xfa04579d: + result = new TL_messageEntityMention(); + break; + case 0x352dca58: + result = new TL_messageEntityMentionName(); + break; + case 0x208e68c9: + result = new TL_inputMessageEntityMentionName(); + break; + case 0xbd610bc9: + result = new TL_messageEntityBold(); + break; case 0x6f635b0d: result = new TL_messageEntityHashtag(); break; - case 0x6cef8ac7: - result = new TL_messageEntityBotCommand(); - break; case 0x28a20571: result = new TL_messageEntityCode(); break; - case 0xfa04579d: - result = new TL_messageEntityMention(); - break; } if (result == null && exception) { throw new RuntimeException(String.format("can't parse magic %x in MessageEntity", constructor)); @@ -10560,8 +10884,26 @@ public class TLRPC { } } - public static class TL_messageEntityUrl extends MessageEntity { - public static int constructor = 0x6ed02538; + public static class TL_messageEntityTextUrl extends MessageEntity { + public static int constructor = 0x76a6d327; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + offset = stream.readInt32(exception); + length = stream.readInt32(exception); + url = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(offset); + stream.writeInt32(length); + stream.writeString(url); + } + } + + public static class TL_messageEntityBotCommand extends MessageEntity { + public static int constructor = 0x6cef8ac7; public void readParams(AbstractSerializedData stream, boolean exception) { @@ -10576,8 +10918,58 @@ public class TLRPC { } } - public static class TL_messageEntityBold extends MessageEntity { - public static int constructor = 0xbd610bc9; + public static class TL_messageEntityEmail extends MessageEntity { + public static int constructor = 0x64e475c2; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + offset = stream.readInt32(exception); + length = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(offset); + stream.writeInt32(length); + } + } + + public static class TL_messageEntityPre extends MessageEntity { + public static int constructor = 0x73924be0; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + offset = stream.readInt32(exception); + length = stream.readInt32(exception); + language = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(offset); + stream.writeInt32(length); + stream.writeString(language); + } + } + + public static class TL_messageEntityUnknown extends MessageEntity { + public static int constructor = 0xbb92ba95; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + offset = stream.readInt32(exception); + length = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(offset); + stream.writeInt32(length); + } + } + + public static class TL_messageEntityUrl extends MessageEntity { + public static int constructor = 0x6ed02538; public void readParams(AbstractSerializedData stream, boolean exception) { @@ -10608,68 +11000,70 @@ public class TLRPC { } } - public static class TL_messageEntityEmail extends MessageEntity { - public static int constructor = 0x64e475c2; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - offset = stream.readInt32(exception); - length = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(offset); - stream.writeInt32(length); - } - } - - public static class TL_messageEntityPre extends MessageEntity { - public static int constructor = 0x73924be0; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - offset = stream.readInt32(exception); - length = stream.readInt32(exception); - language = stream.readString(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(offset); - stream.writeInt32(length); - stream.writeString(language); - } - } - - public static class TL_messageEntityTextUrl extends MessageEntity { - public static int constructor = 0x76a6d327; + public static class TL_messageEntityMention extends MessageEntity { + public static int constructor = 0xfa04579d; public void readParams(AbstractSerializedData stream, boolean exception) { offset = stream.readInt32(exception); - length = stream.readInt32(exception); - url = stream.readString(exception); + length = stream.readInt32(exception); } public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); stream.writeInt32(offset); stream.writeInt32(length); - stream.writeString(url); } } - public static class TL_messageEntityUnknown extends MessageEntity { - public static int constructor = 0xbb92ba95; + public static class TL_messageEntityMentionName extends MessageEntity { + public static int constructor = 0x352dca58; + + public int user_id; + + public void readParams(AbstractSerializedData stream, boolean exception) { + offset = stream.readInt32(exception); + length = stream.readInt32(exception); + user_id = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(offset); + stream.writeInt32(length); + stream.writeInt32(user_id); + } + } + + public static class TL_inputMessageEntityMentionName extends MessageEntity { + public static int constructor = 0x208e68c9; + + public InputUser user_id; + + public void readParams(AbstractSerializedData stream, boolean exception) { + offset = stream.readInt32(exception); + length = stream.readInt32(exception); + user_id = InputUser.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(offset); + stream.writeInt32(length); + user_id.serializeToStream(stream); + } + } + + public static class TL_messageEntityBold extends MessageEntity { + public static int constructor = 0xbd610bc9; public void readParams(AbstractSerializedData stream, boolean exception) { - offset = stream.readInt32(exception); + offset = stream.readInt32(exception); length = stream.readInt32(exception); - } + } - public void serializeToStream(AbstractSerializedData stream) { + public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); stream.writeInt32(offset); stream.writeInt32(length); @@ -10692,42 +11086,10 @@ public class TLRPC { } } - public static class TL_messageEntityBotCommand extends MessageEntity { - public static int constructor = 0x6cef8ac7; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - offset = stream.readInt32(exception); - length = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(offset); - stream.writeInt32(length); - } - } - public static class TL_messageEntityCode extends MessageEntity { public static int constructor = 0x28a20571; - public void readParams(AbstractSerializedData stream, boolean exception) { - offset = stream.readInt32(exception); - length = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(offset); - stream.writeInt32(length); - } - } - - public static class TL_messageEntityMention extends MessageEntity { - public static int constructor = 0xfa04579d; - - public void readParams(AbstractSerializedData stream, boolean exception) { offset = stream.readInt32(exception); length = stream.readInt32(exception); @@ -11177,7 +11539,7 @@ public class TLRPC { } public static class TL_config extends TLObject { - public static int constructor = 0x317ceef4; + public static int constructor = 0xc9411388; public int date; public int expires; @@ -11198,6 +11560,7 @@ public class TLRPC { public int push_chat_limit; public int saved_gifs_limit; public int edit_time_limit; + public int rating_e_decay; public ArrayList disabled_features = new ArrayList<>(); public static TL_config TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { @@ -11247,6 +11610,7 @@ public class TLRPC { push_chat_limit = stream.readInt32(exception); saved_gifs_limit = stream.readInt32(exception); edit_time_limit = stream.readInt32(exception); + rating_e_decay = stream.readInt32(exception); magic = stream.readInt32(exception); if (magic != 0x1cb5c415) { if (exception) { @@ -11290,6 +11654,7 @@ public class TLRPC { stream.writeInt32(push_chat_limit); stream.writeInt32(saved_gifs_limit); stream.writeInt32(edit_time_limit); + stream.writeInt32(rating_e_decay); stream.writeInt32(0x1cb5c415); count = disabled_features.size(); stream.writeInt32(count); @@ -11299,6 +11664,115 @@ public class TLRPC { } } + public static class contacts_TopPeers extends TLObject { + public ArrayList categories = new ArrayList<>(); + public ArrayList chats = new ArrayList<>(); + public ArrayList users = new ArrayList<>(); + + public static contacts_TopPeers TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + contacts_TopPeers result = null; + switch(constructor) { + case 0x70b772a8: + result = new TL_contacts_topPeers(); + break; + case 0xde266ef5: + result = new TL_contacts_topPeersNotModified(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in contacts_TopPeers", constructor)); + } + if (result != null) { + result.readParams(stream, exception); + } + return result; + } + } + + public static class TL_contacts_topPeers extends contacts_TopPeers { + public static int constructor = 0x70b772a8; + + + 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_topPeerCategoryPeers object = TL_topPeerCategoryPeers.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + categories.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 = categories.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + categories.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_contacts_topPeersNotModified extends contacts_TopPeers { + public static int constructor = 0xde266ef5; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + public static class TL_help_support extends TLObject { public static int constructor = 0x17c6b5f6; @@ -11457,43 +11931,6 @@ public class TLRPC { } } - public static class TL_messageGroup extends TLObject { - public static int constructor = 0xe8346f53; - - public int min_id; - public int max_id; - public int count; - public int date; - - public static TL_messageGroup TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { - if (TL_messageGroup.constructor != constructor) { - if (exception) { - throw new RuntimeException(String.format("can't parse magic %x in TL_messageGroup", constructor)); - } else { - return null; - } - } - TL_messageGroup result = new TL_messageGroup(); - result.readParams(stream, exception); - return result; - } - - public void readParams(AbstractSerializedData stream, boolean exception) { - min_id = stream.readInt32(exception); - max_id = stream.readInt32(exception); - count = stream.readInt32(exception); - date = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(min_id); - stream.writeInt32(max_id); - stream.writeInt32(count); - stream.writeInt32(date); - } - } - public static class TL_messages_botResults extends TLObject { public static int constructor = 0x256709a6; @@ -11658,10 +12095,9 @@ public class TLRPC { public ArrayList chats = new ArrayList<>(); public ArrayList users = new ArrayList<>(); public int top_message; - public int top_important_message; public int read_inbox_max_id; + public int read_outbox_max_id; public int unread_count; - public int unread_important_count; public ArrayList messages = new ArrayList<>(); public static updates_ChannelDifference TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { @@ -11673,7 +12109,7 @@ public class TLRPC { case 0x2064674e: result = new TL_updates_channelDifference(); break; - case 0x5e167646: + case 0x410dee07: result = new TL_updates_channelDifferenceTooLong(); break; } @@ -11820,7 +12256,7 @@ public class TLRPC { } public static class TL_updates_channelDifferenceTooLong extends updates_ChannelDifference { - public static int constructor = 0x5e167646; + public static int constructor = 0x410dee07; public void readParams(AbstractSerializedData stream, boolean exception) { @@ -11831,10 +12267,9 @@ public class TLRPC { timeout = stream.readInt32(exception); } top_message = stream.readInt32(exception); - top_important_message = stream.readInt32(exception); read_inbox_max_id = stream.readInt32(exception); + read_outbox_max_id = stream.readInt32(exception); unread_count = stream.readInt32(exception); - unread_important_count = stream.readInt32(exception); int magic = stream.readInt32(exception); if (magic != 0x1cb5c415) { if (exception) { @@ -11891,10 +12326,9 @@ public class TLRPC { stream.writeInt32(timeout); } stream.writeInt32(top_message); - stream.writeInt32(top_important_message); stream.writeInt32(read_inbox_max_id); + stream.writeInt32(read_outbox_max_id); stream.writeInt32(unread_count); - stream.writeInt32(unread_important_count); stream.writeInt32(0x1cb5c415); int count = messages.size(); stream.writeInt32(count); @@ -11918,7 +12352,6 @@ public class TLRPC { public static class ChannelMessagesFilter extends TLObject { public int flags; - public boolean important_only; public boolean exclude_new_messages; public ArrayList ranges = new ArrayList<>(); @@ -11931,9 +12364,6 @@ public class TLRPC { case 0xcd77d957: result = new TL_channelMessagesFilter(); break; - case 0xfa01232e: - result = new TL_channelMessagesFilterCollapsed(); - break; } if (result == null && exception) { throw new RuntimeException(String.format("can't parse magic %x in ChannelMessagesFilter", constructor)); @@ -11960,7 +12390,6 @@ public class TLRPC { public void readParams(AbstractSerializedData stream, boolean exception) { flags = stream.readInt32(exception); - important_only = (flags & 1) != 0; exclude_new_messages = (flags & 2) != 0; int magic = stream.readInt32(exception); if (magic != 0x1cb5c415) { @@ -11981,7 +12410,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - flags = important_only ? (flags | 1) : (flags &~ 1); flags = exclude_new_messages ? (flags | 2) : (flags &~ 2); stream.writeInt32(flags); stream.writeInt32(0x1cb5c415); @@ -11993,15 +12421,6 @@ public class TLRPC { } } - public static class TL_channelMessagesFilterCollapsed extends ChannelMessagesFilter { - public static int constructor = 0xfa01232e; - - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - } - } - public static class TL_contacts_resolvedPeer extends TLObject { public static int constructor = 0x7f077ad9; @@ -13142,6 +13561,9 @@ public class TLRPC { case 0x7328bdb: result = new TL_chatForbidden(); break; + case 0x8537784f: + result = new TL_channelForbidden(); + break; case 0x678e9587: result = new TL_channel_old(); break; @@ -13161,7 +13583,7 @@ public class TLRPC { result = new TL_geoChat(); break; case 0x2d85832c: - result = new TL_channelForbidden(); + result = new TL_channelForbidden_layer52(); break; case 0xa14dca52: result = new TL_channel(); @@ -13217,6 +13639,30 @@ public class TLRPC { } } + public static class TL_channelForbidden extends Chat { + public static int constructor = 0x8537784f; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + broadcast = (flags & 32) != 0; + megagroup = (flags & 256) != 0; + id = stream.readInt32(exception); + access_hash = stream.readInt64(exception); + title = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = broadcast ? (flags | 32) : (flags &~ 32); + flags = megagroup ? (flags | 256) : (flags &~ 256); + stream.writeInt32(flags); + stream.writeInt32(id); + stream.writeInt64(access_hash); + stream.writeString(title); + } + } + public static class TL_chatForbidden extends Chat { public static int constructor = 0x7328bdb; @@ -13442,7 +13888,7 @@ public class TLRPC { } } - public static class TL_channelForbidden extends Chat { + public static class TL_channelForbidden_layer52 extends Chat { public static int constructor = 0x2d85832c; @@ -13827,6 +14273,9 @@ public class TLRPC { case 0x3751b49e: result = new TL_inputMessagesFilterMusic(); break; + case 0x3a20ecb8: + result = new TL_inputMessagesFilterChatPhotos(); + break; case 0x9fc00e65: result = new TL_inputMessagesFilterVideo(); break; @@ -13880,6 +14329,15 @@ public class TLRPC { } } + public static class TL_inputMessagesFilterChatPhotos extends MessagesFilter { + public static int constructor = 0x3a20ecb8; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + public static class TL_inputMessagesFilterVideo extends MessagesFilter { public static int constructor = 0x9fc00e65; @@ -14680,7 +15138,6 @@ public class TLRPC { public int date; public int seq; public int flags; - public boolean unread; public boolean out; public boolean mentioned; public boolean media_unread; @@ -14796,7 +15253,6 @@ public class TLRPC { public void readParams(AbstractSerializedData stream, boolean exception) { flags = stream.readInt32(exception); - unread = (flags & 1) != 0; out = (flags & 2) != 0; mentioned = (flags & 16) != 0; media_unread = (flags & 32) != 0; @@ -14842,7 +15298,6 @@ public class TLRPC { public void readParams(AbstractSerializedData stream, boolean exception) { flags = stream.readInt32(exception); - unread = (flags & 1) != 0; out = (flags & 2) != 0; id = stream.readInt32(exception); pts = stream.readInt32(exception); @@ -14887,7 +15342,6 @@ public class TLRPC { public void readParams(AbstractSerializedData stream, boolean exception) { flags = stream.readInt32(exception); - unread = (flags & 1) != 0; out = (flags & 2) != 0; mentioned = (flags & 16) != 0; media_unread = (flags & 32) != 0; @@ -15671,6 +16125,153 @@ public class TLRPC { } } + public static class TL_messages_peerDialogs extends TLObject { + public static int constructor = 0x3371c354; + + public ArrayList dialogs = new ArrayList<>(); + public ArrayList messages = new ArrayList<>(); + public ArrayList chats = new ArrayList<>(); + public ArrayList users = new ArrayList<>(); + public TL_updates_state state; + + public static TL_messages_peerDialogs TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_messages_peerDialogs.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_messages_peerDialogs", constructor)); + } else { + return null; + } + } + TL_messages_peerDialogs result = new TL_messages_peerDialogs(); + 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_dialog object = TL_dialog.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + dialogs.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++) { + Message object = Message.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + messages.add(object); + } + magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + Chat object = Chat.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + chats.add(object); + } + magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + User object = User.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + users.add(object); + } + state = TL_updates_state.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(0x1cb5c415); + int count = dialogs.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + dialogs.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = messages.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + messages.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = chats.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + chats.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = users.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + users.get(a).serializeToStream(stream); + } + state.serializeToStream(stream); + } + } + + public static class TL_topPeer extends TLObject { + public static int constructor = 0xedcdc05b; + + public Peer peer; + public double rating; + + public static TL_topPeer TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_topPeer.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_topPeer", constructor)); + } else { + return null; + } + } + TL_topPeer result = new TL_topPeer(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + peer = Peer.TLdeserialize(stream, stream.readInt32(exception), exception); + rating = stream.readDouble(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + peer.serializeToStream(stream); + stream.writeDouble(rating); + } + } + public static class InputPhotoCrop extends TLObject { public double crop_left; public double crop_top; @@ -15724,7 +16325,7 @@ public class TLRPC { } public static class messages_Dialogs extends TLObject { - public ArrayList

    dialogs = new ArrayList<>(); + public ArrayList dialogs = new ArrayList<>(); public ArrayList messages = new ArrayList<>(); public ArrayList chats = new ArrayList<>(); public ArrayList users = new ArrayList<>(); @@ -15764,7 +16365,7 @@ public class TLRPC { } int count = stream.readInt32(exception); for (int a = 0; a < count; a++) { - Dialog object = Dialog.TLdeserialize(stream, stream.readInt32(exception), exception); + TL_dialog object = TL_dialog.TLdeserialize(stream, stream.readInt32(exception), exception); if (object == null) { return; } @@ -15861,7 +16462,7 @@ public class TLRPC { } int count = stream.readInt32(exception); for (int a = 0; a < count; a++) { - Dialog object = Dialog.TLdeserialize(stream, stream.readInt32(exception), exception); + TL_dialog object = TL_dialog.TLdeserialize(stream, stream.readInt32(exception), exception); if (object == null) { return; } @@ -16007,7 +16608,7 @@ public class TLRPC { } public static class TL_auth_sendCode extends TLObject { - public static int constructor = 0xccfd70cf; + public static int constructor = 0x86aef0ec; public int flags; public boolean allow_flashcall; @@ -16015,7 +16616,6 @@ public class TLRPC { public boolean current_number; public int api_id; public String api_hash; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return TL_auth_sentCode.TLdeserialize(stream, constructor, exception); @@ -16031,7 +16631,6 @@ public class TLRPC { } stream.writeInt32(api_id); stream.writeString(api_hash); - stream.writeString(lang_code); } } @@ -16178,16 +16777,11 @@ public class TLRPC { } } - public static class TL_account_registerDevice extends TLObject { - public static int constructor = 0x446c712c; + public static class TL_account_registerDevice extends TLObject { + public static int constructor = 0x637ea878; public int token_type; public String token; - public String device_model; - public String system_version; - public String app_version; - public boolean app_sandbox; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return Bool.TLdeserialize(stream, constructor, exception); @@ -16196,12 +16790,7 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); stream.writeInt32(token_type); - stream.writeString(token); - stream.writeString(device_model); - stream.writeString(system_version); - stream.writeString(app_version); - stream.writeBool(app_sandbox); - stream.writeString(lang_code); + stream.writeString(token); } } @@ -16643,7 +17232,6 @@ public class TLRPC { public static int constructor = 0xd4569248; public int flags; - public boolean important_only; public InputPeer peer; public String q; public MessagesFilter filter; @@ -16659,7 +17247,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - flags = important_only ? (flags | 1) : (flags &~ 1); stream.writeInt32(flags); peer.serializeToStream(stream); stream.writeString(q); @@ -16690,8 +17277,10 @@ public class TLRPC { } public static class TL_messages_deleteHistory extends TLObject { - public static int constructor = 0xb7c13bd9; + public static int constructor = 0x1c015b09; + public int flags; + public boolean just_clear; public InputPeer peer; public int max_id; @@ -16701,6 +17290,8 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); + flags = just_clear ? (flags | 1) : (flags &~ 1); + stream.writeInt32(flags); peer.serializeToStream(stream); stream.writeInt32(max_id); } @@ -16846,9 +17437,9 @@ public class TLRPC { public int flags; public boolean no_webpage; - public boolean broadcast; public boolean silent; public boolean background; + public boolean clear_draft; public InputPeer peer; public int reply_to_msg_id; public String message; @@ -16863,9 +17454,9 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); flags = no_webpage ? (flags | 2) : (flags &~ 2); - flags = broadcast ? (flags | 16) : (flags &~ 16); flags = silent ? (flags | 32) : (flags &~ 32); flags = background ? (flags | 64) : (flags &~ 64); + flags = clear_draft ? (flags | 128) : (flags &~ 128); stream.writeInt32(flags); peer.serializeToStream(stream); if ((flags & 1) != 0) { @@ -16891,9 +17482,9 @@ public class TLRPC { public static int constructor = 0xc8f16791; public int flags; - public boolean broadcast; public boolean silent; public boolean background; + public boolean clear_draft; public InputPeer peer; public int reply_to_msg_id; public InputMedia media; @@ -16906,9 +17497,9 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - flags = broadcast ? (flags | 16) : (flags &~ 16); flags = silent ? (flags | 32) : (flags &~ 32); flags = background ? (flags | 64) : (flags &~ 64); + flags = clear_draft ? (flags | 128) : (flags &~ 128); stream.writeInt32(flags); peer.serializeToStream(stream); if ((flags & 1) != 0) { @@ -16926,7 +17517,6 @@ public class TLRPC { public static int constructor = 0x708e0195; public int flags; - public boolean broadcast; public boolean silent; public boolean background; public InputPeer from_peer; @@ -16940,7 +17530,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - flags = broadcast ? (flags | 16) : (flags &~ 16); flags = silent ? (flags | 32) : (flags &~ 32); flags = background ? (flags | 64) : (flags &~ 64); stream.writeInt32(flags); @@ -17295,12 +17884,8 @@ public class TLRPC { } public static class TL_help_getAppUpdate extends TLObject { - public static int constructor = 0xc812ac7e; + public static int constructor = 0xae2de196; - public String device_model; - public String system_version; - public String app_version; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return help_AppUpdate.TLdeserialize(stream, constructor, exception); @@ -17308,10 +17893,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeString(device_model); - stream.writeString(system_version); - stream.writeString(app_version); - stream.writeString(lang_code); } } @@ -17324,22 +17905,21 @@ public class TLRPC { return Bool.TLdeserialize(stream, constructor, exception); } - public void serializeToStream(AbstractSerializedData stream) { + public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeInt32(0x1cb5c415); + stream.writeInt32(0x1cb5c415); int count = events.size(); stream.writeInt32(count); for (int a = 0; a < count; a++) { - events.get(a).serializeToStream(stream); + events.get(a).serializeToStream(stream); } } } /* EDIT BY ME - public static class TL_help_getInviteText extends TLObject { - public static int constructor = 0xa4a95186; + public static class TL_help_getInviteText extends TLObject { + public static int constructor = 0x4d392343; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return TL_help_inviteText.TLdeserialize(stream, constructor, exception); @@ -17347,7 +17927,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeString(lang_code); } } */ @@ -17950,6 +18529,54 @@ public class TLRPC { } } + public static class TL_contacts_getTopPeers extends TLObject { + public static int constructor = 0xd4982db5; + + public int flags; + public boolean correspondents; + public boolean bots_pm; + public boolean bots_inline; + public boolean groups; + public boolean channels; + public int offset; + public int limit; + public int hash; + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return contacts_TopPeers.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = correspondents ? (flags | 1) : (flags &~ 1); + flags = bots_pm ? (flags | 2) : (flags &~ 2); + flags = bots_inline ? (flags | 4) : (flags &~ 4); + flags = groups ? (flags | 1024) : (flags &~ 1024); + flags = channels ? (flags | 32768) : (flags &~ 32768); + stream.writeInt32(flags); + stream.writeInt32(offset); + stream.writeInt32(limit); + stream.writeInt32(hash); + } + } + + public static class TL_contacts_resetTopPeerRating extends TLObject { + public static int constructor = 0x1ae373ac; + + public TopPeerCategory category; + public InputPeer peer; + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return Bool.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + category.serializeToStream(stream); + peer.serializeToStream(stream); + } + } + public static class TL_account_sendChangePhoneCode extends TLObject { public static int constructor = 0x8e57deb; @@ -18426,9 +19053,9 @@ public class TLRPC { public static int constructor = 0xb16e06fe; public int flags; - public boolean broadcast; public boolean silent; public boolean background; + public boolean clear_draft; public InputPeer peer; public int reply_to_msg_id; public long random_id; @@ -18441,9 +19068,9 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - flags = broadcast ? (flags | 16) : (flags &~ 16); flags = silent ? (flags | 32) : (flags &~ 32); flags = background ? (flags | 64) : (flags &~ 64); + flags = clear_draft ? (flags | 128) : (flags &~ 128); stream.writeInt32(flags); peer.serializeToStream(stream); if ((flags & 1) != 0) { @@ -18552,6 +19179,26 @@ public class TLRPC { } } + public static class TL_messages_getPeerDialogs extends TLObject { + public static int constructor = 0x2d9776b9; + + public ArrayList peers = new ArrayList<>(); + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return TL_messages_peerDialogs.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(0x1cb5c415); + int count = peers.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + peers.get(a).serializeToStream(stream); + } + } + } + public static class TL_messages_editInlineBotMessage extends TLObject { public static int constructor = 0x130c2c85; @@ -18588,13 +19235,56 @@ public class TLRPC { } } - public static class TL_help_getAppChangelog extends TLObject { - public static int constructor = 0x5bab7fb2; + public static class TL_messages_saveDraft extends TLObject { + public static int constructor = 0xbc39e14b; + + public int flags; + public boolean no_webpage; + public int reply_to_msg_id; + public InputPeer peer; + public String message; + public ArrayList entities = new ArrayList<>(); + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return Bool.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = no_webpage ? (flags | 2) : (flags &~ 2); + stream.writeInt32(flags); + if ((flags & 1) != 0) { + stream.writeInt32(reply_to_msg_id); + } + peer.serializeToStream(stream); + stream.writeString(message); + if ((flags & 8) != 0) { + stream.writeInt32(0x1cb5c415); + int count = entities.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + entities.get(a).serializeToStream(stream); + } + } + } + } + + public static class TL_messages_getAllDrafts extends TLObject { + public static int constructor = 0x6a3f8d65; + + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return Updates.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + + public static class TL_help_getAppChangelog extends TLObject { + public static int constructor = 0xb921197a; - public String device_model; - public String system_version; - public String app_version; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return help_AppChangelog.TLdeserialize(stream, constructor, exception); @@ -18602,17 +19292,12 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeString(device_model); - stream.writeString(system_version); - stream.writeString(app_version); - stream.writeString(lang_code); } } public static class TL_help_getTermsOfService extends TLObject { - public static int constructor = 0x37d78f83; + public static int constructor = 0x350170f3; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return TL_help_termsOfService.TLdeserialize(stream, constructor, exception); @@ -18620,7 +19305,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeString(lang_code); } } @@ -18663,50 +19347,6 @@ public class TLRPC { } } - public static class TL_channels_getDialogs extends TLObject { - public static int constructor = 0xa9d3d249; - - public int offset; - public int limit; - - public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { - return messages_Dialogs.TLdeserialize(stream, constructor, exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(offset); - stream.writeInt32(limit); - } - } - - public static class TL_channels_getImportantHistory extends TLObject { - public static int constructor = 0x8f494bb2; - - public InputChannel channel; - public int offset_id; - public int offset_date; - public int add_offset; - public int limit; - public int max_id; - public int min_id; - - public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { - return messages_Messages.TLdeserialize(stream, constructor, exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - channel.serializeToStream(stream); - stream.writeInt32(offset_id); - stream.writeInt32(offset_date); - stream.writeInt32(add_offset); - stream.writeInt32(limit); - stream.writeInt32(max_id); - stream.writeInt32(min_id); - } - } - public static class TL_channels_readHistory extends TLObject { public static int constructor = 0xcc104937; @@ -18975,23 +19615,6 @@ public class TLRPC { } } - public static class TL_channels_toggleComments extends TLObject { - public static int constructor = 0xaaa29e88; - - public InputChannel channel; - public boolean enabled; - - public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { - return Updates.TLdeserialize(stream, constructor, exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - channel.serializeToStream(stream); - stream.writeBool(enabled); - } - } - public static class TL_channels_checkUsername extends TLObject { public static int constructor = 0x10e6bd2c; @@ -20691,89 +21314,67 @@ public class TLRPC { //Message end //TL_dialog start - public static class Dialog extends TLObject { - public Peer peer; - public int top_message; - public int top_not_important_message; - public int read_inbox_max_id; - public int unread_count; - public int unread_not_important_count; - public PeerNotifySettings notify_settings; - public int pts; - public int last_message_date; //custom - public int last_message_date_i; //custom - public long id; //custom - public int last_read; //custom + public static class TL_dialog extends TLObject { + public static int constructor = 0x66ffba14; - public static Dialog TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { - Dialog result = null; - switch(constructor) { - case 0x5b8496b2: - result = new TL_dialogChannel(); - break; - case 0xc1dd804a: - result = new TL_dialog(); - break; - } - if (result == null && exception) { - throw new RuntimeException(String.format("can't parse magic %x in Dialog", constructor)); - } - if (result != null) { - result.readParams(stream, exception); - } - return result; - } - } + public int flags; + public Peer peer; + public int top_message; + public int read_inbox_max_id; + public int read_outbox_max_id; + public int unread_count; + public PeerNotifySettings notify_settings; + public int pts; + public DraftMessage draft; + public int last_message_date; //custom + public long id; //custom - public static class TL_dialogChannel extends Dialog { - public static int constructor = 0x5b8496b2; + public static TL_dialog TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_dialog.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_dialog", constructor)); + } else { + return null; + } + } + TL_dialog result = new TL_dialog(); + result.readParams(stream, exception); + return result; + } + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + peer = Peer.TLdeserialize(stream, stream.readInt32(exception), exception); + top_message = stream.readInt32(exception); + read_inbox_max_id = stream.readInt32(exception); + read_outbox_max_id = stream.readInt32(exception); + unread_count = stream.readInt32(exception); + notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); + if ((flags & 1) != 0) { + pts = stream.readInt32(exception); + } + if ((flags & 2) != 0) { + draft = DraftMessage.TLdeserialize(stream, stream.readInt32(exception), exception); + } + } - public void readParams(AbstractSerializedData stream, boolean exception) { - peer = Peer.TLdeserialize(stream, stream.readInt32(exception), exception); - top_not_important_message = stream.readInt32(exception); - top_message = stream.readInt32(exception); - read_inbox_max_id = stream.readInt32(exception); - unread_not_important_count = stream.readInt32(exception); - unread_count = stream.readInt32(exception); - notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); - pts = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - peer.serializeToStream(stream); - stream.writeInt32(top_not_important_message); - stream.writeInt32(top_message); - stream.writeInt32(read_inbox_max_id); - stream.writeInt32(unread_not_important_count); - stream.writeInt32(unread_count); - notify_settings.serializeToStream(stream); - stream.writeInt32(pts); - } - } - - public static class TL_dialog extends Dialog { - public static int constructor = 0xc1dd804a; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - peer = Peer.TLdeserialize(stream, stream.readInt32(exception), exception); - top_message = stream.readInt32(exception); - read_inbox_max_id = stream.readInt32(exception); - unread_count = stream.readInt32(exception); - notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - peer.serializeToStream(stream); - stream.writeInt32(top_message); - stream.writeInt32(read_inbox_max_id); - stream.writeInt32(unread_count); - notify_settings.serializeToStream(stream); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + peer.serializeToStream(stream); + stream.writeInt32(top_message); + stream.writeInt32(read_inbox_max_id); + stream.writeInt32(read_outbox_max_id); + stream.writeInt32(unread_count); + notify_settings.serializeToStream(stream); + if ((flags & 1) != 0) { + stream.writeInt32(pts); + } + if ((flags & 2) != 0) { + draft.serializeToStream(stream); + } + } + } //TL_dialog end //ChatParticipant start diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java index 692259feb..8d420f95a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java @@ -8,6 +8,9 @@ package org.telegram.ui.ActionBar; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Configuration; import android.graphics.drawable.Drawable; @@ -19,9 +22,7 @@ import android.widget.FrameLayout; import android.widget.ImageView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.ApplicationLoader; import org.telegram.ui.Components.LayoutHelper; @@ -50,6 +51,7 @@ public class ActionBar extends FrameLayout { private boolean addToContainer = true; private boolean interceptTouches = true; private int extraHeight; + private AnimatorSet actionModeAnimation; private boolean allowOverlayTitle; private CharSequence lastTitle; @@ -221,26 +223,30 @@ public class ActionBar extends FrameLayout { return; } actionModeVisible = true; - if (Build.VERSION.SDK_INT >= 14) { - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(actionMode, "alpha", 0.0f, 1.0f)); - if (occupyStatusBar && actionModeTop != null) { - animators.add(ObjectAnimatorProxy.ofFloat(actionModeTop, "alpha", 0.0f, 1.0f)); - } - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether(animators); - animatorSetProxy.setDuration(200); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationStart(Object animation) { - actionMode.setVisibility(VISIBLE); - if (occupyStatusBar && actionModeTop != null) { - actionModeTop.setVisibility(VISIBLE); - } + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(actionMode, "alpha", 0.0f, 1.0f)); + if (occupyStatusBar && actionModeTop != null) { + animators.add(ObjectAnimator.ofFloat(actionModeTop, "alpha", 0.0f, 1.0f)); + } + if (actionModeAnimation != null) { + actionModeAnimation.cancel(); + } + actionModeAnimation = new AnimatorSet(); + actionModeAnimation.playTogether(animators); + actionModeAnimation.setDuration(200); + actionModeAnimation.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationStart(Animator animation) { + actionMode.setVisibility(VISIBLE); + if (occupyStatusBar && actionModeTop != null) { + actionModeTop.setVisibility(VISIBLE); } + } - @Override - public void onAnimationEnd(Object animation) { + @Override + public void onAnimationEnd(Animator animation) { + if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { + actionModeAnimation = null; if (titleTextView != null) { titleTextView.setVisibility(INVISIBLE); } @@ -251,29 +257,22 @@ public class ActionBar extends FrameLayout { menu.setVisibility(INVISIBLE); } } - }); - animatorSetProxy.start(); - } else { - actionMode.setVisibility(VISIBLE); - if (occupyStatusBar && actionModeTop != null) { - actionModeTop.setVisibility(VISIBLE); } - if (titleTextView != null) { - titleTextView.setVisibility(INVISIBLE); + + @Override + public void onAnimationCancel(Animator animation) { + if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { + actionModeAnimation = null; + } } - if (subtitleTextView != null) { - subtitleTextView.setVisibility(INVISIBLE); - } - if (menu != null) { - menu.setVisibility(INVISIBLE); - } - } + }); + actionModeAnimation.start(); if (backButtonImageView != null) { Drawable drawable = backButtonImageView.getDrawable(); if (drawable instanceof BackDrawable) { ((BackDrawable) drawable).setRotation(1, true); } - backButtonImageView.setBackgroundDrawable(Theme.createBarSelectorDrawable(itemsBackgroundColor)); + backButtonImageView.setBackgroundDrawable(Theme.createBarSelectorDrawable(Theme.ACTION_BAR_MODE_SELECTOR_COLOR)); } } @@ -282,31 +281,37 @@ public class ActionBar extends FrameLayout { return; } actionModeVisible = false; - if (Build.VERSION.SDK_INT >= 14) { - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(actionMode, "alpha", 0.0f)); - if (occupyStatusBar && actionModeTop != null) { - animators.add(ObjectAnimatorProxy.ofFloat(actionModeTop, "alpha", 0.0f)); - } - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether(animators); - animatorSetProxy.setDuration(200); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(actionMode, "alpha", 0.0f)); + if (occupyStatusBar && actionModeTop != null) { + animators.add(ObjectAnimator.ofFloat(actionModeTop, "alpha", 0.0f)); + } + if (actionModeAnimation != null) { + actionModeAnimation.cancel(); + } + actionModeAnimation = new AnimatorSet(); + actionModeAnimation.playTogether(animators); + actionModeAnimation.setDuration(200); + actionModeAnimation.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { + actionModeAnimation = null; actionMode.setVisibility(INVISIBLE); if (occupyStatusBar && actionModeTop != null) { actionModeTop.setVisibility(INVISIBLE); } } - }); - animatorSetProxy.start(); - } else { - actionMode.setVisibility(INVISIBLE); - if (occupyStatusBar && actionModeTop != null) { - actionModeTop.setVisibility(INVISIBLE); } - } + + @Override + public void onAnimationCancel(Animator animation) { + if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { + actionModeAnimation = null; + } + } + }); + actionModeAnimation.start(); if (titleTextView != null) { titleTextView.setVisibility(VISIBLE); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java index 057635c5b..ce1242084 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java @@ -8,6 +8,9 @@ package org.telegram.ui.ActionBar; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -29,13 +32,8 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.BuildVars; -import org.telegram.messenger.FileLog; import org.telegram.messenger.R; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.ui.Components.LayoutHelper; import java.util.ArrayList; @@ -101,9 +99,6 @@ public class ActionBarLayout extends FrameLayout { getWindowVisibleDisplayFrame(rect); int usableViewHeight = rootView.getHeight() - (rect.top != 0 ? AndroidUtilities.statusBarHeight : 0) - AndroidUtilities.getViewInset(rootView); isKeyboardVisible = usableViewHeight - (rect.bottom - rect.top) > 0; - if (BuildVars.DEBUG_VERSION) { - FileLog.e("tmessages", "keyboard visible = " + isKeyboardVisible + " for " + this); - } if (waitingForKeyboardCloseRunnable != null && !containerView.isKeyboardVisible && !containerViewBack.isKeyboardVisible) { AndroidUtilities.cancelRunOnUIThread(waitingForKeyboardCloseRunnable); waitingForKeyboardCloseRunnable.run(); @@ -117,13 +112,14 @@ public class ActionBarLayout extends FrameLayout { private static Paint scrimPaint; private Runnable waitingForKeyboardCloseRunnable; + private Runnable delayedOpenAnimationRunnable; private LinearLayoutContainer containerView; private LinearLayoutContainer containerViewBack; private DrawerLayoutContainer drawerLayoutContainer; private ActionBar currentActionBar; - private AnimatorSetProxy currentAnimation; + private AnimatorSet currentAnimation; private DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator(1.5f); private AccelerateDecelerateInterpolator accelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator(); @@ -339,9 +335,8 @@ public class ActionBarLayout extends FrameLayout { containerViewBack.setVisibility(View.GONE); startedTracking = false; animationInProgress = false; - - ViewProxy.setTranslationX(containerView, 0); - ViewProxy.setTranslationX(containerViewBack, 0); + containerView.setTranslationX(0); + containerViewBack.setTranslationX(0); setInnerTranslationX(0); } @@ -421,7 +416,7 @@ public class ActionBarLayout extends FrameLayout { currentFragment.onBeginSlide(); beginTrackingSent = true; } - ViewProxy.setTranslationX(containerView, dx); + containerView.setTranslationX(dx); setInnerTranslationX(dx); } } else if (ev != null && ev.getPointerId(0) == startedTrackingPointerId && (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_POINTER_UP)) { @@ -443,8 +438,8 @@ public class ActionBarLayout extends FrameLayout { } } if (startedTracking) { - float x = ViewProxy.getX(containerView); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + float x = containerView.getX(); + AnimatorSet animatorSet = new AnimatorSet(); float velX = velocityTracker.getXVelocity(); float velY = velocityTracker.getYVelocity(); final boolean backAnimation = x < containerView.getMeasuredWidth() / 3.0f && (velX < 3500 || velX < velY); @@ -452,21 +447,21 @@ public class ActionBarLayout extends FrameLayout { if (!backAnimation) { distToMove = containerView.getMeasuredWidth() - x; animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "translationX", containerView.getMeasuredWidth()), - ObjectAnimatorProxy.ofFloat(this, "innerTranslationX", (float) containerView.getMeasuredWidth()) + ObjectAnimator.ofFloat(containerView, "translationX", containerView.getMeasuredWidth()), + ObjectAnimator.ofFloat(this, "innerTranslationX", (float) containerView.getMeasuredWidth()) ); } else { distToMove = x; animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "translationX", 0), - ObjectAnimatorProxy.ofFloat(this, "innerTranslationX", 0.0f) + ObjectAnimator.ofFloat(containerView, "translationX", 0), + ObjectAnimator.ofFloat(this, "innerTranslationX", 0.0f) ); } animatorSet.setDuration(Math.max((int) (200.0f / containerView.getMeasuredWidth() * distToMove), 50)); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animator) { + public void onAnimationEnd(Animator animator) { onSlideAnimationEnd(backAnimation); } }); @@ -533,13 +528,13 @@ public class ActionBarLayout extends FrameLayout { AndroidUtilities.cancelRunOnUIThread(animationRunnable); animationRunnable = null; } - ViewProxy.setAlpha(this, 1.0f); - ViewProxy.setAlpha(containerView, 1.0f); - ViewProxy.setScaleX(containerView, 1.0f); - ViewProxy.setScaleY(containerView, 1.0f); - ViewProxy.setAlpha(containerViewBack, 1.0f); - ViewProxy.setScaleX(containerViewBack, 1.0f); - ViewProxy.setScaleY(containerViewBack, 1.0f); + setAlpha(1.0f); + containerView.setAlpha(1.0f); + containerView.setScaleX(1.0f); + containerView.setScaleY(1.0f); + containerViewBack.setAlpha(1.0f); + containerViewBack.setScaleX(1.0f); + containerViewBack.setScaleY(1.0f); } public boolean checkTransitionAnimation() { @@ -614,11 +609,11 @@ public class ActionBarLayout extends FrameLayout { } float interpolated = decelerateInterpolator.getInterpolation(animationProgress); if (open) { - ViewProxy.setAlpha(containerView, interpolated); - ViewProxy.setTranslationX(containerView, AndroidUtilities.dp(48) * (1.0f - interpolated)); + containerView.setAlpha(interpolated); + containerView.setTranslationX(AndroidUtilities.dp(48) * (1.0f - interpolated)); } else { - ViewProxy.setAlpha(containerViewBack, 1.0f - interpolated); - ViewProxy.setTranslationX(containerViewBack, AndroidUtilities.dp(48) * interpolated); + containerViewBack.setAlpha(1.0f - interpolated); + containerViewBack.setTranslationX(AndroidUtilities.dp(48) * interpolated); } if (animationProgress < 1) { startLayoutAnimation(open, false); @@ -629,6 +624,15 @@ public class ActionBarLayout extends FrameLayout { }); } + public void resumeDelayedFragmentAnimation() { + if (delayedOpenAnimationRunnable == null) { + return; + } + AndroidUtilities.cancelRunOnUIThread(delayedOpenAnimationRunnable); + delayedOpenAnimationRunnable.run(); + delayedOpenAnimationRunnable = null; + } + public boolean presentFragment(final BaseFragment fragment, final boolean removeLast, boolean forceWithoutAnimation, boolean check) { if (checkTransitionAnimation() || delegate != null && check && !delegate.needPresentFragment(fragment, removeLast, forceWithoutAnimation, this) || !fragment.onFragmentCreate()) { return false; @@ -636,7 +640,7 @@ public class ActionBarLayout extends FrameLayout { if (parentActivity.getCurrentFocus() != null) { AndroidUtilities.hideKeyboard(parentActivity.getCurrentFocus()); } - boolean needAnimation = Build.VERSION.SDK_INT > 10 && !forceWithoutAnimation && parentActivity.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).getBoolean("view_animations", true); + boolean needAnimation = !forceWithoutAnimation && parentActivity.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).getBoolean("view_animations", true); final BaseFragment currentFragment = !fragmentsStack.isEmpty() ? fragmentsStack.get(fragmentsStack.size() - 1) : null; @@ -701,21 +705,21 @@ public class ActionBarLayout extends FrameLayout { fragment.onBecomeFullyVisible(); } }; - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(this, "alpha", 0.0f, 1.0f)); + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(this, "alpha", 0.0f, 1.0f)); if (backgroundView != null) { backgroundView.setVisibility(VISIBLE); - animators.add(ObjectAnimatorProxy.ofFloat(backgroundView, "alpha", 0.0f, 1.0f)); + animators.add(ObjectAnimator.ofFloat(backgroundView, "alpha", 0.0f, 1.0f)); } fragment.onTransitionAnimationStart(true, false); - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.playTogether(animators); currentAnimation.setInterpolator(accelerateDecelerateInterpolator); currentAnimation.setDuration(200); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { onAnimationEndCheck(false); } }); @@ -733,25 +737,23 @@ public class ActionBarLayout extends FrameLayout { presentFragmentInternalRemoveOld(removeLast, currentFragment); fragment.onTransitionAnimationEnd(true, false); fragment.onBecomeFullyVisible(); - ViewProxy.setTranslationX(containerView, 0); + containerView.setTranslationX(0); } }; - FileLog.e("tmessages", "onOpenAnimationsStart"); fragment.onTransitionAnimationStart(true, false); - AnimatorSetProxy animation = fragment.onCustomTransitionAnimation(true, new Runnable() { + AnimatorSet animation = fragment.onCustomTransitionAnimation(true, new Runnable() { @Override public void run() { onAnimationEndCheck(false); } }); if (animation == null) { - ViewProxy.setAlpha(containerView, 0.0f); - ViewProxy.setTranslationX(containerView, 48.0f); + containerView.setAlpha(0.0f); + containerView.setTranslationX(48.0f); if (containerView.isKeyboardVisible || containerViewBack.isKeyboardVisible) { waitingForKeyboardCloseRunnable = new Runnable() { @Override public void run() { - FileLog.e("tmessages", "start delayed by keyboard open animation"); if (waitingForKeyboardCloseRunnable != this) { return; } @@ -759,6 +761,18 @@ public class ActionBarLayout extends FrameLayout { } }; AndroidUtilities.runOnUIThread(waitingForKeyboardCloseRunnable, 200); + } else if (fragment.needDelayOpenAnimation()) { + delayedOpenAnimationRunnable = new Runnable() { + @Override + public void run() { + if (delayedOpenAnimationRunnable != this) { + return; + } + delayedOpenAnimationRunnable = null; + startLayoutAnimation(true, true); + } + }; + AndroidUtilities.runOnUIThread(delayedOpenAnimationRunnable, 200); } else { startLayoutAnimation(true, true); } @@ -767,14 +781,14 @@ public class ActionBarLayout extends FrameLayout { //containerView.setLayerType(LAYER_TYPE_HARDWARE, null); //containerViewBack.setLayerType(LAYER_TYPE_HARDWARE, null); } - ViewProxy.setAlpha(containerView, 1.0f); - ViewProxy.setTranslationX(containerView, 0.0f); + containerView.setAlpha(1.0f); + containerView.setTranslationX(0.0f); currentAnimation = animation; } } } else { if (backgroundView != null) { - ViewProxy.setAlpha(backgroundView, 1.0f); + backgroundView.setAlpha(1.0f); backgroundView.setVisibility(VISIBLE); } fragment.onTransitionAnimationStart(true, false); @@ -834,7 +848,7 @@ public class ActionBarLayout extends FrameLayout { AndroidUtilities.hideKeyboard(parentActivity.getCurrentFocus()); } setInnerTranslationX(0); - boolean needAnimation = Build.VERSION.SDK_INT > 10 && animated && parentActivity.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).getBoolean("view_animations", true); + boolean needAnimation = animated && parentActivity.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).getBoolean("view_animations", true); final BaseFragment currentFragment = fragmentsStack.get(fragmentsStack.size() - 1); BaseFragment previousFragment = null; if (fragmentsStack.size() > 1) { @@ -873,7 +887,6 @@ public class ActionBarLayout extends FrameLayout { layoutParams.width = LayoutHelper.MATCH_PARENT; layoutParams.height = LayoutHelper.MATCH_PARENT; fragmentView.setLayoutParams(layoutParams); - FileLog.e("tmessages", "onCloseAnimationStart"); previousFragment.onTransitionAnimationStart(true, true); currentFragment.onTransitionAnimationStart(false, false); previousFragment.onResume(); @@ -898,13 +911,13 @@ public class ActionBarLayout extends FrameLayout { containerViewBack.setLayerType(LAYER_TYPE_NONE, null); } closeLastFragmentInternalRemoveOld(currentFragment); - ViewProxy.setTranslationX(containerViewBack, 0); + containerViewBack.setTranslationX(0); currentFragment.onTransitionAnimationEnd(false, false); previousFragmentFinal.onTransitionAnimationEnd(true, true); previousFragmentFinal.onBecomeFullyVisible(); } }; - AnimatorSetProxy animation = currentFragment.onCustomTransitionAnimation(false, new Runnable() { + AnimatorSet animation = currentFragment.onCustomTransitionAnimation(false, new Runnable() { @Override public void run() { onAnimationEndCheck(false); @@ -918,7 +931,6 @@ public class ActionBarLayout extends FrameLayout { if (waitingForKeyboardCloseRunnable != this) { return; } - FileLog.e("tmessages", "start delayed by keyboard close animation"); startLayoutAnimation(false, true); } }; @@ -957,28 +969,27 @@ public class ActionBarLayout extends FrameLayout { } }; - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(this, "alpha", 1.0f, 0.0f)); + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(this, "alpha", 1.0f, 0.0f)); if (backgroundView != null) { - animators.add(ObjectAnimatorProxy.ofFloat(backgroundView, "alpha", 1.0f, 0.0f)); + animators.add(ObjectAnimator.ofFloat(backgroundView, "alpha", 1.0f, 0.0f)); } - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.playTogether(animators); currentAnimation.setInterpolator(accelerateDecelerateInterpolator); currentAnimation.setDuration(200); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationStart(Object animation) { + public void onAnimationStart(Animator animation) { transitionAnimationStartTime = System.currentTimeMillis(); } @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { onAnimationEndCheck(false); } }); - FileLog.e("tmessages", "onCloseAnimationsStart"); currentAnimation.start(); } else { removeFragmentFromStackInternal(currentFragment); @@ -1104,13 +1115,11 @@ public class ActionBarLayout extends FrameLayout { if (post) { new Handler().post(new Runnable() { public void run() { - FileLog.e("tmessages", "onCloseAnimationEnd"); onCloseAnimationEndRunnable.run(); onCloseAnimationEndRunnable = null; } }); } else { - FileLog.e("tmessages", "onCloseAnimationEnd"); onCloseAnimationEndRunnable.run(); onCloseAnimationEndRunnable = null; } @@ -1124,13 +1133,11 @@ public class ActionBarLayout extends FrameLayout { if (post) { new Handler().post(new Runnable() { public void run() { - FileLog.e("tmessages", "onOpenAnimationEnd"); onOpenAnimationEndRunnable.run(); onOpenAnimationEndRunnable = null; } }); } else { - FileLog.e("tmessages", "onOpenAnimationEnd"); onOpenAnimationEndRunnable.run(); onOpenAnimationEndRunnable = null; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenu.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenu.java index b0fa9ab09..0ca40d4e1 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenu.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenu.java @@ -97,7 +97,8 @@ public class ActionBarMenu extends LinearLayout { } public void hideAllPopupMenus() { - for (int a = 0; a < getChildCount(); a++) { + int count = getChildCount(); + for (int a = 0; a < count; a++) { View view = getChildAt(a); if (view instanceof ActionBarMenuItem) { ((ActionBarMenuItem) view).closeSubMenu(); @@ -116,7 +117,8 @@ public class ActionBarMenu extends LinearLayout { } public void onMenuButtonPressed() { - for (int a = 0; a < getChildCount(); a++) { + int count = getChildCount(); + for (int a = 0; a < count; a++) { View view = getChildAt(a); if (view instanceof ActionBarMenuItem) { ActionBarMenuItem item = (ActionBarMenuItem) view; @@ -135,7 +137,8 @@ public class ActionBarMenu extends LinearLayout { } public void closeSearchField() { - for (int a = 0; a < getChildCount(); a++) { + int count = getChildCount(); + for (int a = 0; a < count; a++) { View view = getChildAt(a); if (view instanceof ActionBarMenuItem) { ActionBarMenuItem item = (ActionBarMenuItem) view; @@ -148,7 +151,8 @@ public class ActionBarMenu extends LinearLayout { } public void openSearchField(boolean toggle, String text) { - for (int a = 0; a < getChildCount(); a++) { + int count = getChildCount(); + for (int a = 0; a < count; a++) { View view = getChildAt(a); if (view instanceof ActionBarMenuItem) { ActionBarMenuItem item = (ActionBarMenuItem) view; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java index 350d9a89e..0706cfaa0 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java @@ -15,7 +15,6 @@ import android.text.Editable; import android.text.TextWatcher; import android.util.TypedValue; import android.view.ActionMode; -import android.view.ContextMenu; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; @@ -33,13 +32,11 @@ import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; import org.telegram.messenger.R; -import org.telegram.messenger.AnimationCompat.ViewProxy; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import java.lang.reflect.Field; -public class ActionBarMenuItem extends FrameLayoutFixed { +public class ActionBarMenuItem extends FrameLayout { public static class ActionBarMenuItemSearchListener { public void onSearchExpand() { @@ -230,7 +227,7 @@ public class ActionBarMenuItem extends FrameLayoutFixed { textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); } textView.setPadding(AndroidUtilities.dp(16), 0, AndroidUtilities.dp(16), 0); - textView.setTextSize(18); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); textView.setMinWidth(AndroidUtilities.dp(196)); textView.setTag(id); textView.setText(text); @@ -336,6 +333,7 @@ public class ActionBarMenuItem extends FrameLayoutFixed { if (searchContainer.getVisibility() == VISIBLE) { if (listener == null || listener != null && listener.canCollapseSearch()) { searchContainer.setVisibility(GONE); + searchField.clearFocus(); setVisibility(VISIBLE); AndroidUtilities.hideKeyboard(searchField); if (listener != null) { @@ -368,12 +366,12 @@ public class ActionBarMenuItem extends FrameLayoutFixed { iconView.setImageResource(resId); } - public EditText getSearchField() { - return searchField; + public ImageView getImageView() { + return iconView; } - public ActionBarMenuItem setIsSearchField(boolean value) { - return setIsSearchField(value, true); + public EditText getSearchField() { + return searchField; } public ActionBarMenuItem setOverrideMenuClick(boolean value) { @@ -381,7 +379,7 @@ public class ActionBarMenuItem extends FrameLayoutFixed { return this; } - public ActionBarMenuItem setIsSearchField(boolean value, boolean needClearButton) { + public ActionBarMenuItem setIsSearchField(boolean value) { if (parentMenu == null) { return this; } @@ -405,31 +403,23 @@ public class ActionBarMenuItem extends FrameLayoutFixed { searchField.setPadding(0, 0, 0, 0); int inputType = searchField.getInputType() | EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS; searchField.setInputType(inputType); - if (android.os.Build.VERSION.SDK_INT < 11) { - searchField.setOnCreateContextMenuListener(new OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - menu.clear(); - } - }); - } else { - searchField.setCustomSelectionActionModeCallback(new ActionMode.Callback() { - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } + searchField.setCustomSelectionActionModeCallback(new ActionMode.Callback() { + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } - public void onDestroyActionMode(ActionMode mode) { + public void onDestroyActionMode(ActionMode mode) { - } + } - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return false; - } + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - }); - } + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + }); searchField.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { @@ -454,7 +444,7 @@ public class ActionBarMenuItem extends FrameLayoutFixed { listener.onTextChanged(searchField); } if (clearButton != null) { - ViewProxy.setAlpha(clearButton, s == null || s.length() == 0 ? 0.6f : 1.0f); + clearButton.setAlpha(s == null || s.length() == 0 ? 0.6f : 1.0f); } } @@ -471,38 +461,33 @@ public class ActionBarMenuItem extends FrameLayoutFixed { } catch (Exception e) { //nothing to do } - if (Build.VERSION.SDK_INT >= 11) { - searchField.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_ACTION_SEARCH); - searchField.setTextIsSelectable(false); - } else { - searchField.setImeOptions(EditorInfo.IME_ACTION_SEARCH); - } + searchField.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_ACTION_SEARCH); + searchField.setTextIsSelectable(false); searchContainer.addView(searchField); FrameLayout.LayoutParams layoutParams2 = (FrameLayout.LayoutParams) searchField.getLayoutParams(); layoutParams2.width = LayoutHelper.MATCH_PARENT; layoutParams2.gravity = Gravity.CENTER_VERTICAL; layoutParams2.height = AndroidUtilities.dp(36); - layoutParams2.rightMargin = needClearButton ? AndroidUtilities.dp(48) : 0; + layoutParams2.rightMargin = AndroidUtilities.dp(48); searchField.setLayoutParams(layoutParams2); - if (needClearButton) { - clearButton = new ImageView(getContext()); - clearButton.setImageResource(R.drawable.ic_close_white); - clearButton.setScaleType(ImageView.ScaleType.CENTER); - clearButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - searchField.setText(""); - AndroidUtilities.showKeyboard(searchField); - } - }); - searchContainer.addView(clearButton); - layoutParams2 = (FrameLayout.LayoutParams) clearButton.getLayoutParams(); - layoutParams2.width = AndroidUtilities.dp(48); - layoutParams2.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT; - layoutParams2.height = LayoutHelper.MATCH_PARENT; - clearButton.setLayoutParams(layoutParams2); - } + clearButton = new ImageView(getContext()); + clearButton.setImageResource(R.drawable.ic_close_white); + clearButton.setScaleType(ImageView.ScaleType.CENTER); + clearButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + searchField.setText(""); + searchField.requestFocus(); + AndroidUtilities.showKeyboard(searchField); + } + }); + searchContainer.addView(clearButton); + layoutParams2 = (FrameLayout.LayoutParams) clearButton.getLayoutParams(); + layoutParams2.width = AndroidUtilities.dp(48); + layoutParams2.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT; + layoutParams2.height = LayoutHelper.MATCH_PARENT; + clearButton.setLayoutParams(layoutParams2); } isSearchField = value; return this; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java index 126612e3d..200187b41 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java @@ -8,6 +8,7 @@ package org.telegram.ui.ActionBar; +import android.animation.AnimatorSet; import android.app.Activity; import android.app.Dialog; import android.content.Context; @@ -17,7 +18,6 @@ import android.os.Bundle; import android.view.View; import android.view.ViewGroup; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; import org.telegram.messenger.FileLog; import org.telegram.tgnet.ConnectionsManager; @@ -158,6 +158,10 @@ public class BaseFragment { } } + public boolean needDelayOpenAnimation() { + return false; + } + public void onResume() { } @@ -255,7 +259,7 @@ public class BaseFragment { } - protected AnimatorSetProxy onCustomTransitionAnimation(boolean isOpen, final Runnable callback) { + protected AnimatorSet onCustomTransitionAnimation(boolean isOpen, final Runnable callback) { return null; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java index 98eef547f..e92a95d0c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java @@ -9,26 +9,28 @@ package org.telegram.ui.ActionBar; import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; -import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; +import android.support.v4.view.NestedScrollingParent; +import android.support.v4.view.NestedScrollingParentHelper; +import android.support.v4.view.ViewCompat; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; +import android.view.VelocityTracker; import android.view.View; -import android.view.ViewAnimationUtils; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.Window; import android.view.WindowInsets; @@ -37,14 +39,10 @@ import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.LocaleController; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; @@ -54,15 +52,16 @@ import java.util.ArrayList; public class BottomSheet extends Dialog { - private LinearLayout containerView; - private FrameLayout container; + protected ViewGroup containerView; + private ContainerView container; private WindowInsets lastInsets; + private Runnable startAnimationRunnable; + private int layoutCount; + private boolean dismissed; private int tag; - private boolean disableBackground; - private DialogInterface.OnClickListener onClickListener; private CharSequence[] items; @@ -70,21 +69,21 @@ public class BottomSheet extends Dialog { private View customView; private CharSequence title; private boolean fullWidth; - private boolean isGrid; - private ColorDrawable backgroundDrawable = new ColorDrawable(0xff000000); + protected ColorDrawable backDrawable = new ColorDrawable(0xff000000); + + private boolean allowCustomAnimation = true; + + private int touchSlop; + private boolean useFastDismiss; private boolean focusable; - private Paint ciclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + protected Paint ciclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Drawable shadowDrawable; protected static int backgroundPaddingTop; protected static int backgroundPaddingLeft; - private boolean useRevealAnimation; - private float revealRadius; - private int revealX; - private int revealY; private boolean applyTopPadding = true; private boolean applyBottomPadding = true; @@ -95,18 +94,319 @@ public class BottomSheet extends Dialog { private BottomSheetDelegateInterface delegate; + protected AnimatorSet currentSheetAnimation; + + private class ContainerView extends FrameLayout implements NestedScrollingParent { + + private VelocityTracker velocityTracker = null; + private int startedTrackingX; + private int startedTrackingY; + private int startedTrackingPointerId; + private boolean maybeStartTracking = false; + private boolean startedTracking = false; + private AnimatorSet currentAnimation = null; + private NestedScrollingParentHelper nestedScrollingParentHelper; + + public ContainerView(Context context) { + super(context); + nestedScrollingParentHelper = new NestedScrollingParentHelper(this); + } + + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return !dismissed && nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL && !canDismissWithSwipe(); + } + + @Override + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { + nestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); + if (dismissed) { + return; + } + cancelCurrentAnimation(); + } + + @Override + public void onStopNestedScroll(View target) { + nestedScrollingParentHelper.onStopNestedScroll(target); + if (dismissed) { + return; + } + float currentTranslation = containerView.getTranslationY(); + checkDismiss(0, 0); + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { + if (dismissed) { + return; + } + cancelCurrentAnimation(); + if (dyUnconsumed != 0) { + float currentTranslation = containerView.getTranslationY(); + currentTranslation -= dyUnconsumed; + if (currentTranslation < 0) { + currentTranslation = 0; + } + containerView.setTranslationY(currentTranslation); + } + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + if (dismissed) { + return; + } + cancelCurrentAnimation(); + float currentTranslation = containerView.getTranslationY(); + if (currentTranslation > 0 && dy > 0) { + currentTranslation -= dy; + consumed[1] = dy; + if (currentTranslation < 0) { + currentTranslation = 0; + consumed[1] += currentTranslation; + } + containerView.setTranslationY(currentTranslation); + } + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + return false; + } + + @Override + public boolean onNestedPreFling(View target, float velocityX, float velocityY) { + return false; + } + + @Override + public int getNestedScrollAxes() { + return nestedScrollingParentHelper.getNestedScrollAxes(); + } + + private void checkDismiss(float velX, float velY) { + float translationY = containerView.getTranslationY(); + boolean backAnimation = translationY < AndroidUtilities.getPixelsInCM(0.8f, false) && (velY < 3500 || Math.abs(velY) < Math.abs(velX)) || velY < 0 && Math.abs(velY) >= 3500; + if (!backAnimation) { + boolean allowOld = allowCustomAnimation; + allowCustomAnimation = false; + useFastDismiss = true; + dismiss(); + allowCustomAnimation = allowOld; + } else { + currentAnimation = new AnimatorSet(); + currentAnimation.playTogether(ObjectAnimator.ofFloat(containerView, "translationY", 0)); + currentAnimation.setDuration((int) (150 * (translationY / AndroidUtilities.getPixelsInCM(0.8f, false)))); + currentAnimation.setInterpolator(new DecelerateInterpolator()); + currentAnimation.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (currentAnimation != null && currentAnimation.equals(animation)) { + currentAnimation = null; + } + } + }); + currentAnimation.start(); + } + } + + private void cancelCurrentAnimation() { + if (currentAnimation != null) { + currentAnimation.cancel(); + currentAnimation = null; + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (dismissed) { + return false; + } + if (ev != null && (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_MOVE) && !startedTracking && !maybeStartTracking) { + startedTrackingX = (int) ev.getX(); + startedTrackingY = (int) ev.getY(); + if (startedTrackingY < containerView.getTop() || startedTrackingX < containerView.getLeft() || startedTrackingX > containerView.getRight()) { + dismiss(); + return true; + } + startedTrackingPointerId = ev.getPointerId(0); + maybeStartTracking = true; + cancelCurrentAnimation(); + if (velocityTracker != null) { + velocityTracker.clear(); + } + } else if (ev != null && ev.getAction() == MotionEvent.ACTION_MOVE && ev.getPointerId(0) == startedTrackingPointerId) { + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain(); + } + float dx = Math.abs((int) (ev.getX() - startedTrackingX)); + float dy = (int) ev.getY() - startedTrackingY; + velocityTracker.addMovement(ev); + if (maybeStartTracking && !startedTracking && (dy > 0 && dy / 3.0f > Math.abs(dx) && Math.abs(dy) >= touchSlop)) { + startedTrackingY = (int) ev.getY(); + maybeStartTracking = false; + startedTracking = true; + requestDisallowInterceptTouchEvent(true); + } else if (startedTracking) { + float translationY = containerView.getTranslationY(); + translationY += dy; + if (translationY < 0) { + translationY = 0; + } + containerView.setTranslationY(translationY); + startedTrackingY = (int) ev.getY(); + } + } else if (ev == null || ev != null && ev.getPointerId(0) == startedTrackingPointerId && (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_POINTER_UP)) { + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain(); + } + velocityTracker.computeCurrentVelocity(1000); + float translationY = containerView.getTranslationY(); + if (startedTracking || translationY != 0) { + checkDismiss(velocityTracker.getXVelocity(), velocityTracker.getYVelocity()); + startedTracking = false; + } else { + maybeStartTracking = false; + startedTracking = false; + } + if (velocityTracker != null) { + velocityTracker.recycle(); + velocityTracker = null; + } + } + return startedTracking; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + if (lastInsets != null && Build.VERSION.SDK_INT >= 21) { + width -= lastInsets.getSystemWindowInsetRight() + lastInsets.getSystemWindowInsetLeft(); + height -= lastInsets.getSystemWindowInsetBottom(); + } + + setMeasuredDimension(width, height); + boolean isPortrait = width < height; + + if (containerView != null) { + if (!fullWidth) { + int widthSpec; + if (AndroidUtilities.isTablet()) { + widthSpec = MeasureSpec.makeMeasureSpec((int) (Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) * 0.8f) + backgroundPaddingLeft * 2, MeasureSpec.EXACTLY); + } else { + widthSpec = MeasureSpec.makeMeasureSpec(isPortrait ? width + backgroundPaddingLeft * 2 : (int) Math.max(width * 0.8f, Math.min(AndroidUtilities.dp(480), width)) + backgroundPaddingLeft * 2, MeasureSpec.EXACTLY); + } + containerView.measure(widthSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); + } else { + containerView.measure(MeasureSpec.makeMeasureSpec(width + backgroundPaddingLeft * 2, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); + } + } + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child.getVisibility() == GONE || child == containerView) { + continue; + } + measureChildWithMargins(child, MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 0, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), 0); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + layoutCount--; + if (containerView != null) { + int t = (bottom - top) - containerView.getMeasuredHeight(); + if (lastInsets != null && Build.VERSION.SDK_INT >= 21) { + left += lastInsets.getSystemWindowInsetLeft(); + right += lastInsets.getSystemWindowInsetLeft(); + } + int l = ((right - left) - containerView.getMeasuredWidth()) / 2; + containerView.layout(l, t, l + containerView.getMeasuredWidth(), t + containerView.getMeasuredHeight()); + } + + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == GONE || child == containerView) { + continue; + } + final FrameLayout.LayoutParams lp = (FrameLayout.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 = (right - left - width) / 2 + lp.leftMargin - lp.rightMargin; + break; + case Gravity.RIGHT: + childLeft = right - width - lp.rightMargin; + break; + case Gravity.LEFT: + default: + childLeft = lp.leftMargin; + } + + switch (verticalGravity) { + case Gravity.TOP: + childTop = lp.topMargin; + break; + case Gravity.CENTER_VERTICAL: + childTop = (bottom - top - height) / 2 + lp.topMargin - lp.bottomMargin; + break; + case Gravity.BOTTOM: + childTop = (bottom - top) - height - lp.bottomMargin; + break; + default: + childTop = lp.topMargin; + } + child.layout(childLeft, childTop, childLeft + width, childTop + height); + } + if (layoutCount == 0 && startAnimationRunnable != null) { + AndroidUtilities.cancelRunOnUIThread(startAnimationRunnable); + startAnimationRunnable.run(); + startAnimationRunnable = null; + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (canDismissWithSwipe()) { + return onTouchEvent(event); + } + return super.onInterceptTouchEvent(event); + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (maybeStartTracking && !startedTracking) { + onTouchEvent(null); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + } + public interface BottomSheetDelegateInterface { void onOpenAnimationStart(); - void onOpenAnimationEnd(); - - void onRevealAnimationStart(boolean open); - - void onRevealAnimationEnd(boolean open); - - void onRevealAnimationProgress(boolean open, float radius, int x, int y); - - View getRevealView(); } public static class BottomSheetDelegate implements BottomSheetDelegateInterface { @@ -119,65 +419,33 @@ public class BottomSheet extends Dialog { public void onOpenAnimationEnd() { } - - @Override - public void onRevealAnimationStart(boolean open) { - - } - - @Override - public void onRevealAnimationEnd(boolean open) { - - } - - @Override - public void onRevealAnimationProgress(boolean open, float radius, int x, int y) { - - } - - @Override - public View getRevealView() { - return null; - } } public static class BottomSheetCell extends FrameLayout { private TextView textView; private ImageView imageView; - private boolean isGrid; public BottomSheetCell(Context context, int type) { super(context); - isGrid = type == 1; setBackgroundResource(R.drawable.list_selector); - if (type != 1) { - setPadding(AndroidUtilities.dp(16), 0, AndroidUtilities.dp(16), 0); - } + setPadding(AndroidUtilities.dp(16), 0, AndroidUtilities.dp(16), 0); imageView = new ImageView(context); imageView.setScaleType(ImageView.ScaleType.CENTER); - if (type == 1) { - addView(imageView, LayoutHelper.createFrame(48, 48, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 0, 8, 0, 0)); - } else { - addView(imageView, LayoutHelper.createFrame(24, 24, Gravity.CENTER_VERTICAL | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT))); - } + addView(imageView, LayoutHelper.createFrame(24, 24, Gravity.CENTER_VERTICAL | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT))); textView = new TextView(context); textView.setLines(1); textView.setSingleLine(true); textView.setGravity(Gravity.CENTER_HORIZONTAL); textView.setEllipsize(TextUtils.TruncateAt.END); - if (type == 1) { - textView.setTextColor(0xff757575); - textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); - addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 0, 60, 0, 0)); - } else if (type == 0) { + if (type == 0) { textView.setTextColor(0xff212121); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); addView(textView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL)); - } else if (type == 2) { + } else if (type == 1) { textView.setGravity(Gravity.CENTER); textView.setTextColor(0xff212121); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); @@ -188,7 +456,7 @@ public class BottomSheet extends Dialog { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(isGrid ? MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(96), MeasureSpec.EXACTLY) : widthMeasureSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(isGrid ? 80 : 48), MeasureSpec.EXACTLY)); + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(48), MeasureSpec.EXACTLY)); } public void setTextColor(int color) { @@ -204,9 +472,7 @@ public class BottomSheet extends Dialog { if (icon != 0) { imageView.setImageResource(icon); imageView.setVisibility(VISIBLE); - if (!isGrid) { - textView.setPadding(LocaleController.isRTL ? 0 : AndroidUtilities.dp(56), 0, LocaleController.isRTL ? AndroidUtilities.dp(56) : 0, 0); - } + textView.setPadding(LocaleController.isRTL ? 0 : AndroidUtilities.dp(56), 0, LocaleController.isRTL ? AndroidUtilities.dp(56) : 0, 0); } else { imageView.setVisibility(INVISIBLE); textView.setPadding(0, 0, 0, 0); @@ -214,12 +480,19 @@ public class BottomSheet extends Dialog { } } + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + } + public BottomSheet(Context context, boolean needFocus) { super(context, R.style.TransparentDialog); - if (Build.VERSION.SDK_INT >= 21 && !"N".equals(Build.VERSION.CODENAME)) { + if (Build.VERSION.SDK_INT >= 21) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); } + ViewConfiguration vc = ViewConfiguration.get(context); + touchSlop = vc.getScaledTouchSlop(); Rect padding = new Rect(); shadowDrawable = context.getResources().getDrawable(R.drawable.sheet_shadow); @@ -227,124 +500,10 @@ public class BottomSheet extends Dialog { backgroundPaddingLeft = padding.left; backgroundPaddingTop = padding.top; - container = new FrameLayout(getContext()) { - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - if (lastInsets != null && Build.VERSION.SDK_INT >= 21) { - width -= lastInsets.getSystemWindowInsetRight() + lastInsets.getSystemWindowInsetLeft(); - } - - setMeasuredDimension(width, height); - boolean isPortrait = width < height; - - if (containerView != null) { - int left = useRevealAnimation && Build.VERSION.SDK_INT <= 19 ? 0 : backgroundPaddingLeft; - if (!fullWidth) { - int widthSpec; - if (AndroidUtilities.isTablet()) { - widthSpec = MeasureSpec.makeMeasureSpec((int) (Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) * 0.8f) + left * 2, MeasureSpec.EXACTLY); - } else { - widthSpec = MeasureSpec.makeMeasureSpec(isPortrait ? width + left * 2 : (int) Math.max(width * 0.8f, Math.min(AndroidUtilities.dp(480), width)) + left * 2, MeasureSpec.EXACTLY); - } - if (lastInsets != null && Build.VERSION.SDK_INT >= 21 && focusable) { - containerView.getLayoutParams(); - containerView.measure(widthSpec, MeasureSpec.makeMeasureSpec(height - lastInsets.getSystemWindowInsetBottom(), MeasureSpec.AT_MOST)); - } else { - containerView.measure(widthSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - } else { - containerView.measure(MeasureSpec.makeMeasureSpec(width + left * 2, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - } - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - if (child.getVisibility() == GONE || child == containerView) { - continue; - } - measureChildWithMargins(child, MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 0, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), 0); - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (containerView != null) { - int t = (bottom - top) - containerView.getMeasuredHeight(); - if (lastInsets != null && Build.VERSION.SDK_INT >= 21) { - left += lastInsets.getSystemWindowInsetLeft(); - right += lastInsets.getSystemWindowInsetLeft(); - if (focusable) { - t -= lastInsets.getSystemWindowInsetBottom(); - } - } - int l = ((right - left) - containerView.getMeasuredWidth()) / 2; - containerView.layout(l, t, l + containerView.getMeasuredWidth(), t + getMeasuredHeight()); - } - - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - if (child.getVisibility() == GONE || child == containerView) { - continue; - } - final FrameLayout.LayoutParams lp = (FrameLayout.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 = (right - left - width) / 2 + lp.leftMargin - lp.rightMargin; - break; - case Gravity.RIGHT: - childLeft = right - width - lp.rightMargin; - break; - case Gravity.LEFT: - default: - childLeft = lp.leftMargin; - } - - switch (verticalGravity) { - case Gravity.TOP: - childTop = lp.topMargin; - break; - case Gravity.CENTER_VERTICAL: - childTop = (bottom - top - height) / 2 + lp.topMargin - lp.bottomMargin; - break; - case Gravity.BOTTOM: - childTop = (bottom - top) - height - lp.bottomMargin; - break; - default: - childTop = lp.topMargin; - } - child.layout(childLeft, childTop, childLeft + width, childTop + height); - } - } - }; - container.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - dismiss(); - return false; - } - }); - container.setBackgroundDrawable(backgroundDrawable); + container = new ContainerView(getContext()); + container.setBackgroundDrawable(backDrawable); focusable = needFocus; - if (Build.VERSION.SDK_INT >= 21 && !"N".equals(Build.VERSION.CODENAME)) { + if (Build.VERSION.SDK_INT >= 21) { container.setFitsSystemWindows(true); container.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { @SuppressLint("NewApi") @@ -357,6 +516,9 @@ public class BottomSheet extends Dialog { }); container.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } + + ciclePaint.setColor(0xffffffff); + backDrawable.setAlpha(0); } @Override @@ -367,113 +529,66 @@ public class BottomSheet extends Dialog { window.setWindowAnimations(R.style.DialogNoAnimation); setContentView(container, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - ciclePaint.setColor(0xffffffff); - - containerView = new LinearLayout(getContext()) { - - @Override - protected void onDraw(Canvas canvas) { - if (useRevealAnimation && Build.VERSION.SDK_INT <= 19) { - canvas.drawCircle(revealX, revealY, revealRadius, ciclePaint); + if (containerView == null) { + containerView = new FrameLayout(getContext()) { + @Override + public boolean hasOverlappingRendering() { + return false; } - } - - @Override - protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - return super.drawChild(canvas, child, drawingTime); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - FileLog.e("tmessages", "container on layout"); - } - }; + }; + containerView.setBackgroundDrawable(shadowDrawable); + containerView.setPadding(backgroundPaddingLeft, (applyTopPadding ? AndroidUtilities.dp(8) : 0) + backgroundPaddingTop, backgroundPaddingLeft, (applyBottomPadding ? AndroidUtilities.dp(8) : 0)); + } if (Build.VERSION.SDK_INT >= 21) { containerView.setFitsSystemWindows(true); } containerView.setVisibility(View.INVISIBLE); - backgroundDrawable.setAlpha(0); - containerView.setWillNotDraw(false); - containerView.setOrientation(LinearLayout.VERTICAL); container.addView(containerView, 0, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.BOTTOM)); - if (title != null) { - TextView titleView = new TextView(getContext()); - titleView.setLines(1); - titleView.setSingleLine(true); - titleView.setText(title); - titleView.setTextColor(0xff757575); - titleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); - titleView.setEllipsize(TextUtils.TruncateAt.MIDDLE); - titleView.setPadding(AndroidUtilities.dp(16), 0, AndroidUtilities.dp(16), AndroidUtilities.dp(8)); - titleView.setGravity(Gravity.CENTER_VERTICAL); - containerView.addView(titleView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); - titleView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return true; - } - }); - } - if (customView != null) { if (customView.getParent() != null) { ViewGroup viewGroup = (ViewGroup) customView.getParent(); viewGroup.removeView(customView); } - containerView.addView(customView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - } - - if (items != null) { - if (customView != null) { - FrameLayout frameLayout = new FrameLayout(getContext()); - frameLayout.setPadding(0, AndroidUtilities.dp(8), 0, 0); - containerView.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 16)); - - View lineView = new View(getContext()); - lineView.setBackgroundColor(0xffd2d2d2); - frameLayout.addView(lineView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); - } - FrameLayout rowLayout = null; - int lastRowLayoutNum = 0; - for (int a = 0; a < items.length; a++) { - BottomSheetCell cell = new BottomSheetCell(getContext(), isGrid ? 1 : 0); - cell.setTextAndIcon(items[a], itemIcons != null ? itemIcons[a] : 0); - if (isGrid) { - int row = a / 3; - if (rowLayout == null || lastRowLayoutNum != row) { - rowLayout = new FrameLayout(getContext()); - lastRowLayoutNum = row; - containerView.addView(rowLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 80, 0, lastRowLayoutNum != 0 ? 8 : 0, 0, 0)); - rowLayout.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return true; - } - }); - } - int col = a % 3; - int gravity; - if (col == 0) { - gravity = Gravity.LEFT | Gravity.TOP; - } else if (col == 1) { - gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; - } else { - gravity = Gravity.RIGHT | Gravity.TOP; - } - rowLayout.addView(cell, LayoutHelper.createFrame(96, 80, gravity)); - } else { - containerView.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); - } - cell.setTag(a); - cell.setOnClickListener(new View.OnClickListener() { + containerView.addView(customView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP)); + } else { + int topOffset = 0; + if (title != null) { + TextView titleView = new TextView(getContext()); + titleView.setLines(1); + titleView.setSingleLine(true); + titleView.setText(title); + titleView.setTextColor(0xff757575); + titleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + titleView.setEllipsize(TextUtils.TruncateAt.MIDDLE); + titleView.setPadding(AndroidUtilities.dp(16), 0, AndroidUtilities.dp(16), AndroidUtilities.dp(8)); + titleView.setGravity(Gravity.CENTER_VERTICAL); + containerView.addView(titleView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48)); + titleView.setOnTouchListener(new View.OnTouchListener() { @Override - public void onClick(View v) { - dismissWithButtonClick((Integer) v.getTag()); + public boolean onTouch(View v, MotionEvent event) { + return true; } }); - itemViews.add(cell); + topOffset += 48; + } + if (items != null) { + FrameLayout rowLayout = null; + int lastRowLayoutNum = 0; + for (int a = 0; a < items.length; a++) { + BottomSheetCell cell = new BottomSheetCell(getContext(), 0); + cell.setTextAndIcon(items[a], itemIcons != null ? itemIcons[a] : 0); + containerView.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.TOP, 0, topOffset, 0, 0)); + topOffset += 48; + cell.setTag(a); + cell.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismissWithButtonClick((Integer) v.getTag()); + } + }); + itemViews.add(cell); + } } } @@ -485,9 +600,7 @@ public class BottomSheet extends Dialog { if (!focusable) { params.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } - if (Build.VERSION.SDK_INT < 21) { - params.height = ViewGroup.LayoutParams.MATCH_PARENT; - } + params.height = ViewGroup.LayoutParams.MATCH_PARENT; window.setAttributes(params); } @@ -498,26 +611,32 @@ public class BottomSheet extends Dialog { getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); } dismissed = false; - if ((Build.VERSION.SDK_INT >= 21 || !useRevealAnimation) && !disableBackground) { - containerView.setBackgroundDrawable(shadowDrawable); - } else { - containerView.setBackgroundDrawable(null); + cancelSheetAnimation(); + if (containerView.getMeasuredHeight() == 0) { + containerView.measure(View.MeasureSpec.makeMeasureSpec(AndroidUtilities.displaySize.x, View.MeasureSpec.AT_MOST), View.MeasureSpec.makeMeasureSpec(AndroidUtilities.displaySize.y, View.MeasureSpec.AT_MOST)); } - int left = useRevealAnimation && Build.VERSION.SDK_INT <= 19 || disableBackground ? 0 : backgroundPaddingLeft; - int top = useRevealAnimation && Build.VERSION.SDK_INT <= 19 || disableBackground ? 0 : backgroundPaddingTop; - containerView.setPadding(left, (applyTopPadding ? AndroidUtilities.dp(8) : 0) + top, left, (applyBottomPadding ? AndroidUtilities.dp(isGrid ? 16 : 8) : 0)); - if (Build.VERSION.SDK_INT >= 21) { - AndroidUtilities.runOnUIThread(new Runnable() { + backDrawable.setAlpha(0); + if (Build.VERSION.SDK_INT >= 18) { + layoutCount = 2; + AndroidUtilities.runOnUIThread(startAnimationRunnable = new Runnable() { @Override public void run() { + if (startAnimationRunnable != this) { + return; + } + startAnimationRunnable = null; startOpenAnimation(); } - }); + }, 150); } else { startOpenAnimation(); } } + protected boolean canDismissWithSwipe() { + return true; + } + public void setCustomView(View view) { customView = view; } @@ -534,168 +653,61 @@ public class BottomSheet extends Dialog { applyBottomPadding = value; } - public void setDisableBackground(boolean value) { - disableBackground = value; - } - - protected void setRevealRadius(float radius) { - revealRadius = radius; - delegate.onRevealAnimationProgress(!dismissed, radius, revealX, revealY); - if (Build.VERSION.SDK_INT <= 19) { - containerView.invalidate(); + private void cancelSheetAnimation() { + if (currentSheetAnimation != null) { + currentSheetAnimation.cancel(); + currentSheetAnimation = null; } } - protected float getRevealRadius() { - return revealRadius; - } - - @SuppressLint("NewApi") - private void startRevealAnimation(final boolean open) { - ViewProxy.setTranslationY(containerView, 0); - - final AnimatorSet animatorSet = new AnimatorSet(); - - View view = delegate.getRevealView(); - if (view.getVisibility() == View.VISIBLE && ((ViewGroup) view.getParent()).getVisibility() == View.VISIBLE) { - final int coords[] = new int[2]; - view.getLocationInWindow(coords); - float top; - if (Build.VERSION.SDK_INT <= 19) { - top = AndroidUtilities.displaySize.y - containerView.getMeasuredHeight() - AndroidUtilities.statusBarHeight; - } else { - top = containerView.getY(); - } - revealX = coords[0] + view.getMeasuredWidth() / 2; - revealY = (int) (coords[1] + view.getMeasuredHeight() / 2 - top); - if (Build.VERSION.SDK_INT <= 19) { - revealY -= AndroidUtilities.statusBarHeight; - } - } else { - revealX = AndroidUtilities.displaySize.x / 2 + backgroundPaddingLeft; - revealY = (int) (AndroidUtilities.displaySize.y - containerView.getY()); - } - - int corners[][] = new int[][]{ - {0, 0}, - {0, containerView.getMeasuredHeight()}, - {containerView.getMeasuredWidth(), 0}, - {containerView.getMeasuredWidth(), containerView.getMeasuredHeight()} - }; - int finalRevealRadius = 0; - for (int a = 0; a < 4; a++) { - finalRevealRadius = Math.max(finalRevealRadius, (int) Math.ceil(Math.sqrt((revealX - corners[a][0]) * (revealX - corners[a][0]) + (revealY - corners[a][1]) * (revealY - corners[a][1])))); - } - int finalRevealX = revealX <= containerView.getMeasuredWidth() ? revealX : containerView.getMeasuredWidth(); - - ArrayList animators = new ArrayList<>(3); - animators.add(ObjectAnimator.ofFloat(this, "revealRadius", open ? 0 : finalRevealRadius, open ? finalRevealRadius : 0)); - animators.add(ObjectAnimator.ofInt(backgroundDrawable, "alpha", open ? 51 : 0)); - if (Build.VERSION.SDK_INT >= 21) { - containerView.setElevation(AndroidUtilities.dp(10)); - try { - animators.add(ViewAnimationUtils.createCircularReveal(containerView, finalRevealX, revealY, open ? 0 : finalRevealRadius, open ? finalRevealRadius : 0)); - } catch (Exception e) { - FileLog.e("tmessages", e); - } - animatorSet.setDuration(300); - } else { - if (!open) { - animatorSet.setDuration(200); - containerView.setPivotX(revealX <= containerView.getMeasuredWidth() ? revealX : containerView.getMeasuredWidth()); - containerView.setPivotY(revealY); - animators.add(ObjectAnimator.ofFloat(containerView, "scaleX", 0.0f)); - animators.add(ObjectAnimator.ofFloat(containerView, "scaleY", 0.0f)); - animators.add(ObjectAnimator.ofFloat(containerView, "alpha", 0.0f)); - } else { - animatorSet.setDuration(250); - containerView.setScaleX(1); - containerView.setScaleY(1); - containerView.setAlpha(1); - if (Build.VERSION.SDK_INT <= 19) { - animatorSet.setStartDelay(20); - } - } - } - animatorSet.playTogether(animators); - animatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - if (delegate != null) { - delegate.onRevealAnimationStart(open); - } - } - - @Override - public void onAnimationEnd(Animator animation) { - if (delegate != null) { - delegate.onRevealAnimationEnd(open); - } - containerView.invalidate(); - if (Build.VERSION.SDK_INT >= 11) { - containerView.setLayerType(View.LAYER_TYPE_NONE, null); - } - if (!open) { - containerView.setVisibility(View.INVISIBLE); - try { - BottomSheet.super.dismiss(); - } catch (Exception e) { - FileLog.e("tmessages", e); - } - } - } - }); - animatorSet.start(); - } - private void startOpenAnimation() { - if (containerView.getMeasuredHeight() == 0) { - containerView.measure(View.MeasureSpec.makeMeasureSpec(AndroidUtilities.displaySize.x, View.MeasureSpec.AT_MOST), View.MeasureSpec.makeMeasureSpec(AndroidUtilities.displaySize.y, View.MeasureSpec.AT_MOST)); - } - backgroundDrawable.setAlpha(0); containerView.setVisibility(View.VISIBLE); - if (useRevealAnimation) { - if (Build.VERSION.SDK_INT >= 20) { - containerView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - } - startRevealAnimation(true); - } else { + + if (!onCustomOpenAnimation()) { if (Build.VERSION.SDK_INT >= 20) { container.setLayerType(View.LAYER_TYPE_HARDWARE, null); } - ViewProxy.setTranslationY(containerView, containerView.getMeasuredHeight()); - backgroundDrawable.setAlpha(0); - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "translationY", 0), - ObjectAnimatorProxy.ofInt(backgroundDrawable, "alpha", 51)); - animatorSetProxy.setDuration(200); - animatorSetProxy.setStartDelay(20); - animatorSetProxy.setInterpolator(new DecelerateInterpolator()); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + containerView.setTranslationY(containerView.getMeasuredHeight()); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(containerView, "translationY", 0), + ObjectAnimator.ofInt(backDrawable, "alpha", 51)); + animatorSet.setDuration(200); + animatorSet.setStartDelay(20); + animatorSet.setInterpolator(new DecelerateInterpolator()); + animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { - if (delegate != null) { - delegate.onOpenAnimationEnd(); - } - if (Build.VERSION.SDK_INT >= 11) { + public void onAnimationEnd(Animator animation) { + if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { + currentSheetAnimation = null; + if (delegate != null) { + delegate.onOpenAnimationEnd(); + } container.setLayerType(View.LAYER_TYPE_NONE, null); } } + + @Override + public void onAnimationCancel(Animator animation) { + if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { + currentSheetAnimation = null; + } + } }); - animatorSetProxy.start(); + animatorSet.start(); + currentSheetAnimation = animatorSet; } } - public void setDelegate(BottomSheetDelegate delegate) { - this.delegate = delegate; + public void setDelegate(BottomSheetDelegateInterface bottomSheetDelegate) { + delegate = bottomSheetDelegate; } public FrameLayout getContainer() { return container; } - public LinearLayout getSheetContainer() { + public ViewGroup getSheetContainer() { return containerView; } @@ -711,59 +723,31 @@ public class BottomSheet extends Dialog { cell.textView.setText(text); } + public boolean isDismissed() { + return dismissed; + } + public void dismissWithButtonClick(final int item) { if (dismissed) { return; } dismissed = true; - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "translationY", containerView.getMeasuredHeight() + AndroidUtilities.dp(10)), - ObjectAnimatorProxy.ofInt(backgroundDrawable, "alpha", 0) + cancelSheetAnimation(); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(containerView, "translationY", containerView.getMeasuredHeight() + AndroidUtilities.dp(10)), + ObjectAnimator.ofInt(backDrawable, "alpha", 0) ); - animatorSetProxy.setDuration(180); - animatorSetProxy.setInterpolator(new AccelerateInterpolator()); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + animatorSet.setDuration(180); + animatorSet.setInterpolator(new AccelerateInterpolator()); + animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { - if (onClickListener != null) { - onClickListener.onClick(BottomSheet.this, item); - } - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - try { - BottomSheet.super.dismiss(); - } catch (Exception e) { - FileLog.e("tmessages", e); - } + public void onAnimationEnd(Animator animation) { + if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { + currentSheetAnimation = null; + if (onClickListener != null) { + onClickListener.onClick(BottomSheet.this, item); } - }); - } - }); - animatorSetProxy.start(); - } - - @Override - public void dismiss() { - if (dismissed) { - return; - } - dismissed = true; - if (useRevealAnimation) { - backgroundDrawable.setAlpha(51); - startRevealAnimation(false); - } else { - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "translationY", containerView.getMeasuredHeight() + AndroidUtilities.dp(10)), - ObjectAnimatorProxy.ofInt(backgroundDrawable, "alpha", 0) - ); - animatorSetProxy.setDuration(180); - animatorSetProxy.setInterpolator(new AccelerateInterpolator()); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { @@ -775,9 +759,80 @@ public class BottomSheet extends Dialog { } }); } - }); - animatorSetProxy.start(); + } + + @Override + public void onAnimationCancel(Animator animation) { + if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { + currentSheetAnimation = null; + } + } + }); + animatorSet.start(); + currentSheetAnimation = animatorSet; + } + + @Override + public void dismiss() { + if (dismissed) { + return; } + dismissed = true; + cancelSheetAnimation(); + if (!allowCustomAnimation || !onCustomCloseAnimation()) { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(containerView, "translationY", containerView.getMeasuredHeight() + AndroidUtilities.dp(10)), + ObjectAnimator.ofInt(backDrawable, "alpha", 0) + ); + if (useFastDismiss) { + int height = containerView.getMeasuredHeight(); + animatorSet.setDuration(Math.max(60, (int) (180 * (height - containerView.getTranslationY()) / (float) height))); + useFastDismiss = false; + } else { + animatorSet.setDuration(180); + } + animatorSet.setInterpolator(new AccelerateInterpolator()); + animatorSet.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { + currentSheetAnimation = null; + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + try { + dismissInternal(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + }); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { + currentSheetAnimation = null; + } + } + }); + animatorSet.start(); + currentSheetAnimation = animatorSet; + } + } + + protected void dismissInternal() { + super.dismiss(); + } + + protected boolean onCustomCloseAnimation() { + return false; + } + + protected boolean onCustomOpenAnimation() { + return false; } public static class Builder { @@ -829,23 +884,11 @@ public class BottomSheet extends Dialog { return this; } - public Builder setUseRevealAnimation() { - if (Build.VERSION.SDK_INT >= 18 && !AndroidUtilities.isTablet()) { - bottomSheet.useRevealAnimation = true; - } - return this; - } - public Builder setDelegate(BottomSheetDelegate delegate) { bottomSheet.setDelegate(delegate); return this; } - public Builder setIsGrid(boolean value) { - bottomSheet.isGrid = value; - return this; - } - public Builder setApplyTopPadding(boolean value) { bottomSheet.applyTopPadding = value; return this; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/DrawerLayoutContainer.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/DrawerLayoutContainer.java index f70a67e1a..df50bcb90 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/DrawerLayoutContainer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/DrawerLayoutContainer.java @@ -8,6 +8,9 @@ package org.telegram.ui.ActionBar; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; @@ -28,9 +31,7 @@ import android.widget.ListView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; public class DrawerLayoutContainer extends FrameLayout { @@ -39,14 +40,14 @@ public class DrawerLayoutContainer extends FrameLayout { private ViewGroup drawerLayout; private ActionBarLayout parentActionBarLayout; - private boolean maybeStartTracking = false; - private boolean startedTracking = false; + private boolean maybeStartTracking; + private boolean startedTracking; private int startedTrackingX; private int startedTrackingY; private int startedTrackingPointerId; - private VelocityTracker velocityTracker = null; + private VelocityTracker velocityTracker; private boolean beginTrackingSent; - private AnimatorSetProxy currentAnimation = null; + private AnimatorSet currentAnimation; private Paint scrimPaint = new Paint(); @@ -57,8 +58,8 @@ public class DrawerLayoutContainer extends FrameLayout { private Drawable shadowLeft; private boolean allowOpenDrawer; - private float drawerPosition = 0; - private boolean drawerOpened = false; + private float drawerPosition; + private boolean drawerOpened; private boolean allowDrawContent = true; public DrawerLayoutContainer(Context context) { @@ -138,7 +139,7 @@ public class DrawerLayoutContainer extends FrameLayout { } else if (drawerPosition < 0) { drawerPosition = 0; } - requestLayout(); + drawerLayout.setTranslationX(drawerPosition); final int newVisibility = drawerPosition > 0 ? VISIBLE : GONE; if (drawerLayout.getVisibility() != newVisibility) { @@ -166,10 +167,8 @@ public class DrawerLayoutContainer extends FrameLayout { AndroidUtilities.hideKeyboard(parentActionBarLayout.parentActivity.getCurrentFocus()); } cancelCurrentAnimation(); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(this, "drawerPosition", drawerLayout.getMeasuredWidth()) - ); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(this, "drawerPosition", drawerLayout.getMeasuredWidth())); animatorSet.setInterpolator(new DecelerateInterpolator()); if (fast) { animatorSet.setDuration(Math.max((int) (200.0f / drawerLayout.getMeasuredWidth() * (drawerLayout.getMeasuredWidth() - drawerPosition)), 50)); @@ -178,7 +177,7 @@ public class DrawerLayoutContainer extends FrameLayout { } animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animator) { + public void onAnimationEnd(Animator animator) { onDrawerAnimationEnd(true); } }); @@ -188,9 +187,9 @@ public class DrawerLayoutContainer extends FrameLayout { public void closeDrawer(boolean fast) { cancelCurrentAnimation(); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(this, "drawerPosition", 0) + ObjectAnimator.ofFloat(this, "drawerPosition", 0) ); animatorSet.setInterpolator(new DecelerateInterpolator()); if (fast) { @@ -200,7 +199,7 @@ public class DrawerLayoutContainer extends FrameLayout { } animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animator) { + public void onAnimationEnd(Animator animator) { onDrawerAnimationEnd(false); } }); @@ -213,7 +212,7 @@ public class DrawerLayoutContainer extends FrameLayout { drawerOpened = opened; if (!opened) { if (drawerLayout instanceof ListView) { - ((ListView)drawerLayout).setSelectionFromTop(0, 0); + ((ListView) drawerLayout).setSelectionFromTop(0, 0); } } } @@ -298,8 +297,8 @@ public class DrawerLayoutContainer extends FrameLayout { requestDisallowInterceptTouchEvent(true); } else if (startedTracking) { if (!beginTrackingSent) { - if (((Activity)getContext()).getCurrentFocus() != null) { - AndroidUtilities.hideKeyboard(((Activity)getContext()).getCurrentFocus()); + if (((Activity) getContext()).getCurrentFocus() != null) { + AndroidUtilities.hideKeyboard(((Activity) getContext()).getCurrentFocus()); } beginTrackingSent = true; } @@ -365,7 +364,6 @@ public class DrawerLayoutContainer extends FrameLayout { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { inLayout = true; - FileLog.w("tmessages", "onLayout"); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); @@ -380,7 +378,7 @@ public class DrawerLayoutContainer extends FrameLayout { if (drawerLayout != child) { child.layout(lp.leftMargin, lp.topMargin, lp.leftMargin + child.getMeasuredWidth(), lp.topMargin + child.getMeasuredHeight()); } else { - child.layout(-child.getMeasuredWidth() + (int)drawerPosition, lp.topMargin, (int)drawerPosition, lp.topMargin + child.getMeasuredHeight()); + child.layout(-child.getMeasuredWidth(), lp.topMargin, 0, lp.topMargin + child.getMeasuredHeight()); } } catch (Exception e) { FileLog.e("tmessages", e); @@ -483,7 +481,7 @@ public class DrawerLayoutContainer extends FrameLayout { } else if (shadowLeft != null) { final float alpha = Math.max(0, Math.min(drawerPosition / AndroidUtilities.dp(20), 1.0f)); if (alpha != 0) { - shadowLeft.setBounds((int)drawerPosition, child.getTop(), (int)drawerPosition + shadowLeft.getIntrinsicWidth(), child.getBottom()); + shadowLeft.setBounds((int) drawerPosition, child.getTop(), (int) drawerPosition + shadowLeft.getIntrinsicWidth(), child.getBottom()); shadowLeft.setAlpha((int) (0xff * alpha)); shadowLeft.draw(canvas); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java index cdda2c80c..165b72c63 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java @@ -41,14 +41,6 @@ public class SimpleTextView extends View implements Drawable.Callback { private int textHeight; private boolean wasLayout; - public enum Alignment { - ALIGN_NORMAL, - ALIGN_OPPOSITE, - ALIGN_CENTER, - ALIGN_LEFT, - ALIGN_RIGHT - } - public SimpleTextView(Context context) { super(context); textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); @@ -71,7 +63,9 @@ public class SimpleTextView extends View implements Drawable.Callback { return; } textPaint.setTextSize(newSize); - recreateLayoutMaybe(); + if (!recreateLayoutMaybe()) { + invalidate(); + } } public void setGravity(int value) { @@ -97,7 +91,7 @@ public class SimpleTextView extends View implements Drawable.Callback { return textPaint; } - private void createLayout(int width) { + private boolean createLayout(int width) { if (text != null) { try { if (leftDrawable != null) { @@ -111,7 +105,7 @@ public class SimpleTextView extends View implements Drawable.Callback { width -= getPaddingLeft() + getPaddingRight(); CharSequence string = TextUtils.ellipsize(text, textPaint, width, TextUtils.TruncateAt.END); if (layout != null && TextUtils.equals(layout.getText(), string)) { - return; + return false; } layout = new StaticLayout(string, 0, string.length(), textPaint, width + AndroidUtilities.dp(8), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); @@ -123,7 +117,7 @@ public class SimpleTextView extends View implements Drawable.Callback { } else if (layout.getLineLeft(0) == 0) { offsetX = width - textWidth; } else { - offsetX = 0; + offsetX = -AndroidUtilities.dp(8); } } } catch (Exception e) { @@ -134,6 +128,8 @@ public class SimpleTextView extends View implements Drawable.Callback { textWidth = 0; textHeight = 0; } + invalidate(); + return true; } @Override @@ -143,7 +139,7 @@ public class SimpleTextView extends View implements Drawable.Callback { createLayout(width - getPaddingLeft() - getPaddingRight()); int finalHeight; - if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { + if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { finalHeight = height; } else { finalHeight = textHeight; @@ -153,9 +149,7 @@ public class SimpleTextView extends View implements Drawable.Callback { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (changed) { - wasLayout = true; - } + wasLayout = true; } public int getTextWidth() { @@ -193,7 +187,9 @@ public class SimpleTextView extends View implements Drawable.Callback { if (drawable != null) { drawable.setCallback(this); } - recreateLayoutMaybe(); + if (!recreateLayoutMaybe()) { + invalidate(); + } } public void setRightDrawable(Drawable drawable) { @@ -207,7 +203,9 @@ public class SimpleTextView extends View implements Drawable.Callback { if (drawable != null) { drawable.setCallback(this); } - recreateLayoutMaybe(); + if (!recreateLayoutMaybe()) { + invalidate(); + } } public void setText(CharSequence value) { @@ -223,16 +221,18 @@ public class SimpleTextView extends View implements Drawable.Callback { return; } drawablePadding = value; - recreateLayoutMaybe(); + if (!recreateLayoutMaybe()) { + invalidate(); + } } - private void recreateLayoutMaybe() { + private boolean recreateLayoutMaybe() { if (wasLayout) { - createLayout(getMeasuredWidth()); - invalidate(); + return createLayout(getMeasuredWidth()); } else { requestLayout(); } + return true; } public CharSequence getText() { @@ -276,6 +276,15 @@ public class SimpleTextView extends View implements Drawable.Callback { @Override public void invalidateDrawable(Drawable who) { - invalidate(); + if (who == leftDrawable) { + invalidate(leftDrawable.getBounds()); + } else if (who == rightDrawable) { + invalidate(rightDrawable.getBounds()); + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java index eb2785e5f..29dca66cd 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java @@ -55,7 +55,13 @@ public class Theme { public static final int ACTION_BAR_VIOLET_SELECTOR_COLOR = 0xff735fbe; public static final int ACTION_BAR_YELLOW_SELECTOR_COLOR = 0xffef9f09; + public static final int ATTACH_SHEET_TEXT_COLOR = 0xff757575; + public static final int DIALOGS_MESSAGE_TEXT_COLOR = 0xff8f8f8f; + public static final int DIALOGS_NAME_TEXT_COLOR = 0xff4d83b3; + public static final int DIALOGS_ATTACH_TEXT_COLOR = 0xff4d83b3; + public static final int DIALOGS_PRINTING_TEXT_COLOR = 0xff4d83b3; + public static final int DIALOGS_DRAFT_TEXT_COLOR = 0xffdd4b39; public static final int CHAT_UNREAD_TEXT_COLOR = 0xff5695cc; public static final int CHAT_ADD_CONTACT_TEXT_COLOR = 0xff4a82b5; @@ -64,6 +70,7 @@ public class Theme { public static final int CHAT_BOTTOM_CHAT_OVERLAY_TEXT_COLOR = 0xff3a8ccf; public static final int CHAT_GIF_HINT_TEXT_COLOR = 0xffffffff; public static final int CHAT_EMPTY_VIEW_TEXT_COLOR = 0xffffffff; + public static final int CHAT_SEARCH_COUNT_TEXT_COLOR = 0xff4e9ad4; public static final int INAPP_PLAYER_PERFORMER_TEXT_COLOR = 0xff2f3438; public static final int INAPP_PLAYER_TITLE_TEXT_COLOR = 0xff2f3438; @@ -95,6 +102,7 @@ public class Theme { public static final int SECRET_CHAT_INFO_TEXT_COLOR = 0xffffffff; + public static final int MSG_SELECTED_BACKGROUND_COLOR = 0x6633b5e5; public static final int MSG_WEB_PREVIEW_DURATION_TEXT_COLOR = 0xffffffff; public static final int MSG_SECRET_TIME_TEXT_COLOR = 0xffe4e2e0; public static final int MSG_STICKER_NAME_TEXT_COLOR = 0xffffffff; @@ -180,6 +188,7 @@ public class Theme { public static final int MSG_TEXT_COLOR = 0xff000000; public static final int MSG_LINK_TEXT_COLOR = 0xff2678b6; public static final int MSG_LINK_SELECT_BACKGROUND_COLOR = 0x3362a9e3; + public static final int MSG_TEXT_SELECT_BACKGROUND_COLOR = 0x6662a9e3; public static Drawable backgroundDrawableIn; @@ -331,15 +340,6 @@ public class Theme { geoInDrawable = context.getResources().getDrawable(R.drawable.location_b); geoOutDrawable = context.getResources().getDrawable(R.drawable.location_g); - attachButtonDrawables[0] = context.getResources().getDrawable(R.drawable.attach_camera_states); - attachButtonDrawables[1] = context.getResources().getDrawable(R.drawable.attach_gallery_states); - attachButtonDrawables[2] = context.getResources().getDrawable(R.drawable.attach_video_states); - attachButtonDrawables[3] = context.getResources().getDrawable(R.drawable.attach_audio_states); - attachButtonDrawables[4] = context.getResources().getDrawable(R.drawable.attach_file_states); - attachButtonDrawables[5] = context.getResources().getDrawable(R.drawable.attach_contact_states); - attachButtonDrawables[6] = context.getResources().getDrawable(R.drawable.attach_location_states); - attachButtonDrawables[7] = context.getResources().getDrawable(R.drawable.attach_hide_states); - cornerOuter[0] = context.getResources().getDrawable(R.drawable.corner_out_tl); cornerOuter[1] = context.getResources().getDrawable(R.drawable.corner_out_tr); cornerOuter[2] = context.getResources().getDrawable(R.drawable.corner_out_br); @@ -368,6 +368,19 @@ public class Theme { } } + public static void loadChatResources(Context context) { + if (attachButtonDrawables[0] == null) { + attachButtonDrawables[0] = context.getResources().getDrawable(R.drawable.attach_camera_states); + attachButtonDrawables[1] = context.getResources().getDrawable(R.drawable.attach_gallery_states); + attachButtonDrawables[2] = context.getResources().getDrawable(R.drawable.attach_video_states); + attachButtonDrawables[3] = context.getResources().getDrawable(R.drawable.attach_audio_states); + attachButtonDrawables[4] = context.getResources().getDrawable(R.drawable.attach_file_states); + attachButtonDrawables[5] = context.getResources().getDrawable(R.drawable.attach_contact_states); + attachButtonDrawables[6] = context.getResources().getDrawable(R.drawable.attach_location_states); + attachButtonDrawables[7] = context.getResources().getDrawable(R.drawable.attach_hide_states); + } + } + public static Drawable createBarSelectorDrawable(int color) { return createBarSelectorDrawable(color, true); } @@ -411,9 +424,7 @@ public class Theme { stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, new ColorDrawable(color)); stateListDrawable.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(color)); stateListDrawable.addState(new int[]{android.R.attr.state_selected}, new ColorDrawable(color)); - if (Build.VERSION.SDK_INT >= 11) { - stateListDrawable.addState(new int[]{android.R.attr.state_activated}, new ColorDrawable(color)); - } + stateListDrawable.addState(new int[]{android.R.attr.state_activated}, new ColorDrawable(color)); stateListDrawable.addState(new int[]{}, new ColorDrawable(0x00000000)); return stateListDrawable; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/BaseSearchAdapterRecycler.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/BaseSearchAdapterRecycler.java index c5c0912ea..9aa9b0fcb 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/BaseSearchAdapterRecycler.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/BaseSearchAdapterRecycler.java @@ -127,7 +127,7 @@ public abstract class BaseSearchAdapterRecycler extends RecyclerView.Adapter { }); } - public void addHashtagsFromMessage(String message) { + public void addHashtagsFromMessage(CharSequence message) { if (message == null) { return; } @@ -140,7 +140,7 @@ public abstract class BaseSearchAdapterRecycler extends RecyclerView.Adapter { if (message.charAt(start) != '@' && message.charAt(start) != '#') { start++; } - String hashtag = message.substring(start, end); + String hashtag = message.subSequence(start, end).toString(); if (hashtagsByText == null) { hashtagsByText = new HashMap<>(); hashtags = new ArrayList<>(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/ChatActivityAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/ChatActivityAdapter.java deleted file mode 100644 index f01a9184e..000000000 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/ChatActivityAdapter.java +++ /dev/null @@ -1,396 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.x - * It is licensed under GNU GPL v. 2 or later. - * You should have received a copy of the license in this archive (see LICENSE). - * - * Copyright Nikolai Kudashov, 2013-2016. - */ - -package org.telegram.ui.Adapters; - -public class ChatActivityAdapter { - - /*private Context mContext; - - public ChatAdapter(Context context) { - mContext = context; - } - - @Override - public boolean areAllItemsEnabled() { - return true; - } - - @Override - public boolean isEnabled(int i) { - return true; - } - - @Override - public int getCount() { - int count = messages.size(); - if (count != 0) { - if (!endReached) { - count++; - } - if (!forward_end_reached) { - count++; - } - } - return count; - } - - @Override - public Object getItem(int i) { - return null; - } - - @Override - public long getItemId(int i) { - return i; - } - - @Override - public boolean hasStableIds() { - return true; - } - - @Override - public View getView(int i, View view, ViewGroup viewGroup) { - int offset = 1; - if ((!endReached || !forward_end_reached) && messages.size() != 0) { - if (!endReached) { - offset = 0; - } - if (i == 0 && !endReached || !forward_end_reached && i == (messages.size() + 1 - offset)) { - View progressBar = null; - if (view == null) { - LayoutInflater li = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = li.inflate(R.layout.chat_loading_layout, viewGroup, false); - progressBar = view.findViewById(R.id.progressLayout); - if (ApplicationLoader.isCustomTheme()) { - progressBar.setBackgroundResource(R.drawable.system_loader2); - } else { - progressBar.setBackgroundResource(R.drawable.system_loader1); - } - } else { - progressBar = view.findViewById(R.id.progressLayout); - } - progressBar.setVisibility(loadsCount > 1 ? View.VISIBLE : View.INVISIBLE); - - return view; - } - } - final MessageObject message = messages.get(messages.size() - i - offset); - int type = message.contentType; - if (view == null) { - if (type == 0) { - view = new ChatMessageCell(mContext); - } - if (type == 1) { - view = new ChatMediaCell(mContext); - } else if (type == 2) { - view = new ChatAudioCell(mContext); - } else if (type == 3) { - view = new ChatContactCell(mContext); - } else if (type == 6) { - LayoutInflater li = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = li.inflate(R.layout.chat_unread_layout, viewGroup, false); - } else if (type == 4) { - view = new ChatActionCell(mContext); - } - - if (view instanceof ChatBaseCell) { - ((ChatBaseCell) view).setDelegate(new ChatBaseCell.ChatBaseCellDelegate() { - @Override - public void didPressedUserAvatar(ChatBaseCell cell, TLRPC.User user) { - if (actionBar.isActionModeShowed()) { - processRowSelect(cell); - return; - } - if (user != null && user.id != UserConfig.getClientUserId()) { - Bundle args = new Bundle(); - args.putInt("user_id", user.id); - presentFragment(new ProfileActivity(args)); - } - } - - @Override - public void didPressedCancelSendButton(ChatBaseCell cell) { - MessageObject message = cell.getMessageObject(); - if (message.messageOwner.send_state != 0) { - SendMessagesHelper.getInstance().cancelSendingMessage(message); - } - } - - @Override - public void didLongPressed(ChatBaseCell cell) { - createMenu(cell, false); - } - - @Override - public boolean canPerformActions() { - return actionBar != null && !actionBar.isActionModeShowed(); - } - - @Override - public void didPressUrl(String url) { - if (url.startsWith("@")) { - openProfileWithUsername(url.substring(1)); - } else if (url.startsWith("#")) { - MessagesActivity fragment = new MessagesActivity(null); - fragment.setSearchString(url); - presentFragment(fragment); - } - } - - @Override - public void didPressReplyMessage(ChatBaseCell cell, int id) { - scrollToMessageId(id, cell.getMessageObject().getId(), true); - } - }); - if (view instanceof ChatMediaCell) { - ((ChatMediaCell) view).setAllowedToSetPhoto(openAnimationEnded); - ((ChatMediaCell) view).setMediaDelegate(new ChatMediaCell.ChatMediaCellDelegate() { - @Override - public void didClickedImage(ChatMediaCell cell) { - MessageObject message = cell.getMessageObject(); - if (message.isSendError()) { - createMenu(cell, false); - return; - } else if (message.isSending()) { - return; - } - if (message.type == 1) { - PhotoViewer.getInstance().setParentActivity(getParentActivity()); - PhotoViewer.getInstance().openPhoto(message, ChatActivity.this); - } else if (message.type == 3) { - sendSecretMessageRead(message); - try { - File f = null; - if (message.messageOwner.attachPath != null && message.messageOwner.attachPath.length() != 0) { - f = new File(message.messageOwner.attachPath); - } - if (f == null || f != null && !f.exists()) { - f = FileLoader.getPathToMessage(message.messageOwner); - } - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.fromFile(f), "video/mp4"); - getParentActivity().startActivityForResult(intent, 500); - } catch (Exception e) { - alertUserOpenError(message); - } - } else if (message.type == 4) { - if (!isGoogleMapsInstalled()) { - return; - } - LocationActivity fragment = new LocationActivity(); - fragment.setMessageObject(message); - presentFragment(fragment); - } else if (message.type == 9) { - File f = null; - String fileName = message.getFileName(); - if (message.messageOwner.attachPath != null && message.messageOwner.attachPath.length() != 0) { - f = new File(message.messageOwner.attachPath); - } - if (f == null || f != null && !f.exists()) { - f = FileLoader.getPathToMessage(message.messageOwner); - } - if (f != null && f.exists()) { - String realMimeType = null; - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - if (message.type == 8 || message.type == 9) { - MimeTypeMap myMime = MimeTypeMap.getSingleton(); - int idx = fileName.lastIndexOf('.'); - if (idx != -1) { - String ext = fileName.substring(idx + 1); - realMimeType = myMime.getMimeTypeFromExtension(ext.toLowerCase()); - if (realMimeType == null) { - realMimeType = message.messageOwner.media.document.mime_type; - if (realMimeType == null || realMimeType.length() == 0) { - realMimeType = null; - } - } - if (realMimeType != null) { - intent.setDataAndType(Uri.fromFile(f), realMimeType); - } else { - intent.setDataAndType(Uri.fromFile(f), "text/plain"); - } - } else { - intent.setDataAndType(Uri.fromFile(f), "text/plain"); - } - } - if (realMimeType != null) { - try { - getParentActivity().startActivityForResult(intent, 500); - } catch (Exception e) { - intent.setDataAndType(Uri.fromFile(f), "text/plain"); - getParentActivity().startActivityForResult(intent, 500); - } - } else { - getParentActivity().startActivityForResult(intent, 500); - } - } catch (Exception e) { - alertUserOpenError(message); - } - } - } - } - - @Override - public void didPressedOther(ChatMediaCell cell) { - createMenu(cell, true); - } - }); - } else if (view instanceof ChatContactCell) { - ((ChatContactCell) view).setContactDelegate(new ChatContactCell.ChatContactCellDelegate() { - @Override - public void didClickAddButton(ChatContactCell cell, TLRPC.User user) { - if (actionBar.isActionModeShowed()) { - processRowSelect(cell); - return; - } - MessageObject messageObject = cell.getMessageObject(); - Bundle args = new Bundle(); - args.putInt("user_id", messageObject.messageOwner.media.user_id); - args.putString("phone", messageObject.messageOwner.media.phone_number); - args.putBoolean("addContact", true); - presentFragment(new ContactAddActivity(args)); - } - - @Override - public void didClickPhone(ChatContactCell cell) { - if (actionBar.isActionModeShowed()) { - processRowSelect(cell); - return; - } - final MessageObject messageObject = cell.getMessageObject(); - if (getParentActivity() == null || messageObject.messageOwner.media.phone_number == null || messageObject.messageOwner.media.phone_number.length() == 0) { - return; - } - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - builder.setItems(new CharSequence[]{LocaleController.getString("Copy", R.string.Copy), LocaleController.getString("Call", R.string.Call)}, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - if (i == 1) { - try { - Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + messageObject.messageOwner.media.phone_number)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - getParentActivity().startActivityForResult(intent, 500); - } catch (Exception e) { - FileLog.e("tmessages", e); - } - } else if (i == 0) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(messageObject.messageOwner.media.phone_number); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", messageObject.messageOwner.media.phone_number); - clipboard.setPrimaryClip(clip); - } - } - } - } - ); - showDialog(builder.create()); - } - }); - } - } else if (view instanceof ChatActionCell) { - ((ChatActionCell) view).setDelegate(new ChatActionCell.ChatActionCellDelegate() { - @Override - public void didClickedImage(ChatActionCell cell) { - MessageObject message = cell.getMessageObject(); - PhotoViewer.getInstance().setParentActivity(getParentActivity()); - PhotoViewer.getInstance().openPhoto(message, ChatActivity.this); - } - - @Override - public void didLongPressed(ChatActionCell cell) { - createMenu(cell, false); - } - - @Override - public void needOpenUserProfile(int uid) { - if (uid != UserConfig.getClientUserId()) { - Bundle args = new Bundle(); - args.putInt("user_id", uid); - presentFragment(new ProfileActivity(args)); - } - } - }); - } - } - - boolean selected = false; - boolean disableSelection = false; - if (actionBar.isActionModeShowed()) { - if (selectedMessagesIds.containsKey(message.getId())) { - view.setBackgroundColor(0x6633b5e5); - selected = true; - } else { - view.setBackgroundColor(0); - } - disableSelection = true; - } else { - view.setBackgroundColor(0); - } - - if (view instanceof ChatBaseCell) { - ChatBaseCell baseCell = (ChatBaseCell) view; - baseCell.isChat = currentChat != null; - baseCell.setMessageObject(message); - baseCell.setCheckPressed(!disableSelection, disableSelection && selected); - if (view instanceof ChatAudioCell && MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_AUDIO)) { - ((ChatAudioCell) view).downloadAudioIfNeed(); - } - baseCell.setHighlighted(highlightMessageId != Integer.MAX_VALUE && message.getId() == highlightMessageId); - } else if (view instanceof ChatActionCell) { - ChatActionCell actionCell = (ChatActionCell) view; - actionCell.setMessageObject(message); - } - if (type == 6) { - TextView messageTextView = (TextView) view.findViewById(R.id.chat_message_text); - messageTextView.setText(LocaleController.formatPluralString("NewMessages", unread_to_load)); - } - - return view; - } - - @Override - public int getItemViewType(int i) { - int offset = 1; - if (!endReached && messages.size() != 0) { - offset = 0; - if (i == 0) { - return 5; - } - } - if (!forward_end_reached && i == (messages.size() + 1 - offset)) { - return 5; - } - MessageObject message = messages.get(messages.size() - i - offset); - return message.contentType; - } - - @Override - public int getViewTypeCount() { - return 7; - } - - @Override - public boolean isEmpty() { - int count = messages.size(); - if (count != 0) { - if (!endReached) { - count++; - } - if (!forward_end_reached) { - count++; - } - } - return count == 0; - }*/ -} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/ContactsAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/ContactsAdapter.java index 13f79f1ff..ade259fc5 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/ContactsAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/ContactsAdapter.java @@ -9,7 +9,6 @@ package org.telegram.ui.Adapters; import android.content.Context; -import android.os.Build; import android.view.View; import android.view.ViewGroup; @@ -19,7 +18,6 @@ import org.telegram.tgnet.TLRPC; import org.telegram.messenger.ContactsController; import org.telegram.messenger.MessagesController; import org.telegram.messenger.R; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.ui.Cells.DividerCell; import org.telegram.ui.Cells.GreySectionCell; import org.telegram.ui.Cells.LetterSectionCell; @@ -229,12 +227,13 @@ public class ContactsAdapter extends BaseSectionsAdapter { convertView = new TextCell(mContext); } ContactsController.Contact contact = ContactsController.getInstance().phoneBookContacts.get(position); + TextCell textCell = (TextCell) convertView; if (contact.first_name != null && contact.last_name != null) { - ((TextCell) convertView).setText(contact.first_name + " " + contact.last_name); + textCell.setText(contact.first_name + " " + contact.last_name); } else if (contact.first_name != null && contact.last_name == null) { - ((TextCell) convertView).setText(contact.first_name); + textCell.setText(contact.first_name); } else { - ((TextCell) convertView).setText(contact.last_name); + textCell.setText(contact.last_name); } } else if (type == 0) { if (convertView == null) { @@ -249,13 +248,13 @@ public class ContactsAdapter extends BaseSectionsAdapter { TLRPC.User user = MessagesController.getInstance().getUser(arr.get(position).user_id); ((UserCell) convertView).setData(user, null, null, 0); if (checkedMap != null) { - ((UserCell) convertView).setChecked(checkedMap.containsKey(user.id), !scrolling && Build.VERSION.SDK_INT > 10); + ((UserCell) convertView).setChecked(checkedMap.containsKey(user.id), !scrolling); } if (ignoreUsers != null) { if (ignoreUsers.containsKey(user.id)) { - ViewProxy.setAlpha(convertView, 0.5f); + convertView.setAlpha(0.5f); } else { - ViewProxy.setAlpha(convertView, 1.0f); + convertView.setAlpha(1.0f); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java index 11a1ccec8..46557a3ac 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java @@ -49,7 +49,7 @@ public class DialogsAdapter extends RecyclerView.Adapter { return current != getItemCount() || current == 1; } - private ArrayList getDialogsArray() { + private ArrayList getDialogsArray() { if (dialogsType == 0) { return MessagesController.getInstance().dialogs; } else if (dialogsType == 1) { @@ -73,8 +73,8 @@ public class DialogsAdapter extends RecyclerView.Adapter { return count; } - public TLRPC.Dialog getItem(int i) { - ArrayList arrayList = getDialogsArray(); + public TLRPC.TL_dialog getItem(int i) { + ArrayList arrayList = getDialogsArray(); if (i < 0 || i >= arrayList.size()) { return null; } @@ -110,7 +110,7 @@ public class DialogsAdapter extends RecyclerView.Adapter { if (viewHolder.getItemViewType() == 0) { DialogCell cell = (DialogCell) viewHolder.itemView; cell.useSeparator = (i != getItemCount() - 1); - TLRPC.Dialog dialog = getItem(i); + TLRPC.TL_dialog dialog = getItem(i); if (dialogsType == 0) { if (AndroidUtilities.isTablet()) { cell.setDialogSelected(dialog.id == openedDialogId); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java index 5f738ff28..45c34f658 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java @@ -10,6 +10,7 @@ package org.telegram.ui.Adapters; import android.content.Context; import android.text.TextUtils; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -22,6 +23,8 @@ import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.MessagesController; import org.telegram.messenger.MessagesStorage; +import org.telegram.messenger.query.SearchQuery; +import org.telegram.messenger.support.widget.LinearLayoutManager; import org.telegram.messenger.support.widget.RecyclerView; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; @@ -33,8 +36,10 @@ import org.telegram.tgnet.TLRPC; import org.telegram.ui.Cells.DialogCell; import org.telegram.ui.Cells.GreySectionCell; import org.telegram.ui.Cells.HashtagSearchCell; +import org.telegram.ui.Cells.HintDialogCell; import org.telegram.ui.Cells.LoadingCell; import org.telegram.ui.Cells.ProfileSearchCell; +import org.telegram.ui.Components.RecyclerListView; import java.util.ArrayList; import java.util.Collections; @@ -43,6 +48,7 @@ import java.util.HashMap; import java.util.Locale; import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { @@ -55,7 +61,7 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { private String lastSearchText; private int reqId = 0; private int lastReqId; - private MessagesActivitySearchAdapterDelegate delegate; + private DialogsSearchAdapterDelegate delegate; private int needMessagesSearch; private boolean messagesSearchEndReached; private String lastMessagesSearchString; @@ -84,8 +90,58 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { long did; } - public interface MessagesActivitySearchAdapterDelegate { + public interface DialogsSearchAdapterDelegate { void searchStateChanged(boolean searching); + void didPressedOnSubDialog(int did); + void needRemoveHint(int did); + } + + private class CategoryAdapterRecycler extends RecyclerView.Adapter { + + public void setIndex(int value) { + notifyDataSetChanged(); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = new HintDialogCell(mContext); + view.setLayoutParams(new RecyclerView.LayoutParams(AndroidUtilities.dp(80), AndroidUtilities.dp(100))); + return new Holder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + HintDialogCell cell = (HintDialogCell) holder.itemView; + + TLRPC.TL_topPeer peer = SearchQuery.hints.get(position); + TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); + TLRPC.Chat chat = null; + TLRPC.User user = null; + int did = 0; + if (peer.peer.user_id != 0) { + did = peer.peer.user_id; + user = MessagesController.getInstance().getUser(peer.peer.user_id); + } else if (peer.peer.channel_id != 0) { + did = -peer.peer.channel_id; + chat = MessagesController.getInstance().getChat(peer.peer.channel_id); + } else if (peer.peer.chat_id != 0) { + did = -peer.peer.chat_id; + chat = MessagesController.getInstance().getChat(peer.peer.chat_id); + } + cell.setTag(did); + String name = ""; + if (user != null) { + name = ContactsController.formatName(user.first_name, user.last_name); + } else if (chat != null) { + name = chat.title; + } + cell.setDialog(did, false, name); + } + + @Override + public int getItemCount() { + return SearchQuery.hints.size(); + } } public DialogsSearchAdapter(Context context, int messagesSearch, int type) { @@ -93,9 +149,10 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { needMessagesSearch = messagesSearch; dialogsType = type; loadRecentSearch(); + SearchQuery.loadHints(true); } - public void setDelegate(MessagesActivitySearchAdapterDelegate delegate) { + public void setDelegate(DialogsSearchAdapterDelegate delegate) { this.delegate = delegate; } @@ -171,8 +228,17 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { if (req.offset_id == 0) { searchResultMessages.clear(); } - for (TLRPC.Message message : res.messages) { + for (int a = 0; a < res.messages.size(); a++) { + TLRPC.Message message = res.messages.get(a); searchResultMessages.add(new MessageObject(message, null, false)); + long dialog_id = MessageObject.getDialogId(message); + ConcurrentHashMap read_max = message.out ? MessagesController.getInstance().dialogs_read_outbox_max : MessagesController.getInstance().dialogs_read_inbox_max; + Integer value = read_max.get(dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, dialog_id); + read_max.put(dialog_id, value); + } + message.unread = value < message.id; } messagesSearchEndReached = res.messages.size() != 20; notifyDataSetChanged(); @@ -189,11 +255,11 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { } public boolean hasRecentRearch() { - return !recentSearchObjects.isEmpty(); + return !recentSearchObjects.isEmpty() || !SearchQuery.hints.isEmpty(); } public boolean isRecentSearchDisplayed() { - return needMessagesSearch != 2 && (lastSearchText == null || lastSearchText.length() == 0) && !recentSearchObjects.isEmpty(); + return needMessagesSearch != 2 && (lastSearchText == null || lastSearchText.length() == 0) && (!recentSearchObjects.isEmpty() || !SearchQuery.hints.isEmpty()); } public void loadRecentSearch() { @@ -320,6 +386,8 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { }); } + + public void putRecentSearch(final long did, TLObject object) { RecentSearchObject recentSearchObject = recentSearchObjectsById.get(did); if (recentSearchObject == null) { @@ -467,9 +535,10 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { found = 2; } if (found != 0) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.User user = TLRPC.User.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); DialogSearchResult dialogSearchResult = dialogsResult.get((long) user.id); if (user.status != null) { user.status.expires = cursor.intValue(1); @@ -482,7 +551,6 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { dialogSearchResult.object = user; resultCount++; } - data.reuse(); break; } } @@ -500,9 +568,10 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { } for (String q : search) { if (name.startsWith(q) || name.contains(" " + q) || tName != null && (tName.startsWith(q) || tName.contains(" " + q))) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.Chat chat = TLRPC.Chat.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (!(chat == null || chat.deactivated || ChatObject.isChannel(chat) && ChatObject.isNotInChat(chat))) { long dialog_id; if (chat.id > 0) { @@ -516,7 +585,6 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { resultCount++; } } - data.reuse(); break; } } @@ -539,7 +607,8 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { username = name.substring(usernamePos + 2); } int found = 0; - for (String q : search) { + for (int a = 0; a < search.length; a++) { + String q = search[a]; if (name.startsWith(q) || name.contains(" " + q) || tName != null && (tName.startsWith(q) || tName.contains(" " + q))) { found = 1; } else if (username != null && username.startsWith(q)) { @@ -547,12 +616,20 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { } if (found != 0) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - NativeByteBuffer data2 = new NativeByteBuffer(cursor.byteArrayLength(6)); - if (data != null && cursor.byteBufferValue(0, data) != 0 && cursor.byteBufferValue(6, data2) != 0) { - TLRPC.EncryptedChat chat = TLRPC.EncryptedChat.TLdeserialize(data, data.readInt32(false), false); + TLRPC.EncryptedChat chat = null; + TLRPC.User user = null; + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { + chat = TLRPC.EncryptedChat.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); + } + data = cursor.byteBufferValue(6); + if (data != null) { + user = TLRPC.User.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); + } + if (chat != null && user != null) { DialogSearchResult dialogSearchResult = dialogsResult.get((long) chat.id << 32); - chat.user_id = cursor.intValue(2); chat.a_or_b = cursor.byteArrayValue(3); chat.auth_key = cursor.byteArrayValue(4); @@ -569,7 +646,6 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { chat.future_auth_key = cursor.byteArrayValue(15); chat.key_hash = cursor.byteArrayValue(16); - TLRPC.User user = TLRPC.User.TLdeserialize(data2, data2.readInt32(false), false); if (user.status != null) { user.status.expires = cursor.intValue(7); } @@ -582,8 +658,6 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { encUsers.add(user); resultCount++; } - data.reuse(); - data2.reuse(); break; } } @@ -644,9 +718,10 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { found = 2; } if (found != 0) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.User user = TLRPC.User.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (user.status != null) { user.status.expires = cursor.intValue(1); } @@ -657,7 +732,6 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { } resultArray.add(user); } - data.reuse(); break; } } @@ -680,7 +754,8 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { if (searchId != lastSearchId) { return; } - for (TLObject obj : result) { + for (int a = 0; a < result.size(); a++) { + TLObject obj = result.get(a); if (obj instanceof TLRPC.User) { TLRPC.User user = (TLRPC.User) obj; MessagesController.getInstance().putUser(user, true); @@ -692,9 +767,7 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { MessagesController.getInstance().putEncryptedChat(chat, true); } } - for (TLRPC.User user : encUsers) { - MessagesController.getInstance().putUser(user, true); - } + MessagesController.getInstance().putUsers(encUsers, true); searchResult = result; searchResultNames = names; notifyDataSetChanged(); @@ -801,8 +874,8 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { @Override public int getItemCount() { - if (needMessagesSearch != 2 && (lastSearchText == null || lastSearchText.length() == 0) && !recentSearchObjects.isEmpty()) { - return recentSearchObjects.size() + 1; + if (isRecentSearchDisplayed()) { + return (!recentSearchObjects.isEmpty() ? recentSearchObjects.size() + 1 : 0) + (!SearchQuery.hints.isEmpty() ? 2 : 0); } if (!searchResultHashtags.isEmpty()) { return searchResultHashtags.size() + 1; @@ -820,9 +893,10 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { } public Object getItem(int i) { - if (needMessagesSearch != 2 && (lastSearchText == null || lastSearchText.length() == 0) && !recentSearchObjects.isEmpty()) { - if (i > 0 && i - 1 < recentSearchObjects.size()) { - TLObject object = recentSearchObjects.get(i - 1).object; + if (isRecentSearchDisplayed()) { + int offset = (!SearchQuery.hints.isEmpty() ? 2 : 0); + if (i > offset && i - 1 - offset < recentSearchObjects.size()) { + TLObject object = recentSearchObjects.get(i - 1 - offset).object; if (object instanceof TLRPC.User) { TLRPC.User user = MessagesController.getInstance().getUser(((TLRPC.User) object).id); if (user != null) { @@ -884,8 +958,53 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { case 4: view = new HashtagSearchCell(mContext); break; + case 5: + RecyclerListView horizontalListView = new RecyclerListView(mContext) { + @Override + public boolean onInterceptTouchEvent(MotionEvent e) { + if (getParent() != null && getParent().getParent() != null) { + getParent().getParent().requestDisallowInterceptTouchEvent(true); + } + return super.onInterceptTouchEvent(e); + } + }; + horizontalListView.setTag(9); + horizontalListView.setItemAnimator(null); + horizontalListView.setLayoutAnimation(null); + LinearLayoutManager layoutManager = new LinearLayoutManager(mContext) { + @Override + public boolean supportsPredictiveItemAnimations() { + return false; + } + }; + layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); + horizontalListView.setLayoutManager(layoutManager); + //horizontalListView.setDisallowInterceptTouchEvents(true); + horizontalListView.setAdapter(new CategoryAdapterRecycler()); + horizontalListView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + if (delegate != null) { + delegate.didPressedOnSubDialog((Integer) view.getTag()); + } + } + }); + horizontalListView.setOnItemLongClickListener(new RecyclerListView.OnItemLongClickListener() { + @Override + public boolean onItemClick(View view, int position) { + if (delegate != null) { + delegate.needRemoveHint((Integer) view.getTag()); + } + return true; + } + }); + view = horizontalListView; + } + if (viewType == 5) { + view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, AndroidUtilities.dp(100))); + } else { + view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)); } - view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)); return new Holder(view); } @@ -918,7 +1037,7 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { user = MessagesController.getInstance().getUser(encryptedChat.user_id); } - if (needMessagesSearch != 2 && (lastSearchText == null || lastSearchText.length() == 0) && !recentSearchObjects.isEmpty()) { + if (isRecentSearchDisplayed()) { isRecent = true; cell.useSeparator = position != getItemCount() - 1; } else { @@ -952,8 +1071,13 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { } case 1: { GreySectionCell cell = (GreySectionCell) holder.itemView; - if (needMessagesSearch != 2 && (lastSearchText == null || lastSearchText.length() == 0) && !recentSearchObjects.isEmpty()) { - cell.setText(LocaleController.getString("Recent", R.string.Recent).toUpperCase()); + if (isRecentSearchDisplayed()) { + int offset = (!SearchQuery.hints.isEmpty() ? 2 : 0); + if (position < offset) { + cell.setText(LocaleController.getString("ChatHints", R.string.ChatHints).toUpperCase()); + } else { + cell.setText(LocaleController.getString("Recent", R.string.Recent).toUpperCase()); + } } else if (!searchResultHashtags.isEmpty()) { cell.setText(LocaleController.getString("Hashtags", R.string.Hashtags).toUpperCase()); } else if (!globalSearch.isEmpty() && position == searchResult.size()) { @@ -979,13 +1103,26 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { cell.setNeedDivider(position != searchResultHashtags.size()); break; } + case 5: { + RecyclerListView recyclerListView = (RecyclerListView) holder.itemView; + ((CategoryAdapterRecycler) recyclerListView.getAdapter()).setIndex(position / 2); + break; + } } } @Override public int getItemViewType(int i) { - if (needMessagesSearch != 2 && (lastSearchText == null || lastSearchText.length() == 0) && !recentSearchObjects.isEmpty()) { - return i == 0 ? 1 : 0; + if (isRecentSearchDisplayed()) { + int offset = (!SearchQuery.hints.isEmpty() ? 2 : 0); + if (i <= offset) { + if (i == offset || i % 2 == 0) { + return 1; + } else { + return 5; + } + } + return 0; } if (!searchResultHashtags.isEmpty()) { return i == 0 ? 1 : 4; 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 dc0e10b6b..185fa140e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/MentionsAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/MentionsAdapter.java @@ -20,11 +20,8 @@ import android.os.Build; import android.view.View; import android.view.ViewGroup; -import org.telegram.SQLite.SQLiteCursor; -import org.telegram.SQLite.SQLitePreparedStatement; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; -import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.MessagesController; @@ -32,7 +29,7 @@ import org.telegram.messenger.MessagesStorage; import org.telegram.messenger.R; import org.telegram.messenger.SendMessagesHelper; import org.telegram.messenger.UserObject; -import org.telegram.messenger.support.widget.LinearLayoutManager; +import org.telegram.messenger.query.SearchQuery; import org.telegram.messenger.support.widget.RecyclerView; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.RequestDelegate; @@ -66,7 +63,6 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { private Context mContext; private long dialog_id; private TLRPC.ChatFull info; - private ArrayList botRecent; private ArrayList searchResultUsernames; private ArrayList searchResultHashtags; private ArrayList searchResultCommands; @@ -78,6 +74,7 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { private MentionsAdapterDelegate delegate; private HashMap botInfo; private int resultStartPosition; + private boolean allowNewMentions = true; private int resultLength; private String lastText; private int lastPosition; @@ -86,8 +83,6 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { private boolean needBotContext = true; private boolean isDarkTheme; private int botsCount; - private boolean loadingBotRecent; - private boolean botRecentLoaded; private String searchingContextUsername; private String searchingContextQuery; @@ -123,10 +118,10 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { } }; - public MentionsAdapter(Context context, boolean isDarkTheme, long did, MentionsAdapterDelegate delegate) { + public MentionsAdapter(Context context, boolean darkTheme, long did, MentionsAdapterDelegate mentionsAdapterDelegate) { mContext = context; - this.delegate = delegate; - this.isDarkTheme = isDarkTheme; + delegate = mentionsAdapterDelegate; + isDarkTheme = darkTheme; dialog_id = did; } @@ -152,6 +147,10 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { noUserName = false; } + public void setAllowNewMentions(boolean value) { + allowNewMentions = value; + } + public void setParentFragment(BaseFragment fragment) { parentFragment = fragment; } @@ -163,136 +162,12 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { } } - private void loadBotRecent() { - if (loadingBotRecent || botRecentLoaded) { - return; - } - loadingBotRecent = true; - MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { - @Override - public void run() { - try { - SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized("SELECT id FROM bot_recent ORDER BY date DESC"); - ArrayList uids = null; - while (cursor.next()) { - if (uids == null) { - uids = new ArrayList<>(); - } - uids.add(cursor.intValue(0)); - } - cursor.dispose(); - if (uids != null) { - final ArrayList uidsFinal = uids; - final ArrayList users = MessagesStorage.getInstance().getUsers(uids); - Collections.sort(users, new Comparator() { - @Override - public int compare(TLRPC.User lhs, TLRPC.User rhs) { - int idx1 = uidsFinal.indexOf(lhs.id); - int idx2 = uidsFinal.indexOf(rhs.id); - if (idx1 > idx2) { - return 1; - } else if (idx1 < idx2) { - return -1; - } - return 0; - } - }); - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - botRecent = users; - loadingBotRecent = false; - botRecentLoaded = true; - if (lastText != null) { - searchUsernameOrHashtag(lastText, lastPosition, messages); - } - } - }); - } else { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - loadingBotRecent = false; - botRecentLoaded = true; - } - }); - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } - } - }); - } - - public void addRecentBot() { - if (foundContextBot == null) { - return; - } - if (botRecent == null) { - botRecent = new ArrayList<>(); - } else { - for (int a = 0; a < botRecent.size(); a++) { - TLRPC.User user = botRecent.get(a); - if (user.id == foundContextBot.id) { - botRecent.remove(a); - break; - } - } - } - botRecent.add(0, foundContextBot); - final int uid = foundContextBot.id; - MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { - @Override - public void run() { - try { - SQLitePreparedStatement state = MessagesStorage.getInstance().getDatabase().executeFast("REPLACE INTO bot_recent VALUES(?, ?)"); - state.requery(); - state.bindInteger(1, uid); - state.bindInteger(2, (int) (System.currentTimeMillis() / 1000)); - state.step(); - state.dispose(); - } catch (Exception e) { - FileLog.e("tmessages", e); - } - } - }); - } - - public void removeRecentBot(final TLRPC.User bot) { - if (botRecent == null || bot == null) { - return; - } - for (int a = 0; a < botRecent.size(); a++) { - TLRPC.User user = botRecent.get(a); - if (user.id == bot.id) { - botRecent.remove(a); - if (botRecent.isEmpty()) { - botRecent = null; - } - break; - } - } - MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { - @Override - public void run() { - try { - MessagesStorage.getInstance().getDatabase().executeFast("DELETE FROM bot_recent WHERE id = " + bot.id).stepThis().dispose(); - } catch (Exception e) { - FileLog.e("tmessages", e); - } - } - }); - } - public void setNeedUsernames(boolean value) { needUsernames = value; } public void setNeedBotContext(boolean value) { needBotContext = value; - if (needBotContext) { - loadBotRecent(); - } } public void setBotInfo(HashMap info) { @@ -498,10 +373,6 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { } } - public int getOrientation() { - return searchResultBotContext != null && !searchResultBotContext.isEmpty() && contextMedia ? LinearLayoutManager.HORIZONTAL : LinearLayoutManager.VERTICAL; - } - public String getBotCaption() { if (foundContextBot != null) { return foundContextBot.bot_inline_placeholder; @@ -593,7 +464,7 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { searchResultCommandsHelp = null; searchResultCommandsUsers = null; if (added) { - boolean hasTop = getOrientation() == LinearLayoutManager.VERTICAL && searchResultBotContextSwitch != null; + boolean hasTop = searchResultBotContextSwitch != null; notifyItemChanged(searchResultBotContext.size() - res.results.size() + (hasTop ? 1 : 0) - 1); notifyItemRangeInserted(searchResultBotContext.size() - res.results.size() + (hasTop ? 1 : 0), res.results.size()); } else { @@ -657,32 +528,23 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { char ch = text.charAt(a); if (a == 0 || text.charAt(a - 1) == ' ' || text.charAt(a - 1) == '\n') { if (ch == '@') { - if (needUsernames || needBotContext && botRecent != null && a == 0) { + if (needUsernames || needBotContext && a == 0) { if (hasIllegalUsernameCharacters) { delegate.needChangePanelVisibility(false); return; } - if (info == null && (botRecent == null || a != 0)) { + if (info == null && a != 0) { lastText = text; lastPosition = position; messages = messageObjects; delegate.needChangePanelVisibility(false); return; } - if (loadingBotRecent) { - lastText = text; - lastPosition = position; - messages = messageObjects; - } dogPostion = a; foundType = 0; resultStartPosition = a; resultLength = result.length() + 1; break; - } else if (loadingBotRecent) { - lastText = text; - lastPosition = position; - messages = messageObjects; } } else if (ch == '#') { if (!hashtagsLoadedFromDb) { @@ -725,12 +587,20 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { String usernameString = result.toString().toLowerCase(); ArrayList newResult = new ArrayList<>(); final HashMap newResultsHashMap = new HashMap<>(); - if (needBotContext && dogPostion == 0 && botRecent != null) { - for (int a = 0; a < botRecent.size(); a++) { - TLRPC.User user = botRecent.get(a); + if (needBotContext && dogPostion == 0 && !SearchQuery.inlineBots.isEmpty()) { + int count = 0; + for (int a = 0; a < SearchQuery.inlineBots.size(); a++) { + TLRPC.User user = MessagesController.getInstance().getUser(SearchQuery.inlineBots.get(a).peer.user_id); + if (user == null) { + continue; + } if (user.username != null && user.username.length() > 0 && (usernameString.length() > 0 && user.username.toLowerCase().startsWith(usernameString) || usernameString.length() == 0)) { newResult.add(user); newResultsHashMap.put(user.id, user); + count++; + } + if (count == 5) { + break; } } } @@ -741,8 +611,23 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { if (user == null || UserObject.isUserSelf(user) || newResultsHashMap.containsKey(user.id)) { continue; } - if (user.username != null && user.username.length() > 0 && (usernameString.length() > 0 && user.username.toLowerCase().startsWith(usernameString) || usernameString.length() == 0)) { - newResult.add(user); + if (usernameString.length() == 0) { + if (!user.deleted && (allowNewMentions || !allowNewMentions && user.username != null && user.username.length() != 0)) { + newResult.add(user); + } + } else { + if (user.username != null && user.username.length() > 0 && user.username.toLowerCase().startsWith(usernameString)) { + newResult.add(user); + } else { + if (!allowNewMentions && (user.username == null || user.username.length() == 0)) { + continue; + } + if (user.first_name != null && user.first_name.length() > 0 && user.first_name.toLowerCase().startsWith(usernameString)) { + newResult.add(user); + } else if (user.last_name != null && user.last_name.length() > 0 && user.last_name.toLowerCase().startsWith(usernameString)) { + newResult.add(user); + } + } } } } @@ -828,7 +713,7 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { @Override public int getItemCount() { if (searchResultBotContext != null) { - return searchResultBotContext.size() + (getOrientation() == LinearLayoutManager.VERTICAL && searchResultBotContextSwitch != null ? 1 : 0); + return searchResultBotContext.size() + (searchResultBotContextSwitch != null ? 1 : 0); } else if (searchResultUsernames != null) { return searchResultUsernames.size(); } else if (searchResultHashtags != null) { @@ -842,7 +727,7 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { @Override public int getItemViewType(int position) { if (searchResultBotContext != null) { - if (position == 0 && getOrientation() == LinearLayoutManager.VERTICAL && searchResultBotContextSwitch != null) { + if (position == 0 && searchResultBotContextSwitch != null) { return 2; } return 1; @@ -853,8 +738,7 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { public Object getItem(int i) { if (searchResultBotContext != null) { - boolean hasTop = getOrientation() == LinearLayoutManager.VERTICAL && searchResultBotContextSwitch != null; - if (hasTop) { + if (searchResultBotContextSwitch != null) { if (i == 0) { return searchResultBotContextSwitch; } else { @@ -903,6 +787,10 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { return searchResultBotContext != null; } + public boolean isMediaLayout() { + return contextMedia; + } + @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view; @@ -926,7 +814,7 @@ public class MentionsAdapter extends BaseSearchAdapterRecycler { @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (searchResultBotContext != null) { - boolean hasTop = getOrientation() == LinearLayoutManager.VERTICAL && searchResultBotContextSwitch != null; + boolean hasTop = searchResultBotContextSwitch != null; if (holder.getItemViewType() == 2) { if (hasTop) { ((BotSwitchCell) holder.itemView).setText(searchResultBotContextSwitch.text); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/StickersAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/StickersAdapter.java index 22f692da3..18e466c32 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/StickersAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/StickersAdapter.java @@ -8,12 +8,15 @@ package org.telegram.ui.Adapters; +import android.app.Activity; import android.content.Context; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; +import org.telegram.messenger.FileLog; import org.telegram.messenger.NotificationCenter; +import org.telegram.messenger.Utilities; import org.telegram.messenger.query.StickersQuery; import org.telegram.messenger.support.widget.RecyclerView; import org.telegram.messenger.FileLoader; @@ -22,6 +25,8 @@ import org.telegram.ui.Cells.StickerCell; import java.io.File; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; public class StickersAdapter extends RecyclerView.Adapter implements NotificationCenter.NotificationCenterDelegate { @@ -32,6 +37,8 @@ public class StickersAdapter extends RecyclerView.Adapter implements Notificatio private StickersAdapterDelegate delegate; private String lastSticker; private boolean visible; + private ArrayList newRecentStickers = new ArrayList<>(); + private long recentLoadDate; public interface StickersAdapterDelegate { void needChangePanelVisibility(boolean show); @@ -110,7 +117,42 @@ public class StickersAdapter extends RecyclerView.Adapter implements Notificatio visible = false; } } else { - stickers = newStickers; + stickers = newStickers != null && !newStickers.isEmpty() ? new ArrayList<>(newStickers) : null; + if (stickers != null) { + if (Math.abs(recentLoadDate - System.currentTimeMillis()) > 10 * 1000) { + recentLoadDate = System.currentTimeMillis(); + try { + String str = mContext.getSharedPreferences("emoji", Activity.MODE_PRIVATE).getString("stickers2", ""); + String[] args = str.split(","); + for (int a = 0; a < args.length; a++) { + if (args[a].length() == 0) { + continue; + } + long id = Utilities.parseLong(args[a]); + if (id != 0) { + newRecentStickers.add(id); + } + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + if (!newRecentStickers.isEmpty()) { + Collections.sort(stickers, new Comparator() { + @Override + public int compare(TLRPC.Document lhs, TLRPC.Document rhs) { + int idx1 = newRecentStickers.indexOf(lhs.id); + int idx2 = newRecentStickers.indexOf(rhs.id); + if (idx1 > idx2) { + return -1; + } else if (idx1 < idx2) { + return 1; + } + return 0; + } + }); + } + } checkStickerFilesExistAndDownload(); delegate.needChangePanelVisibility(stickers != null && !stickers.isEmpty() && stickersToLoad.isEmpty()); notifyDataSetChanged(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java index 7be4b309f..2d8b76f4e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java @@ -318,8 +318,7 @@ public class AudioPlayerActivity extends BaseFragment implements NotificationCen updateTitle(id == NotificationCenter.audioDidReset && (Boolean) args[1]); } else if (id == NotificationCenter.audioProgressDidChanged) { MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); - - if (messageObject.isMusic()) { + if (messageObject != null && messageObject.isMusic()) { updateProgress(messageObject); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java index ae365bbe0..92ff709ab 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java @@ -10,7 +10,6 @@ package org.telegram.ui; import android.content.Context; import android.database.Cursor; -import android.os.Build; import android.provider.MediaStore; import android.view.Gravity; import android.view.View; @@ -108,9 +107,7 @@ public class AudioSelectActivity extends BaseFragment implements NotificationCen listView.setDivider(null); listView.setDividerHeight(0); listView.setAdapter(listViewAdapter = new ListAdapter(context)); - if (Build.VERSION.SDK_INT >= 11) { - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 48)); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java index efde21e42..fb41fc92f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java @@ -118,9 +118,7 @@ public class BlockedUsersActivity extends BaseFragment implements NotificationCe listView.setDivider(null); listView.setDividerHeight(0); listView.setAdapter(listViewAdapter = new ListAdapter(context)); - if (Build.VERSION.SDK_INT >= 11) { - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java index ccd898b41..81355575a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java @@ -386,8 +386,6 @@ public class CacheControlActivity extends BaseFragment { SQLitePreparedStatement state5 = database.executeFast("REPLACE INTO messages_holes VALUES(?, ?, ?)"); SQLitePreparedStatement state6 = database.executeFast("REPLACE INTO media_holes_v2 VALUES(?, ?, ?, ?)"); - SQLitePreparedStatement state7 = database.executeFast("REPLACE INTO messages_imp_holes VALUES(?, ?, ?)"); - SQLitePreparedStatement state8 = database.executeFast("REPLACE INTO channel_group VALUES(?, ?, ?, ?)"); database.beginTransaction(); for (int a = 0; a < dialogsToCleanup.size(); a++) { @@ -403,21 +401,21 @@ public class CacheControlActivity extends BaseFragment { } cursor = database.queryFinalized("SELECT last_mid_i, last_mid FROM dialogs WHERE did = " + did); - ArrayList arrayList = new ArrayList<>(); + int messageId = -1; if (cursor.next()) { long last_mid_i = cursor.longValue(0); long last_mid = cursor.longValue(1); SQLiteCursor cursor2 = database.queryFinalized("SELECT data FROM messages WHERE uid = " + did + " AND mid IN (" + last_mid_i + "," + last_mid + ")"); try { while (cursor2.next()) { - NativeByteBuffer data = new NativeByteBuffer(cursor2.byteArrayLength(0)); - if (data != null && cursor2.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor2.byteBufferValue(0); + if (data != null) { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); if (message != null) { - arrayList.add(message); + messageId = message.id; } } - data.reuse(); } } catch (Exception e) { FileLog.e("tmessages", e); @@ -425,22 +423,20 @@ public class CacheControlActivity extends BaseFragment { cursor2.dispose(); database.executeFast("DELETE FROM messages WHERE uid = " + did + " AND mid != " + last_mid_i + " AND mid != " + last_mid).stepThis().dispose(); - database.executeFast("DELETE FROM channel_group WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM messages_holes WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM messages_imp_holes WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM bot_keyboard WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_counts_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_holes_v2 WHERE uid = " + did).stepThis().dispose(); BotQuery.clearBotKeyboard(did, null); - MessagesStorage.createFirstHoles(did, state5, state6, state7, state8, arrayList); + if (messageId != -1) { + MessagesStorage.createFirstHoles(did, state5, state6, messageId); + } } cursor.dispose(); } state5.dispose(); state6.dispose(); - state7.dispose(); - state8.dispose(); database.commitTransaction(); database.executeFast("VACUUM").stepThis().dispose(); } catch (Exception e) { @@ -518,7 +514,7 @@ public class CacheControlActivity extends BaseFragment { clear[a] = false; } } - BottomSheet.BottomSheetCell cell = new BottomSheet.BottomSheetCell(getParentActivity(), 2); + BottomSheet.BottomSheetCell cell = new BottomSheet.BottomSheetCell(getParentActivity(), 1); cell.setBackgroundResource(R.drawable.list_selector); cell.setTextAndIcon(LocaleController.getString("ClearMediaCache", R.string.ClearMediaCache).toUpperCase(), 0); cell.setTextColor(0xffcd5a5a); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/AboutLinkCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/AboutLinkCell.java index 160f91b83..f4aa9af9b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/AboutLinkCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/AboutLinkCell.java @@ -195,7 +195,13 @@ public class AboutLinkCell extends FrameLayout { if (pressedLink != null) { canvas.drawPath(urlPath, urlPaint); } - textLayout.draw(canvas); + try { + if (textLayout != null) { + textLayout.draw(canvas); + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } canvas.restore(); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java index 5e6f04df9..4a12f052b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java @@ -12,7 +12,6 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.RectF; import android.text.Layout; import android.text.Spannable; import android.text.StaticLayout; @@ -44,7 +43,6 @@ public class ChatActionCell extends BaseCell { private static TextPaint textPaint; private static Paint backPaint; - private static RectF rect; private URLSpan pressedLink; @@ -74,7 +72,6 @@ public class ChatActionCell extends BaseCell { textPaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); backPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - rect = new RectF(); } backPaint.setColor(ApplicationLoader.getServiceMessageColor()); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatBaseCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatBaseCell.java deleted file mode 100644 index a3cf79580..000000000 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatBaseCell.java +++ /dev/null @@ -1,1234 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.x. - * It is licensed under GNU GPL v. 2 or later. - * You should have received a copy of the license in this archive (see LICENSE). - * - * Copyright Nikolai Kudashov, 2013-2016. - */ - -package org.telegram.ui.Cells; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.text.Layout; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.StaticLayout; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.style.ClickableSpan; -import android.view.MotionEvent; -import android.view.SoundEffectConstants; - -import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.ContactsController; -import org.telegram.messenger.Emoji; -import org.telegram.messenger.LocaleController; -import org.telegram.messenger.MediaController; -import org.telegram.messenger.UserObject; -import org.telegram.messenger.FileLoader; -import org.telegram.messenger.FileLog; -import org.telegram.tgnet.TLRPC; -import org.telegram.messenger.MessagesController; -import org.telegram.messenger.R; -import org.telegram.messenger.MessageObject; -import org.telegram.messenger.ImageReceiver; -import org.telegram.ui.Components.AvatarDrawable; -import org.telegram.ui.ActionBar.Theme; -import org.telegram.ui.Components.TypefaceSpan; - -public class ChatBaseCell extends BaseCell implements MediaController.FileDownloadProgressListener { - - public interface ChatBaseCellDelegate { - void didPressedUserAvatar(ChatBaseCell cell, TLRPC.User user); - void didPressedViaBot(ChatBaseCell cell, String username); - void didPressedChannelAvatar(ChatBaseCell cell, TLRPC.Chat chat, int postId); - void didPressedCancelSendButton(ChatBaseCell cell); - void didLongPressed(ChatBaseCell cell); - void didPressedReplyMessage(ChatBaseCell cell, int id); - void didPressedUrl(MessageObject messageObject, ClickableSpan url, boolean longPress); - void needOpenWebView(String url, String title, String description, String originalUrl, int w, int h); - void didPressedImage(ChatBaseCell cell); - void didPressedShare(ChatBaseCell cell); - void didPressedOther(ChatBaseCell cell); - void didPressedBotButton(ChatBaseCell cell, TLRPC.KeyboardButton button); - boolean needPlayAudio(MessageObject messageObject); - boolean canPerformActions(); - } - - private int TAG; - - public boolean isChat; - protected boolean isPressed; - protected boolean forwardName; - protected boolean isHighlighted; - protected boolean mediaBackground; - protected boolean isCheckPressed = true; - private boolean wasLayout; - protected boolean isAvatarVisible; - protected boolean drawBackground = true; - protected int substractBackgroundHeight; - protected boolean allowAssistant; - protected Drawable currentBackgroundDrawable; - protected MessageObject currentMessageObject; - private int viaWidth; - private int viaNameWidth; - protected int availableTimeWidth; - - protected static TextPaint timePaint; - private static TextPaint namePaint; - private static TextPaint forwardNamePaint; - protected static TextPaint replyNamePaint; - protected static TextPaint replyTextPaint; - protected static Paint replyLinePaint; - - protected int backgroundWidth = 100; - - protected int layoutWidth; - protected int layoutHeight; - - private ImageReceiver avatarImage; - private AvatarDrawable avatarDrawable; - private boolean avatarPressed; - private boolean forwardNamePressed; - private boolean forwardBotPressed; - - private StaticLayout replyNameLayout; - private StaticLayout replyTextLayout; - private ImageReceiver replyImageReceiver; - private int replyStartX; - private int replyStartY; - protected int replyNameWidth; - private float replyNameOffset; - protected int replyTextWidth; - private float replyTextOffset; - private boolean needReplyImage; - private boolean replyPressed; - private TLRPC.FileLocation currentReplyPhoto; - - protected boolean drawShareButton; - private boolean sharePressed; - private int shareStartX; - private int shareStartY; - - private StaticLayout nameLayout; - protected int nameWidth; - private float nameOffsetX; - private float nameX; - private float nameY; - protected boolean drawName; - protected boolean drawNameLayout; - - private StaticLayout[] forwardedNameLayout = new StaticLayout[2]; - protected int forwardedNameWidth; - protected boolean drawForwardedName; - private int forwardNameX; - private int forwardNameY; - private float forwardNameOffsetX[] = new float[2]; - - private StaticLayout timeLayout; - protected int timeWidth; - private int timeTextWidth; - private int timeX; - private String currentTimeString; - protected boolean drawTime = true; - - private StaticLayout viewsLayout; - private int viewsTextWidth; - private String currentViewsString; - - private TLRPC.User currentUser; - private TLRPC.Chat currentChat; - private TLRPC.FileLocation currentPhoto; - private String currentNameString; - - private TLRPC.User currentForwardUser; - private TLRPC.User currentViaBotUser; - private TLRPC.Chat currentForwardChannel; - private String currentForwardNameString; - - protected ChatBaseCellDelegate delegate; - - protected int namesOffset; - - private int lastSendState; - private int lastDeleteDate; - private int lastViewsCount; - - public ChatBaseCell(Context context) { - super(context); - if (timePaint == null) { - timePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); - timePaint.setTextSize(AndroidUtilities.dp(12)); - - namePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); - namePaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - namePaint.setTextSize(AndroidUtilities.dp(14)); - - forwardNamePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); - forwardNamePaint.setTextSize(AndroidUtilities.dp(14)); - - replyNamePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); - replyNamePaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - replyNamePaint.setTextSize(AndroidUtilities.dp(14)); - - replyTextPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); - replyTextPaint.setTextSize(AndroidUtilities.dp(14)); - replyTextPaint.linkColor = Theme.MSG_LINK_TEXT_COLOR; - - replyLinePaint = new Paint(); - } - avatarImage = new ImageReceiver(this); - avatarImage.setRoundRadius(AndroidUtilities.dp(21)); - avatarDrawable = new AvatarDrawable(); - replyImageReceiver = new ImageReceiver(this); - TAG = MediaController.getInstance().generateObserverTag(); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - avatarImage.onDetachedFromWindow(); - replyImageReceiver.onDetachedFromWindow(); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - avatarImage.onAttachedToWindow(); - replyImageReceiver.onAttachedToWindow(); - } - - @Override - public void setPressed(boolean pressed) { - super.setPressed(pressed); - invalidate(); - } - - public void setDelegate(ChatBaseCellDelegate delegate) { - this.delegate = delegate; - } - - public void setHighlighted(boolean value) { - if (isHighlighted == value) { - return; - } - isHighlighted = value; - invalidate(); - } - - public void setCheckPressed(boolean value, boolean pressed) { - isCheckPressed = value; - isPressed = pressed; - invalidate(); - } - - public void setAllowAssistant(boolean value) { - allowAssistant = value; - } - - protected boolean isUserDataChanged() { - if (currentMessageObject == null || currentUser == null && currentChat == null) { - return false; - } - if (lastSendState != currentMessageObject.messageOwner.send_state) { - return true; - } - if (lastDeleteDate != currentMessageObject.messageOwner.destroyTime) { - return true; - } - if (lastViewsCount != currentMessageObject.messageOwner.views) { - return true; - } - - TLRPC.User newUser = null; - TLRPC.Chat newChat = null; - if (currentMessageObject.isFromUser()) { - newUser = MessagesController.getInstance().getUser(currentMessageObject.messageOwner.from_id); - } else if (currentMessageObject.messageOwner.from_id < 0) { - newChat = MessagesController.getInstance().getChat(-currentMessageObject.messageOwner.from_id); - } else if (currentMessageObject.messageOwner.post) { - newChat = MessagesController.getInstance().getChat(currentMessageObject.messageOwner.to_id.channel_id); - } - TLRPC.FileLocation newPhoto = null; - - if (isAvatarVisible) { - if (newUser != null && newUser.photo != null){ - newPhoto = newUser.photo.photo_small; - } else if (newChat != null && newChat.photo != null) { - newPhoto = newChat.photo.photo_small; - } - } - - if (replyTextLayout == null && currentMessageObject.replyMessageObject != null) { - return true; - } - - if (currentPhoto == null && newPhoto != null || currentPhoto != null && newPhoto == null || currentPhoto != null && newPhoto != null && (currentPhoto.local_id != newPhoto.local_id || currentPhoto.volume_id != newPhoto.volume_id)) { - return true; - } - - TLRPC.FileLocation newReplyPhoto = null; - - if (currentMessageObject.replyMessageObject != null) { - TLRPC.PhotoSize photoSize = FileLoader.getClosestPhotoSizeWithSize(currentMessageObject.replyMessageObject.photoThumbs, 80); - if (photoSize != null && currentMessageObject.replyMessageObject.type != 13) { - newReplyPhoto = photoSize.location; - } - } - - if (currentReplyPhoto == null && newReplyPhoto != null) { - return true; - } - - String newNameString = null; - if (drawName && isChat && !currentMessageObject.isOutOwner()) { - if (newUser != null) { - newNameString = UserObject.getUserName(newUser); - } else if (newChat != null) { - newNameString = newChat.title; - } - } - - if (currentNameString == null && newNameString != null || currentNameString != null && newNameString == null || currentNameString != null && newNameString != null && !currentNameString.equals(newNameString)) { - return true; - } - - if (drawForwardedName) { - newNameString = currentMessageObject.getForwardedName(); - return currentForwardNameString == null && newNameString != null || currentForwardNameString != null && newNameString == null || currentForwardNameString != null && newNameString != null && !currentForwardNameString.equals(newNameString); - } - return false; - } - - protected void measureTime(MessageObject messageObject) { - boolean hasSign = !messageObject.isOutOwner() && messageObject.messageOwner.from_id > 0 && messageObject.messageOwner.post; - TLRPC.User signUser = MessagesController.getInstance().getUser(messageObject.messageOwner.from_id); - if (hasSign && signUser == null) { - hasSign = false; - } - if (hasSign) { - currentTimeString = ", " + LocaleController.getInstance().formatterDay.format((long) (messageObject.messageOwner.date) * 1000); - } else { - currentTimeString = LocaleController.getInstance().formatterDay.format((long) (messageObject.messageOwner.date) * 1000); - } - timeTextWidth = timeWidth = (int) Math.ceil(timePaint.measureText(currentTimeString)); - if ((messageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { - currentViewsString = String.format("%s", LocaleController.formatShortNumber(Math.max(1, messageObject.messageOwner.views), null)); - viewsTextWidth = (int) Math.ceil(timePaint.measureText(currentViewsString)); - timeWidth += viewsTextWidth + Theme.viewsCountDrawable[0].getIntrinsicWidth() + AndroidUtilities.dp(10); - } - if (hasSign) { - if (availableTimeWidth == 0) { - availableTimeWidth = AndroidUtilities.dp(1000); - } - CharSequence name = ContactsController.formatName(signUser.first_name, signUser.last_name).replace('\n', ' '); - int widthForSign = availableTimeWidth - timeWidth; - int width = (int) Math.ceil(timePaint.measureText(name, 0, name.length())); - if (width > widthForSign) { - name = TextUtils.ellipsize(name, timePaint, widthForSign, TextUtils.TruncateAt.END); - width = widthForSign; - } - currentTimeString = name + currentTimeString; - timeTextWidth += width; - timeWidth += width; - } - } - - protected boolean checkNeedDrawShareButton(MessageObject messageObject) { - if (messageObject.isFromUser()) { - TLRPC.User user = MessagesController.getInstance().getUser(messageObject.messageOwner.from_id); - if (user != null && user.bot && messageObject.type != 13 && !(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaEmpty || messageObject.messageOwner.media == null - || messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage && !(messageObject.messageOwner.media.webpage instanceof TLRPC.TL_webPage))) { - return true; - } - } else if (messageObject.messageOwner.from_id < 0 || messageObject.messageOwner.post) { - if (messageObject.messageOwner.to_id.channel_id != 0 && (messageObject.messageOwner.via_bot_id == 0 && messageObject.messageOwner.reply_to_msg_id == 0 || messageObject.type != 13)) { - return true; - } - } - return false; - } - - public void setMessageObject(MessageObject messageObject) { - currentMessageObject = messageObject; - lastSendState = messageObject.messageOwner.send_state; - lastDeleteDate = messageObject.messageOwner.destroyTime; - lastViewsCount = messageObject.messageOwner.views; - isPressed = false; - isCheckPressed = true; - isAvatarVisible = false; - wasLayout = false; - drawShareButton = checkNeedDrawShareButton(messageObject); - replyNameLayout = null; - replyTextLayout = null; - replyNameWidth = 0; - replyTextWidth = 0; - viaWidth = 0; - viaNameWidth = 0; - currentReplyPhoto = null; - currentUser = null; - currentChat = null; - currentViaBotUser = null; - drawNameLayout = false; - - if ((messageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { - if (currentMessageObject.isContentUnread() && !currentMessageObject.isOut()) { - MessagesController.getInstance().addToViewsQueue(currentMessageObject.messageOwner, false); - currentMessageObject.setContentIsRead(); - } else if (!currentMessageObject.viewsReloaded) { - MessagesController.getInstance().addToViewsQueue(currentMessageObject.messageOwner, true); - currentMessageObject.viewsReloaded = true; - } - } - - if (currentMessageObject.isFromUser()) { - currentUser = MessagesController.getInstance().getUser(currentMessageObject.messageOwner.from_id); - } else if (currentMessageObject.messageOwner.from_id < 0) { - currentChat = MessagesController.getInstance().getChat(-currentMessageObject.messageOwner.from_id); - } else if (currentMessageObject.messageOwner.post) { - currentChat = MessagesController.getInstance().getChat(currentMessageObject.messageOwner.to_id.channel_id); - } - - if (isChat && !messageObject.isOutOwner() && messageObject.isFromUser()) { - isAvatarVisible = true; - if (currentUser != null) { - if (currentUser.photo != null) { - currentPhoto = currentUser.photo.photo_small; - } else { - currentPhoto = null; - } - avatarDrawable.setInfo(currentUser); - } else if (currentChat != null) { - if (currentChat.photo != null) { - currentPhoto = currentChat.photo.photo_small; - } else { - currentPhoto = null; - } - avatarDrawable.setInfo(currentChat); - } else { - currentPhoto = null; - avatarDrawable.setInfo(messageObject.messageOwner.from_id, null, null, false); - } - avatarImage.setImage(currentPhoto, "50_50", avatarDrawable, null, false); - } - - - measureTime(messageObject); - - namesOffset = 0; - - String viaUsername = null; - CharSequence viaString = null; - if (messageObject.messageOwner.via_bot_id != 0) { - TLRPC.User botUser = MessagesController.getInstance().getUser(messageObject.messageOwner.via_bot_id); - if (botUser != null && botUser.username != null && botUser.username.length() > 0) { - viaUsername = "@" + botUser.username; - viaString = AndroidUtilities.replaceTags(String.format(" via %s", viaUsername)); - viaWidth = (int) Math.ceil(replyNamePaint.measureText(viaString, 0, viaString.length())); - currentViaBotUser = botUser; - } - } else if (messageObject.messageOwner.via_bot_name != null && messageObject.messageOwner.via_bot_name.length() > 0) { - viaUsername = "@" + messageObject.messageOwner.via_bot_name; - viaString = AndroidUtilities.replaceTags(String.format(" via %s", viaUsername)); - viaWidth = (int) Math.ceil(replyNamePaint.measureText(viaString, 0, viaString.length())); - } - - boolean authorName = drawName && isChat && !currentMessageObject.isOutOwner(); - boolean viaBot = (messageObject.messageOwner.fwd_from == null || messageObject.type == 14) && viaUsername != null; - if (authorName || viaBot) { - drawNameLayout = true; - nameWidth = getMaxNameWidth(); - if (nameWidth < 0) { - nameWidth = AndroidUtilities.dp(100); - } - - if (authorName) { - if (currentUser != null) { - currentNameString = UserObject.getUserName(currentUser); - } else if (currentChat != null) { - currentNameString = currentChat.title; - } else { - currentNameString = "DELETED"; - } - } else { - currentNameString = ""; - } - CharSequence nameStringFinal = TextUtils.ellipsize(currentNameString.replace('\n', ' '), namePaint, nameWidth - (viaBot ? viaWidth : 0), TextUtils.TruncateAt.END); - if (viaBot) { - viaNameWidth = (int) Math.ceil(namePaint.measureText(nameStringFinal, 0, nameStringFinal.length())); - if (viaNameWidth != 0) { - viaNameWidth += AndroidUtilities.dp(4); - } - int color; - if (currentMessageObject.type == 13) { - color = Theme.MSG_STICKER_VIA_BOT_NAME_TEXT_COLOR; - } else { - color = currentMessageObject.isOutOwner() ? Theme.MSG_OUT_VIA_BOT_NAME_TEXT_COLOR : Theme.MSG_IN_VIA_BOT_NAME_TEXT_COLOR; - } - if (currentNameString.length() > 0) { - SpannableStringBuilder stringBuilder = new SpannableStringBuilder(String.format("%s via %s", nameStringFinal, viaUsername)); - stringBuilder.setSpan(new TypefaceSpan(Typeface.DEFAULT, 0, color), nameStringFinal.length() + 1, nameStringFinal.length() + 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - stringBuilder.setSpan(new TypefaceSpan(AndroidUtilities.getTypeface("fonts/rmedium.ttf"), 0, color), nameStringFinal.length() + 5, stringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - nameStringFinal = stringBuilder; - } else { - SpannableStringBuilder stringBuilder = new SpannableStringBuilder(String.format("via %s", viaUsername)); - stringBuilder.setSpan(new TypefaceSpan(Typeface.DEFAULT, 0, color), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - stringBuilder.setSpan(new TypefaceSpan(AndroidUtilities.getTypeface("fonts/rmedium.ttf"), 0, color), 4, stringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - nameStringFinal = stringBuilder; - } - nameStringFinal = TextUtils.ellipsize(nameStringFinal, namePaint, nameWidth, TextUtils.TruncateAt.END); - } - try { - nameLayout = new StaticLayout(nameStringFinal, namePaint, nameWidth + AndroidUtilities.dp(2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - if (nameLayout != null && nameLayout.getLineCount() > 0) { - nameWidth = (int) Math.ceil(nameLayout.getLineWidth(0)); - if (messageObject.type != 13) { - namesOffset += AndroidUtilities.dp(19); - } - nameOffsetX = nameLayout.getLineLeft(0); - } else { - nameWidth = 0; - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } - if (currentNameString.length() == 0) { - currentNameString = null; - } - } else { - currentNameString = null; - nameLayout = null; - nameWidth = 0; - } - - currentForwardUser = null; - currentForwardNameString = null; - forwardedNameLayout[0] = null; - forwardedNameLayout[1] = null; - forwardedNameWidth = 0; - if (drawForwardedName && messageObject.isForwarded()) { - currentForwardChannel = null; - if (messageObject.messageOwner.fwd_from.channel_id != 0) { - currentForwardChannel = MessagesController.getInstance().getChat(messageObject.messageOwner.fwd_from.channel_id); - } - if (messageObject.messageOwner.fwd_from.from_id != 0) { - currentForwardUser = MessagesController.getInstance().getUser(messageObject.messageOwner.fwd_from.from_id); - } - - if (currentForwardUser != null || currentForwardChannel != null) { - if (currentForwardChannel != null) { - if (currentForwardUser != null) { - currentForwardNameString = String.format("%s (%s)", currentForwardChannel.title, UserObject.getUserName(currentForwardUser)); - } else { - currentForwardNameString = currentForwardChannel.title; - } - } else if (currentForwardUser != null) { - currentForwardNameString = UserObject.getUserName(currentForwardUser); - } - - forwardedNameWidth = getMaxNameWidth(); - int fromWidth = (int) Math.ceil(forwardNamePaint.measureText(LocaleController.getString("From", R.string.From) + " ")); - CharSequence name = TextUtils.ellipsize(currentForwardNameString.replace('\n', ' '), replyNamePaint, forwardedNameWidth - fromWidth - viaWidth, TextUtils.TruncateAt.END); - CharSequence lastLine; - if (viaString != null) { - viaNameWidth = (int) Math.ceil(forwardNamePaint.measureText(LocaleController.getString("From", R.string.From) + " " + name)); - lastLine = AndroidUtilities.replaceTags(String.format("%s %s via %s", LocaleController.getString("From", R.string.From), name, viaUsername)); - } else { - lastLine = AndroidUtilities.replaceTags(String.format("%s %s", LocaleController.getString("From", R.string.From), name)); - } - lastLine = TextUtils.ellipsize(lastLine, forwardNamePaint, forwardedNameWidth, TextUtils.TruncateAt.END); - try { - forwardedNameLayout[1] = new StaticLayout(lastLine, forwardNamePaint, forwardedNameWidth + AndroidUtilities.dp(2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - lastLine = TextUtils.ellipsize(AndroidUtilities.replaceTags(LocaleController.getString("ForwardedMessage", R.string.ForwardedMessage)), forwardNamePaint, forwardedNameWidth, TextUtils.TruncateAt.END); - forwardedNameLayout[0] = new StaticLayout(lastLine, forwardNamePaint, forwardedNameWidth + AndroidUtilities.dp(2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - 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); - } catch (Exception e) { - FileLog.e("tmessages", e); - } - } - } - - if (messageObject.isReply()) { - namesOffset += AndroidUtilities.dp(42); - if (messageObject.type != 0) { - if (messageObject.type == 13) { - namesOffset -= AndroidUtilities.dp(42); - } else { - namesOffset += AndroidUtilities.dp(5); - } - } - - int maxWidth = getMaxNameWidth(); - if (messageObject.type != 13) { - maxWidth -= AndroidUtilities.dp(10); - } - - CharSequence stringFinalName = null; - CharSequence stringFinalText = null; - if (messageObject.replyMessageObject != null) { - TLRPC.PhotoSize photoSize = FileLoader.getClosestPhotoSizeWithSize(messageObject.replyMessageObject.photoThumbs, 80); - if (photoSize == null || messageObject.replyMessageObject.type == 13 || messageObject.type == 13 && !AndroidUtilities.isTablet() || messageObject.replyMessageObject.isSecretMedia()) { - replyImageReceiver.setImageBitmap((Drawable) null); - needReplyImage = false; - } else { - currentReplyPhoto = photoSize.location; - replyImageReceiver.setImage(photoSize.location, "50_50", null, null, true); - needReplyImage = true; - maxWidth -= AndroidUtilities.dp(44); - } - - String name = null; - if (messageObject.replyMessageObject.isFromUser()) { - TLRPC.User user = MessagesController.getInstance().getUser(messageObject.replyMessageObject.messageOwner.from_id); - if (user != null) { - name = UserObject.getUserName(user); - } - } else if (messageObject.replyMessageObject.messageOwner.from_id < 0) { - TLRPC.Chat chat = MessagesController.getInstance().getChat(-messageObject.replyMessageObject.messageOwner.from_id); - if (chat != null) { - name = chat.title; - } - } else { - TLRPC.Chat chat = MessagesController.getInstance().getChat(messageObject.replyMessageObject.messageOwner.to_id.channel_id); - if (chat != null) { - name = chat.title; - } - } - - if (name != null) { - stringFinalName = TextUtils.ellipsize(name.replace('\n', ' '), replyNamePaint, maxWidth, TextUtils.TruncateAt.END); - } - if (messageObject.replyMessageObject.messageText != null && messageObject.replyMessageObject.messageText.length() > 0) { - String mess = messageObject.replyMessageObject.messageText.toString(); - if (mess.length() > 150) { - mess = mess.substring(0, 150); - } - mess = mess.replace('\n', ' '); - stringFinalText = Emoji.replaceEmoji(mess, replyTextPaint.getFontMetricsInt(), AndroidUtilities.dp(14), false); - stringFinalText = TextUtils.ellipsize(stringFinalText, replyTextPaint, maxWidth, TextUtils.TruncateAt.END); - } - } - if (stringFinalName == null) { - stringFinalName = LocaleController.getString("Loading", R.string.Loading); - } - try { - replyNameLayout = new StaticLayout(stringFinalName, 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); - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } - try { - if (stringFinalText != null) { - replyTextLayout = new StaticLayout(stringFinalText, 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)); - replyTextOffset = replyTextLayout.getLineLeft(0); - } - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } - } - - requestLayout(); - } - - public final MessageObject getMessageObject() { - return currentMessageObject; - } - - protected int getMaxNameWidth() { - return backgroundWidth - AndroidUtilities.dp(mediaBackground ? 22 : 31); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - boolean result = false; - float x = event.getX(); - float y = event.getY(); - if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (delegate == null || delegate.canPerformActions()) { - if (isAvatarVisible && avatarImage.isInsideImage(x, y)) { - avatarPressed = true; - result = true; - } else if (drawForwardedName && forwardedNameLayout[0] != null && x >= forwardNameX && x <= forwardNameX + forwardedNameWidth && y >= forwardNameY && y <= forwardNameY + AndroidUtilities.dp(32)) { - if (viaWidth != 0 && x >= forwardNameX + viaNameWidth + AndroidUtilities.dp(4)) { - forwardBotPressed = true; - } else { - forwardNamePressed = true; - } - result = true; - } else if (drawNameLayout && nameLayout != null && viaWidth != 0 && x >= nameX + viaNameWidth && x <= nameX + viaNameWidth + viaWidth && y >= nameY - AndroidUtilities.dp(4) && y <= nameY + AndroidUtilities.dp(20)) { - forwardBotPressed = true; - result = true; - } else if (currentMessageObject.isReply() && x >= replyStartX && x <= replyStartX + Math.max(replyNameWidth, replyTextWidth) && y >= replyStartY && y <= replyStartY + AndroidUtilities.dp(35)) { - replyPressed = true; - result = true; - } else if (drawShareButton && x >= shareStartX && x <= shareStartX + AndroidUtilities.dp(40) && y >= shareStartY && y <= shareStartY + AndroidUtilities.dp(32)) { - sharePressed = true; - result = true; - invalidate(); - } - if (result) { - startCheckLongPress(); - } - } - } else { - if (event.getAction() != MotionEvent.ACTION_MOVE) { - cancelCheckLongPress(); - } - if (avatarPressed) { - if (event.getAction() == MotionEvent.ACTION_UP) { - avatarPressed = false; - playSoundEffect(SoundEffectConstants.CLICK); - if (delegate != null) { - if (currentUser != null) { - delegate.didPressedUserAvatar(this, currentUser); - } else if (currentChat != null) { - delegate.didPressedChannelAvatar(this, currentChat, 0); - } - } - } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { - avatarPressed = false; - } else if (event.getAction() == MotionEvent.ACTION_MOVE) { - if (isAvatarVisible && !avatarImage.isInsideImage(x, y)) { - avatarPressed = false; - } - } - } else if (forwardNamePressed) { - if (event.getAction() == MotionEvent.ACTION_UP) { - forwardNamePressed = false; - playSoundEffect(SoundEffectConstants.CLICK); - if (delegate != null) { - if (currentForwardChannel != null) { - delegate.didPressedChannelAvatar(this, currentForwardChannel, currentMessageObject.messageOwner.fwd_from.channel_post); - } else if (currentForwardUser != null) { - delegate.didPressedUserAvatar(this, currentForwardUser); - } - } - } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { - forwardNamePressed = false; - } else if (event.getAction() == MotionEvent.ACTION_MOVE) { - if (!(x >= forwardNameX && x <= forwardNameX + forwardedNameWidth && y >= forwardNameY && y <= forwardNameY + AndroidUtilities.dp(32))) { - forwardNamePressed = false; - } - } - } else if (forwardBotPressed) { - if (event.getAction() == MotionEvent.ACTION_UP) { - forwardBotPressed = false; - playSoundEffect(SoundEffectConstants.CLICK); - if (delegate != null) { - delegate.didPressedViaBot(this, currentViaBotUser != null ? currentViaBotUser.username : currentMessageObject.messageOwner.via_bot_name); - } - } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { - forwardBotPressed = false; - } else if (event.getAction() == MotionEvent.ACTION_MOVE) { - if (drawForwardedName && forwardedNameLayout[0] != null) { - if (!(x >= forwardNameX && x <= forwardNameX + forwardedNameWidth && y >= forwardNameY && y <= forwardNameY + AndroidUtilities.dp(32))) { - forwardBotPressed = false; - } - } else { - if (!(x >= nameX + viaNameWidth && x <= nameX + viaNameWidth + viaWidth && y >= nameY - AndroidUtilities.dp(4) && y <= nameY + AndroidUtilities.dp(20))) { - forwardBotPressed = false; - } - } - } - } else if (replyPressed) { - if (event.getAction() == MotionEvent.ACTION_UP) { - replyPressed = false; - playSoundEffect(SoundEffectConstants.CLICK); - if (delegate != null) { - delegate.didPressedReplyMessage(this, currentMessageObject.messageOwner.reply_to_msg_id); - } - } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { - replyPressed = false; - } else if (event.getAction() == MotionEvent.ACTION_MOVE) { - if (!(x >= replyStartX && x <= replyStartX + Math.max(replyNameWidth, replyTextWidth) && y >= replyStartY && y <= replyStartY + AndroidUtilities.dp(35))) { - replyPressed = false; - } - } - } else if (sharePressed) { - if (event.getAction() == MotionEvent.ACTION_UP) { - sharePressed = false; - playSoundEffect(SoundEffectConstants.CLICK); - if (delegate != null) { - delegate.didPressedShare(this); - } - } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { - sharePressed = false; - } else if (event.getAction() == MotionEvent.ACTION_MOVE) { - if (!(x >= shareStartX && x <= shareStartX + AndroidUtilities.dp(40) && y >= shareStartY && y <= shareStartY + AndroidUtilities.dp(32))) { - sharePressed = false; - } - } - invalidate(); - } - } - return result; - } - - @SuppressLint("DrawAllocation") - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (currentMessageObject == null) { - super.onLayout(changed, left, top, right, bottom); - return; - } - - if (changed || !wasLayout) { - layoutWidth = getMeasuredWidth(); - layoutHeight = getMeasuredHeight() - substractBackgroundHeight; - if (timeTextWidth < 0) { - timeTextWidth = AndroidUtilities.dp(10); - } - timeLayout = new StaticLayout(currentTimeString, timePaint, timeTextWidth + AndroidUtilities.dp(6), 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); - } 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); - } else { - timeX = layoutWidth - timeWidth - AndroidUtilities.dp(42.0f); - } - } - - if ((currentMessageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { - viewsLayout = new StaticLayout(currentViewsString, timePaint, viewsTextWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - } else { - viewsLayout = null; - } - - if (isAvatarVisible) { - avatarImage.setImageCoords(AndroidUtilities.dp(6), layoutHeight - AndroidUtilities.dp(44), AndroidUtilities.dp(42), AndroidUtilities.dp(42)); - } - - wasLayout = true; - } - } - - protected void drawContent(Canvas canvas) { - - } - - public ImageReceiver getPhotoImage() { - return null; - } - - @Override - protected void onLongPress() { - if (delegate != null) { - delegate.didLongPressed(this); - } - } - - protected boolean isDrawSelectedBackground() { - return isPressed() && isCheckPressed || !isCheckPressed && isPressed || isHighlighted; - } - - @Override - protected void onDraw(Canvas canvas) { - if (currentMessageObject == null) { - return; - } - - if (!wasLayout) { - requestLayout(); - return; - } - - if (isAvatarVisible) { - avatarImage.draw(canvas); - } - - if (mediaBackground) { - timePaint.setColor(Theme.MSG_MEDIA_TIME_TEXT_COLOR); - } else { - if (currentMessageObject.isOutOwner()) { - timePaint.setColor(isDrawSelectedBackground() ? Theme.MSG_OUT_TIME_SELECTED_TEXT_COLOR : Theme.MSG_OUT_TIME_TEXT_COLOR); - } else { - timePaint.setColor(isDrawSelectedBackground() ? Theme.MSG_IN_TIME_SELECTED_TEXT_COLOR : Theme.MSG_IN_TIME_TEXT_COLOR); - } - } - - if (currentMessageObject.isOutOwner()) { - if (isDrawSelectedBackground()) { - if (!mediaBackground) { - currentBackgroundDrawable = Theme.backgroundDrawableOutSelected; - } else { - currentBackgroundDrawable = Theme.backgroundMediaDrawableOutSelected; - } - } else { - if (!mediaBackground) { - currentBackgroundDrawable = Theme.backgroundDrawableOut; - } else { - currentBackgroundDrawable = Theme.backgroundMediaDrawableOut; - } - } - setDrawableBounds(currentBackgroundDrawable, layoutWidth - backgroundWidth - (!mediaBackground ? 0 : AndroidUtilities.dp(9)), AndroidUtilities.dp(1), backgroundWidth - (mediaBackground ? 0 : AndroidUtilities.dp(3)), layoutHeight - AndroidUtilities.dp(2)); - } else { - if (isDrawSelectedBackground()) { - if (!mediaBackground) { - currentBackgroundDrawable = Theme.backgroundDrawableInSelected; - } else { - currentBackgroundDrawable = Theme.backgroundMediaDrawableInSelected; - } - } else { - if (!mediaBackground) { - currentBackgroundDrawable = Theme.backgroundDrawableIn; - } else { - currentBackgroundDrawable = Theme.backgroundMediaDrawableIn; - } - } - if (isChat && currentMessageObject.isFromUser()) { - setDrawableBounds(currentBackgroundDrawable, AndroidUtilities.dp(48 + (!mediaBackground ? 3 : 9)), AndroidUtilities.dp(1), backgroundWidth - (mediaBackground ? 0 : AndroidUtilities.dp(3)), layoutHeight - AndroidUtilities.dp(2)); - } else { - setDrawableBounds(currentBackgroundDrawable, (!mediaBackground ? AndroidUtilities.dp(3) : AndroidUtilities.dp(9)), AndroidUtilities.dp(1), backgroundWidth - (mediaBackground ? 0 : AndroidUtilities.dp(3)), layoutHeight - AndroidUtilities.dp(2)); - } - } - if (drawBackground && currentBackgroundDrawable != null) { - currentBackgroundDrawable.draw(canvas); - } - - drawContent(canvas); - - if (drawShareButton) { - Theme.shareDrawable.setColorFilter(sharePressed ? Theme.colorPressedFilter : Theme.colorFilter); - setDrawableBounds(Theme.shareDrawable, shareStartX = currentBackgroundDrawable.getBounds().right + AndroidUtilities.dp(8), shareStartY = layoutHeight - AndroidUtilities.dp(41)); - Theme.shareDrawable.draw(canvas); - setDrawableBounds(Theme.shareIconDrawable, shareStartX + AndroidUtilities.dp(9), shareStartY + AndroidUtilities.dp(9)); - Theme.shareIconDrawable.draw(canvas); - } - - if (drawNameLayout && nameLayout != null) { - canvas.save(); - - if (currentMessageObject.type == 13) { - namePaint.setColor(Theme.MSG_STICKER_NAME_TEXT_COLOR); - int backWidth; - if (currentMessageObject.isOutOwner()) { - nameX = AndroidUtilities.dp(28); - } else { - nameX = currentBackgroundDrawable.getBounds().right + AndroidUtilities.dp(22); - } - nameY = layoutHeight - AndroidUtilities.dp(38); - Theme.systemDrawable.setColorFilter(Theme.colorFilter); - Theme.systemDrawable.setBounds((int) nameX - AndroidUtilities.dp(12), (int) nameY - AndroidUtilities.dp(5), (int) nameX + AndroidUtilities.dp(12) + nameWidth, (int) nameY + AndroidUtilities.dp(22)); - Theme.systemDrawable.draw(canvas); - } else { - if (mediaBackground || currentMessageObject.isOutOwner()) { - nameX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(11) - nameOffsetX; - } else { - nameX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(17) - nameOffsetX; - } - if (currentUser != null) { - namePaint.setColor(AvatarDrawable.getNameColorForId(currentUser.id)); - } else if (currentChat != null) { - namePaint.setColor(AvatarDrawable.getNameColorForId(currentChat.id)); - } else { - namePaint.setColor(AvatarDrawable.getNameColorForId(0)); - } - nameY = AndroidUtilities.dp(10); - } - canvas.translate(nameX, nameY); - nameLayout.draw(canvas); - canvas.restore(); - } - - if (drawForwardedName && forwardedNameLayout[0] != null && forwardedNameLayout[1] != null) { - forwardNameY = AndroidUtilities.dp(10 + (drawNameLayout ? 19 : 0)); - if (currentMessageObject.isOutOwner()) { - forwardNamePaint.setColor(Theme.MSG_OUT_FORDWARDED_NAME_TEXT_COLOR); - forwardNameX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(11); - } else { - forwardNamePaint.setColor(Theme.MSG_IN_FORDWARDED_NAME_TEXT_COLOR); - if (mediaBackground) { - forwardNameX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(11); - } else { - forwardNameX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(17); - } - } - for (int a = 0; a < 2; a++) { - canvas.save(); - canvas.translate(forwardNameX - forwardNameOffsetX[a], forwardNameY + AndroidUtilities.dp(16) * a); - forwardedNameLayout[a].draw(canvas); - canvas.restore(); - } - } - - if (currentMessageObject.isReply()) { - if (currentMessageObject.type == 13) { - replyLinePaint.setColor(Theme.MSG_STICKER_REPLY_LINE_COLOR); - replyNamePaint.setColor(Theme.MSG_STICKER_REPLY_NAME_TEXT_COLOR); - replyTextPaint.setColor(Theme.MSG_STICKER_REPLY_MESSAGE_TEXT_COLOR); - if (currentMessageObject.isOutOwner()) { - replyStartX = AndroidUtilities.dp(23); - } else { - replyStartX = currentBackgroundDrawable.getBounds().right + AndroidUtilities.dp(17); - } - replyStartY = layoutHeight - AndroidUtilities.dp(58); - if (nameLayout != null) { - replyStartY -= AndroidUtilities.dp(25 + 6); - } - int backWidth = Math.max(replyNameWidth, replyTextWidth) + AndroidUtilities.dp(14 + (needReplyImage ? 44 : 0)); - Theme.systemDrawable.setColorFilter(Theme.colorFilter); - Theme.systemDrawable.setBounds(replyStartX - AndroidUtilities.dp(7), replyStartY - AndroidUtilities.dp(6), replyStartX - AndroidUtilities.dp(7) + backWidth, replyStartY + AndroidUtilities.dp(41)); - Theme.systemDrawable.draw(canvas); - } else { - if (currentMessageObject.isOutOwner()) { - replyLinePaint.setColor(Theme.MSG_OUT_REPLY_LINE_COLOR); - replyNamePaint.setColor(Theme.MSG_OUT_REPLY_NAME_TEXT_COLOR); - if (currentMessageObject.replyMessageObject != null && currentMessageObject.replyMessageObject.type == 0) { - replyTextPaint.setColor(Theme.MSG_OUT_REPLY_MESSAGE_TEXT_COLOR); - } else { - replyTextPaint.setColor(isDrawSelectedBackground() ? Theme.MSG_OUT_REPLY_MEDIA_MESSAGE_SELETED_TEXT_COLOR : Theme.MSG_OUT_REPLY_MEDIA_MESSAGE_TEXT_COLOR); - } - replyStartX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(12); - } else { - replyLinePaint.setColor(Theme.MSG_IN_REPLY_LINE_COLOR); - replyNamePaint.setColor(Theme.MSG_IN_REPLY_NAME_TEXT_COLOR); - if (currentMessageObject.replyMessageObject != null && currentMessageObject.replyMessageObject.type == 0) { - replyTextPaint.setColor(Theme.MSG_IN_REPLY_MESSAGE_TEXT_COLOR); - } else { - replyTextPaint.setColor(isDrawSelectedBackground() ? Theme.MSG_IN_REPLY_MEDIA_MESSAGE_SELETED_TEXT_COLOR : Theme.MSG_IN_REPLY_MEDIA_MESSAGE_TEXT_COLOR); - } - if (mediaBackground) { - replyStartX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(12); - } else { - replyStartX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(18); - } - } - replyStartY = AndroidUtilities.dp(12 + (drawForwardedName && forwardedNameLayout[0] != null ? 36 : 0) + (drawNameLayout && nameLayout != null ? 20 : 0)); - } - canvas.drawRect(replyStartX, replyStartY, replyStartX + AndroidUtilities.dp(2), replyStartY + AndroidUtilities.dp(35), replyLinePaint); - if (needReplyImage) { - replyImageReceiver.setImageCoords(replyStartX + AndroidUtilities.dp(10), replyStartY, AndroidUtilities.dp(35), AndroidUtilities.dp(35)); - replyImageReceiver.draw(canvas); - } - - if (replyNameLayout != null) { - canvas.save(); - canvas.translate(replyStartX - replyNameOffset + AndroidUtilities.dp(10 + (needReplyImage ? 44 : 0)), replyStartY); - replyNameLayout.draw(canvas); - canvas.restore(); - } - if (replyTextLayout != null) { - canvas.save(); - canvas.translate(replyStartX - replyTextOffset + AndroidUtilities.dp(10 + (needReplyImage ? 44 : 0)), replyStartY + AndroidUtilities.dp(19)); - replyTextLayout.draw(canvas); - canvas.restore(); - } - } - - if (drawTime || !mediaBackground) { - if (mediaBackground) { - Drawable drawable; - if (currentMessageObject.type == 13) { - drawable = Theme.timeStickerBackgroundDrawable; - } else { - drawable = Theme.timeBackgroundDrawable; - } - 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 additionalX = 0; - if ((currentMessageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { - additionalX = (int) (timeWidth - timeLayout.getLineWidth(0)); - - if (currentMessageObject.isSending()) { - if (!currentMessageObject.isOutOwner()) { - setDrawableBounds(Theme.clockMediaDrawable, timeX + AndroidUtilities.dp(11), layoutHeight - AndroidUtilities.dp(13.0f) - Theme.clockMediaDrawable.getIntrinsicHeight()); - Theme.clockMediaDrawable.draw(canvas); - } - } else if (currentMessageObject.isSendError()) { - if (!currentMessageObject.isOutOwner()) { - setDrawableBounds(Theme.errorDrawable, timeX + AndroidUtilities.dp(11), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.errorDrawable.getIntrinsicHeight()); - Theme.errorDrawable.draw(canvas); - } - } else { - Drawable countDrawable = Theme.viewsMediaCountDrawable; - setDrawableBounds(countDrawable, timeX, layoutHeight - AndroidUtilities.dp(9.5f) - timeLayout.getHeight()); - countDrawable.draw(canvas); - - if (viewsLayout != null) { - canvas.save(); - canvas.translate(timeX + countDrawable.getIntrinsicWidth() + AndroidUtilities.dp(3), layoutHeight - AndroidUtilities.dp(11.3f) - timeLayout.getHeight()); - viewsLayout.draw(canvas); - canvas.restore(); - } - } - } - - canvas.save(); - canvas.translate(timeX + additionalX, layoutHeight - AndroidUtilities.dp(11.3f) - timeLayout.getHeight()); - timeLayout.draw(canvas); - canvas.restore(); - } else { - int additionalX = 0; - if ((currentMessageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { - additionalX = (int) (timeWidth - timeLayout.getLineWidth(0)); - - if (currentMessageObject.isSending()) { - if (!currentMessageObject.isOutOwner()) { - Drawable clockDrawable = Theme.clockChannelDrawable[isDrawSelectedBackground() ? 1 : 0]; - setDrawableBounds(clockDrawable, timeX + AndroidUtilities.dp(11), layoutHeight - AndroidUtilities.dp(8.5f) - clockDrawable.getIntrinsicHeight()); - clockDrawable.draw(canvas); - } - } else if (currentMessageObject.isSendError()) { - if (!currentMessageObject.isOutOwner()) { - setDrawableBounds(Theme.errorDrawable, timeX + AndroidUtilities.dp(11), layoutHeight - AndroidUtilities.dp(6.5f) - Theme.errorDrawable.getIntrinsicHeight()); - Theme.errorDrawable.draw(canvas); - } - } else { - if (!currentMessageObject.isOutOwner()) { - setDrawableBounds(Theme.viewsCountDrawable[isDrawSelectedBackground() ? 1 : 0], timeX, layoutHeight - AndroidUtilities.dp(4.5f) - timeLayout.getHeight()); - Theme.viewsCountDrawable[isDrawSelectedBackground() ? 1 : 0].draw(canvas); - } else { - setDrawableBounds(Theme.viewsOutCountDrawable, timeX, layoutHeight - AndroidUtilities.dp(4.5f) - timeLayout.getHeight()); - Theme.viewsOutCountDrawable.draw(canvas); - } - - if (viewsLayout != null) { - canvas.save(); - canvas.translate(timeX + Theme.viewsOutCountDrawable.getIntrinsicWidth() + AndroidUtilities.dp(3), layoutHeight - AndroidUtilities.dp(6.5f) - timeLayout.getHeight()); - viewsLayout.draw(canvas); - canvas.restore(); - } - } - } - - canvas.save(); - canvas.translate(timeX + additionalX, layoutHeight - AndroidUtilities.dp(6.5f) - timeLayout.getHeight()); - timeLayout.draw(canvas); - canvas.restore(); - //canvas.drawRect(timeX, layoutHeight - AndroidUtilities.dp(6.5f) - timeLayout.getHeight(), timeX + availableTimeWidth, layoutHeight - AndroidUtilities.dp(4.5f) - timeLayout.getHeight(), timePaint); - } - - if (currentMessageObject.isOutOwner()) { - boolean drawCheck1 = false; - boolean drawCheck2 = false; - boolean drawClock = false; - boolean drawError = false; - boolean isBroadcast = (int)(currentMessageObject.getDialogId() >> 32) == 1; - - if (currentMessageObject.isSending()) { - drawCheck1 = false; - drawCheck2 = false; - drawClock = true; - drawError = false; - } else if (currentMessageObject.isSendError()) { - drawCheck1 = false; - drawCheck2 = false; - drawClock = false; - drawError = true; - } else if (currentMessageObject.isSent()) { - if (!currentMessageObject.isUnread()) { - drawCheck1 = true; - drawCheck2 = true; - } else { - drawCheck1 = false; - drawCheck2 = true; - } - drawClock = false; - drawError = false; - } - - if (drawClock) { - if (!mediaBackground) { - setDrawableBounds(Theme.clockDrawable, layoutWidth - AndroidUtilities.dp(18.5f) - Theme.clockDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(8.5f) - Theme.clockDrawable.getIntrinsicHeight()); - Theme.clockDrawable.draw(canvas); - } else { - setDrawableBounds(Theme.clockMediaDrawable, layoutWidth - AndroidUtilities.dp(22.0f) - Theme.clockMediaDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.clockMediaDrawable.getIntrinsicHeight()); - Theme.clockMediaDrawable.draw(canvas); - } - } - if (isBroadcast) { - if (drawCheck1 || drawCheck2) { - if (!mediaBackground) { - setDrawableBounds(Theme.broadcastDrawable, layoutWidth - AndroidUtilities.dp(20.5f) - Theme.broadcastDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(8.0f) - Theme.broadcastDrawable.getIntrinsicHeight()); - Theme.broadcastDrawable.draw(canvas); - } else { - setDrawableBounds(Theme.broadcastMediaDrawable, layoutWidth - AndroidUtilities.dp(24.0f) - Theme.broadcastMediaDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(13.0f) - Theme.broadcastMediaDrawable.getIntrinsicHeight()); - Theme.broadcastMediaDrawable.draw(canvas); - } - } - } else { - if (drawCheck2) { - if (!mediaBackground) { - if (drawCheck1) { - setDrawableBounds(Theme.checkDrawable, layoutWidth - AndroidUtilities.dp(22.5f) - Theme.checkDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(8.0f) - Theme.checkDrawable.getIntrinsicHeight()); - } else { - setDrawableBounds(Theme.checkDrawable, layoutWidth - AndroidUtilities.dp(18.5f) - Theme.checkDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(8.0f) - Theme.checkDrawable.getIntrinsicHeight()); - } - Theme.checkDrawable.draw(canvas); - } else { - if (drawCheck1) { - setDrawableBounds(Theme.checkMediaDrawable, layoutWidth - AndroidUtilities.dp(26.3f) - Theme.checkMediaDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.checkMediaDrawable.getIntrinsicHeight()); - } else { - setDrawableBounds(Theme.checkMediaDrawable, layoutWidth - AndroidUtilities.dp(21.5f) - Theme.checkMediaDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.checkMediaDrawable.getIntrinsicHeight()); - } - Theme.checkMediaDrawable.draw(canvas); - } - } - if (drawCheck1) { - if (!mediaBackground) { - setDrawableBounds(Theme.halfCheckDrawable, layoutWidth - AndroidUtilities.dp(18) - Theme.halfCheckDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(8.0f) - Theme.halfCheckDrawable.getIntrinsicHeight()); - Theme.halfCheckDrawable.draw(canvas); - } else { - setDrawableBounds(Theme.halfCheckMediaDrawable, layoutWidth - AndroidUtilities.dp(21.5f) - Theme.halfCheckMediaDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(12.5f) - Theme.halfCheckMediaDrawable.getIntrinsicHeight()); - Theme.halfCheckMediaDrawable.draw(canvas); - } - } - } - if (drawError) { - if (!mediaBackground) { - setDrawableBounds(Theme.errorDrawable, layoutWidth - AndroidUtilities.dp(18) - Theme.errorDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(7) - Theme.errorDrawable.getIntrinsicHeight()); - Theme.errorDrawable.draw(canvas); - } else { - setDrawableBounds(Theme.errorDrawable, layoutWidth - AndroidUtilities.dp(20.5f) - Theme.errorDrawable.getIntrinsicWidth(), layoutHeight - AndroidUtilities.dp(11.5f) - Theme.errorDrawable.getIntrinsicHeight()); - Theme.errorDrawable.draw(canvas); - } - } - } - } - } - - @Override - public void onFailedDownload(String fileName) { - - } - - @Override - public void onSuccessDownload(String fileName) { - - } - - @Override - public void onProgressDownload(String fileName, float progress) { - - } - - @Override - public void onProgressUpload(String fileName, float progress, boolean isEncrypted) { - - } - - @Override - public int getObserverTag() { - return TAG; - } -} 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 16253ff43..47222194a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatMessageCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatMessageCell.java @@ -8,17 +8,20 @@ package org.telegram.ui.Cells; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; 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.os.Build; import android.text.Layout; import android.text.Spannable; import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; @@ -42,6 +45,7 @@ import org.telegram.messenger.MessageObject; import org.telegram.messenger.MessagesController; import org.telegram.messenger.R; import org.telegram.messenger.SendMessagesHelper; +import org.telegram.messenger.UserObject; import org.telegram.messenger.Utilities; import org.telegram.messenger.browser.Browser; import org.telegram.tgnet.ConnectionsManager; @@ -53,6 +57,7 @@ import org.telegram.ui.Components.SeekBar; import org.telegram.ui.Components.SeekBarWaveform; import org.telegram.ui.Components.StaticLayoutEx; import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.TypefaceSpan; import org.telegram.ui.Components.URLSpanBotCommand; import org.telegram.ui.Components.URLSpanNoUnderline; import org.telegram.ui.PhotoViewer; @@ -62,7 +67,26 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; -public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDelegate, ImageReceiver.ImageReceiverDelegate { +import static org.telegram.messenger.AndroidUtilities.*; + +public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate, ImageReceiver.ImageReceiverDelegate, MediaController.FileDownloadProgressListener { + + public interface ChatMessageCellDelegate { + void didPressedUserAvatar(ChatMessageCell cell, TLRPC.User user); + void didPressedViaBot(ChatMessageCell cell, String username); + void didPressedChannelAvatar(ChatMessageCell cell, TLRPC.Chat chat, int postId); + void didPressedCancelSendButton(ChatMessageCell cell); + void didLongPressed(ChatMessageCell cell); + void didPressedReplyMessage(ChatMessageCell cell, int id); + void didPressedUrl(MessageObject messageObject, ClickableSpan url, boolean longPress); + void needOpenWebView(String url, String title, String description, String originalUrl, int w, int h); + void didPressedImage(ChatMessageCell cell); + void didPressedShare(ChatMessageCell cell); + void didPressedOther(ChatMessageCell cell); + void didPressedBotButton(ChatMessageCell cell, TLRPC.KeyboardButton button); + boolean needPlayAudio(MessageObject messageObject); + boolean canPerformActions(); + } private final static int DOCUMENT_ATTACH_TYPE_NONE = 0; private final static int DOCUMENT_ATTACH_TYPE_DOCUMENT = 1; @@ -77,7 +101,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele private int y; private int width; private int height; - private StaticLayout caption; + private StaticLayout title; private TLRPC.KeyboardButton button; private int angle; private float progressAlpha; @@ -89,6 +113,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele private int totalHeight; private int keyboardHeight; private int linkBlockNum; + private int linkSelectionBlockNum; private Rect scrollRect = new Rect(); @@ -99,7 +124,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele private RadialProgress radialProgress; private ImageReceiver photoImage; - private AvatarDrawable avatarDrawable; + private AvatarDrawable contactAvatarDrawable; private boolean disallowLongPress; @@ -116,25 +141,25 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele private int descriptionX; private int titleX; private int authorX; - private StaticLayout siteCaptionLayout; + private StaticLayout siteNameLayout; private StaticLayout titleLayout; private StaticLayout descriptionLayout; - private StaticLayout durationLayout; + private StaticLayout videoInfoLayout; private StaticLayout authorLayout; + private StaticLayout docTitleLayout; + private int docTitleOffsetX; + private StaticLayout captionLayout; private int captionX; private int captionY; private int captionHeight; - private int nameOffsetX; private StaticLayout infoLayout; private int infoWidth; private String currentUrl; - private boolean allowedToSetPhoto = true; - private int buttonX; private int buttonY; private int buttonState; @@ -160,6 +185,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele private static TextPaint locationTitlePaint; private static TextPaint locationAddressPaint; private static Paint urlPaint; + private static Paint urlSelectionPaint; private static TextPaint durationPaint; private ClickableSpan pressedLink; @@ -167,7 +193,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele private boolean linkPreviewPressed; private ArrayList urlPathCache = new ArrayList<>(); private ArrayList urlPath = new ArrayList<>(); - //private LinkPath urlPath = new LinkPath(); + private ArrayList urlPathSelection = new ArrayList<>(); private boolean useSeekBarWaweform; private SeekBar seekBar; @@ -175,7 +201,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele private int seekBarX; private int seekBarY; - private StaticLayout timeLayout; + private StaticLayout durationLayout; private String lastTimeString; private int timeWidthAudio; private int timeAudioX; @@ -198,25 +224,115 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele private int widthForButtons; private int pressedBotButton; + // + private int TAG; + + public boolean isChat; + private boolean isPressed; + private boolean forwardName; + private boolean isHighlighted; + private boolean mediaBackground; + private boolean isCheckPressed = true; + private boolean wasLayout; + private boolean isAvatarVisible; + private boolean drawBackground = true; + private int substractBackgroundHeight; + private boolean allowAssistant; + private Drawable currentBackgroundDrawable; + private MessageObject currentMessageObject; + private int viaWidth; + private int viaNameWidth; + private int availableTimeWidth; + + private static TextPaint timePaint; + private static TextPaint namePaint; + private static TextPaint forwardNamePaint; + private static TextPaint replyNamePaint; + private static TextPaint replyTextPaint; + private static Paint replyLinePaint; + + private int backgroundWidth = 100; + + private int layoutWidth; + private int layoutHeight; + + private ImageReceiver avatarImage; + private AvatarDrawable avatarDrawable; + private boolean avatarPressed; + private boolean forwardNamePressed; + private boolean forwardBotPressed; + + private StaticLayout replyNameLayout; + private StaticLayout replyTextLayout; + private ImageReceiver replyImageReceiver; + private int replyStartX; + private int replyStartY; + private int replyNameWidth; + private float replyNameOffset; + private int replyTextWidth; + private float replyTextOffset; + private boolean needReplyImage; + private boolean replyPressed; + private TLRPC.FileLocation currentReplyPhoto; + + private boolean drawShareButton; + private boolean sharePressed; + private int shareStartX; + private int shareStartY; + + private StaticLayout nameLayout; + private int nameWidth; + private float nameOffsetX; + private float nameX; + private float nameY; + private boolean drawName; + private boolean drawNameLayout; + + private StaticLayout[] forwardedNameLayout = new StaticLayout[2]; + private int forwardedNameWidth; + private boolean drawForwardedName; + private int forwardNameX; + private int forwardNameY; + private float forwardNameOffsetX[] = new float[2]; + + private StaticLayout timeLayout; + private int timeWidth; + private int timeTextWidth; + private int timeX; + private String currentTimeString; + private boolean drawTime = true; + + private StaticLayout viewsLayout; + private int viewsTextWidth; + private String currentViewsString; + + private TLRPC.User currentUser; + private TLRPC.Chat currentChat; + private TLRPC.FileLocation currentPhoto; + private String currentNameString; + + private TLRPC.User currentForwardUser; + private TLRPC.User currentViaBotUser; + private TLRPC.Chat currentForwardChannel; + private String currentForwardNameString; + + private ChatMessageCellDelegate delegate; + + private int namesOffset; + + private int lastSendState; + private int lastDeleteDate; + private int lastViewsCount; + public ChatMessageCell(Context context) { super(context); - avatarDrawable = new AvatarDrawable(); - photoImage = new ImageReceiver(this); - photoImage.setDelegate(this); - radialProgress = new RadialProgress(this); - seekBar = new SeekBar(context); - seekBar.setDelegate(this); - seekBarWaveform = new SeekBarWaveform(context); - seekBarWaveform.setDelegate(this); - seekBarWaveform.setParentView(this); - if (infoPaint == null) { infoPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - infoPaint.setTextSize(AndroidUtilities.dp(12)); + infoPaint.setTextSize(dp(12)); docNamePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - docNamePaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - docNamePaint.setTextSize(AndroidUtilities.dp(15)); + docNamePaint.setTypeface(getTypeface("fonts/rmedium.ttf")); + docNamePaint.setTextSize(dp(15)); docBackPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -227,44 +343,82 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele botProgressPaint.setColor(Theme.MSG_BOT_PROGRESS_COLOR); botProgressPaint.setStrokeCap(Paint.Cap.ROUND); botProgressPaint.setStyle(Paint.Style.STROKE); - botProgressPaint.setStrokeWidth(AndroidUtilities.dp(2)); + botProgressPaint.setStrokeWidth(dp(2)); locationTitlePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - locationTitlePaint.setTextSize(AndroidUtilities.dp(15)); - locationTitlePaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + locationTitlePaint.setTextSize(dp(15)); + locationTitlePaint.setTypeface(getTypeface("fonts/rmedium.ttf")); locationAddressPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - locationAddressPaint.setTextSize(AndroidUtilities.dp(13)); + locationAddressPaint.setTextSize(dp(13)); urlPaint = new Paint(); urlPaint.setColor(Theme.MSG_LINK_SELECT_BACKGROUND_COLOR); + urlSelectionPaint = new Paint(); + urlSelectionPaint.setColor(Theme.MSG_TEXT_SELECT_BACKGROUND_COLOR); + audioTimePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); - audioTimePaint.setTextSize(AndroidUtilities.dp(12)); + audioTimePaint.setTextSize(dp(12)); audioTitlePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - audioTitlePaint.setTextSize(AndroidUtilities.dp(16)); - audioTitlePaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + audioTitlePaint.setTextSize(dp(16)); + audioTitlePaint.setTypeface(getTypeface("fonts/rmedium.ttf")); audioPerformerPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - audioPerformerPaint.setTextSize(AndroidUtilities.dp(15)); + audioPerformerPaint.setTextSize(dp(15)); botButtonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - botButtonPaint.setTextSize(AndroidUtilities.dp(15)); + botButtonPaint.setTextSize(dp(15)); botButtonPaint.setColor(Theme.MSG_BOT_BUTTON_TEXT_COLOR); - botButtonPaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + botButtonPaint.setTypeface(getTypeface("fonts/rmedium.ttf")); contactNamePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - contactNamePaint.setTextSize(AndroidUtilities.dp(15)); - contactNamePaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + contactNamePaint.setTextSize(dp(15)); + contactNamePaint.setTypeface(getTypeface("fonts/rmedium.ttf")); contactPhonePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - contactPhonePaint.setTextSize(AndroidUtilities.dp(13)); + contactPhonePaint.setTextSize(dp(13)); durationPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - durationPaint.setTextSize(AndroidUtilities.dp(12)); + durationPaint.setTextSize(dp(12)); durationPaint.setColor(Theme.MSG_WEB_PREVIEW_DURATION_TEXT_COLOR); + + timePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); + timePaint.setTextSize(dp(12)); + + namePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); + namePaint.setTypeface(getTypeface("fonts/rmedium.ttf")); + namePaint.setTextSize(dp(14)); + + forwardNamePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); + forwardNamePaint.setTextSize(dp(14)); + + replyNamePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); + replyNamePaint.setTypeface(getTypeface("fonts/rmedium.ttf")); + replyNamePaint.setTextSize(dp(14)); + + replyTextPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); + replyTextPaint.setTextSize(dp(14)); + replyTextPaint.linkColor = Theme.MSG_LINK_TEXT_COLOR; + + replyLinePaint = new Paint(); } + avatarImage = new ImageReceiver(this); + avatarImage.setRoundRadius(dp(21)); + avatarDrawable = new AvatarDrawable(); + replyImageReceiver = new ImageReceiver(this); + TAG = MediaController.getInstance().generateObserverTag(); + + contactAvatarDrawable = new AvatarDrawable(); + photoImage = new ImageReceiver(this); + photoImage.setDelegate(this); + radialProgress = new RadialProgress(this); + seekBar = new SeekBar(context); + seekBar.setDelegate(this); + seekBarWaveform = new SeekBarWaveform(context); + seekBarWaveform.setDelegate(this); + seekBarWaveform.setParentView(this); radialProgress = new RadialProgress(this); } @@ -273,21 +427,29 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (pressedLink == null || pressedLinkType != type && type != -1) { return; } - resetUrlPaths(); + resetUrlPaths(false); pressedLink = null; pressedLinkType = -1; invalidate(); } - private void resetUrlPaths() { - if (urlPath.isEmpty()) { - return; + private void resetUrlPaths(boolean text) { + if (text) { + if (urlPathSelection.isEmpty()) { + return; + } + urlPathCache.addAll(urlPathSelection); + urlPathSelection.clear(); + } else { + if (urlPath.isEmpty()) { + return; + } + urlPathCache.addAll(urlPath); + urlPath.clear(); } - urlPathCache.addAll(urlPath); - urlPath.clear(); } - private LinkPath obtainNewUrlPath() { + private LinkPath obtainNewUrlPath(boolean text) { LinkPath linkPath; if (!urlPathCache.isEmpty()) { linkPath = urlPathCache.get(0); @@ -295,7 +457,11 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } else { linkPath = new LinkPath(); } - urlPath.add(linkPath); + if (text) { + urlPathSelection.add(linkPath); + } else { + urlPath.add(linkPath); + } return linkPath; } @@ -335,9 +501,9 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele pressedLink = link[0]; linkBlockNum = blockNum; pressedLinkType = 1; - resetUrlPaths(); + resetUrlPaths(false); try { - LinkPath path = obtainNewUrlPath(); + LinkPath path = obtainNewUrlPath(false); int start = buffer.getSpanStart(pressedLink) - block.charactersOffset; int end = buffer.getSpanEnd(pressedLink); int length = block.textLayout.getText().length(); @@ -351,7 +517,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (nextLink == null || nextLink.length == 0 || nextLink[0] != pressedLink) { break; } - path = obtainNewUrlPath(); + path = obtainNewUrlPath(false); path.setCurrentLayout(nextBlock.textLayout, 0, nextBlock.height); nextBlock.textLayout.getSelectionPath(0, end - nextBlock.charactersOffset, path); if (end < block.charactersOffset + length - 1) { @@ -367,7 +533,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (nextLink == null || nextLink.length == 0 || nextLink[0] != pressedLink) { break; } - path = obtainNewUrlPath(); + path = obtainNewUrlPath(false); start = buffer.getSpanStart(pressedLink) - nextBlock.charactersOffset; path.setCurrentLayout(nextBlock.textLayout, start, -nextBlock.height); nextBlock.textLayout.getSelectionPath(start, buffer.getSpanEnd(pressedLink) - nextBlock.charactersOffset, path); @@ -426,9 +592,9 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (!ignore) { pressedLink = link[0]; pressedLinkType = 3; - resetUrlPaths(); + resetUrlPaths(false); try { - LinkPath path = obtainNewUrlPath(); + LinkPath path = obtainNewUrlPath(false); int start = buffer.getSpanStart(pressedLink); path.setCurrentLayout(captionLayout, start, 0); captionLayout.getSelectionPath(start, buffer.getSpanEnd(pressedLink), path); @@ -461,22 +627,24 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele 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 + dp(8)) { if (event.getAction() == MotionEvent.ACTION_DOWN) { if (documentAttachType != DOCUMENT_ATTACH_TYPE_DOCUMENT && drawPhotoImage && photoImage.isInsideImage(x, y)) { - if (drawImageButton && buttonState != -1 && x >= buttonX && x <= buttonX + AndroidUtilities.dp(48) && y >= buttonY && y <= buttonY + AndroidUtilities.dp(48)) { + if (drawImageButton && buttonState != -1 && x >= buttonX && x <= buttonX + dp(48) && y >= buttonY && y <= buttonY + dp(48)) { buttonPressed = 1; return true; - } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF && buttonState == -1 && MediaController.getInstance().canAutoplayGifs()) { - linkPreviewPressed = false; - return false; } else { linkPreviewPressed = true; + TLRPC.WebPage webPage = currentMessageObject.messageOwner.media.webpage; + if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF && buttonState == -1 && MediaController.getInstance().canAutoplayGifs() && (photoImage.getAnimation() == null || !TextUtils.isEmpty(webPage.embed_url))) { + linkPreviewPressed = false; + return false; + } return true; } } else if (descriptionLayout != null && y >= descriptionY) { try { - x -= textX + AndroidUtilities.dp(10) + descriptionX; + x -= textX + dp(10) + descriptionX; y -= descriptionY; final int line = descriptionLayout.getLineForVertical(y); final int off = descriptionLayout.getOffsetForHorizontal(line, x); @@ -493,9 +661,9 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele pressedLink = link[0]; linkBlockNum = -10; pressedLinkType = 2; - resetUrlPaths(); + resetUrlPaths(false); try { - LinkPath path = obtainNewUrlPath(); + LinkPath path = obtainNewUrlPath(false); int start = buffer.getSpanStart(pressedLink); path.setCurrentLayout(descriptionLayout, start, 0); descriptionLayout.getSelectionPath(start, buffer.getSpanEnd(pressedLink), path); @@ -533,13 +701,17 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (drawImageButton) { if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { if (buttonState == -1) { - buttonState = 2; - currentMessageObject.audioProgress = 1; - photoImage.setAllowStartAnimation(false); - photoImage.stopAnimation(); - radialProgress.setBackground(getDrawableForCurrentState(), false, false); - invalidate(); - playSoundEffect(SoundEffectConstants.CLICK); + if (MediaController.getInstance().canAutoplayGifs()) { + delegate.didPressedImage(this); + } else { + buttonState = 2; + currentMessageObject.audioProgress = 1; + photoImage.setAllowStartAnimation(false); + photoImage.stopAnimation(); + radialProgress.setBackground(getDrawableForCurrentState(), false, false); + invalidate(); + playSoundEffect(SoundEffectConstants.CLICK); + } } else if (buttonState == 2 || buttonState == 0) { didPressedButton(false); playSoundEffect(SoundEffectConstants.CLICK); @@ -551,7 +723,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } else { TLRPC.WebPage webPage = currentMessageObject.messageOwner.media.webpage; if (webPage != null) { - if (Build.VERSION.SDK_INT >= 16 && webPage.embed_url != null && webPage.embed_url.length() != 0) { + if (Build.VERSION.SDK_INT >= 16 && !TextUtils.isEmpty(webPage.embed_url)) { delegate.needOpenWebView(webPage.embed_url, webPage.site_name, webPage.description, webPage.url, webPage.embed_width, webPage.embed_height); } else { Browser.openUrl(getContext(), webPage.url); @@ -570,7 +742,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } private boolean checkOtherButtonMotionEvent(MotionEvent event) { - if (documentAttachType != DOCUMENT_ATTACH_TYPE_DOCUMENT && currentMessageObject.type != 12 && documentAttachType != DOCUMENT_ATTACH_TYPE_MUSIC && documentAttachType != DOCUMENT_ATTACH_TYPE_VIDEO) { + if (documentAttachType != DOCUMENT_ATTACH_TYPE_DOCUMENT && currentMessageObject.type != 12 && documentAttachType != DOCUMENT_ATTACH_TYPE_MUSIC && documentAttachType != DOCUMENT_ATTACH_TYPE_VIDEO && documentAttachType != DOCUMENT_ATTACH_TYPE_GIF && currentMessageObject.type != 8) { return false; } @@ -579,7 +751,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele boolean result = false; if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (x >= otherX - AndroidUtilities.dp(20) && x <= otherX + AndroidUtilities.dp(20) && y >= otherY - AndroidUtilities.dp(4) && y <= otherY + AndroidUtilities.dp(30)) { + if (x >= otherX - dp(20) && x <= otherX + dp(20) && y >= otherY - dp(4) && y <= otherY + dp(30)) { otherPressed = true; result = true; } @@ -594,7 +766,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } return result; } - + private boolean checkPhotoImageMotionEvent(MotionEvent event) { if (!drawPhotoImage && documentAttachType != DOCUMENT_ATTACH_TYPE_DOCUMENT) { return false; @@ -605,13 +777,13 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele boolean result = false; if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (buttonState != -1 && x >= buttonX && x <= buttonX + AndroidUtilities.dp(48) && y >= buttonY && y <= buttonY + AndroidUtilities.dp(48)) { + if (buttonState != -1 && x >= buttonX && x <= buttonX + dp(48) && y >= buttonY && y <= buttonY + dp(48)) { buttonPressed = 1; invalidate(); result = true; } else { if (documentAttachType == DOCUMENT_ATTACH_TYPE_DOCUMENT) { - if (x >= photoImage.getImageX() && x <= photoImage.getImageX() + backgroundWidth - AndroidUtilities.dp(50) && y >= photoImage.getImageY() && y <= photoImage.getImageY() + photoImage.getImageHeight()) { + if (x >= photoImage.getImageX() && x <= photoImage.getImageX() + backgroundWidth - dp(50) && y >= photoImage.getImageY() && y <= photoImage.getImageY() + photoImage.getImageHeight()) { imagePressed = true; result = true; } @@ -629,14 +801,16 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } } } - if (imagePressed && currentMessageObject.isSecretPhoto()) { - imagePressed = false; - } else if (imagePressed && currentMessageObject.isSendError()) { - imagePressed = false; - result = false; - } else if (imagePressed && currentMessageObject.type == 8 && buttonState == -1 && MediaController.getInstance().canAutoplayGifs()) { - imagePressed = false; - result = false; + if (imagePressed) { + if (currentMessageObject.isSecretPhoto()) { + imagePressed = false; + } else if (currentMessageObject.isSendError()) { + imagePressed = false; + result = false; + } else if (currentMessageObject.type == 8 && buttonState == -1 && MediaController.getInstance().canAutoplayGifs() && photoImage.getAnimation() == null) { + imagePressed = false; + result = false; + } } } else { if (event.getAction() == MotionEvent.ACTION_UP) { @@ -644,6 +818,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele buttonPressed = 0; playSoundEffect(SoundEffectConstants.CLICK); didPressedButton(false); + radialProgress.swapBackground(getDrawableForCurrentState()); invalidate(); } else if (imagePressed) { imagePressed = false; @@ -670,7 +845,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele int y = (int) event.getY(); boolean result; if (useSeekBarWaweform) { - result = seekBarWaveform.onTouch(event.getAction(), event.getX() - seekBarX - AndroidUtilities.dp(13), event.getY() - seekBarY); + result = seekBarWaveform.onTouch(event.getAction(), event.getX() - seekBarX - dp(13), event.getY() - seekBarY); } else { result = seekBar.onTouch(event.getAction(), event.getX() - seekBarX, event.getY() - seekBarY); } @@ -683,10 +858,10 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele disallowLongPress = true; invalidate(); } else { - int side = AndroidUtilities.dp(36); + int side = dp(36); boolean area; if (buttonState == 0 || buttonState == 1 || buttonState == 2) { - area = x >= buttonX - AndroidUtilities.dp(12) && x <= buttonX - AndroidUtilities.dp(12) + backgroundWidth && y >= namesOffset + mediaOffsetY && y <= layoutHeight; + area = x >= buttonX - dp(12) && x <= buttonX - dp(12) + backgroundWidth && y >= namesOffset + mediaOffsetY && y <= layoutHeight; } else { area = x >= buttonX && x <= buttonX + side && y >= buttonY && y <= buttonY + side; } @@ -730,13 +905,13 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (event.getAction() == MotionEvent.ACTION_DOWN) { int addX; if (currentMessageObject.isOutOwner()) { - addX = getMeasuredWidth() - widthForButtons - AndroidUtilities.dp(10); + addX = getMeasuredWidth() - widthForButtons - dp(10); } else { - addX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(mediaBackground ? 1 : 7); + addX = currentBackgroundDrawable.getBounds().left + dp(mediaBackground ? 1 : 7); } for (int a = 0; a < botButtons.size(); a++) { BotButton button = botButtons.get(a); - int y2 = button.y + layoutHeight - AndroidUtilities.dp(2); + int y2 = button.y + layoutHeight - dp(2); if (x >= button.x + addX && x <= button.x + addX + button.width && y >= y2 && y <= y2 + button.height) { pressedBotButton = a; invalidate(); @@ -801,7 +976,129 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele cancelCheckLongPress(); } - return result || super.onTouchEvent(event); + if (!result) { + float x = event.getX(); + float y = event.getY(); + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (delegate == null || delegate.canPerformActions()) { + if (isAvatarVisible && avatarImage.isInsideImage(x, y)) { + avatarPressed = true; + result = true; + } else if (drawForwardedName && forwardedNameLayout[0] != null && x >= forwardNameX && x <= forwardNameX + forwardedNameWidth && y >= forwardNameY && y <= forwardNameY + dp(32)) { + if (viaWidth != 0 && x >= forwardNameX + viaNameWidth + dp(4)) { + forwardBotPressed = true; + } else { + forwardNamePressed = true; + } + result = true; + } else if (drawNameLayout && nameLayout != null && viaWidth != 0 && x >= nameX + viaNameWidth && x <= nameX + viaNameWidth + viaWidth && y >= nameY - dp(4) && y <= nameY + dp(20)) { + forwardBotPressed = true; + result = true; + } else if (currentMessageObject.isReply() && x >= replyStartX && x <= replyStartX + Math.max(replyNameWidth, replyTextWidth) && y >= replyStartY && y <= replyStartY + dp(35)) { + replyPressed = true; + result = true; + } else if (drawShareButton && x >= shareStartX && x <= shareStartX + dp(40) && y >= shareStartY && y <= shareStartY + dp(32)) { + sharePressed = true; + result = true; + invalidate(); + } + if (result) { + startCheckLongPress(); + } + } + } else { + if (event.getAction() != MotionEvent.ACTION_MOVE) { + cancelCheckLongPress(); + } + if (avatarPressed) { + if (event.getAction() == MotionEvent.ACTION_UP) { + avatarPressed = false; + playSoundEffect(SoundEffectConstants.CLICK); + if (delegate != null) { + if (currentUser != null) { + delegate.didPressedUserAvatar(this, currentUser); + } else if (currentChat != null) { + delegate.didPressedChannelAvatar(this, currentChat, 0); + } + } + } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { + avatarPressed = false; + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (isAvatarVisible && !avatarImage.isInsideImage(x, y)) { + avatarPressed = false; + } + } + } else if (forwardNamePressed) { + if (event.getAction() == MotionEvent.ACTION_UP) { + forwardNamePressed = false; + playSoundEffect(SoundEffectConstants.CLICK); + if (delegate != null) { + if (currentForwardChannel != null) { + delegate.didPressedChannelAvatar(this, currentForwardChannel, currentMessageObject.messageOwner.fwd_from.channel_post); + } else if (currentForwardUser != null) { + delegate.didPressedUserAvatar(this, currentForwardUser); + } + } + } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { + forwardNamePressed = false; + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (!(x >= forwardNameX && x <= forwardNameX + forwardedNameWidth && y >= forwardNameY && y <= forwardNameY + dp(32))) { + forwardNamePressed = false; + } + } + } else if (forwardBotPressed) { + if (event.getAction() == MotionEvent.ACTION_UP) { + forwardBotPressed = false; + playSoundEffect(SoundEffectConstants.CLICK); + if (delegate != null) { + delegate.didPressedViaBot(this, currentViaBotUser != null ? currentViaBotUser.username : currentMessageObject.messageOwner.via_bot_name); + } + } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { + forwardBotPressed = false; + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (drawForwardedName && forwardedNameLayout[0] != null) { + if (!(x >= forwardNameX && x <= forwardNameX + forwardedNameWidth && y >= forwardNameY && y <= forwardNameY + dp(32))) { + forwardBotPressed = false; + } + } else { + if (!(x >= nameX + viaNameWidth && x <= nameX + viaNameWidth + viaWidth && y >= nameY - dp(4) && y <= nameY + dp(20))) { + forwardBotPressed = false; + } + } + } + } else if (replyPressed) { + if (event.getAction() == MotionEvent.ACTION_UP) { + replyPressed = false; + playSoundEffect(SoundEffectConstants.CLICK); + if (delegate != null) { + delegate.didPressedReplyMessage(this, currentMessageObject.messageOwner.reply_to_msg_id); + } + } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { + replyPressed = false; + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (!(x >= replyStartX && x <= replyStartX + Math.max(replyNameWidth, replyTextWidth) && y >= replyStartY && y <= replyStartY + dp(35))) { + replyPressed = false; + } + } + } else if (sharePressed) { + if (event.getAction() == MotionEvent.ACTION_UP) { + sharePressed = false; + playSoundEffect(SoundEffectConstants.CLICK); + if (delegate != null) { + delegate.didPressedShare(this); + } + } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { + sharePressed = false; + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (!(x >= shareStartX && x <= shareStartX + dp(40) && y >= shareStartY && y <= shareStartY + dp(32))) { + sharePressed = false; + } + } + invalidate(); + } + } + } + return result; } public void updateAudioProgress() { @@ -836,7 +1133,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (lastTimeString == null || lastTimeString != null && !lastTimeString.equals(timeString)) { lastTimeString = timeString; timeWidthAudio = (int) Math.ceil(audioTimePaint.measureText(timeString)); - timeLayout = new StaticLayout(timeString, audioTimePaint, timeWidthAudio, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + durationLayout = new StaticLayout(timeString, audioTimePaint, timeWidthAudio, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); } } else { int currentProgress = 0; @@ -854,7 +1151,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (lastTimeString == null || lastTimeString != null && !lastTimeString.equals(timeString)) { lastTimeString = timeString; int timeWidth = (int) Math.ceil(audioTimePaint.measureText(timeString)); - timeLayout = new StaticLayout(timeString, audioTimePaint, timeWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + durationLayout = new StaticLayout(timeString, audioTimePaint, timeWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); } } invalidate(); @@ -922,7 +1219,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele StaticLayout layout = new StaticLayout(text, paint, smallWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); for (int a = 0; a < linesCount; a++) { Layout.Directions directions = layout.getLineDirections(a); - if (layout.getLineLeft(a) != 0 || Build.VERSION.SDK_INT >= 14 && (layout.isRtlCharAt(layout.getLineStart(a)) || layout.isRtlCharAt(layout.getLineEnd(a)))) { + if (layout.getLineLeft(a) != 0 || layout.isRtlCharAt(layout.getLineStart(a)) || layout.isRtlCharAt(layout.getLineEnd(a))) { maxWidth = smallWidth; } int pos = layout.getLineEnd(a); @@ -940,7 +1237,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele break; } } - return StaticLayoutEx.createStaticLayout(stringBuilder, paint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, AndroidUtilities.dp(1), false, TextUtils.TruncateAt.END, maxWidth, maxLines); + return StaticLayoutEx.createStaticLayout(stringBuilder, paint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, dp(1), false, TextUtils.TruncateAt.END, maxWidth, maxLines); } private void didClickedImage() { @@ -955,12 +1252,16 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele delegate.didPressedUserAvatar(this, user); } else if (currentMessageObject.type == 8) { if (buttonState == -1) { - buttonState = 2; - currentMessageObject.audioProgress = 1; - photoImage.setAllowStartAnimation(false); - photoImage.stopAnimation(); - radialProgress.setBackground(getDrawableForCurrentState(), false, false); - invalidate(); + if (MediaController.getInstance().canAutoplayGifs()) { + delegate.didPressedImage(this); + } else { + buttonState = 2; + currentMessageObject.audioProgress = 1; + photoImage.setAllowStartAnimation(false); + photoImage.stopAnimation(); + radialProgress.setBackground(getDrawableForCurrentState(), false, false); + invalidate(); + } } else if (buttonState == 2 || buttonState == 0) { didPressedButton(false); } @@ -1010,7 +1311,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } double lat = object.messageOwner.media.geo.lat; double lon = object.messageOwner.media.geo._long; - String url = String.format(Locale.US, "https://maps.googleapis.com/maps/api/staticmap?center=%f,%f&zoom=15&size=100x100&maptype=roadmap&scale=%d&markers=color:red|size:mid|%f,%f&sensor=false", lat, lon, Math.min(2, (int) Math.ceil(AndroidUtilities.density)), lat, lon); + String url = String.format(Locale.US, "https://maps.googleapis.com/maps/api/staticmap?center=%f,%f&zoom=15&size=100x100&maptype=roadmap&scale=%d&markers=color:red|size:mid|%f,%f&sensor=false", lat, lon, Math.min(2, (int) Math.ceil(density)), lat, lon); if (!url.equals(currentUrl)) { return true; } @@ -1025,12 +1326,83 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele return false; } - @Override - protected boolean isUserDataChanged() { - return currentMessageObject != null && (!hasLinkPreview && currentMessageObject.messageOwner.media != null && currentMessageObject.messageOwner.media.webpage instanceof TLRPC.TL_webPage || super.isUserDataChanged()); + private boolean isUserDataChanged() { + if (currentMessageObject != null && (!hasLinkPreview && currentMessageObject.messageOwner.media != null && currentMessageObject.messageOwner.media.webpage instanceof TLRPC.TL_webPage)) { + return true; + } + if (currentMessageObject == null || currentUser == null && currentChat == null) { + return false; + } + if (lastSendState != currentMessageObject.messageOwner.send_state) { + return true; + } + if (lastDeleteDate != currentMessageObject.messageOwner.destroyTime) { + return true; + } + if (lastViewsCount != currentMessageObject.messageOwner.views) { + return true; + } + + TLRPC.User newUser = null; + TLRPC.Chat newChat = null; + if (currentMessageObject.isFromUser()) { + newUser = MessagesController.getInstance().getUser(currentMessageObject.messageOwner.from_id); + } else if (currentMessageObject.messageOwner.from_id < 0) { + newChat = MessagesController.getInstance().getChat(-currentMessageObject.messageOwner.from_id); + } else if (currentMessageObject.messageOwner.post) { + newChat = MessagesController.getInstance().getChat(currentMessageObject.messageOwner.to_id.channel_id); + } + TLRPC.FileLocation newPhoto = null; + + if (isAvatarVisible) { + if (newUser != null && newUser.photo != null){ + newPhoto = newUser.photo.photo_small; + } else if (newChat != null && newChat.photo != null) { + newPhoto = newChat.photo.photo_small; + } + } + + if (replyTextLayout == null && currentMessageObject.replyMessageObject != null) { + return true; + } + + if (currentPhoto == null && newPhoto != null || currentPhoto != null && newPhoto == null || currentPhoto != null && newPhoto != null && (currentPhoto.local_id != newPhoto.local_id || currentPhoto.volume_id != newPhoto.volume_id)) { + return true; + } + + TLRPC.FileLocation newReplyPhoto = null; + + if (currentMessageObject.replyMessageObject != null) { + TLRPC.PhotoSize photoSize = FileLoader.getClosestPhotoSizeWithSize(currentMessageObject.replyMessageObject.photoThumbs, 80); + if (photoSize != null && currentMessageObject.replyMessageObject.type != 13) { + newReplyPhoto = photoSize.location; + } + } + + if (currentReplyPhoto == null && newReplyPhoto != null) { + return true; + } + + String newNameString = null; + if (drawName && isChat && !currentMessageObject.isOutOwner()) { + if (newUser != null) { + newNameString = UserObject.getUserName(newUser); + } else if (newChat != null) { + newNameString = newChat.title; + } + } + + if (currentNameString == null && newNameString != null || currentNameString != null && newNameString == null || currentNameString != null && newNameString != null && !currentNameString.equals(newNameString)) { + return true; + } + + if (drawForwardedName) { + newNameString = currentMessageObject.getForwardedName(); + return currentForwardNameString == null && newNameString != null || currentForwardNameString != null && newNameString == null || currentForwardNameString != null && newNameString != null && !currentForwardNameString.equals(newNameString); + } + return false; } - @Override public ImageReceiver getPhotoImage() { return photoImage; } @@ -1038,6 +1410,8 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); + avatarImage.onDetachedFromWindow(); + replyImageReceiver.onDetachedFromWindow(); photoImage.onDetachedFromWindow(); MediaController.getInstance().removeLoadingFileObserver(this); } @@ -1045,6 +1419,8 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + avatarImage.onAttachedToWindow(); + replyImageReceiver.onAttachedToWindow(); if (drawPhotoImage) { if (photoImage.onAttachedToWindow()) { updateButtonState(false); @@ -1072,46 +1448,47 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele pressedBotButton = -1; invalidate(); } - super.onLongPress(); + if (delegate != null) { + delegate.didLongPressed(this); + } } - @Override public void setCheckPressed(boolean value, boolean pressed) { - super.setCheckPressed(value, pressed); - if (radialProgress.swapBackground(getDrawableForCurrentState())) { - invalidate(); - } + isCheckPressed = value; + isPressed = pressed; + radialProgress.swapBackground(getDrawableForCurrentState()); if (useSeekBarWaweform) { seekBarWaveform.setSelected(isDrawSelectedBackground()); } else { seekBar.setSelected(isDrawSelectedBackground()); } + invalidate(); } - @Override public void setHighlighted(boolean value) { - super.setHighlighted(value); - if (radialProgress.swapBackground(getDrawableForCurrentState())) { - invalidate(); + if (isHighlighted == value) { + return; } + isHighlighted = value; + radialProgress.swapBackground(getDrawableForCurrentState()); if (useSeekBarWaweform) { seekBarWaveform.setSelected(isDrawSelectedBackground()); } else { seekBar.setSelected(isDrawSelectedBackground()); } + invalidate(); } @Override public void setPressed(boolean pressed) { super.setPressed(pressed); - if (radialProgress.swapBackground(getDrawableForCurrentState())) { - invalidate(); - } + radialProgress.swapBackground(getDrawableForCurrentState()); if (useSeekBarWaweform) { seekBarWaveform.setSelected(isDrawSelectedBackground()); } else { seekBar.setSelected(isDrawSelectedBackground()); } + invalidate(); } @Override @@ -1159,11 +1536,11 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele break; } } - availableTimeWidth = maxWidth - AndroidUtilities.dp(76 + 18) - (int) Math.ceil(audioTimePaint.measureText("00:00")); + availableTimeWidth = maxWidth - dp(76 + 18) - (int) Math.ceil(audioTimePaint.measureText("00:00")); measureTime(messageObject); - int minSize = AndroidUtilities.dp(40 + 14 + 20 + 90 + 10) + timeWidth; + int minSize = dp(40 + 14 + 20 + 90 + 10) + timeWidth; if (!hasLinkPreview) { - backgroundWidth = Math.min(maxWidth, minSize + duration * AndroidUtilities.dp(10)); + backgroundWidth = Math.min(maxWidth, minSize + duration * dp(10)); } if (messageObject.isOutOwner()) { @@ -1183,9 +1560,9 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele seekBar.setColors(Theme.MSG_IN_AUDIO_SEEKBAR_COLOR, Theme.MSG_IN_AUDIO_SEEKBAR_FILL_COLOR, Theme.MSG_IN_AUDIO_SEEKBAR_SELECTED_COLOR); } - maxWidth = maxWidth - AndroidUtilities.dp(86); + maxWidth = maxWidth - dp(86); - CharSequence stringFinal = TextUtils.ellipsize(messageObject.getMusicTitle().replace('\n', ' '), audioTitlePaint, maxWidth - AndroidUtilities.dp(12), TextUtils.TruncateAt.END); + CharSequence stringFinal = TextUtils.ellipsize(messageObject.getMusicTitle().replace('\n', ' '), audioTitlePaint, maxWidth - dp(12), TextUtils.TruncateAt.END); songLayout = new StaticLayout(stringFinal, audioTitlePaint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); if (songLayout.getLineCount() > 0) { songX = -(int) Math.ceil(songLayout.getLineLeft(0)); @@ -1206,7 +1583,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } } int durationWidth = (int) Math.ceil(audioTimePaint.measureText(String.format("%d:%02d / %d:%02d", duration / 60, duration % 60, duration / 60, duration % 60))); - availableTimeWidth = backgroundWidth - AndroidUtilities.dp(76 + 18) - durationWidth; + availableTimeWidth = backgroundWidth - dp(76 + 18) - durationWidth; return durationWidth; } else if (MessageObject.isVideoDocument(documentAttach)) { documentAttachType = DOCUMENT_ATTACH_TYPE_VIDEO; @@ -1220,7 +1597,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } int minutes = duration / 60; int seconds = duration - minutes * 60; - String str = String.format("%d:%02d, %s", minutes, seconds, AndroidUtilities.formatFileSize(documentAttach.size)); + String str = String.format("%d:%02d, %s", minutes, seconds, formatFileSize(documentAttach.size)); infoWidth = (int) Math.ceil(infoPaint.measureText(str)); infoLayout = new StaticLayout(str, infoPaint, infoWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); @@ -1228,34 +1605,34 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } else { drawPhotoImage = documentAttach.mime_type != null && documentAttach.mime_type.toLowerCase().startsWith("image/") || documentAttach.thumb instanceof TLRPC.TL_photoSize && !(documentAttach.thumb.location instanceof TLRPC.TL_fileLocationUnavailable); if (!drawPhotoImage) { - maxWidth += AndroidUtilities.dp(30); + maxWidth += dp(30); } documentAttachType = DOCUMENT_ATTACH_TYPE_DOCUMENT; String name = FileLoader.getDocumentFileName(documentAttach); if (name == null || name.length() == 0) { name = LocaleController.getString("AttachDocument", R.string.AttachDocument); } - captionLayout = StaticLayoutEx.createStaticLayout(name, docNamePaint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false, TextUtils.TruncateAt.MIDDLE, maxWidth, drawPhotoImage ? 2 : 1); - nameOffsetX = Integer.MIN_VALUE; - int captionWidth; - if (captionLayout != null && captionLayout.getLineCount() > 0) { + docTitleLayout = StaticLayoutEx.createStaticLayout(name, docNamePaint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false, TextUtils.TruncateAt.MIDDLE, maxWidth, drawPhotoImage ? 2 : 1); + docTitleOffsetX = Integer.MIN_VALUE; + int width; + if (docTitleLayout != null && docTitleLayout.getLineCount() > 0) { int maxLineWidth = 0; - for (int a = 0; a < captionLayout.getLineCount(); a++) { - maxLineWidth = Math.max(maxLineWidth, (int) Math.ceil(captionLayout.getLineWidth(a))); - nameOffsetX = Math.max(nameOffsetX, (int) Math.ceil(-captionLayout.getLineLeft(a))); + for (int a = 0; a < docTitleLayout.getLineCount(); a++) { + maxLineWidth = Math.max(maxLineWidth, (int) Math.ceil(docTitleLayout.getLineWidth(a))); + docTitleOffsetX = Math.max(docTitleOffsetX, (int) Math.ceil(-docTitleLayout.getLineLeft(a))); } - captionWidth = Math.min(maxWidth, maxLineWidth); + width = Math.min(maxWidth, maxLineWidth); } else { - captionWidth = maxWidth; - nameOffsetX = 0; + width = maxWidth; + docTitleOffsetX = 0; } - String str = AndroidUtilities.formatFileSize(documentAttach.size) + " " + FileLoader.getDocumentExtension(documentAttach); - infoWidth = Math.min(maxWidth, (int) Math.ceil(infoPaint.measureText(str))); + String str = formatFileSize(documentAttach.size) + " " + FileLoader.getDocumentExtension(documentAttach); + infoWidth = Math.min(maxWidth - AndroidUtilities.dp(30), (int) Math.ceil(infoPaint.measureText(str))); CharSequence str2 = TextUtils.ellipsize(str, infoPaint, infoWidth, TextUtils.TruncateAt.END); try { if (infoWidth < 0) { - infoWidth = AndroidUtilities.dp(10); + infoWidth = dp(10); } infoLayout = new StaticLayout(str2, infoPaint, infoWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); } catch (Exception e) { @@ -1263,7 +1640,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } if (drawPhotoImage) { - currentPhotoObject = FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, AndroidUtilities.getPhotoSize()); + currentPhotoObject = FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, getPhotoSize()); photoImage.setNeedsQualityThumb(true); photoImage.setShouldGenerateQualityThumb(true); photoImage.setParentMessageObject(messageObject); @@ -1274,31 +1651,101 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele photoImage.setImageBitmap((BitmapDrawable) null); } } - return captionWidth; + return width; } } private void calcBackgroundWidth(int maxWidth, int timeMore, int maxChildWidth) { if (hasLinkPreview || maxWidth - currentMessageObject.lastLineWidth < timeMore) { - totalHeight += AndroidUtilities.dp(14); - backgroundWidth = Math.max(maxChildWidth, currentMessageObject.lastLineWidth) + AndroidUtilities.dp(31); - backgroundWidth = Math.max(backgroundWidth, timeWidth + AndroidUtilities.dp(31)); + totalHeight += dp(14); + backgroundWidth = Math.max(maxChildWidth, currentMessageObject.lastLineWidth) + dp(31); + backgroundWidth = Math.max(backgroundWidth, timeWidth + dp(31)); } else { int diff = maxChildWidth - currentMessageObject.lastLineWidth; if (diff >= 0 && diff <= timeMore) { - backgroundWidth = maxChildWidth + timeMore - diff + AndroidUtilities.dp(31); + backgroundWidth = maxChildWidth + timeMore - diff + dp(31); } else { - backgroundWidth = Math.max(maxChildWidth, currentMessageObject.lastLineWidth + timeMore) + AndroidUtilities.dp(31); + backgroundWidth = Math.max(maxChildWidth, currentMessageObject.lastLineWidth + timeMore) + dp(31); + } + } + } + + public void setHighlightedText(String text) { + if (currentMessageObject.messageOwner.message == null || currentMessageObject == null || currentMessageObject.type != 0 || TextUtils.isEmpty(currentMessageObject.messageText) || text == null) { + if (!urlPathSelection.isEmpty()) { + linkSelectionBlockNum = -1; + resetUrlPaths(true); + invalidate(); + } + return; + } + int start = TextUtils.indexOf(currentMessageObject.messageOwner.message.toLowerCase(), text.toLowerCase()); + if (start == -1) { + if (!urlPathSelection.isEmpty()) { + linkSelectionBlockNum = -1; + resetUrlPaths(true); + invalidate(); + } + return; + } + int end = start + text.length(); + for (int c = 0; c < currentMessageObject.textLayoutBlocks.size(); c++) { + MessageObject.TextLayoutBlock block = currentMessageObject.textLayoutBlocks.get(c); + if (start >= block.charactersOffset && start < block.charactersOffset + block.textLayout.getText().length()) { + linkSelectionBlockNum = c; + resetUrlPaths(true); + try { + LinkPath path = obtainNewUrlPath(true); + int length = block.textLayout.getText().length(); + path.setCurrentLayout(block.textLayout, start, 0); + block.textLayout.getSelectionPath(start, end - block.charactersOffset, path); + if (end >= block.charactersOffset + length) { + for (int a = c + 1; a < currentMessageObject.textLayoutBlocks.size(); a++) { + MessageObject.TextLayoutBlock nextBlock = currentMessageObject.textLayoutBlocks.get(a); + length = nextBlock.textLayout.getText().length(); + path = obtainNewUrlPath(true); + path.setCurrentLayout(nextBlock.textLayout, 0, nextBlock.height); + nextBlock.textLayout.getSelectionPath(0, end - nextBlock.charactersOffset, path); + if (end < block.charactersOffset + length - 1) { + break; + } + } + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + invalidate(); + break; } } } - @Override public void setMessageObject(MessageObject messageObject) { boolean messageIdChanged = currentMessageObject == null || currentMessageObject.getId() != messageObject.getId(); boolean messageChanged = currentMessageObject != messageObject || messageObject.forceUpdate; boolean dataChanged = currentMessageObject == messageObject && (isUserDataChanged() || photoNotSet); if (messageChanged || dataChanged || isPhotoDataChanged(messageObject)) { + currentMessageObject = messageObject; + lastSendState = messageObject.messageOwner.send_state; + lastDeleteDate = messageObject.messageOwner.destroyTime; + lastViewsCount = messageObject.messageOwner.views; + isPressed = false; + isCheckPressed = true; + isAvatarVisible = false; + wasLayout = false; + drawShareButton = checkNeedDrawShareButton(messageObject); + replyNameLayout = null; + replyTextLayout = null; + replyNameWidth = 0; + replyTextWidth = 0; + viaWidth = 0; + viaNameWidth = 0; + currentReplyPhoto = null; + currentUser = null; + currentChat = null; + currentViaBotUser = null; + drawNameLayout = false; + resetPressedLink(-1); messageObject.forceUpdate = false; drawPhotoImage = false; @@ -1308,14 +1755,15 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele pressedBotButton = -1; linkPreviewHeight = 0; mediaOffsetY = 0; - durationLayout = null; documentAttachType = DOCUMENT_ATTACH_TYPE_NONE; documentAttach = null; descriptionLayout = null; titleLayout = null; - siteCaptionLayout = null; + videoInfoLayout = null; + siteNameLayout = null; authorLayout = null; captionLayout = null; + docTitleLayout = null; drawImageButton = false; currentPhotoObject = null; currentPhotoObjectThumb = null; @@ -1334,7 +1782,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele photoImage.setNeedsQualityThumb(false); photoImage.setShouldGenerateQualityThumb(false); photoImage.setParentMessageObject(null); - photoImage.setRoundRadius(AndroidUtilities.dp(3)); + photoImage.setRoundRadius(dp(3)); if (messageChanged) { firstVisibleBlockNum = 0; @@ -1346,48 +1794,48 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele drawForwardedName = true; int maxWidth; - if (AndroidUtilities.isTablet()) { + if (isTablet()) { if (isChat && !messageObject.isOutOwner() && messageObject.isFromUser()) { - maxWidth = AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(122); + maxWidth = getMinTabletSide() - dp(122); drawName = true; } else { drawName = messageObject.messageOwner.to_id.channel_id != 0 && !messageObject.isOutOwner(); - maxWidth = AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(80); + maxWidth = getMinTabletSide() - dp(80); } } else { if (isChat && !messageObject.isOutOwner() && messageObject.isFromUser()) { - maxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) - AndroidUtilities.dp(122); + maxWidth = Math.min(displaySize.x, displaySize.y) - dp(122); drawName = true; } else { - maxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) - AndroidUtilities.dp(80); + maxWidth = Math.min(displaySize.x, displaySize.y) - dp(80); drawName = messageObject.messageOwner.to_id.channel_id != 0 && !messageObject.isOutOwner(); } } measureTime(messageObject); - int timeMore = timeWidth + AndroidUtilities.dp(6); + int timeMore = timeWidth + dp(6); if (messageObject.isOutOwner()) { - timeMore += AndroidUtilities.dp(20.5f); + timeMore += dp(20.5f); } hasLinkPreview = messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage && messageObject.messageOwner.media.webpage instanceof TLRPC.TL_webPage; backgroundWidth = maxWidth; if (hasLinkPreview || maxWidth - messageObject.lastLineWidth < timeMore) { - backgroundWidth = Math.max(backgroundWidth, messageObject.lastLineWidth) + AndroidUtilities.dp(31); - backgroundWidth = Math.max(backgroundWidth, timeWidth + AndroidUtilities.dp(31)); + backgroundWidth = Math.max(backgroundWidth, messageObject.lastLineWidth) + dp(31); + backgroundWidth = Math.max(backgroundWidth, timeWidth + dp(31)); } else { int diff = backgroundWidth - messageObject.lastLineWidth; if (diff >= 0 && diff <= timeMore) { - backgroundWidth = backgroundWidth + timeMore - diff + AndroidUtilities.dp(31); + backgroundWidth = backgroundWidth + timeMore - diff + dp(31); } else { - backgroundWidth = Math.max(backgroundWidth, messageObject.lastLineWidth + timeMore) + AndroidUtilities.dp(31); + backgroundWidth = Math.max(backgroundWidth, messageObject.lastLineWidth + timeMore) + dp(31); } } - availableTimeWidth = backgroundWidth - AndroidUtilities.dp(31); + availableTimeWidth = backgroundWidth - dp(31); - super.setMessageObject(messageObject); + setMessageObjectInternal(messageObject); backgroundWidth = messageObject.textWidth; - totalHeight = messageObject.textHeight + AndroidUtilities.dp(19.5f) + namesOffset; + totalHeight = messageObject.textHeight + dp(19.5f) + namesOffset; int maxChildWidth = Math.max(backgroundWidth, nameWidth); maxChildWidth = Math.max(maxChildWidth, forwardedNameWidth); @@ -1397,30 +1845,30 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (hasLinkPreview) { int linkPreviewMaxWidth; - if (AndroidUtilities.isTablet()) { + if (isTablet()) { if (messageObject.isFromUser() && (currentMessageObject.messageOwner.to_id.channel_id != 0 || currentMessageObject.messageOwner.to_id.chat_id != 0) && !currentMessageObject.isOut()) { - linkPreviewMaxWidth = AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(122); + linkPreviewMaxWidth = getMinTabletSide() - dp(122); } else { - linkPreviewMaxWidth = AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(80); + linkPreviewMaxWidth = getMinTabletSide() - dp(80); } } else { if (messageObject.isFromUser() && (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); + linkPreviewMaxWidth = Math.min(displaySize.x, displaySize.y) - dp(122); } else { - linkPreviewMaxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) - AndroidUtilities.dp(80); + linkPreviewMaxWidth = Math.min(displaySize.x, displaySize.y) - dp(80); } } if (drawShareButton) { - linkPreviewMaxWidth -= AndroidUtilities.dp(20); + linkPreviewMaxWidth -= dp(20); } TLRPC.TL_webPage webPage = (TLRPC.TL_webPage) messageObject.messageOwner.media.webpage; if (webPage.site_name != null && webPage.photo != null && webPage.site_name.toLowerCase().equals("instagram")) { - linkPreviewMaxWidth = Math.max(AndroidUtilities.displaySize.y / 3, currentMessageObject.textWidth); + linkPreviewMaxWidth = Math.max(displaySize.y / 3, currentMessageObject.textWidth); } - int additinalWidth = AndroidUtilities.dp(10); + int additinalWidth = dp(10); int restLinesCount = 3; int additionalHeight = 0; linkPreviewMaxWidth -= additinalWidth; @@ -1434,12 +1882,12 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (webPage.site_name != null) { try { int width = (int) Math.ceil(replyNamePaint.measureText(webPage.site_name)); - siteCaptionLayout = new StaticLayout(webPage.site_name, replyNamePaint, Math.min(width, linkPreviewMaxWidth), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - int height = siteCaptionLayout.getLineBottom(siteCaptionLayout.getLineCount() - 1); + siteNameLayout = new StaticLayout(webPage.site_name, 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; additionalHeight += height; - width = siteCaptionLayout.getWidth(); + width = siteNameLayout.getWidth(); maxChildWidth = Math.max(maxChildWidth, width + additinalWidth); maxWebWidth = Math.max(maxWebWidth, width + additinalWidth); } catch (Exception e) { @@ -1450,31 +1898,32 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele boolean titleIsRTL = false; if (webPage.title != null) { try { - titleX = 0; + titleX = Integer.MAX_VALUE; if (linkPreviewHeight != 0) { - linkPreviewHeight += AndroidUtilities.dp(2); - totalHeight += AndroidUtilities.dp(2); + linkPreviewHeight += dp(2); + totalHeight += dp(2); } int restLines = 0; if (!isSmallImage || webPage.description == null) { - titleLayout = StaticLayoutEx.createStaticLayout(webPage.title, replyNamePaint, linkPreviewMaxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, AndroidUtilities.dp(1), false, TextUtils.TruncateAt.END, linkPreviewMaxWidth, 4); + titleLayout = StaticLayoutEx.createStaticLayout(webPage.title, replyNamePaint, linkPreviewMaxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, dp(1), false, TextUtils.TruncateAt.END, linkPreviewMaxWidth, 4); } else { restLines = restLinesCount; - titleLayout = generateStaticLayout(webPage.title, replyNamePaint, linkPreviewMaxWidth, linkPreviewMaxWidth - AndroidUtilities.dp(48 + 4), restLinesCount, 4); + titleLayout = generateStaticLayout(webPage.title, replyNamePaint, linkPreviewMaxWidth, linkPreviewMaxWidth - dp(48 + 4), restLinesCount, 4); restLinesCount -= titleLayout.getLineCount(); } int height = titleLayout.getLineBottom(titleLayout.getLineCount() - 1); linkPreviewHeight += height; totalHeight += height; + boolean checkForRtl = true; for (int a = 0; a < titleLayout.getLineCount(); a++) { int lineLeft = (int) titleLayout.getLineLeft(a); if (lineLeft != 0) { titleIsRTL = true; - if (titleX == 0) { - titleX = -lineLeft; - } else { - titleX = Math.max(titleX, -lineLeft); - } + } + if (titleX == Integer.MAX_VALUE) { + titleX = -lineLeft; + } else { + titleX = Math.max(titleX, -lineLeft); } int width; if (lineLeft != 0) { @@ -1483,7 +1932,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele width = (int) Math.ceil(titleLayout.getLineWidth(a)); } if (a < restLines || lineLeft != 0 && isSmallImage) { - width += AndroidUtilities.dp(48 + 4); + width += dp(48 + 4); } maxChildWidth = Math.max(maxChildWidth, width + additinalWidth); maxWebWidth = Math.max(maxWebWidth, width + additinalWidth); @@ -1497,14 +1946,14 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (webPage.author != null) { try { if (linkPreviewHeight != 0) { - linkPreviewHeight += AndroidUtilities.dp(2); - totalHeight += AndroidUtilities.dp(2); + linkPreviewHeight += dp(2); + totalHeight += dp(2); } //int width = Math.min((int) Math.ceil(replyNamePaint.measureText(webPage.author)), linkPreviewMaxWidth); if (restLinesCount == 3 && (!isSmallImage || webPage.description == null)) { authorLayout = new StaticLayout(webPage.author, replyNamePaint, linkPreviewMaxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); } else { - authorLayout = generateStaticLayout(webPage.author, replyNamePaint, linkPreviewMaxWidth, linkPreviewMaxWidth - AndroidUtilities.dp(48 + 4), restLinesCount, 1); + authorLayout = generateStaticLayout(webPage.author, replyNamePaint, linkPreviewMaxWidth, linkPreviewMaxWidth - dp(48 + 4), restLinesCount, 1); restLinesCount -= authorLayout.getLineCount(); } int height = authorLayout.getLineBottom(authorLayout.getLineCount() - 1); @@ -1531,15 +1980,15 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele descriptionX = 0; currentMessageObject.generateLinkDescription(); if (linkPreviewHeight != 0) { - linkPreviewHeight += AndroidUtilities.dp(2); - totalHeight += AndroidUtilities.dp(2); + linkPreviewHeight += dp(2); + totalHeight += dp(2); } int restLines = 0; if (restLinesCount == 3 && !isSmallImage) { - descriptionLayout = StaticLayoutEx.createStaticLayout(messageObject.linkDescription, replyTextPaint, linkPreviewMaxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, AndroidUtilities.dp(1), false, TextUtils.TruncateAt.END, linkPreviewMaxWidth, 6); + descriptionLayout = StaticLayoutEx.createStaticLayout(messageObject.linkDescription, replyTextPaint, linkPreviewMaxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, dp(1), false, TextUtils.TruncateAt.END, linkPreviewMaxWidth, 6); } else { restLines = restLinesCount; - descriptionLayout = generateStaticLayout(messageObject.linkDescription, replyTextPaint, linkPreviewMaxWidth, linkPreviewMaxWidth - AndroidUtilities.dp(48 + 4), restLinesCount, 6); + descriptionLayout = generateStaticLayout(messageObject.linkDescription, replyTextPaint, linkPreviewMaxWidth, linkPreviewMaxWidth - dp(48 + 4), restLinesCount, 6); } int height = descriptionLayout.getLineBottom(descriptionLayout.getLineCount() - 1); linkPreviewHeight += height; @@ -1571,7 +2020,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele width = hasRTL ? descriptionLayout.getWidth() : (int) Math.ceil(descriptionLayout.getLineWidth(a)); } if (a < restLines || restLines != 0 && lineLeft != 0 && isSmallImage) { - width += AndroidUtilities.dp(48 + 4); + width += dp(48 + 4); } if (maxWebWidth < width + additinalWidth) { if (titleIsRTL) { @@ -1582,14 +2031,14 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } maxWebWidth = width + additinalWidth; } - if (restLines == 0 || !isSmallImage) { + /*if (restLines == 0 || !isSmallImage) { if (titleIsRTL) { - titleX = -AndroidUtilities.dp(4); + titleX = -dp(4); } if (authorIsRTL) { - authorX = -AndroidUtilities.dp(4); + authorX = -dp(4); } - } + }*/ maxChildWidth = Math.max(maxChildWidth, width + additinalWidth); } } catch (Exception e) { @@ -1602,7 +2051,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele smallImage = false; isSmallImage = false; } - int maxPhotoWidth = smallImage ? AndroidUtilities.dp(48) : linkPreviewMaxWidth; + int maxPhotoWidth = smallImage ? dp(48) : linkPreviewMaxWidth; if (webPage.document != null) { TLRPC.Document document = webPage.document; @@ -1622,7 +2071,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } } if (currentPhotoObject.w == 0 || currentPhotoObject.h == 0) { - currentPhotoObject.w = currentPhotoObject.h = AndroidUtilities.dp(150); + currentPhotoObject.w = currentPhotoObject.h = dp(150); } } documentAttachType = DOCUMENT_ATTACH_TYPE_GIF; @@ -1638,7 +2087,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } } if (currentPhotoObject.w == 0 || currentPhotoObject.h == 0) { - currentPhotoObject.w = currentPhotoObject.h = AndroidUtilities.dp(150); + currentPhotoObject.w = currentPhotoObject.h = dp(150); } } createDocumentLayout(0, messageObject); @@ -1654,7 +2103,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } } if (currentPhotoObject.w == 0 || currentPhotoObject.h == 0) { - currentPhotoObject.w = currentPhotoObject.h = AndroidUtilities.dp(150); + currentPhotoObject.w = currentPhotoObject.h = dp(150); } } documentAttach = document; @@ -1662,50 +2111,50 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } else { calcBackgroundWidth(maxWidth, timeMore, maxChildWidth); if (!MessageObject.isStickerDocument(document)) { - if (backgroundWidth < maxWidth + AndroidUtilities.dp(20)) { - backgroundWidth = maxWidth + AndroidUtilities.dp(20); + if (backgroundWidth < maxWidth + dp(20)) { + backgroundWidth = maxWidth + dp(20); } if (MessageObject.isVoiceDocument(document)) { - createDocumentLayout(backgroundWidth - AndroidUtilities.dp(10), messageObject); - mediaOffsetY = currentMessageObject.textHeight + AndroidUtilities.dp(8) + linkPreviewHeight; - totalHeight += AndroidUtilities.dp(30 + 14); - linkPreviewHeight += AndroidUtilities.dp(44); + createDocumentLayout(backgroundWidth - dp(10), messageObject); + mediaOffsetY = currentMessageObject.textHeight + dp(8) + linkPreviewHeight; + totalHeight += dp(30 + 14); + linkPreviewHeight += dp(44); calcBackgroundWidth(maxWidth, timeMore, maxChildWidth); } else if (MessageObject.isMusicDocument(document)) { - int durationWidth = createDocumentLayout(backgroundWidth - AndroidUtilities.dp(10), messageObject); - mediaOffsetY = currentMessageObject.textHeight + AndroidUtilities.dp(8) + linkPreviewHeight; - totalHeight += AndroidUtilities.dp(42 + 14); - linkPreviewHeight += AndroidUtilities.dp(56); + int durationWidth = createDocumentLayout(backgroundWidth - dp(10), messageObject); + mediaOffsetY = currentMessageObject.textHeight + dp(8) + linkPreviewHeight; + totalHeight += dp(42 + 14); + linkPreviewHeight += dp(56); - maxWidth = maxWidth - AndroidUtilities.dp(86); - maxChildWidth = Math.max(maxChildWidth, durationWidth + additinalWidth + AndroidUtilities.dp(86 + 8)); + maxWidth = maxWidth - dp(86); + maxChildWidth = Math.max(maxChildWidth, durationWidth + additinalWidth + dp(86 + 8)); if (songLayout != null && songLayout.getLineCount() > 0) { - maxChildWidth = (int) Math.max(maxChildWidth, songLayout.getLineWidth(0) + additinalWidth + AndroidUtilities.dp(86)); + maxChildWidth = (int) Math.max(maxChildWidth, songLayout.getLineWidth(0) + additinalWidth + dp(86)); } if (performerLayout != null && performerLayout.getLineCount() > 0) { - maxChildWidth = (int) Math.max(maxChildWidth, performerLayout.getLineWidth(0) + additinalWidth + AndroidUtilities.dp(86)); + maxChildWidth = (int) Math.max(maxChildWidth, performerLayout.getLineWidth(0) + additinalWidth + dp(86)); } calcBackgroundWidth(maxWidth, timeMore, maxChildWidth); } else { - createDocumentLayout(backgroundWidth - AndroidUtilities.dp(86 + 24 + 58), messageObject); + createDocumentLayout(backgroundWidth - dp(86 + 24 + 58), messageObject); drawImageButton = true; if (drawPhotoImage) { - totalHeight += AndroidUtilities.dp(86 + 14); - linkPreviewHeight += AndroidUtilities.dp(86); - photoImage.setImageCoords(0, totalHeight + namesOffset, AndroidUtilities.dp(86), AndroidUtilities.dp(86)); + totalHeight += dp(86 + 14); + linkPreviewHeight += dp(86); + photoImage.setImageCoords(0, totalHeight + namesOffset, dp(86), dp(86)); } else { - mediaOffsetY = currentMessageObject.textHeight + AndroidUtilities.dp(8) + linkPreviewHeight; - photoImage.setImageCoords(0, totalHeight + namesOffset - AndroidUtilities.dp(14), AndroidUtilities.dp(56), AndroidUtilities.dp(56)); - totalHeight += AndroidUtilities.dp(50 + 14); - linkPreviewHeight += AndroidUtilities.dp(50); + mediaOffsetY = currentMessageObject.textHeight + dp(8) + linkPreviewHeight; + photoImage.setImageCoords(0, totalHeight + namesOffset - dp(14), dp(56), dp(56)); + totalHeight += dp(50 + 14); + linkPreviewHeight += dp(50); } } } } } else if (webPage.photo != null) { drawImageButton = webPage.type != null && webPage.type.equals("photo"); - currentPhotoObject = FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, drawImageButton ? AndroidUtilities.getPhotoSize() : maxPhotoWidth, !drawImageButton); + currentPhotoObject = FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, drawImageButton ? getPhotoSize() : maxPhotoWidth, !drawImageButton); currentPhotoObjectThumb = FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, 80); if (currentPhotoObjectThumb == currentPhotoObject) { currentPhotoObjectThumb = null; @@ -1716,15 +2165,15 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (currentPhotoObject != null) { drawImageButton = webPage.type != null && (webPage.type.equals("photo") || webPage.type.equals("document") && documentAttachType != DOCUMENT_ATTACH_TYPE_STICKER || webPage.type.equals("gif") || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO); if (linkPreviewHeight != 0) { - linkPreviewHeight += AndroidUtilities.dp(2); - totalHeight += AndroidUtilities.dp(2); + linkPreviewHeight += dp(2); + totalHeight += dp(2); } if (documentAttachType == DOCUMENT_ATTACH_TYPE_STICKER) { - if (AndroidUtilities.isTablet()) { - maxPhotoWidth = (int) (AndroidUtilities.getMinTabletSide() * 0.5f); + if (isTablet()) { + maxPhotoWidth = (int) (getMinTabletSide() * 0.5f); } else { - maxPhotoWidth = (int) (AndroidUtilities.displaySize.x * 0.5f); + maxPhotoWidth = (int) (displaySize.x * 0.5f); } } @@ -1741,23 +2190,23 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } else { width = currentPhotoObject.w; height = currentPhotoObject.h; - float scale = width / (float) (maxPhotoWidth - AndroidUtilities.dp(2)); + float scale = width / (float) (maxPhotoWidth - dp(2)); width /= scale; height /= scale; if (webPage.site_name == null || webPage.site_name != null && !webPage.site_name.toLowerCase().equals("instagram") && documentAttachType == 0) { - if (height > AndroidUtilities.displaySize.y / 3) { - height = AndroidUtilities.displaySize.y / 3; + if (height > displaySize.y / 3) { + height = displaySize.y / 3; } } } if (isSmallImage) { - if (AndroidUtilities.dp(50) + additionalHeight > linkPreviewHeight) { - totalHeight += AndroidUtilities.dp(50) + additionalHeight - linkPreviewHeight + AndroidUtilities.dp(8); - linkPreviewHeight = AndroidUtilities.dp(50) + additionalHeight; + if (dp(50) + additionalHeight > linkPreviewHeight) { + totalHeight += dp(50) + additionalHeight - linkPreviewHeight + dp(8); + linkPreviewHeight = dp(50) + additionalHeight; } - linkPreviewHeight -= AndroidUtilities.dp(8); + linkPreviewHeight -= dp(8); } else { - totalHeight += height + AndroidUtilities.dp(12); + totalHeight += height + dp(12); linkPreviewHeight += height; } @@ -1802,12 +2251,12 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele int seconds = webPage.duration - minutes * 60; String str = String.format("%d:%02d", minutes, seconds); durationWidth = (int) Math.ceil(durationPaint.measureText(str)); - durationLayout = new StaticLayout(str, durationPaint, durationWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + videoInfoLayout = new StaticLayout(str, durationPaint, durationWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); } } else { photoImage.setImageBitmap((Drawable) null); - linkPreviewHeight -= AndroidUtilities.dp(6); - totalHeight += AndroidUtilities.dp(4); + linkPreviewHeight -= dp(6); + totalHeight += dp(4); } calcBackgroundWidth(maxWidth, timeMore, maxChildWidth); } @@ -1819,20 +2268,20 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele drawName = false; drawForwardedName = true; 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)); + photoImage.setRoundRadius(dp(22)); + if (isTablet()) { + backgroundWidth = Math.min(getMinTabletSide() - dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), dp(270)); } else { - backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(displaySize.x - dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), dp(270)); } - availableTimeWidth = backgroundWidth - AndroidUtilities.dp(31); + availableTimeWidth = backgroundWidth - dp(31); int uid = messageObject.messageOwner.media.user_id; TLRPC.User user = MessagesController.getInstance().getUser(uid); - int maxWidth = getMaxNameWidth() - AndroidUtilities.dp(110); + int maxWidth = getMaxNameWidth() - dp(110); if (maxWidth < 0) { - maxWidth = AndroidUtilities.dp(10); + maxWidth = dp(10); } TLRPC.FileLocation currentPhoto = null; @@ -1840,15 +2289,12 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (user.photo != null) { currentPhoto = user.photo.photo_small; } - avatarDrawable.setInfo(user); + contactAvatarDrawable.setInfo(user); } - photoImage.setImage(currentPhoto, "50_50", user != null ? avatarDrawable : Theme.contactDrawable[messageObject.isOutOwner() ? 1 : 0], null, false); + photoImage.setImage(currentPhoto, "50_50", user != null ? contactAvatarDrawable : Theme.contactDrawable[messageObject.isOutOwner() ? 1 : 0], null, false); String phone = messageObject.messageOwner.media.phone_number; if (phone != null && phone.length() != 0) { - if (!phone.startsWith("+")) { - phone = "+" + phone; - } phone = PhoneFormat.getInstance().format(phone); } else { phone = LocaleController.getString("NumberUnknown", R.string.NumberUnknown); @@ -1858,42 +2304,48 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (currentNameString.length() == 0) { currentNameString = phone; } - titleLayout = new StaticLayout(TextUtils.ellipsize(currentNameString, contactNamePaint, maxWidth, TextUtils.TruncateAt.END), contactNamePaint, maxWidth + AndroidUtilities.dp(2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - captionLayout = new StaticLayout(TextUtils.ellipsize(phone.replace('\n', ' '), contactPhonePaint, maxWidth, TextUtils.TruncateAt.END), contactPhonePaint, maxWidth + AndroidUtilities.dp(2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + titleLayout = new StaticLayout(TextUtils.ellipsize(currentNameString, contactNamePaint, maxWidth, TextUtils.TruncateAt.END), contactNamePaint, maxWidth + dp(2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + docTitleLayout = new StaticLayout(TextUtils.ellipsize(phone.replace('\n', ' '), contactPhonePaint, maxWidth, TextUtils.TruncateAt.END), contactPhonePaint, maxWidth + dp(2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - super.setMessageObject(messageObject); + setMessageObjectInternal(messageObject); if (drawForwardedName && messageObject.isForwarded()) { - namesOffset += AndroidUtilities.dp(5); + namesOffset += dp(5); } else if (drawNameLayout && messageObject.messageOwner.reply_to_msg_id == 0) { - namesOffset += AndroidUtilities.dp(7); + namesOffset += dp(7); } - totalHeight = AndroidUtilities.dp(70) + namesOffset; + totalHeight = dp(70) + namesOffset; + if (docTitleLayout.getLineCount() > 0) { + int timeLeft = backgroundWidth - AndroidUtilities.dp(40 + 18 + 44 + 8) - (int) Math.ceil(docTitleLayout.getLineWidth(0)); + if (timeLeft < timeWidth) { + totalHeight += AndroidUtilities.dp(8); + } + } } 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)); + if (isTablet()) { + backgroundWidth = Math.min(getMinTabletSide() - dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), dp(270)); } else { - backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(displaySize.x - dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), dp(270)); } createDocumentLayout(backgroundWidth, messageObject); - super.setMessageObject(messageObject); + setMessageObjectInternal(messageObject); - totalHeight = AndroidUtilities.dp(70) + namesOffset; + totalHeight = dp(70) + namesOffset; } 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)); + if (isTablet()) { + backgroundWidth = Math.min(getMinTabletSide() - dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), dp(270)); } else { - backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(displaySize.x - dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), dp(270)); } createDocumentLayout(backgroundWidth, messageObject); - super.setMessageObject(messageObject); + setMessageObjectInternal(messageObject); - totalHeight = AndroidUtilities.dp(82) + namesOffset; + totalHeight = dp(82) + namesOffset; } else { drawForwardedName = messageObject.messageOwner.fwd_from != null && messageObject.type != 13; mediaBackground = messageObject.type != 9; @@ -1912,42 +2364,54 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele 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)); + if (isTablet()) { + backgroundWidth = Math.min(getMinTabletSide() - dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), dp(270)); } else { - backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(displaySize.x - dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), dp(270)); } if (checkNeedDrawShareButton(messageObject)) { - backgroundWidth -= AndroidUtilities.dp(20); + backgroundWidth -= dp(20); } - int maxWidth = backgroundWidth - AndroidUtilities.dp(86 + 52); - + int maxWidth = backgroundWidth - dp(86 + 52); createDocumentLayout(maxWidth, messageObject); + if (!TextUtils.isEmpty(messageObject.caption)) { + maxWidth += AndroidUtilities.dp(86); + } if (drawPhotoImage) { - photoWidth = AndroidUtilities.dp(86); - photoHeight = AndroidUtilities.dp(86); + photoWidth = dp(86); + photoHeight = dp(86); } else { - photoWidth = AndroidUtilities.dp(56); - photoHeight = AndroidUtilities.dp(56); + photoWidth = dp(56); + photoHeight = dp(56); + maxWidth += AndroidUtilities.dp(TextUtils.isEmpty(messageObject.caption) ? 51 : 21); } availableTimeWidth = maxWidth; + if (!drawPhotoImage) { + if (TextUtils.isEmpty(messageObject.caption) && infoLayout.getLineCount() > 0) { + measureTime(messageObject); + int timeLeft = backgroundWidth - AndroidUtilities.dp(40 + 18 + 56 + 8) - (int) Math.ceil(infoLayout.getLineWidth(0)); + if (timeLeft < timeWidth) { + photoHeight += AndroidUtilities.dp(8); + } + } + } } else if (messageObject.type == 4) { //geo double lat = messageObject.messageOwner.media.geo.lat; double lon = messageObject.messageOwner.media.geo._long; 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)); + if (isTablet()) { + backgroundWidth = Math.min(getMinTabletSide() - dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), dp(270)); } else { - backgroundWidth = Math.min(AndroidUtilities.displaySize.x - AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), AndroidUtilities.dp(270)); + backgroundWidth = Math.min(displaySize.x - dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 102 : 50), dp(270)); } if (checkNeedDrawShareButton(messageObject)) { - backgroundWidth -= AndroidUtilities.dp(20); + backgroundWidth -= dp(20); } - int maxWidth = backgroundWidth - AndroidUtilities.dp(86 + 37); + int maxWidth = backgroundWidth - dp(86 + 37); - captionLayout = StaticLayoutEx.createStaticLayout(messageObject.messageOwner.media.title, locationTitlePaint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false, TextUtils.TruncateAt.END, maxWidth, 2); - int lineCount = captionLayout.getLineCount(); + docTitleLayout = StaticLayoutEx.createStaticLayout(messageObject.messageOwner.media.title, locationTitlePaint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false, TextUtils.TruncateAt.END, maxWidth, 2); + int lineCount = docTitleLayout.getLineCount(); if (messageObject.messageOwner.media.address != null && messageObject.messageOwner.media.address.length() > 0) { infoLayout = StaticLayoutEx.createStaticLayout(messageObject.messageOwner.media.address, locationAddressPaint, maxWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false, TextUtils.TruncateAt.END, maxWidth, Math.min(3, 3 - lineCount)); } else { @@ -1956,15 +2420,15 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele mediaBackground = false; availableTimeWidth = maxWidth; - photoWidth = AndroidUtilities.dp(86); - photoHeight = AndroidUtilities.dp(86); - currentUrl = String.format(Locale.US, "https://maps.googleapis.com/maps/api/staticmap?center=%f,%f&zoom=15&size=72x72&maptype=roadmap&scale=%d&markers=color:red|size:mid|%f,%f&sensor=false", lat, lon, Math.min(2, (int) Math.ceil(AndroidUtilities.density)), lat, lon); + photoWidth = dp(86); + photoHeight = dp(86); + currentUrl = String.format(Locale.US, "https://maps.googleapis.com/maps/api/staticmap?center=%f,%f&zoom=15&size=72x72&maptype=roadmap&scale=%d&markers=color:red|size:mid|%f,%f&sensor=false", lat, lon, Math.min(2, (int) Math.ceil(density)), lat, lon); } else { - availableTimeWidth = AndroidUtilities.dp(200 - 14); - photoWidth = AndroidUtilities.dp(200); - photoHeight = AndroidUtilities.dp(100); - backgroundWidth = photoWidth + AndroidUtilities.dp(12); - currentUrl = String.format(Locale.US, "https://maps.googleapis.com/maps/api/staticmap?center=%f,%f&zoom=15&size=200x100&maptype=roadmap&scale=%d&markers=color:red|size:mid|%f,%f&sensor=false", lat, lon, Math.min(2, (int) Math.ceil(AndroidUtilities.density)), lat, lon); + availableTimeWidth = dp(200 - 14); + photoWidth = dp(200); + photoHeight = dp(100); + backgroundWidth = photoWidth + dp(12); + currentUrl = String.format(Locale.US, "https://maps.googleapis.com/maps/api/staticmap?center=%f,%f&zoom=15&size=200x100&maptype=roadmap&scale=%d&markers=color:red|size:mid|%f,%f&sensor=false", lat, lon, Math.min(2, (int) Math.ceil(density)), lat, lon); } photoImage.setImage(currentUrl, null, messageObject.isOutOwner() ? Theme.geoOutDrawable : Theme.geoInDrawable, null, 0); } else if (messageObject.type == 13) { //webp @@ -1977,27 +2441,26 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele break; } } - float maxHeight = AndroidUtilities.displaySize.y * 0.4f; + float maxHeight; float maxWidth; - if (AndroidUtilities.isTablet()) { - maxWidth = AndroidUtilities.getMinTabletSide() * 0.5f; + if (isTablet()) { + maxHeight = maxWidth = getMinTabletSide() * 0.4f; } else { - maxWidth = AndroidUtilities.displaySize.x * 0.5f; + maxHeight = maxWidth = Math.min(displaySize.x, displaySize.y) * 0.5f; } if (photoWidth == 0) { photoHeight = (int) maxHeight; - photoWidth = photoHeight + AndroidUtilities.dp(100); + photoWidth = photoHeight + dp(100); } + photoHeight *= maxWidth / photoWidth; + photoWidth = (int) maxWidth; if (photoHeight > maxHeight) { photoWidth *= maxHeight / photoHeight; photoHeight = (int) maxHeight; - } else { - photoHeight *= maxWidth / photoWidth; - photoWidth = (int) maxWidth; } documentAttachType = DOCUMENT_ATTACH_TYPE_STICKER; - availableTimeWidth = photoWidth - AndroidUtilities.dp(14); - backgroundWidth = photoWidth + AndroidUtilities.dp(12); + availableTimeWidth = photoWidth - dp(14); + backgroundWidth = photoWidth + dp(12); currentPhotoObjectThumb = FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, 80); if (messageObject.attachPathExists) { photoImage.setImage(null, messageObject.messageOwner.attachPath, @@ -2016,22 +2479,22 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } } else { int maxPhotoWidth; - if (AndroidUtilities.isTablet()) { - maxPhotoWidth = photoWidth = (int) (AndroidUtilities.getMinTabletSide() * 0.7f); + if (isTablet()) { + maxPhotoWidth = photoWidth = (int) (getMinTabletSide() * 0.7f); } else { - maxPhotoWidth = photoWidth = (int) (Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) * 0.7f); + maxPhotoWidth = photoWidth = (int) (Math.min(displaySize.x, displaySize.y) * 0.7f); } - photoHeight = photoWidth + AndroidUtilities.dp(100); + photoHeight = photoWidth + dp(100); if (checkNeedDrawShareButton(messageObject)) { - maxPhotoWidth -= AndroidUtilities.dp(20); - photoWidth -= AndroidUtilities.dp(20); + maxPhotoWidth -= dp(20); + photoWidth -= dp(20); } - if (photoWidth > AndroidUtilities.getPhotoSize()) { - photoWidth = AndroidUtilities.getPhotoSize(); + if (photoWidth > getPhotoSize()) { + photoWidth = getPhotoSize(); } - if (photoHeight > AndroidUtilities.getPhotoSize()) { - photoHeight = AndroidUtilities.getPhotoSize(); + if (photoHeight > getPhotoSize()) { + photoHeight = getPhotoSize(); } if (messageObject.type == 1) { //photo @@ -2043,7 +2506,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele photoImage.setShouldGenerateQualityThumb(true); photoImage.setParentMessageObject(messageObject); } else if (messageObject.type == 8) { //gif - String str = AndroidUtilities.formatFileSize(messageObject.messageOwner.media.document.size); + String str = formatFileSize(messageObject.messageOwner.media.document.size); infoWidth = (int) Math.ceil(infoPaint.measureText(str)); infoLayout = new StaticLayout(str, infoPaint, infoWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); @@ -2056,7 +2519,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele mediaBackground = false; } - currentPhotoObject = FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, AndroidUtilities.getPhotoSize()); + currentPhotoObject = FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, getPhotoSize()); int w = 0; int h = 0; @@ -2070,18 +2533,18 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele w = (int) (currentPhotoObject.w / scale); h = (int) (currentPhotoObject.h / scale); if (w == 0) { - w = AndroidUtilities.dp(150); + w = dp(150); } if (h == 0) { - h = AndroidUtilities.dp(150); + h = dp(150); } if (h > photoHeight) { float scale2 = h; h = photoHeight; scale2 /= h; w = (int) (w / scale2); - } else if (h < AndroidUtilities.dp(120)) { - h = AndroidUtilities.dp(120); + } else if (h < dp(120)) { + h = dp(120); float hScale = (float) currentPhotoObject.h / h; if (currentPhotoObject.w / hScale < photoWidth) { w = (int) (currentPhotoObject.w / hScale); @@ -2101,8 +2564,8 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele h = photoHeight; scale2 /= h; w = (int) (w / scale2); - } else if (h < AndroidUtilities.dp(120)) { - h = AndroidUtilities.dp(120); + } else if (h < dp(120)) { + h = dp(120); float hScale = (float) attribute.h / h; if (attribute.w / hScale < photoWidth) { w = (int) (attribute.w / hScale); @@ -2115,44 +2578,44 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (w == 0 || h == 0) { - w = h = AndroidUtilities.dp(150); + w = h = dp(150); } if (messageObject.type == 3) { - if (w < infoWidth + AndroidUtilities.dp(16 + 24)) { - w = infoWidth + AndroidUtilities.dp(16 + 24); + if (w < infoWidth + dp(16 + 24)) { + w = infoWidth + dp(16 + 24); } } - availableTimeWidth = maxPhotoWidth - AndroidUtilities.dp(14); + availableTimeWidth = maxPhotoWidth - dp(14); measureTime(messageObject); - int timeWidthTotal = timeWidth + AndroidUtilities.dp(14 + (messageObject.isOutOwner() ? 20 : 0)); + int timeWidthTotal = timeWidth + dp(14 + (messageObject.isOutOwner() ? 20 : 0)); if (w < timeWidthTotal) { w = timeWidthTotal; } if (messageObject.isSecretPhoto()) { - if (AndroidUtilities.isTablet()) { - w = h = (int) (AndroidUtilities.getMinTabletSide() * 0.5f); + if (isTablet()) { + w = h = (int) (getMinTabletSide() * 0.5f); } else { - w = h = (int) (Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) * 0.5f); + w = h = (int) (Math.min(displaySize.x, displaySize.y) * 0.5f); } } photoWidth = w; photoHeight = h; - backgroundWidth = w + AndroidUtilities.dp(12); + backgroundWidth = w + dp(12); if (!mediaBackground) { - backgroundWidth += AndroidUtilities.dp(9); + backgroundWidth += dp(9); } if (messageObject.caption != null) { try { - captionLayout = new StaticLayout(messageObject.caption, MessageObject.getTextPaint(), photoWidth - AndroidUtilities.dp(10), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); - if (captionLayout != null && captionLayout.getLineCount() > 0) { + captionLayout = new StaticLayout(messageObject.caption, MessageObject.getTextPaint(), photoWidth - dp(10), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + if (captionLayout.getLineCount() > 0) { captionHeight = captionLayout.getHeight(); - additionHeight += captionHeight + AndroidUtilities.dp(9); + additionHeight += captionHeight + dp(9); float lastLineWidth = captionLayout.getLineWidth(captionLayout.getLineCount() - 1) + captionLayout.getLineLeft(captionLayout.getLineCount() - 1); - if (photoWidth - AndroidUtilities.dp(8) - lastLineWidth < timeWidthTotal) { - additionHeight += AndroidUtilities.dp(14); + if (photoWidth - dp(8) - lastLineWidth < timeWidthTotal) { + additionHeight += dp(14); } } } catch (Exception e) { @@ -2160,7 +2623,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } } - currentPhotoFilter = String.format(Locale.US, "%d_%d", (int) (w / AndroidUtilities.density), (int) (h / AndroidUtilities.density)); + currentPhotoFilter = String.format(Locale.US, "%d_%d", (int) (w / density), (int) (h / density)); if (messageObject.photoThumbs != null && messageObject.photoThumbs.size() > 1 || messageObject.type == 3 || messageObject.type == 8) { if (messageObject.isSecretPhoto()) { currentPhotoFilter += "_b2"; @@ -2187,14 +2650,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele photoExist = false; } if (photoExist || MediaController.getInstance().canDownloadMedia(MediaController.AUTODOWNLOAD_MASK_PHOTO) || FileLoader.getInstance().isLoadingFile(fileName)) { - if (allowedToSetPhoto || ImageLoader.getInstance().getImageFromMemory(currentPhotoObject.location, null, currentPhotoFilter) != null) { - allowedToSetPhoto = true; - photoImage.setImage(currentPhotoObject.location, currentPhotoFilter, currentPhotoObjectThumb != null ? currentPhotoObjectThumb.location : null, currentPhotoFilter, noSize ? 0 : currentPhotoObject.size, null, false); - } else if (currentPhotoObjectThumb != null) { - photoImage.setImage(null, null, currentPhotoObjectThumb.location, currentPhotoFilter, 0, null, false); - } else { - photoImage.setImageBitmap((Drawable) null); - } + photoImage.setImage(currentPhotoObject.location, currentPhotoFilter, currentPhotoObjectThumb != null ? currentPhotoObjectThumb.location : null, currentPhotoFilter, noSize ? 0 : currentPhotoObject.size, null, false); } else { photoNotSet = true; if (currentPhotoObjectThumb != null) { @@ -2229,18 +2685,36 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele photoImage.setImage(null, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilter, 0, null, false); } } - super.setMessageObject(messageObject); + setMessageObjectInternal(messageObject); if (drawForwardedName) { - namesOffset += AndroidUtilities.dp(5); + namesOffset += dp(5); } else if (drawNameLayout && messageObject.messageOwner.reply_to_msg_id == 0) { - namesOffset += AndroidUtilities.dp(7); + namesOffset += dp(7); } invalidate(); - photoImage.setImageCoords(0, AndroidUtilities.dp(7) + namesOffset, photoWidth, photoHeight); - totalHeight = photoHeight + AndroidUtilities.dp(14) + namesOffset + additionHeight; + photoImage.setImageCoords(0, dp(7) + namesOffset, photoWidth, photoHeight); + totalHeight = photoHeight + dp(14) + namesOffset + additionHeight; + } + if (captionLayout == null && messageObject.caption != null && messageObject.type != 13) { + try { + int width = backgroundWidth - AndroidUtilities.dp(31); + captionLayout = new StaticLayout(messageObject.caption, MessageObject.getTextPaint(), width - dp(10), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + if (captionLayout.getLineCount() > 0) { + int timeWidthTotal = timeWidth + (messageObject.isOutOwner() ? dp(20) : 0); + captionHeight = captionLayout.getHeight(); + totalHeight += captionHeight + dp(9); + float lastLineWidth = captionLayout.getLineWidth(captionLayout.getLineCount() - 1) + captionLayout.getLineLeft(captionLayout.getLineCount() - 1); + if (width - dp(8) - lastLineWidth < timeWidthTotal) { + totalHeight += dp(14); + captionHeight += dp(14); + } + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } } botButtons.clear(); @@ -2249,16 +2723,16 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } if (messageObject.messageOwner.reply_markup instanceof TLRPC.TL_replyInlineMarkup) { int rows = messageObject.messageOwner.reply_markup.rows.size(); - substractBackgroundHeight = keyboardHeight = AndroidUtilities.dp(44 + 4) * rows + AndroidUtilities.dp(1); + substractBackgroundHeight = keyboardHeight = dp(44 + 4) * rows + dp(1); widthForButtons = backgroundWidth; boolean fullWidth = false; if (messageObject.wantedBotKeyboardWidth > widthForButtons) { - int maxButtonWidth = -AndroidUtilities.dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 62 : 10); - if (AndroidUtilities.isTablet()) { - maxButtonWidth += AndroidUtilities.getMinTabletSide(); + int maxButtonWidth = -dp(isChat && messageObject.isFromUser() && !messageObject.isOutOwner() ? 62 : 10); + if (isTablet()) { + maxButtonWidth += getMinTabletSide(); } else { - maxButtonWidth += Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y); + maxButtonWidth += Math.min(displaySize.x, displaySize.y); } widthForButtons = Math.max(backgroundWidth, Math.min(messageObject.wantedBotKeyboardWidth, maxButtonWidth)); fullWidth = true; @@ -2268,7 +2742,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele for (int a = 0; a < rows; a++) { TLRPC.TL_keyboardButtonRow row = messageObject.messageOwner.reply_markup.rows.get(a); int buttonsCount = row.buttons.size(); - int buttonWidth = (widthForButtons - (AndroidUtilities.dp(5) * (buttonsCount - 1)) - AndroidUtilities.dp(!fullWidth && mediaBackground ? 0 : 9) - AndroidUtilities.dp(2)) / buttonsCount; + int buttonWidth = (widthForButtons - (dp(5) * (buttonsCount - 1)) - dp(!fullWidth && mediaBackground ? 0 : 9) - dp(2)) / buttonsCount; for (int b = 0; b < row.buttons.size(); b++) { BotButton botButton = new BotButton(); botButton.button = row.buttons.get(b); @@ -2282,13 +2756,13 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele botButton.lastUpdateTime = System.currentTimeMillis(); } botButtonsByData.put(key, botButton); - botButton.x = b * (buttonWidth + AndroidUtilities.dp(5)); - botButton.y = a * AndroidUtilities.dp(44 + 4) + AndroidUtilities.dp(5); + botButton.x = b * (buttonWidth + dp(5)); + botButton.y = a * dp(44 + 4) + dp(5); botButton.width = buttonWidth; - botButton.height = AndroidUtilities.dp(44); - CharSequence caption = Emoji.replaceEmoji(botButton.button.text, botButtonPaint.getFontMetricsInt(), AndroidUtilities.dp(15), false); - caption = TextUtils.ellipsize(caption, botButtonPaint, buttonWidth - AndroidUtilities.dp(10), TextUtils.TruncateAt.END); - botButton.caption = new StaticLayout(caption, botButtonPaint, buttonWidth - AndroidUtilities.dp(10), Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); + botButton.height = dp(44); + CharSequence buttonText = Emoji.replaceEmoji(botButton.button.text, botButtonPaint.getFontMetricsInt(), dp(15), false); + buttonText = TextUtils.ellipsize(buttonText, botButtonPaint, buttonWidth - dp(10), TextUtils.TruncateAt.END); + botButton.title = new StaticLayout(buttonText, botButtonPaint, buttonWidth - dp(10), Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); botButtons.add(botButton); if (b == row.buttons.size() - 1) { maxButtonsWidth = Math.max(maxButtonsWidth, botButton.x + botButton.width); @@ -2310,122 +2784,159 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), totalHeight + keyboardHeight); } + @SuppressLint("DrawAllocation") @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); + if (currentMessageObject == null) { + super.onLayout(changed, left, top, right, bottom); + return; + } + + if (changed || !wasLayout) { + layoutWidth = getMeasuredWidth(); + layoutHeight = getMeasuredHeight() - substractBackgroundHeight; + if (timeTextWidth < 0) { + timeTextWidth = dp(10); + } + timeLayout = new StaticLayout(currentTimeString, timePaint, timeTextWidth + dp(6), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + if (!mediaBackground) { + if (!currentMessageObject.isOutOwner()) { + timeX = backgroundWidth - dp(9) - timeWidth + (isChat && currentMessageObject.isFromUser() ? dp(48) : 0); + } else { + timeX = layoutWidth - timeWidth - dp(38.5f); + } + } else { + if (!currentMessageObject.isOutOwner()) { + timeX = backgroundWidth - dp(4) - timeWidth + (isChat && currentMessageObject.isFromUser() ? dp(48) : 0); + } else { + timeX = layoutWidth - timeWidth - dp(42.0f); + } + } + + if ((currentMessageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { + viewsLayout = new StaticLayout(currentViewsString, timePaint, viewsTextWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + } else { + viewsLayout = null; + } + + if (isAvatarVisible) { + avatarImage.setImageCoords(dp(6), layoutHeight - dp(44), dp(42), dp(42)); + } + + wasLayout = true; + } if (currentMessageObject.type == 0) { - textY = AndroidUtilities.dp(10) + namesOffset; + textY = dp(10) + namesOffset; } 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); + seekBarX = layoutWidth - backgroundWidth + dp(57); + buttonX = layoutWidth - backgroundWidth + dp(14); + timeAudioX = layoutWidth - backgroundWidth + dp(67); } else { if (isChat && currentMessageObject.isFromUser()) { - seekBarX = AndroidUtilities.dp(114); - buttonX = AndroidUtilities.dp(71); - timeAudioX = AndroidUtilities.dp(124); + seekBarX = dp(114); + buttonX = dp(71); + timeAudioX = dp(124); } else { - seekBarX = AndroidUtilities.dp(66); - buttonX = AndroidUtilities.dp(23); - timeAudioX = AndroidUtilities.dp(76); + seekBarX = dp(66); + buttonX = dp(23); + timeAudioX = dp(76); } } if (hasLinkPreview) { - seekBarX += AndroidUtilities.dp(10); - buttonX += AndroidUtilities.dp(10); - timeAudioX += AndroidUtilities.dp(10); + seekBarX += dp(10); + buttonX += dp(10); + timeAudioX += dp(10); } - seekBarWaveform.setSize(backgroundWidth - AndroidUtilities.dp(92 + (hasLinkPreview ? 10 : 0)), AndroidUtilities.dp(30)); - seekBar.setSize(backgroundWidth - AndroidUtilities.dp(72 + (hasLinkPreview ? 10 : 0)), AndroidUtilities.dp(30)); - seekBarY = AndroidUtilities.dp(13) + namesOffset + mediaOffsetY; - buttonY = AndroidUtilities.dp(13) + namesOffset + mediaOffsetY; - radialProgress.setProgressRect(buttonX, buttonY, buttonX + AndroidUtilities.dp(44), buttonY + AndroidUtilities.dp(44)); + seekBarWaveform.setSize(backgroundWidth - dp(92 + (hasLinkPreview ? 10 : 0)), dp(30)); + seekBar.setSize(backgroundWidth - dp(72 + (hasLinkPreview ? 10 : 0)), dp(30)); + seekBarY = dp(13) + namesOffset + mediaOffsetY; + buttonY = dp(13) + namesOffset + mediaOffsetY; + radialProgress.setProgressRect(buttonX, buttonY, buttonX + dp(44), buttonY + dp(44)); updateAudioProgress(); } 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); + seekBarX = layoutWidth - backgroundWidth + dp(56); + buttonX = layoutWidth - backgroundWidth + dp(14); + timeAudioX = layoutWidth - backgroundWidth + dp(67); } else { if (isChat && currentMessageObject.isFromUser()) { - seekBarX = AndroidUtilities.dp(113); - buttonX = AndroidUtilities.dp(71); - timeAudioX = AndroidUtilities.dp(124); + seekBarX = dp(113); + buttonX = dp(71); + timeAudioX = dp(124); } else { - seekBarX = AndroidUtilities.dp(65); - buttonX = AndroidUtilities.dp(23); - timeAudioX = AndroidUtilities.dp(76); + seekBarX = dp(65); + buttonX = dp(23); + timeAudioX = dp(76); } } if (hasLinkPreview) { - seekBarX += AndroidUtilities.dp(10); - buttonX += AndroidUtilities.dp(10); - timeAudioX += AndroidUtilities.dp(10); + seekBarX += dp(10); + buttonX += dp(10); + timeAudioX += dp(10); } - seekBar.setSize(backgroundWidth - AndroidUtilities.dp(65 + (hasLinkPreview ? 10 : 0)), AndroidUtilities.dp(30)); - seekBarY = AndroidUtilities.dp(29) + namesOffset + mediaOffsetY; - buttonY = AndroidUtilities.dp(13) + namesOffset + mediaOffsetY; - radialProgress.setProgressRect(buttonX, buttonY, buttonX + AndroidUtilities.dp(44), buttonY + AndroidUtilities.dp(44)); + seekBar.setSize(backgroundWidth - dp(65 + (hasLinkPreview ? 10 : 0)), dp(30)); + seekBarY = dp(29) + namesOffset + mediaOffsetY; + buttonY = dp(13) + namesOffset + mediaOffsetY; + radialProgress.setProgressRect(buttonX, buttonY, buttonX + dp(44), buttonY + dp(44)); updateAudioProgress(); } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_DOCUMENT && !drawPhotoImage) { if (currentMessageObject.isOutOwner()) { - buttonX = layoutWidth - backgroundWidth + AndroidUtilities.dp(14); + buttonX = layoutWidth - backgroundWidth + dp(14); } else { if (isChat && currentMessageObject.isFromUser()) { - buttonX = AndroidUtilities.dp(71); + buttonX = dp(71); } else { - buttonX = AndroidUtilities.dp(23); + buttonX = dp(23); } } if (hasLinkPreview) { - buttonX += AndroidUtilities.dp(10); + buttonX += dp(10); } - buttonY = AndroidUtilities.dp(13) + namesOffset + mediaOffsetY; - radialProgress.setProgressRect(buttonX, buttonY, buttonX + AndroidUtilities.dp(44), buttonY + AndroidUtilities.dp(44)); - photoImage.setImageCoords(buttonX - AndroidUtilities.dp(10), buttonY - AndroidUtilities.dp(10), photoImage.getImageWidth(), photoImage.getImageHeight()); + buttonY = dp(13) + namesOffset + mediaOffsetY; + radialProgress.setProgressRect(buttonX, buttonY, buttonX + dp(44), buttonY + dp(44)); + photoImage.setImageCoords(buttonX - dp(10), buttonY - dp(10), photoImage.getImageWidth(), photoImage.getImageHeight()); } else if (currentMessageObject.type == 12) { int x; if (currentMessageObject.isOutOwner()) { - x = layoutWidth - backgroundWidth + AndroidUtilities.dp(14); + x = layoutWidth - backgroundWidth + dp(14); } else { if (isChat && currentMessageObject.isFromUser()) { - x = AndroidUtilities.dp(72); + x = dp(72); } else { - x = AndroidUtilities.dp(23); + x = dp(23); } } - photoImage.setImageCoords(x, AndroidUtilities.dp(13) + namesOffset, AndroidUtilities.dp(44), AndroidUtilities.dp(44)); + photoImage.setImageCoords(x, dp(13) + namesOffset, dp(44), dp(44)); } else { int x; if (currentMessageObject.isOutOwner()) { if (mediaBackground) { - x = layoutWidth - backgroundWidth - AndroidUtilities.dp(3); + x = layoutWidth - backgroundWidth - dp(3); } else { - x = layoutWidth - backgroundWidth + AndroidUtilities.dp(6); + x = layoutWidth - backgroundWidth + dp(6); } } else { if (isChat && currentMessageObject.isFromUser()) { - x = AndroidUtilities.dp(63); + x = dp(63); } else { - x = AndroidUtilities.dp(15); + x = dp(15); } } photoImage.setImageCoords(x, photoImage.getImageY(), photoImage.getImageWidth(), photoImage.getImageHeight()); - buttonX = (int) (x + (photoImage.getImageWidth() - AndroidUtilities.dp(48)) / 2.0f); - buttonY = (int) (AndroidUtilities.dp(7) + (photoImage.getImageHeight() - AndroidUtilities.dp(48)) / 2.0f) + namesOffset; - radialProgress.setProgressRect(buttonX, buttonY, buttonX + AndroidUtilities.dp(48), buttonY + AndroidUtilities.dp(48)); - deleteProgressRect.set(buttonX + AndroidUtilities.dp(3), buttonY + AndroidUtilities.dp(3), buttonX + AndroidUtilities.dp(45), buttonY + AndroidUtilities.dp(45)); + buttonX = (int) (x + (photoImage.getImageWidth() - dp(48)) / 2.0f); + buttonY = (int) (dp(7) + (photoImage.getImageHeight() - dp(48)) / 2.0f) + namesOffset; + radialProgress.setProgressRect(buttonX, buttonY, buttonX + dp(48), buttonY + dp(48)); + deleteProgressRect.set(buttonX + dp(3), buttonY + dp(3), buttonX + dp(45), buttonY + dp(45)); } } - @Override - protected void drawContent(Canvas canvas) { + private void drawContent(Canvas canvas) { if (needNewVisiblePart && currentMessageObject.type == 0) { getLocalVisibleRect(scrollRect); @@ -2441,12 +2952,12 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele boolean imageDrawn = false; if (currentMessageObject.type == 0 && currentMessageObject.textLayoutBlocks != null && !currentMessageObject.textLayoutBlocks.isEmpty()) { if (currentMessageObject.isOutOwner()) { - textX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(11); + textX = currentBackgroundDrawable.getBounds().left + dp(11); } else { - textX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(17); + textX = currentBackgroundDrawable.getBounds().left + dp(17); } - textY = AndroidUtilities.dp(10) + namesOffset; + textY = dp(10) + namesOffset; if (firstVisibleBlockNum >= 0) { for (int a = firstVisibleBlockNum; a <= lastVisibleBlockNum; a++) { @@ -2461,6 +2972,11 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele canvas.drawPath(urlPath.get(b), urlPaint); } } + if (a == linkSelectionBlockNum && !urlPathSelection.isEmpty()) { + for (int b = 0; b < urlPathSelection.size(); b++) { + canvas.drawPath(urlPathSelection.get(b), urlSelectionPaint); + } + } try { block.textLayout.draw(canvas); } catch (Exception e) { @@ -2471,32 +2987,32 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } if (hasLinkPreview) { - int startY = textY + currentMessageObject.textHeight + AndroidUtilities.dp(8); - int linkX = textX + AndroidUtilities.dp(1); + int startY = textY + currentMessageObject.textHeight + dp(8); + int linkX = textX + dp(1); int linkPreviewY = startY; int smallImageStartY = 0; replyLinePaint.setColor(currentMessageObject.isOutOwner() ? Theme.MSG_OUT_WEB_PREVIEW_LINE_COLOR : Theme.MSG_IN_WEB_PREVIEW_LINE_COLOR); - canvas.drawRect(linkX, linkPreviewY - AndroidUtilities.dp(3), linkX + AndroidUtilities.dp(2), linkPreviewY + linkPreviewHeight + AndroidUtilities.dp(3), replyLinePaint); + canvas.drawRect(linkX, linkPreviewY - dp(3), linkX + dp(2), linkPreviewY + linkPreviewHeight + dp(3), replyLinePaint); - if (siteCaptionLayout != null) { + if (siteNameLayout != null) { replyNamePaint.setColor(currentMessageObject.isOutOwner() ? Theme.MSG_OUT_SITE_NAME_TEXT_COLOR : Theme.MSG_IN_SITE_NAME_TEXT_COLOR); canvas.save(); - canvas.translate(linkX + AndroidUtilities.dp(10), linkPreviewY - AndroidUtilities.dp(3)); - siteCaptionLayout.draw(canvas); + canvas.translate(linkX + dp(10), linkPreviewY - dp(3)); + siteNameLayout.draw(canvas); canvas.restore(); - linkPreviewY += siteCaptionLayout.getLineBottom(siteCaptionLayout.getLineCount() - 1); + linkPreviewY += siteNameLayout.getLineBottom(siteNameLayout.getLineCount() - 1); } replyNamePaint.setColor(Theme.MSG_TEXT_COLOR); replyTextPaint.setColor(Theme.MSG_TEXT_COLOR); if (titleLayout != null) { if (linkPreviewY != startY) { - linkPreviewY += AndroidUtilities.dp(2); + linkPreviewY += dp(2); } - smallImageStartY = linkPreviewY - AndroidUtilities.dp(1); + smallImageStartY = linkPreviewY - dp(1); canvas.save(); - canvas.translate(linkX + AndroidUtilities.dp(10) + titleX, linkPreviewY - AndroidUtilities.dp(3)); + canvas.translate(linkX + dp(10) + titleX, linkPreviewY - dp(3)); titleLayout.draw(canvas); canvas.restore(); linkPreviewY += titleLayout.getLineBottom(titleLayout.getLineCount() - 1); @@ -2504,13 +3020,13 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (authorLayout != null) { if (linkPreviewY != startY) { - linkPreviewY += AndroidUtilities.dp(2); + linkPreviewY += dp(2); } if (smallImageStartY == 0) { - smallImageStartY = linkPreviewY - AndroidUtilities.dp(1); + smallImageStartY = linkPreviewY - dp(1); } canvas.save(); - canvas.translate(linkX + AndroidUtilities.dp(10) + authorX, linkPreviewY - AndroidUtilities.dp(3)); + canvas.translate(linkX + dp(10) + authorX, linkPreviewY - dp(3)); authorLayout.draw(canvas); canvas.restore(); linkPreviewY += authorLayout.getLineBottom(authorLayout.getLineCount() - 1); @@ -2518,14 +3034,14 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (descriptionLayout != null) { if (linkPreviewY != startY) { - linkPreviewY += AndroidUtilities.dp(2); + linkPreviewY += dp(2); } if (smallImageStartY == 0) { - smallImageStartY = linkPreviewY - AndroidUtilities.dp(1); + smallImageStartY = linkPreviewY - dp(1); } - descriptionY = linkPreviewY - AndroidUtilities.dp(3); + descriptionY = linkPreviewY - dp(3); canvas.save(); - canvas.translate(linkX + AndroidUtilities.dp(10) + descriptionX, descriptionY); + canvas.translate(linkX + dp(10) + descriptionX, descriptionY); if (pressedLink != null && linkBlockNum == -10) { for (int b = 0; b < urlPath.size(); b++) { canvas.drawPath(urlPath.get(b), urlPaint); @@ -2538,31 +3054,31 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (drawPhotoImage) { if (linkPreviewY != startY) { - linkPreviewY += AndroidUtilities.dp(2); + linkPreviewY += dp(2); } if (isSmallImage) { - photoImage.setImageCoords(linkX + backgroundWidth - AndroidUtilities.dp(81), smallImageStartY, photoImage.getImageWidth(), photoImage.getImageHeight()); + photoImage.setImageCoords(linkX + backgroundWidth - dp(81), smallImageStartY, photoImage.getImageWidth(), photoImage.getImageHeight()); } else { - photoImage.setImageCoords(linkX + AndroidUtilities.dp(10), linkPreviewY, photoImage.getImageWidth(), photoImage.getImageHeight()); + photoImage.setImageCoords(linkX + dp(10), linkPreviewY, photoImage.getImageWidth(), photoImage.getImageHeight()); if (drawImageButton) { - int size = AndroidUtilities.dp(48); + int size = dp(48); buttonX = (int) (photoImage.getImageX() + (photoImage.getImageWidth() - size) / 2.0f); buttonY = (int) (photoImage.getImageY() + (photoImage.getImageHeight() - size) / 2.0f); - radialProgress.setProgressRect(buttonX, buttonY, buttonX + AndroidUtilities.dp(48), buttonY + AndroidUtilities.dp(48)); + radialProgress.setProgressRect(buttonX, buttonY, buttonX + dp(48), buttonY + dp(48)); } } imageDrawn = photoImage.draw(canvas); - if (durationLayout != null) { - int x = photoImage.getImageX() + photoImage.getImageWidth() - AndroidUtilities.dp(8) - durationWidth; - int y = photoImage.getImageY() + photoImage.getImageHeight() - AndroidUtilities.dp(19); - Theme.timeBackgroundDrawable.setBounds(x - AndroidUtilities.dp(4), y - AndroidUtilities.dp(1.5f), x + durationWidth + AndroidUtilities.dp(4), y + AndroidUtilities.dp(14.5f)); + if (videoInfoLayout != null) { + int x = photoImage.getImageX() + photoImage.getImageWidth() - dp(8) - durationWidth; + int y = photoImage.getImageY() + photoImage.getImageHeight() - dp(19); + Theme.timeBackgroundDrawable.setBounds(x - dp(4), y - dp(1.5f), x + durationWidth + dp(4), y + dp(14.5f)); Theme.timeBackgroundDrawable.draw(canvas); canvas.save(); canvas.translate(x, y); - durationLayout.draw(canvas); + videoInfoLayout.draw(canvas); canvas.restore(); } } @@ -2590,14 +3106,19 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele float progress = Math.max(0, (long) currentMessageObject.messageOwner.destroyTime * 1000 - msTime) / (currentMessageObject.messageOwner.ttl * 1000.0f); canvas.drawArc(deleteProgressRect, -90, -360 * progress, true, deleteProgressPaint); if (progress != 0) { - int offset = AndroidUtilities.dp(2); + int offset = dp(2); invalidate((int) deleteProgressRect.left - offset, (int) deleteProgressRect.top - offset, (int) deleteProgressRect.right + offset * 2, (int) deleteProgressRect.bottom + offset * 2); } updateSecretTimeText(currentMessageObject); } } - if (documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { + if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF || currentMessageObject.type == 8) { + if (photoImage.getVisible()) { + setDrawableBounds(Theme.docMenuDrawable[3], otherX = photoImage.getImageX() + photoImage.getImageWidth() - dp(14), otherY = photoImage.getImageY() + dp(8.1f)); + Theme.docMenuDrawable[3].draw(canvas); + } + } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { if (currentMessageObject.isOutOwner()) { audioTitlePaint.setColor(Theme.MSG_OUT_AUDIO_TITLE_TEXT_COLOR); audioPerformerPaint.setColor(Theme.MSG_OUT_AUDIO_PERFORMER_TEXT_COLOR); @@ -2612,7 +3133,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele radialProgress.draw(canvas); canvas.save(); - canvas.translate(timeAudioX + songX, AndroidUtilities.dp(13) + namesOffset + mediaOffsetY); + canvas.translate(timeAudioX + songX, dp(13) + namesOffset + mediaOffsetY); songLayout.draw(canvas); canvas.restore(); @@ -2621,14 +3142,14 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele canvas.translate(seekBarX, seekBarY); seekBar.draw(canvas); } else { - canvas.translate(timeAudioX + performerX, AndroidUtilities.dp(35) + namesOffset + mediaOffsetY); + canvas.translate(timeAudioX + performerX, dp(35) + namesOffset + mediaOffsetY); performerLayout.draw(canvas); } canvas.restore(); canvas.save(); - canvas.translate(timeAudioX, AndroidUtilities.dp(57) + namesOffset + mediaOffsetY); - timeLayout.draw(canvas); + canvas.translate(timeAudioX, dp(57) + namesOffset + mediaOffsetY); + durationLayout.draw(canvas); canvas.restore(); Drawable menuDrawable; @@ -2637,7 +3158,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } else { menuDrawable = Theme.docMenuDrawable[isDrawSelectedBackground() ? 2 : 0]; } - setDrawableBounds(menuDrawable, otherX = buttonX + backgroundWidth - AndroidUtilities.dp(currentMessageObject.type == 0 ? 58 : 48), otherY = buttonY - AndroidUtilities.dp(5)); + setDrawableBounds(menuDrawable, otherX = buttonX + backgroundWidth - dp(currentMessageObject.type == 0 ? 58 : 48), otherY = buttonY - dp(5)); menuDrawable.draw(canvas); } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO) { if (currentMessageObject.isOutOwner()) { @@ -2651,7 +3172,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele canvas.save(); if (useSeekBarWaweform) { - canvas.translate(seekBarX + AndroidUtilities.dp(13), seekBarY); + canvas.translate(seekBarX + dp(13), seekBarY); seekBarWaveform.draw(canvas); } else { canvas.translate(seekBarX, seekBarY); @@ -2660,109 +3181,104 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele canvas.restore(); canvas.save(); - canvas.translate(timeAudioX, AndroidUtilities.dp(44) + namesOffset + mediaOffsetY); - timeLayout.draw(canvas); + canvas.translate(timeAudioX, dp(44) + namesOffset + mediaOffsetY); + durationLayout.draw(canvas); canvas.restore(); if (currentMessageObject.type != 0 && currentMessageObject.messageOwner.to_id.channel_id == 0 && currentMessageObject.isContentUnread()) { docBackPaint.setColor(currentMessageObject.isOutOwner() ? Theme.MSG_OUT_VOICE_SEEKBAR_FILL_COLOR : Theme.MSG_IN_VOICE_SEEKBAR_FILL_COLOR); - canvas.drawCircle(timeAudioX + timeWidthAudio + AndroidUtilities.dp(6), AndroidUtilities.dp(51) + namesOffset + mediaOffsetY, AndroidUtilities.dp(3), docBackPaint); + canvas.drawCircle(timeAudioX + timeWidthAudio + dp(6), dp(51) + namesOffset + mediaOffsetY, dp(3), docBackPaint); } } + if (currentMessageObject.type == 1 || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO) { - if (documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO) { - setDrawableBounds(Theme.docMenuDrawable[3], otherX = photoImage.getImageX() + photoImage.getImageWidth() - AndroidUtilities.dp(14), otherY = photoImage.getImageY() + AndroidUtilities.dp(8.1f)); - Theme.docMenuDrawable[3].draw(canvas); - } - - if (captionLayout != null) { - canvas.save(); - canvas.translate(captionX = photoImage.getImageX() + AndroidUtilities.dp(5), captionY = photoImage.getImageY() + photoImage.getImageHeight() + AndroidUtilities.dp(6)); - if (pressedLink != null) { - for (int b = 0; b < urlPath.size(); b++) { - canvas.drawPath(urlPath.get(b), urlPaint); - } - } - try { - captionLayout.draw(canvas); - } catch (Exception e) { - FileLog.e("tmessages", e); - } - canvas.restore(); - } - if (infoLayout != null && (buttonState == 1 || buttonState == 0 || buttonState == 3 || currentMessageObject.isSecretPhoto())) { - infoPaint.setColor(Theme.MSG_MEDIA_INFO_TEXT_COLOR); - setDrawableBounds(Theme.timeBackgroundDrawable, photoImage.getImageX() + AndroidUtilities.dp(4), photoImage.getImageY() + AndroidUtilities.dp(4), infoWidth + AndroidUtilities.dp(8), AndroidUtilities.dp(16.5f)); - Theme.timeBackgroundDrawable.draw(canvas); - - canvas.save(); - canvas.translate(photoImage.getImageX() + AndroidUtilities.dp(8), photoImage.getImageY() + AndroidUtilities.dp(5.5f)); - infoLayout.draw(canvas); - canvas.restore(); - } - } else if (currentMessageObject.type == 4) { - if (captionLayout != null) { - if (currentMessageObject.isOutOwner()) { - locationTitlePaint.setColor(Theme.MSG_OUT_VENUE_NAME_TEXT_COLOR); - locationAddressPaint.setColor(isDrawSelectedBackground() ? Theme.MSG_OUT_VENUE_INFO_SELECTED_TEXT_COLOR : Theme.MSG_OUT_VENUE_INFO_TEXT_COLOR); - } else { - locationTitlePaint.setColor(Theme.MSG_IN_VENUE_NAME_TEXT_COLOR); - locationAddressPaint.setColor(isDrawSelectedBackground() ? Theme.MSG_IN_VENUE_INFO_SELECTED_TEXT_COLOR : Theme.MSG_IN_VENUE_INFO_TEXT_COLOR); + if (photoImage.getVisible()) { + if (documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO) { + setDrawableBounds(Theme.docMenuDrawable[3], otherX = photoImage.getImageX() + photoImage.getImageWidth() - dp(14), otherY = photoImage.getImageY() + dp(8.1f)); + Theme.docMenuDrawable[3].draw(canvas); } - canvas.save(); - canvas.translate(nameOffsetX + photoImage.getImageX() + photoImage.getImageWidth() + AndroidUtilities.dp(10), photoImage.getImageY() + AndroidUtilities.dp(8)); - captionLayout.draw(canvas); - canvas.restore(); + if (infoLayout != null && (buttonState == 1 || buttonState == 0 || buttonState == 3 || currentMessageObject.isSecretPhoto())) { + infoPaint.setColor(Theme.MSG_MEDIA_INFO_TEXT_COLOR); + setDrawableBounds(Theme.timeBackgroundDrawable, photoImage.getImageX() + dp(4), photoImage.getImageY() + dp(4), infoWidth + dp(8), dp(16.5f)); + Theme.timeBackgroundDrawable.draw(canvas); - if (infoLayout != null) { canvas.save(); - canvas.translate(photoImage.getImageX() + photoImage.getImageWidth() + AndroidUtilities.dp(10), photoImage.getImageY() + captionLayout.getLineBottom(captionLayout.getLineCount() - 1) + AndroidUtilities.dp(13)); + canvas.translate(photoImage.getImageX() + dp(8), photoImage.getImageY() + dp(5.5f)); infoLayout.draw(canvas); canvas.restore(); } } - } else if (currentMessageObject.type == 8) { - if (captionLayout != null) { - canvas.save(); - canvas.translate(captionX = photoImage.getImageX() + AndroidUtilities.dp(5), captionY = photoImage.getImageY() + photoImage.getImageHeight() + AndroidUtilities.dp(6)); - if (pressedLink != null) { - for (int b = 0; b < urlPath.size(); b++) { - canvas.drawPath(urlPath.get(b), urlPaint); + } else { + if (currentMessageObject.type == 4) { + if (docTitleLayout != null) { + if (currentMessageObject.isOutOwner()) { + locationTitlePaint.setColor(Theme.MSG_OUT_VENUE_NAME_TEXT_COLOR); + locationAddressPaint.setColor(isDrawSelectedBackground() ? Theme.MSG_OUT_VENUE_INFO_SELECTED_TEXT_COLOR : Theme.MSG_OUT_VENUE_INFO_TEXT_COLOR); + } else { + locationTitlePaint.setColor(Theme.MSG_IN_VENUE_NAME_TEXT_COLOR); + locationAddressPaint.setColor(isDrawSelectedBackground() ? Theme.MSG_IN_VENUE_INFO_SELECTED_TEXT_COLOR : Theme.MSG_IN_VENUE_INFO_TEXT_COLOR); + } + + canvas.save(); + canvas.translate(docTitleOffsetX + photoImage.getImageX() + photoImage.getImageWidth() + dp(10), photoImage.getImageY() + dp(8)); + docTitleLayout.draw(canvas); + canvas.restore(); + + if (infoLayout != null) { + canvas.save(); + canvas.translate(photoImage.getImageX() + photoImage.getImageWidth() + dp(10), photoImage.getImageY() + docTitleLayout.getLineBottom(docTitleLayout.getLineCount() - 1) + dp(13)); + infoLayout.draw(canvas); + canvas.restore(); } } - try { - captionLayout.draw(canvas); - } catch (Exception e) { - FileLog.e("tmessages", e); + } else if (currentMessageObject.type == 12) { + contactNamePaint.setColor(currentMessageObject.isOutOwner() ? Theme.MSG_OUT_CONTACT_NAME_TEXT_COLOR : Theme.MSG_IN_CONTACT_NAME_TEXT_COLOR); + contactPhonePaint.setColor(currentMessageObject.isOutOwner() ? Theme.MSG_OUT_CONTACT_PHONE_TEXT_COLOR : Theme.MSG_IN_CONTACT_PHONE_TEXT_COLOR); + if (titleLayout != null) { + canvas.save(); + canvas.translate(photoImage.getImageX() + photoImage.getImageWidth() + dp(9), dp(16) + namesOffset); + titleLayout.draw(canvas); + canvas.restore(); + } + if (docTitleLayout != null) { + canvas.save(); + canvas.translate(photoImage.getImageX() + photoImage.getImageWidth() + dp(9), dp(39) + namesOffset); + docTitleLayout.draw(canvas); + canvas.restore(); } - canvas.restore(); - } - } else if (currentMessageObject.type == 12) { - contactNamePaint.setColor(currentMessageObject.isOutOwner() ? Theme.MSG_OUT_CONTACT_NAME_TEXT_COLOR : Theme.MSG_IN_CONTACT_NAME_TEXT_COLOR); - contactPhonePaint.setColor(currentMessageObject.isOutOwner() ? Theme.MSG_OUT_CONTACT_PHONE_TEXT_COLOR : Theme.MSG_IN_CONTACT_PHONE_TEXT_COLOR); - if (titleLayout != null) { - canvas.save(); - canvas.translate(photoImage.getImageX() + photoImage.getImageWidth() + AndroidUtilities.dp(9), AndroidUtilities.dp(16) + namesOffset); - titleLayout.draw(canvas); - canvas.restore(); - } - if (captionLayout != null) { - canvas.save(); - canvas.translate(photoImage.getImageX() + photoImage.getImageWidth() + AndroidUtilities.dp(9), AndroidUtilities.dp(39) + namesOffset); - captionLayout.draw(canvas); - canvas.restore(); - } - Drawable menuDrawable; - if (currentMessageObject.isOutOwner()) { - menuDrawable = Theme.docMenuDrawable[1]; - } else { - menuDrawable = Theme.docMenuDrawable[isDrawSelectedBackground() ? 2 : 0]; + Drawable menuDrawable; + if (currentMessageObject.isOutOwner()) { + menuDrawable = Theme.docMenuDrawable[1]; + } else { + menuDrawable = Theme.docMenuDrawable[isDrawSelectedBackground() ? 2 : 0]; + } + setDrawableBounds(menuDrawable, otherX = photoImage.getImageX() + backgroundWidth - dp(48), otherY = photoImage.getImageY() - dp(5)); + menuDrawable.draw(canvas); } - setDrawableBounds(menuDrawable, otherX = photoImage.getImageX() + backgroundWidth - AndroidUtilities.dp(48), otherY = photoImage.getImageY() - AndroidUtilities.dp(5)); - menuDrawable.draw(canvas); } + + if (captionLayout != null) { + canvas.save(); + if (currentMessageObject.type == 1 || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO || currentMessageObject.type == 8) { + canvas.translate(captionX = photoImage.getImageX() + dp(5), captionY = photoImage.getImageY() + photoImage.getImageHeight() + dp(6)); + } else { + canvas.translate(captionX = currentBackgroundDrawable.getBounds().left + dp(currentMessageObject.isOutOwner() ? 11 : 17), captionY = totalHeight - captionHeight - dp(10)); + } + if (pressedLink != null) { + for (int b = 0; b < urlPath.size(); b++) { + canvas.drawPath(urlPath.get(b), urlPaint); + } + } + try { + captionLayout.draw(canvas); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + canvas.restore(); + } + if (documentAttachType == DOCUMENT_ATTACH_TYPE_DOCUMENT) { Drawable menuDrawable; if (currentMessageObject.isOutOwner()) { @@ -2782,14 +3298,14 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele int subtitleY; if (drawPhotoImage) { if (currentMessageObject.type == 0) { - setDrawableBounds(menuDrawable, otherX = photoImage.getImageX() + backgroundWidth - AndroidUtilities.dp(56), otherY = photoImage.getImageY() + AndroidUtilities.dp(1)); + setDrawableBounds(menuDrawable, otherX = photoImage.getImageX() + backgroundWidth - dp(56), otherY = photoImage.getImageY() + dp(1)); } else { - setDrawableBounds(menuDrawable, otherX = photoImage.getImageX() + backgroundWidth - AndroidUtilities.dp(40), otherY = photoImage.getImageY() + AndroidUtilities.dp(1)); + setDrawableBounds(menuDrawable, otherX = photoImage.getImageX() + backgroundWidth - dp(40), otherY = photoImage.getImageY() + dp(1)); } - x = photoImage.getImageX() + photoImage.getImageWidth() + AndroidUtilities.dp(10); - titleY = photoImage.getImageY() + AndroidUtilities.dp(8); - subtitleY = photoImage.getImageY() + captionLayout.getLineBottom(captionLayout.getLineCount() - 1) + AndroidUtilities.dp(13); + x = photoImage.getImageX() + photoImage.getImageWidth() + dp(10); + titleY = photoImage.getImageY() + dp(8); + subtitleY = photoImage.getImageY() + docTitleLayout.getLineBottom(docTitleLayout.getLineCount() - 1) + dp(13); if (buttonState >= 0 && buttonState < 4) { if (!imageDrawn) { int image = buttonState; @@ -2806,7 +3322,7 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele if (!imageDrawn) { rect.set(photoImage.getImageX(), photoImage.getImageY(), photoImage.getImageX() + photoImage.getImageWidth(), photoImage.getImageY() + photoImage.getImageHeight()); - canvas.drawRoundRect(rect, AndroidUtilities.dp(3), AndroidUtilities.dp(3), docBackPaint); + canvas.drawRoundRect(rect, dp(3), dp(3), docBackPaint); if (currentMessageObject.isOutOwner()) { radialProgress.setProgressColor(isDrawSelectedBackground() ? Theme.MSG_OUT_FILE_PROGRESS_SELECTED_COLOR : Theme.MSG_OUT_FILE_PROGRESS_COLOR); } else { @@ -2819,10 +3335,10 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele radialProgress.setProgressColor(Theme.MSG_MEDIA_PROGRESS_COLOR); } } else { - setDrawableBounds(menuDrawable, otherX = buttonX + backgroundWidth - AndroidUtilities.dp(currentMessageObject.type == 0 ? 58 : 48), otherY = buttonY - AndroidUtilities.dp(5)); - x = buttonX + AndroidUtilities.dp(53); - titleY = buttonY + AndroidUtilities.dp(4); - subtitleY = buttonY + AndroidUtilities.dp(27); + setDrawableBounds(menuDrawable, otherX = buttonX + backgroundWidth - dp(currentMessageObject.type == 0 ? 58 : 48), otherY = buttonY - dp(5)); + x = buttonX + dp(53); + titleY = buttonY + dp(4); + subtitleY = buttonY + dp(27); if (currentMessageObject.isOutOwner()) { radialProgress.setProgressColor(isDrawSelectedBackground() || buttonPressed != 0 ? Theme.MSG_OUT_AUDIO_SELECTED_PROGRESS_COLOR : Theme.MSG_OUT_AUDIO_PROGRESS_COLOR); } else { @@ -2832,10 +3348,10 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele menuDrawable.draw(canvas); try { - if (captionLayout != null) { + if (docTitleLayout != null) { canvas.save(); - canvas.translate(x + nameOffsetX, titleY); - captionLayout.draw(canvas); + canvas.translate(x + docTitleOffsetX, titleY); + docTitleLayout.draw(canvas); canvas.restore(); } } catch (Exception e) { @@ -2853,44 +3369,44 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele FileLog.e("tmessages", e); } } - if (drawImageButton) { + if (drawImageButton && photoImage.getVisible()) { radialProgress.draw(canvas); } if (!botButtons.isEmpty()) { int addX; if (currentMessageObject.isOutOwner()) { - addX = getMeasuredWidth() - widthForButtons - AndroidUtilities.dp(10); + addX = getMeasuredWidth() - widthForButtons - dp(10); } else { - addX = currentBackgroundDrawable.getBounds().left + AndroidUtilities.dp(mediaBackground ? 1 : 7); + addX = currentBackgroundDrawable.getBounds().left + dp(mediaBackground ? 1 : 7); } for (int a = 0; a < botButtons.size(); a++) { BotButton button = botButtons.get(a); - int y = button.y + layoutHeight - AndroidUtilities.dp(2); + int y = button.y + layoutHeight - dp(2); Theme.systemDrawable.setColorFilter(a == pressedBotButton ? Theme.colorPressedFilter : Theme.colorFilter); Theme.systemDrawable.setBounds(button.x + addX, y, button.x + addX + button.width, y + button.height); Theme.systemDrawable.draw(canvas); canvas.save(); - canvas.translate(button.x + addX + AndroidUtilities.dp(5), y + (AndroidUtilities.dp(44) - button.caption.getLineBottom(button.caption.getLineCount() - 1)) / 2); - button.caption.draw(canvas); + canvas.translate(button.x + addX + dp(5), y + (dp(44) - button.title.getLineBottom(button.title.getLineCount() - 1)) / 2); + button.title.draw(canvas); canvas.restore(); if (button.button instanceof TLRPC.TL_keyboardButtonUrl) { - int x = button.x + button.width - AndroidUtilities.dp(3) - Theme.botLink.getIntrinsicWidth() + addX; - setDrawableBounds(Theme.botLink, x, y + AndroidUtilities.dp(3)); + int x = button.x + button.width - dp(3) - Theme.botLink.getIntrinsicWidth() + addX; + setDrawableBounds(Theme.botLink, x, y + dp(3)); Theme.botLink.draw(canvas); } else if (button.button instanceof TLRPC.TL_keyboardButtonSwitchInline) { - int x = button.x + button.width - AndroidUtilities.dp(3) - Theme.botInline.getIntrinsicWidth() + addX; - setDrawableBounds(Theme.botInline, x, y + AndroidUtilities.dp(3)); + int x = button.x + button.width - dp(3) - Theme.botInline.getIntrinsicWidth() + addX; + setDrawableBounds(Theme.botInline, x, y + dp(3)); Theme.botInline.draw(canvas); } else if (button.button instanceof TLRPC.TL_keyboardButtonCallback || button.button instanceof TLRPC.TL_keyboardButtonRequestGeoLocation) { boolean drawProgress = button.button instanceof TLRPC.TL_keyboardButtonCallback && SendMessagesHelper.getInstance().isSendingCallback(currentMessageObject, button.button) || button.button instanceof TLRPC.TL_keyboardButtonRequestGeoLocation && SendMessagesHelper.getInstance().isSendingCurrentLocation(currentMessageObject, button.button); if (drawProgress || !drawProgress && button.progressAlpha != 0) { botProgressPaint.setAlpha(Math.min(255, (int) (button.progressAlpha * 255))); - int x = button.x + button.width - AndroidUtilities.dp(9 + 3) + addX; - rect.set(x, y + AndroidUtilities.dp(4), x + AndroidUtilities.dp(8), y + AndroidUtilities.dp(8 + 4)); + int x = button.x + button.width - dp(9 + 3) + addX; + rect.set(x, y + dp(4), x + dp(8), y + dp(8 + 4)); canvas.drawArc(rect, button.angle, 220, false, botProgressPaint); - invalidate((int) rect.left - AndroidUtilities.dp(2), (int) rect.top - AndroidUtilities.dp(2), (int) rect.right + AndroidUtilities.dp(2), (int) rect.bottom + AndroidUtilities.dp(2)); + invalidate((int) rect.left - dp(2), (int) rect.top - dp(2), (int) rect.right + dp(2), (int) rect.bottom + dp(2)); long newTime = System.currentTimeMillis(); if (Math.abs(button.lastUpdateTime - System.currentTimeMillis()) < 1000) { long delta = (newTime - button.lastUpdateTime); @@ -2959,26 +3475,25 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele return null; } - @Override - protected int getMaxNameWidth() { + private int getMaxNameWidth() { if (documentAttachType == DOCUMENT_ATTACH_TYPE_STICKER) { int maxWidth; - if (AndroidUtilities.isTablet()) { + if (isTablet()) { if (isChat && !currentMessageObject.isOutOwner() && currentMessageObject.isFromUser()) { - maxWidth = AndroidUtilities.getMinTabletSide() - AndroidUtilities.dp(42); + maxWidth = getMinTabletSide() - dp(42); } else { - maxWidth = AndroidUtilities.getMinTabletSide(); + maxWidth = getMinTabletSide(); } } else { if (isChat && !currentMessageObject.isOutOwner() && currentMessageObject.isFromUser()) { - maxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) - AndroidUtilities.dp(42); + maxWidth = Math.min(displaySize.x, displaySize.y) - dp(42); } else { - maxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y); + maxWidth = Math.min(displaySize.x, displaySize.y); } } - return maxWidth - backgroundWidth - AndroidUtilities.dp(57); + return maxWidth - backgroundWidth - dp(57); } - return super.getMaxNameWidth(); + return backgroundWidth - dp(mediaBackground ? 22 : 31); } public void updateButtonState(boolean animated) { @@ -3159,20 +3674,6 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } } - public void setAllowedToSetPhoto(boolean value) { - if (allowedToSetPhoto == value) { - return; - } - if (currentMessageObject != null && currentMessageObject.type == 1) { - allowedToSetPhoto = value; - if (value) { - MessageObject temp = currentMessageObject; - currentMessageObject = null; - setMessageObject(temp); - } - } - } - private void didPressedButton(boolean animated) { if (buttonState == 0) { if (documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO || documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { @@ -3340,4 +3841,735 @@ public class ChatMessageCell extends ChatBaseCell implements SeekBar.SeekBarDele } } } + + public void setDelegate(ChatMessageCellDelegate chatMessageCellDelegate) { + delegate = chatMessageCellDelegate; + } + + public void setAllowAssistant(boolean value) { + allowAssistant = value; + } + + private void measureTime(MessageObject messageObject) { + boolean hasSign = !messageObject.isOutOwner() && messageObject.messageOwner.from_id > 0 && messageObject.messageOwner.post; + TLRPC.User signUser = MessagesController.getInstance().getUser(messageObject.messageOwner.from_id); + if (hasSign && signUser == null) { + hasSign = false; + } + String timeString; + TLRPC.User author = null; + if (currentMessageObject.isFromUser()) { + author = MessagesController.getInstance().getUser(messageObject.messageOwner.from_id); + } + if (messageObject.messageOwner.via_bot_id == 0 && messageObject.messageOwner.via_bot_name == null && (author == null || !author.bot) && (messageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_EDITED) != 0) { + timeString = LocaleController.getString("EditedMessage", R.string.EditedMessage) + " " + LocaleController.getInstance().formatterDay.format((long) (messageObject.messageOwner.date) * 1000); + } else { + timeString = LocaleController.getInstance().formatterDay.format((long) (messageObject.messageOwner.date) * 1000); + } + if (hasSign) { + currentTimeString = ", " + timeString; + } else { + currentTimeString = timeString; + } + timeTextWidth = timeWidth = (int) Math.ceil(timePaint.measureText(currentTimeString)); + if ((messageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { + currentViewsString = String.format("%s", LocaleController.formatShortNumber(Math.max(1, messageObject.messageOwner.views), null)); + viewsTextWidth = (int) Math.ceil(timePaint.measureText(currentViewsString)); + timeWidth += viewsTextWidth + Theme.viewsCountDrawable[0].getIntrinsicWidth() + dp(10); + } + if (hasSign) { + if (availableTimeWidth == 0) { + availableTimeWidth = dp(1000); + } + CharSequence name = ContactsController.formatName(signUser.first_name, signUser.last_name).replace('\n', ' '); + int widthForSign = availableTimeWidth - timeWidth; + int width = (int) Math.ceil(timePaint.measureText(name, 0, name.length())); + if (width > widthForSign) { + name = TextUtils.ellipsize(name, timePaint, widthForSign, TextUtils.TruncateAt.END); + width = widthForSign; + } + currentTimeString = name + currentTimeString; + timeTextWidth += width; + timeWidth += width; + } + } + + private boolean isDrawSelectedBackground() { + return isPressed() && isCheckPressed || !isCheckPressed && isPressed || isHighlighted; + } + + private boolean checkNeedDrawShareButton(MessageObject messageObject) { + if (messageObject.type == 13) { + return false; + } else if (messageObject.messageOwner.fwd_from != null && messageObject.messageOwner.fwd_from.channel_id != 0 && !messageObject.isOut()) { + return true; + } else if (messageObject.isFromUser()) { + if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaEmpty || messageObject.messageOwner.media == null || messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage && !(messageObject.messageOwner.media.webpage instanceof TLRPC.TL_webPage)) { + return false; + } + TLRPC.User user = MessagesController.getInstance().getUser(messageObject.messageOwner.from_id); + if (user != null && user.bot) { + return true; + } + if (messageObject.isMegagroup() && !messageObject.isOut()) { + TLRPC.Chat chat = MessagesController.getInstance().getChat(messageObject.messageOwner.to_id.channel_id); + return chat != null && chat.username != null && chat.username.length() > 0 && !(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaContact) && !(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGeo); + } + } else if (messageObject.messageOwner.from_id < 0 || messageObject.messageOwner.post) { + if (messageObject.messageOwner.to_id.channel_id != 0 && (messageObject.messageOwner.via_bot_id == 0 && messageObject.messageOwner.reply_to_msg_id == 0 || messageObject.type != 13)) { + return true; + } + } + return false; + } + + private void setMessageObjectInternal(MessageObject messageObject) { + if ((messageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { + if (currentMessageObject.isContentUnread() && !currentMessageObject.isOut()) { + MessagesController.getInstance().addToViewsQueue(currentMessageObject.messageOwner, false); + currentMessageObject.setContentIsRead(); + } else if (!currentMessageObject.viewsReloaded) { + MessagesController.getInstance().addToViewsQueue(currentMessageObject.messageOwner, true); + currentMessageObject.viewsReloaded = true; + } + } + + if (currentMessageObject.isFromUser()) { + currentUser = MessagesController.getInstance().getUser(currentMessageObject.messageOwner.from_id); + } else if (currentMessageObject.messageOwner.from_id < 0) { + currentChat = MessagesController.getInstance().getChat(-currentMessageObject.messageOwner.from_id); + } else if (currentMessageObject.messageOwner.post) { + currentChat = MessagesController.getInstance().getChat(currentMessageObject.messageOwner.to_id.channel_id); + } + + if (isChat && !messageObject.isOutOwner() && messageObject.isFromUser()) { + isAvatarVisible = true; + if (currentUser != null) { + if (currentUser.photo != null) { + currentPhoto = currentUser.photo.photo_small; + } else { + currentPhoto = null; + } + avatarDrawable.setInfo(currentUser); + } else if (currentChat != null) { + if (currentChat.photo != null) { + currentPhoto = currentChat.photo.photo_small; + } else { + currentPhoto = null; + } + avatarDrawable.setInfo(currentChat); + } else { + currentPhoto = null; + avatarDrawable.setInfo(messageObject.messageOwner.from_id, null, null, false); + } + avatarImage.setImage(currentPhoto, "50_50", avatarDrawable, null, false); + } + + + measureTime(messageObject); + + namesOffset = 0; + + String viaUsername = null; + CharSequence viaString = null; + if (messageObject.messageOwner.via_bot_id != 0) { + TLRPC.User botUser = MessagesController.getInstance().getUser(messageObject.messageOwner.via_bot_id); + if (botUser != null && botUser.username != null && botUser.username.length() > 0) { + viaUsername = "@" + botUser.username; + viaString = replaceTags(String.format(" via %s", viaUsername)); + viaWidth = (int) Math.ceil(replyNamePaint.measureText(viaString, 0, viaString.length())); + currentViaBotUser = botUser; + } + } else if (messageObject.messageOwner.via_bot_name != null && messageObject.messageOwner.via_bot_name.length() > 0) { + viaUsername = "@" + messageObject.messageOwner.via_bot_name; + viaString = replaceTags(String.format(" via %s", viaUsername)); + viaWidth = (int) Math.ceil(replyNamePaint.measureText(viaString, 0, viaString.length())); + } + + boolean authorName = drawName && isChat && !currentMessageObject.isOutOwner(); + boolean viaBot = (messageObject.messageOwner.fwd_from == null || messageObject.type == 14) && viaUsername != null; + if (authorName || viaBot) { + drawNameLayout = true; + nameWidth = getMaxNameWidth(); + if (nameWidth < 0) { + nameWidth = dp(100); + } + + if (authorName) { + if (currentUser != null) { + currentNameString = UserObject.getUserName(currentUser); + } else if (currentChat != null) { + currentNameString = currentChat.title; + } else { + currentNameString = "DELETED"; + } + } else { + currentNameString = ""; + } + CharSequence nameStringFinal = TextUtils.ellipsize(currentNameString.replace('\n', ' '), namePaint, nameWidth - (viaBot ? viaWidth : 0), TextUtils.TruncateAt.END); + if (viaBot) { + viaNameWidth = (int) Math.ceil(namePaint.measureText(nameStringFinal, 0, nameStringFinal.length())); + if (viaNameWidth != 0) { + viaNameWidth += dp(4); + } + int color; + if (currentMessageObject.type == 13) { + color = Theme.MSG_STICKER_VIA_BOT_NAME_TEXT_COLOR; + } else { + color = currentMessageObject.isOutOwner() ? Theme.MSG_OUT_VIA_BOT_NAME_TEXT_COLOR : Theme.MSG_IN_VIA_BOT_NAME_TEXT_COLOR; + } + if (currentNameString.length() > 0) { + SpannableStringBuilder stringBuilder = new SpannableStringBuilder(String.format("%s via %s", nameStringFinal, viaUsername)); + stringBuilder.setSpan(new TypefaceSpan(Typeface.DEFAULT, 0, color), nameStringFinal.length() + 1, nameStringFinal.length() + 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + stringBuilder.setSpan(new TypefaceSpan(getTypeface("fonts/rmedium.ttf"), 0, color), nameStringFinal.length() + 5, stringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + nameStringFinal = stringBuilder; + } else { + SpannableStringBuilder stringBuilder = new SpannableStringBuilder(String.format("via %s", viaUsername)); + stringBuilder.setSpan(new TypefaceSpan(Typeface.DEFAULT, 0, color), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + stringBuilder.setSpan(new TypefaceSpan(getTypeface("fonts/rmedium.ttf"), 0, color), 4, stringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + nameStringFinal = stringBuilder; + } + nameStringFinal = TextUtils.ellipsize(nameStringFinal, namePaint, nameWidth, TextUtils.TruncateAt.END); + } + try { + nameLayout = new StaticLayout(nameStringFinal, namePaint, nameWidth + dp(2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + if (nameLayout != null && nameLayout.getLineCount() > 0) { + nameWidth = (int) Math.ceil(nameLayout.getLineWidth(0)); + if (messageObject.type != 13) { + namesOffset += dp(19); + } + nameOffsetX = nameLayout.getLineLeft(0); + } else { + nameWidth = 0; + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + if (currentNameString.length() == 0) { + currentNameString = null; + } + } else { + currentNameString = null; + nameLayout = null; + nameWidth = 0; + } + + currentForwardUser = null; + currentForwardNameString = null; + currentForwardChannel = null; + forwardedNameLayout[0] = null; + forwardedNameLayout[1] = null; + forwardedNameWidth = 0; + if (drawForwardedName && messageObject.isForwarded()) { + if (messageObject.messageOwner.fwd_from.channel_id != 0) { + currentForwardChannel = MessagesController.getInstance().getChat(messageObject.messageOwner.fwd_from.channel_id); + } + if (messageObject.messageOwner.fwd_from.from_id != 0) { + currentForwardUser = MessagesController.getInstance().getUser(messageObject.messageOwner.fwd_from.from_id); + } + + if (currentForwardUser != null || currentForwardChannel != null) { + if (currentForwardChannel != null) { + if (currentForwardUser != null) { + currentForwardNameString = String.format("%s (%s)", currentForwardChannel.title, UserObject.getUserName(currentForwardUser)); + } else { + currentForwardNameString = currentForwardChannel.title; + } + } else if (currentForwardUser != null) { + currentForwardNameString = UserObject.getUserName(currentForwardUser); + } + + forwardedNameWidth = getMaxNameWidth(); + int fromWidth = (int) Math.ceil(forwardNamePaint.measureText(LocaleController.getString("From", R.string.From) + " ")); + CharSequence name = TextUtils.ellipsize(currentForwardNameString.replace('\n', ' '), replyNamePaint, forwardedNameWidth - fromWidth - viaWidth, TextUtils.TruncateAt.END); + CharSequence lastLine; + if (viaString != null) { + viaNameWidth = (int) Math.ceil(forwardNamePaint.measureText(LocaleController.getString("From", R.string.From) + " " + name)); + lastLine = replaceTags(String.format("%s %s via %s", LocaleController.getString("From", R.string.From), name, viaUsername)); + } else { + lastLine = replaceTags(String.format("%s %s", LocaleController.getString("From", R.string.From), name)); + } + lastLine = TextUtils.ellipsize(lastLine, forwardNamePaint, forwardedNameWidth, TextUtils.TruncateAt.END); + try { + forwardedNameLayout[1] = new StaticLayout(lastLine, forwardNamePaint, forwardedNameWidth + dp(2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + lastLine = TextUtils.ellipsize(replaceTags(LocaleController.getString("ForwardedMessage", R.string.ForwardedMessage)), forwardNamePaint, forwardedNameWidth, TextUtils.TruncateAt.END); + forwardedNameLayout[0] = new StaticLayout(lastLine, forwardNamePaint, forwardedNameWidth + dp(2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + 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 += dp(36); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + } + + if (messageObject.isReply()) { + namesOffset += dp(42); + if (messageObject.type != 0) { + if (messageObject.type == 13) { + namesOffset -= dp(42); + } else { + namesOffset += dp(5); + } + } + + int maxWidth = getMaxNameWidth(); + if (messageObject.type != 13) { + maxWidth -= dp(10); + } + + CharSequence stringFinalName = null; + CharSequence stringFinalText = null; + if (messageObject.replyMessageObject != null) { + TLRPC.PhotoSize photoSize = FileLoader.getClosestPhotoSizeWithSize(messageObject.replyMessageObject.photoThumbs, 80); + if (photoSize == null || messageObject.replyMessageObject.type == 13 || messageObject.type == 13 && !isTablet() || messageObject.replyMessageObject.isSecretMedia()) { + replyImageReceiver.setImageBitmap((Drawable) null); + needReplyImage = false; + } else { + currentReplyPhoto = photoSize.location; + replyImageReceiver.setImage(photoSize.location, "50_50", null, null, true); + needReplyImage = true; + maxWidth -= dp(44); + } + + String name = null; + if (messageObject.replyMessageObject.isFromUser()) { + TLRPC.User user = MessagesController.getInstance().getUser(messageObject.replyMessageObject.messageOwner.from_id); + if (user != null) { + name = UserObject.getUserName(user); + } + } else if (messageObject.replyMessageObject.messageOwner.from_id < 0) { + TLRPC.Chat chat = MessagesController.getInstance().getChat(-messageObject.replyMessageObject.messageOwner.from_id); + if (chat != null) { + name = chat.title; + } + } else { + TLRPC.Chat chat = MessagesController.getInstance().getChat(messageObject.replyMessageObject.messageOwner.to_id.channel_id); + if (chat != null) { + name = chat.title; + } + } + + if (name != null) { + stringFinalName = TextUtils.ellipsize(name.replace('\n', ' '), replyNamePaint, maxWidth, TextUtils.TruncateAt.END); + } + if (messageObject.replyMessageObject.messageText != null && messageObject.replyMessageObject.messageText.length() > 0) { + String mess = messageObject.replyMessageObject.messageText.toString(); + if (mess.length() > 150) { + mess = mess.substring(0, 150); + } + mess = mess.replace('\n', ' '); + stringFinalText = Emoji.replaceEmoji(mess, replyTextPaint.getFontMetricsInt(), dp(14), false); + stringFinalText = TextUtils.ellipsize(stringFinalText, replyTextPaint, maxWidth, TextUtils.TruncateAt.END); + } + } + if (stringFinalName == null) { + stringFinalName = LocaleController.getString("Loading", R.string.Loading); + } + try { + replyNameLayout = new StaticLayout(stringFinalName, replyNamePaint, maxWidth + dp(6), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + if (replyNameLayout.getLineCount() > 0) { + replyNameWidth = (int)Math.ceil(replyNameLayout.getLineWidth(0)) + dp(12 + (needReplyImage ? 44 : 0)); + replyNameOffset = replyNameLayout.getLineLeft(0); + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + try { + if (stringFinalText != null) { + replyTextLayout = new StaticLayout(stringFinalText, replyTextPaint, maxWidth + dp(6), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); + if (replyTextLayout.getLineCount() > 0) { + replyTextWidth = (int) Math.ceil(replyTextLayout.getLineWidth(0)) + dp(12 + (needReplyImage ? 44 : 0)); + replyTextOffset = replyTextLayout.getLineLeft(0); + } + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + + requestLayout(); + } + + @Override + protected void onDraw(Canvas canvas) { + if (currentMessageObject == null) { + return; + } + + if (!wasLayout) { + requestLayout(); + return; + } + + if (isAvatarVisible) { + avatarImage.draw(canvas); + } + + if (mediaBackground) { + timePaint.setColor(Theme.MSG_MEDIA_TIME_TEXT_COLOR); + } else { + if (currentMessageObject.isOutOwner()) { + timePaint.setColor(isDrawSelectedBackground() ? Theme.MSG_OUT_TIME_SELECTED_TEXT_COLOR : Theme.MSG_OUT_TIME_TEXT_COLOR); + } else { + timePaint.setColor(isDrawSelectedBackground() ? Theme.MSG_IN_TIME_SELECTED_TEXT_COLOR : Theme.MSG_IN_TIME_TEXT_COLOR); + } + } + + if (currentMessageObject.isOutOwner()) { + if (isDrawSelectedBackground()) { + if (!mediaBackground) { + currentBackgroundDrawable = Theme.backgroundDrawableOutSelected; + } else { + currentBackgroundDrawable = Theme.backgroundMediaDrawableOutSelected; + } + } else { + if (!mediaBackground) { + currentBackgroundDrawable = Theme.backgroundDrawableOut; + } else { + currentBackgroundDrawable = Theme.backgroundMediaDrawableOut; + } + } + setDrawableBounds(currentBackgroundDrawable, layoutWidth - backgroundWidth - (!mediaBackground ? 0 : dp(9)), dp(1), backgroundWidth - (mediaBackground ? 0 : dp(3)), layoutHeight - dp(2)); + } else { + if (isDrawSelectedBackground()) { + if (!mediaBackground) { + currentBackgroundDrawable = Theme.backgroundDrawableInSelected; + } else { + currentBackgroundDrawable = Theme.backgroundMediaDrawableInSelected; + } + } else { + if (!mediaBackground) { + currentBackgroundDrawable = Theme.backgroundDrawableIn; + } else { + currentBackgroundDrawable = Theme.backgroundMediaDrawableIn; + } + } + if (isChat && currentMessageObject.isFromUser()) { + setDrawableBounds(currentBackgroundDrawable, dp(48 + (!mediaBackground ? 3 : 9)), dp(1), backgroundWidth - (mediaBackground ? 0 : dp(3)), layoutHeight - dp(2)); + } else { + setDrawableBounds(currentBackgroundDrawable, (!mediaBackground ? dp(3) : dp(9)), dp(1), backgroundWidth - (mediaBackground ? 0 : dp(3)), layoutHeight - dp(2)); + } + } + if (drawBackground && currentBackgroundDrawable != null) { + currentBackgroundDrawable.draw(canvas); + } + + drawContent(canvas); + + if (drawShareButton) { + Theme.shareDrawable.setColorFilter(sharePressed ? Theme.colorPressedFilter : Theme.colorFilter); + if (currentMessageObject.isOutOwner()) { + shareStartX = currentBackgroundDrawable.getBounds().left - dp(8) - Theme.shareDrawable.getIntrinsicWidth(); + } else { + shareStartX = currentBackgroundDrawable.getBounds().right + dp(8); + } + setDrawableBounds(Theme.shareDrawable, shareStartX, shareStartY = layoutHeight - dp(41)); + Theme.shareDrawable.draw(canvas); + setDrawableBounds(Theme.shareIconDrawable, shareStartX + dp(9), shareStartY + dp(9)); + Theme.shareIconDrawable.draw(canvas); + } + + if (drawNameLayout && nameLayout != null) { + canvas.save(); + + if (currentMessageObject.type == 13) { + namePaint.setColor(Theme.MSG_STICKER_NAME_TEXT_COLOR); + int backWidth; + if (currentMessageObject.isOutOwner()) { + nameX = dp(28); + } else { + nameX = currentBackgroundDrawable.getBounds().right + dp(22); + } + nameY = layoutHeight - dp(38); + Theme.systemDrawable.setColorFilter(Theme.colorFilter); + Theme.systemDrawable.setBounds((int) nameX - dp(12), (int) nameY - dp(5), (int) nameX + dp(12) + nameWidth, (int) nameY + dp(22)); + Theme.systemDrawable.draw(canvas); + } else { + if (mediaBackground || currentMessageObject.isOutOwner()) { + nameX = currentBackgroundDrawable.getBounds().left + dp(11) - nameOffsetX; + } else { + nameX = currentBackgroundDrawable.getBounds().left + dp(17) - nameOffsetX; + } + if (currentUser != null) { + namePaint.setColor(AvatarDrawable.getNameColorForId(currentUser.id)); + } else if (currentChat != null) { + namePaint.setColor(AvatarDrawable.getNameColorForId(currentChat.id)); + } else { + namePaint.setColor(AvatarDrawable.getNameColorForId(0)); + } + nameY = dp(10); + } + canvas.translate(nameX, nameY); + nameLayout.draw(canvas); + canvas.restore(); + } + + if (drawForwardedName && forwardedNameLayout[0] != null && forwardedNameLayout[1] != null) { + forwardNameY = dp(10 + (drawNameLayout ? 19 : 0)); + if (currentMessageObject.isOutOwner()) { + forwardNamePaint.setColor(Theme.MSG_OUT_FORDWARDED_NAME_TEXT_COLOR); + forwardNameX = currentBackgroundDrawable.getBounds().left + dp(11); + } else { + forwardNamePaint.setColor(Theme.MSG_IN_FORDWARDED_NAME_TEXT_COLOR); + if (mediaBackground) { + forwardNameX = currentBackgroundDrawable.getBounds().left + dp(11); + } else { + forwardNameX = currentBackgroundDrawable.getBounds().left + dp(17); + } + } + for (int a = 0; a < 2; a++) { + canvas.save(); + canvas.translate(forwardNameX - forwardNameOffsetX[a], forwardNameY + dp(16) * a); + forwardedNameLayout[a].draw(canvas); + canvas.restore(); + } + } + + if (currentMessageObject.isReply()) { + if (currentMessageObject.type == 13) { + replyLinePaint.setColor(Theme.MSG_STICKER_REPLY_LINE_COLOR); + replyNamePaint.setColor(Theme.MSG_STICKER_REPLY_NAME_TEXT_COLOR); + replyTextPaint.setColor(Theme.MSG_STICKER_REPLY_MESSAGE_TEXT_COLOR); + if (currentMessageObject.isOutOwner()) { + replyStartX = dp(23); + } else { + replyStartX = currentBackgroundDrawable.getBounds().right + dp(17); + } + replyStartY = layoutHeight - dp(58); + if (nameLayout != null) { + replyStartY -= dp(25 + 6); + } + int backWidth = Math.max(replyNameWidth, replyTextWidth) + dp(14 + (needReplyImage ? 44 : 0)); + Theme.systemDrawable.setColorFilter(Theme.colorFilter); + Theme.systemDrawable.setBounds(replyStartX - dp(7), replyStartY - dp(6), replyStartX - dp(7) + backWidth, replyStartY + dp(41)); + Theme.systemDrawable.draw(canvas); + } else { + if (currentMessageObject.isOutOwner()) { + replyLinePaint.setColor(Theme.MSG_OUT_REPLY_LINE_COLOR); + replyNamePaint.setColor(Theme.MSG_OUT_REPLY_NAME_TEXT_COLOR); + if (currentMessageObject.replyMessageObject != null && currentMessageObject.replyMessageObject.type == 0) { + replyTextPaint.setColor(Theme.MSG_OUT_REPLY_MESSAGE_TEXT_COLOR); + } else { + replyTextPaint.setColor(isDrawSelectedBackground() ? Theme.MSG_OUT_REPLY_MEDIA_MESSAGE_SELETED_TEXT_COLOR : Theme.MSG_OUT_REPLY_MEDIA_MESSAGE_TEXT_COLOR); + } + replyStartX = currentBackgroundDrawable.getBounds().left + dp(12); + } else { + replyLinePaint.setColor(Theme.MSG_IN_REPLY_LINE_COLOR); + replyNamePaint.setColor(Theme.MSG_IN_REPLY_NAME_TEXT_COLOR); + if (currentMessageObject.replyMessageObject != null && currentMessageObject.replyMessageObject.type == 0) { + replyTextPaint.setColor(Theme.MSG_IN_REPLY_MESSAGE_TEXT_COLOR); + } else { + replyTextPaint.setColor(isDrawSelectedBackground() ? Theme.MSG_IN_REPLY_MEDIA_MESSAGE_SELETED_TEXT_COLOR : Theme.MSG_IN_REPLY_MEDIA_MESSAGE_TEXT_COLOR); + } + if (mediaBackground) { + replyStartX = currentBackgroundDrawable.getBounds().left + dp(12); + } else { + replyStartX = currentBackgroundDrawable.getBounds().left + dp(18); + } + } + replyStartY = dp(12 + (drawForwardedName && forwardedNameLayout[0] != null ? 36 : 0) + (drawNameLayout && nameLayout != null ? 20 : 0)); + } + canvas.drawRect(replyStartX, replyStartY, replyStartX + dp(2), replyStartY + dp(35), replyLinePaint); + if (needReplyImage) { + replyImageReceiver.setImageCoords(replyStartX + dp(10), replyStartY, dp(35), dp(35)); + replyImageReceiver.draw(canvas); + } + + if (replyNameLayout != null) { + canvas.save(); + canvas.translate(replyStartX - replyNameOffset + dp(10 + (needReplyImage ? 44 : 0)), replyStartY); + replyNameLayout.draw(canvas); + canvas.restore(); + } + if (replyTextLayout != null) { + canvas.save(); + canvas.translate(replyStartX - replyTextOffset + dp(10 + (needReplyImage ? 44 : 0)), replyStartY + dp(19)); + replyTextLayout.draw(canvas); + canvas.restore(); + } + } + + if (drawTime || !mediaBackground) { + if (mediaBackground) { + Drawable drawable; + if (currentMessageObject.type == 13) { + drawable = Theme.timeStickerBackgroundDrawable; + } else { + drawable = Theme.timeBackgroundDrawable; + } + setDrawableBounds(drawable, timeX - dp(4), layoutHeight - dp(27), timeWidth + dp(8 + (currentMessageObject.isOutOwner() ? 20 : 0)), dp(17)); + drawable.draw(canvas); + + int additionalX = 0; + if ((currentMessageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { + additionalX = (int) (timeWidth - timeLayout.getLineWidth(0)); + + if (currentMessageObject.isSending()) { + if (!currentMessageObject.isOutOwner()) { + setDrawableBounds(Theme.clockMediaDrawable, timeX + dp(11), layoutHeight - dp(13.0f) - Theme.clockMediaDrawable.getIntrinsicHeight()); + Theme.clockMediaDrawable.draw(canvas); + } + } else if (currentMessageObject.isSendError()) { + if (!currentMessageObject.isOutOwner()) { + setDrawableBounds(Theme.errorDrawable, timeX + dp(11), layoutHeight - dp(12.5f) - Theme.errorDrawable.getIntrinsicHeight()); + Theme.errorDrawable.draw(canvas); + } + } else { + Drawable countDrawable = Theme.viewsMediaCountDrawable; + setDrawableBounds(countDrawable, timeX, layoutHeight - dp(9.5f) - timeLayout.getHeight()); + countDrawable.draw(canvas); + + if (viewsLayout != null) { + canvas.save(); + canvas.translate(timeX + countDrawable.getIntrinsicWidth() + dp(3), layoutHeight - dp(11.3f) - timeLayout.getHeight()); + viewsLayout.draw(canvas); + canvas.restore(); + } + } + } + + canvas.save(); + canvas.translate(timeX + additionalX, layoutHeight - dp(11.3f) - timeLayout.getHeight()); + timeLayout.draw(canvas); + canvas.restore(); + } else { + int additionalX = 0; + if ((currentMessageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { + additionalX = (int) (timeWidth - timeLayout.getLineWidth(0)); + + if (currentMessageObject.isSending()) { + if (!currentMessageObject.isOutOwner()) { + Drawable clockDrawable = Theme.clockChannelDrawable[isDrawSelectedBackground() ? 1 : 0]; + setDrawableBounds(clockDrawable, timeX + dp(11), layoutHeight - dp(8.5f) - clockDrawable.getIntrinsicHeight()); + clockDrawable.draw(canvas); + } + } else if (currentMessageObject.isSendError()) { + if (!currentMessageObject.isOutOwner()) { + setDrawableBounds(Theme.errorDrawable, timeX + dp(11), layoutHeight - dp(6.5f) - Theme.errorDrawable.getIntrinsicHeight()); + Theme.errorDrawable.draw(canvas); + } + } else { + if (!currentMessageObject.isOutOwner()) { + setDrawableBounds(Theme.viewsCountDrawable[isDrawSelectedBackground() ? 1 : 0], timeX, layoutHeight - dp(4.5f) - timeLayout.getHeight()); + Theme.viewsCountDrawable[isDrawSelectedBackground() ? 1 : 0].draw(canvas); + } else { + setDrawableBounds(Theme.viewsOutCountDrawable, timeX, layoutHeight - dp(4.5f) - timeLayout.getHeight()); + Theme.viewsOutCountDrawable.draw(canvas); + } + + if (viewsLayout != null) { + canvas.save(); + canvas.translate(timeX + Theme.viewsOutCountDrawable.getIntrinsicWidth() + dp(3), layoutHeight - dp(6.5f) - timeLayout.getHeight()); + viewsLayout.draw(canvas); + canvas.restore(); + } + } + } + + canvas.save(); + canvas.translate(timeX + additionalX, layoutHeight - dp(6.5f) - timeLayout.getHeight()); + timeLayout.draw(canvas); + canvas.restore(); + //canvas.drawRect(timeX, layoutHeight - AndroidUtilities.dp(6.5f) - timeLayout.getHeight(), timeX + availableTimeWidth, layoutHeight - AndroidUtilities.dp(4.5f) - timeLayout.getHeight(), timePaint); + } + + if (currentMessageObject.isOutOwner()) { + boolean drawCheck1 = false; + boolean drawCheck2 = false; + boolean drawClock = false; + boolean drawError = false; + boolean isBroadcast = (int)(currentMessageObject.getDialogId() >> 32) == 1; + + if (currentMessageObject.isSending()) { + drawCheck1 = false; + drawCheck2 = false; + drawClock = true; + drawError = false; + } else if (currentMessageObject.isSendError()) { + drawCheck1 = false; + drawCheck2 = false; + drawClock = false; + drawError = true; + } else if (currentMessageObject.isSent()) { + if (!currentMessageObject.isUnread()) { + drawCheck1 = true; + drawCheck2 = true; + } else { + drawCheck1 = false; + drawCheck2 = true; + } + drawClock = false; + drawError = false; + } + + if (drawClock) { + if (!mediaBackground) { + setDrawableBounds(Theme.clockDrawable, layoutWidth - dp(18.5f) - Theme.clockDrawable.getIntrinsicWidth(), layoutHeight - dp(8.5f) - Theme.clockDrawable.getIntrinsicHeight()); + Theme.clockDrawable.draw(canvas); + } else { + setDrawableBounds(Theme.clockMediaDrawable, layoutWidth - dp(22.0f) - Theme.clockMediaDrawable.getIntrinsicWidth(), layoutHeight - dp(12.5f) - Theme.clockMediaDrawable.getIntrinsicHeight()); + Theme.clockMediaDrawable.draw(canvas); + } + } + if (isBroadcast) { + if (drawCheck1 || drawCheck2) { + if (!mediaBackground) { + setDrawableBounds(Theme.broadcastDrawable, layoutWidth - dp(20.5f) - Theme.broadcastDrawable.getIntrinsicWidth(), layoutHeight - dp(8.0f) - Theme.broadcastDrawable.getIntrinsicHeight()); + Theme.broadcastDrawable.draw(canvas); + } else { + setDrawableBounds(Theme.broadcastMediaDrawable, layoutWidth - dp(24.0f) - Theme.broadcastMediaDrawable.getIntrinsicWidth(), layoutHeight - dp(13.0f) - Theme.broadcastMediaDrawable.getIntrinsicHeight()); + Theme.broadcastMediaDrawable.draw(canvas); + } + } + } else { + if (drawCheck2) { + if (!mediaBackground) { + if (drawCheck1) { + setDrawableBounds(Theme.checkDrawable, layoutWidth - dp(22.5f) - Theme.checkDrawable.getIntrinsicWidth(), layoutHeight - dp(8.0f) - Theme.checkDrawable.getIntrinsicHeight()); + } else { + setDrawableBounds(Theme.checkDrawable, layoutWidth - dp(18.5f) - Theme.checkDrawable.getIntrinsicWidth(), layoutHeight - dp(8.0f) - Theme.checkDrawable.getIntrinsicHeight()); + } + Theme.checkDrawable.draw(canvas); + } else { + if (drawCheck1) { + setDrawableBounds(Theme.checkMediaDrawable, layoutWidth - dp(26.3f) - Theme.checkMediaDrawable.getIntrinsicWidth(), layoutHeight - dp(12.5f) - Theme.checkMediaDrawable.getIntrinsicHeight()); + } else { + setDrawableBounds(Theme.checkMediaDrawable, layoutWidth - dp(21.5f) - Theme.checkMediaDrawable.getIntrinsicWidth(), layoutHeight - dp(12.5f) - Theme.checkMediaDrawable.getIntrinsicHeight()); + } + Theme.checkMediaDrawable.draw(canvas); + } + } + if (drawCheck1) { + if (!mediaBackground) { + setDrawableBounds(Theme.halfCheckDrawable, layoutWidth - dp(18) - Theme.halfCheckDrawable.getIntrinsicWidth(), layoutHeight - dp(8.0f) - Theme.halfCheckDrawable.getIntrinsicHeight()); + Theme.halfCheckDrawable.draw(canvas); + } else { + setDrawableBounds(Theme.halfCheckMediaDrawable, layoutWidth - dp(21.5f) - Theme.halfCheckMediaDrawable.getIntrinsicWidth(), layoutHeight - dp(12.5f) - Theme.halfCheckMediaDrawable.getIntrinsicHeight()); + Theme.halfCheckMediaDrawable.draw(canvas); + } + } + } + if (drawError) { + if (!mediaBackground) { + setDrawableBounds(Theme.errorDrawable, layoutWidth - dp(18) - Theme.errorDrawable.getIntrinsicWidth(), layoutHeight - dp(7) - Theme.errorDrawable.getIntrinsicHeight()); + Theme.errorDrawable.draw(canvas); + } else { + setDrawableBounds(Theme.errorDrawable, layoutWidth - dp(20.5f) - Theme.errorDrawable.getIntrinsicWidth(), layoutHeight - dp(11.5f) - Theme.errorDrawable.getIntrinsicHeight()); + Theme.errorDrawable.draw(canvas); + } + } + } + } + } + + @Override + public int getObserverTag() { + return TAG; + } + + public MessageObject getMessageObject() { + return currentMessageObject; + } } 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 5c789c3dd..48755d591 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ContextLinkCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ContextLinkCell.java @@ -233,7 +233,7 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa } } - int width = 1; + int width; int w = 0; int h = 0; @@ -268,7 +268,7 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa if (mediaWebpage) { width = (int) (w / (h / (float) AndroidUtilities.dp(80))); - if (Build.VERSION.SDK_INT >= 11 && documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { + if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { currentPhotoFilterThumb = currentPhotoFilter = String.format(Locale.US, "%d_%d_b", (int) (width / AndroidUtilities.density), 80); } else { currentPhotoFilter = String.format(Locale.US, "%d_%d", (int) (width / AndroidUtilities.density), 80); @@ -277,16 +277,17 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa } else { currentPhotoFilter = "52_52"; } + linkImageView.setAspectFit(documentAttachType == DOCUMENT_ATTACH_TYPE_STICKER); if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { - if (documentAttach != null && Build.VERSION.SDK_INT >= 11) { + if (documentAttach != null) { linkImageView.setImage(documentAttach, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilter, documentAttach.size, ext, false); } else { linkImageView.setImage(null, url, null, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilter, -1, ext, true); } } else { if (currentPhotoObject != null) { - linkImageView.setImage(currentPhotoObject.location, currentPhotoFilter, currentPhotoObjectThumb != null ? currentPhotoObjectThumb.location : null, currentPhotoFilterThumb, 0, ext, false); + linkImageView.setImage(currentPhotoObject.location, currentPhotoFilter, currentPhotoObjectThumb != null ? currentPhotoObjectThumb.location : null, currentPhotoFilterThumb, currentPhotoObject.size, ext, false); } else { linkImageView.setImage(null, url, currentPhotoFilter, null, currentPhotoObjectThumb != null ? currentPhotoObjectThumb.location : null, currentPhotoFilterThumb, -1, ext, true); } @@ -296,24 +297,24 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa if (mediaWebpage) { setBackgroundDrawable(null); - if (inlineResult == null) { + //if (inlineResult == null) { width = viewWidth; int height = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(width, height); - if (needDivider) { - height -= AndroidUtilities.dp(2); + if (height == 0) { + height = AndroidUtilities.dp(100); } + setMeasuredDimension(width, height); int x = (width - AndroidUtilities.dp(24)) / 2; int y = (height - AndroidUtilities.dp(24)) / 2; radialProgress.setProgressRect(x, y, x + AndroidUtilities.dp(24), y + AndroidUtilities.dp(24)); linkImageView.setImageCoords(0, 0, width, height); - } else { + /*} else { setMeasuredDimension(width + AndroidUtilities.dp(5), AndroidUtilities.dp(90)); int x = AndroidUtilities.dp(5) + (width - AndroidUtilities.dp(24)) / 2; int y = (AndroidUtilities.dp(90) - AndroidUtilities.dp(24)) / 2; radialProgress.setProgressRect(x, y, x + AndroidUtilities.dp(24), y + AndroidUtilities.dp(24)); linkImageView.setImageCoords(AndroidUtilities.dp(5), AndroidUtilities.dp(5), width, AndroidUtilities.dp(80)); - } + }*/ } else { setBackgroundResource(R.drawable.list_selector); int height = 0; @@ -554,7 +555,7 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa } public void updateButtonState(boolean animated) { - if (!mediaWebpage || Build.VERSION.SDK_INT < 11) { + if (!mediaWebpage) { return; } String fileName = null; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java index d7a67358f..59c30be3a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java @@ -14,9 +14,12 @@ import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.Layout; +import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; import android.view.MotionEvent; import org.telegram.messenger.AndroidUtilities; @@ -26,6 +29,7 @@ import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.UserObject; import org.telegram.messenger.FileLog; +import org.telegram.messenger.query.DraftQuery; import org.telegram.tgnet.TLRPC; import org.telegram.messenger.ContactsController; import org.telegram.messenger.Emoji; @@ -33,6 +37,7 @@ import org.telegram.messenger.MessagesController; import org.telegram.messenger.R; import org.telegram.messenger.UserConfig; import org.telegram.messenger.ImageReceiver; +import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Components.AvatarDrawable; import java.util.ArrayList; @@ -63,6 +68,7 @@ public class DialogCell extends BaseCell { private static Paint backPaint; private long currentDialogId; + private int currentEditDate; private boolean isDialogCell; private int lastMessageDate; private int unreadCount; @@ -80,6 +86,7 @@ public class DialogCell extends BaseCell { private TLRPC.Chat chat = null; private TLRPC.EncryptedChat encryptedChat = null; private CharSequence lastPrintString = null; + private TLRPC.DraftMessage draftMessage; public boolean useSeparator = false; @@ -140,8 +147,8 @@ public class DialogCell extends BaseCell { messagePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); messagePaint.setTextSize(AndroidUtilities.dp(16)); - messagePaint.setColor(0xff8f8f8f); - messagePaint.linkColor = 0xff8f8f8f; + messagePaint.setColor(Theme.DIALOGS_MESSAGE_TEXT_COLOR); + messagePaint.linkColor = Theme.DIALOGS_MESSAGE_TEXT_COLOR; linePaint = new Paint(); linePaint.setColor(0xffdcdcdc); @@ -151,7 +158,7 @@ public class DialogCell extends BaseCell { messagePrintingPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); messagePrintingPaint.setTextSize(AndroidUtilities.dp(16)); - messagePrintingPaint.setColor(0xff4d83b3); + messagePrintingPaint.setColor(Theme.DIALOGS_PRINTING_TEXT_COLOR); timePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); timePaint.setTextSize(AndroidUtilities.dp(13)); @@ -183,7 +190,7 @@ public class DialogCell extends BaseCell { avatarDrawable = new AvatarDrawable(); } - public void setDialog(TLRPC.Dialog dialog, int i, int type) { + public void setDialog(TLRPC.TL_dialog dialog, int i, int type) { currentDialogId = dialog.id; isDialogCell = true; index = i; @@ -196,6 +203,7 @@ public class DialogCell extends BaseCell { message = messageObject; isDialogCell = false; lastMessageDate = date; + currentEditDate = messageObject != null ? messageObject.messageOwner.edit_date : 0; unreadCount = 0; lastUnreadState = messageObject != null && messageObject.isUnread(); if (message != null) { @@ -316,115 +324,149 @@ public class DialogCell extends BaseCell { } } - if (message == null) { - if (printingString != null) { - lastPrintString = messageString = printingString; - currentMessagePaint = messagePrintingPaint; + int lastDate = lastMessageDate; + if (lastMessageDate == 0 && message != null) { + lastDate = message.messageOwner.date; + } + + if (isDialogCell) { + draftMessage = DraftQuery.getDraft(currentDialogId); + if (draftMessage != null && (TextUtils.isEmpty(draftMessage.message) && draftMessage.reply_to_msg_id == 0 || lastDate > draftMessage.date && unreadCount != 0) || + ChatObject.isChannel(chat) && !chat.megagroup && !chat.creator && !chat.editor || + chat != null && (chat.left || chat.kicked)) { + draftMessage = null; + } + } else { + draftMessage = null; + } + + if (printingString != null) { + lastPrintString = messageString = printingString; + currentMessagePaint = messagePrintingPaint; + } else { + lastPrintString = null; + + if (draftMessage != null) { + checkMessage = false; + if (TextUtils.isEmpty(draftMessage.message)) { + String draftString = LocaleController.getString("Draft", R.string.Draft); + SpannableStringBuilder stringBuilder = SpannableStringBuilder.valueOf(draftString); + stringBuilder.setSpan(new ForegroundColorSpan(Theme.DIALOGS_DRAFT_TEXT_COLOR), 0, draftString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + messageString = stringBuilder; + } else { + String mess = draftMessage.message; + if (mess.length() > 150) { + mess = mess.substring(0, 150); + } + String draftString = LocaleController.getString("Draft", R.string.Draft); + SpannableStringBuilder stringBuilder = SpannableStringBuilder.valueOf(String.format("%s: %s", draftString, mess.replace('\n', ' '))); + stringBuilder.setSpan(new ForegroundColorSpan(Theme.DIALOGS_DRAFT_TEXT_COLOR), 0, draftString.length() + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + messageString = Emoji.replaceEmoji(stringBuilder, messagePaint.getFontMetricsInt(), AndroidUtilities.dp(20), false); + } } else { - lastPrintString = null; - if (encryptedChat != null) { - currentMessagePaint = messagePrintingPaint; - if (encryptedChat instanceof TLRPC.TL_encryptedChatRequested) { - messageString = LocaleController.getString("EncryptionProcessing", R.string.EncryptionProcessing); - } else if (encryptedChat instanceof TLRPC.TL_encryptedChatWaiting) { - if (user != null && user.first_name != null) { - messageString = LocaleController.formatString("AwaitingEncryption", R.string.AwaitingEncryption, user.first_name); - } else { - messageString = LocaleController.formatString("AwaitingEncryption", R.string.AwaitingEncryption, ""); - } - } else if (encryptedChat instanceof TLRPC.TL_encryptedChatDiscarded) { - messageString = LocaleController.getString("EncryptionRejected", R.string.EncryptionRejected); - } else if (encryptedChat instanceof TLRPC.TL_encryptedChat) { - if (encryptedChat.admin_id == UserConfig.getClientUserId()) { + if (message == null) { + if (encryptedChat != null) { + currentMessagePaint = messagePrintingPaint; + if (encryptedChat instanceof TLRPC.TL_encryptedChatRequested) { + messageString = LocaleController.getString("EncryptionProcessing", R.string.EncryptionProcessing); + } else if (encryptedChat instanceof TLRPC.TL_encryptedChatWaiting) { if (user != null && user.first_name != null) { - messageString = LocaleController.formatString("EncryptedChatStartedOutgoing", R.string.EncryptedChatStartedOutgoing, user.first_name); + messageString = LocaleController.formatString("AwaitingEncryption", R.string.AwaitingEncryption, user.first_name); } else { - messageString = LocaleController.formatString("EncryptedChatStartedOutgoing", R.string.EncryptedChatStartedOutgoing, ""); + messageString = LocaleController.formatString("AwaitingEncryption", R.string.AwaitingEncryption, ""); } + } else if (encryptedChat instanceof TLRPC.TL_encryptedChatDiscarded) { + messageString = LocaleController.getString("EncryptionRejected", R.string.EncryptionRejected); + } else if (encryptedChat instanceof TLRPC.TL_encryptedChat) { + if (encryptedChat.admin_id == UserConfig.getClientUserId()) { + if (user != null && user.first_name != null) { + messageString = LocaleController.formatString("EncryptedChatStartedOutgoing", R.string.EncryptedChatStartedOutgoing, user.first_name); + } else { + messageString = LocaleController.formatString("EncryptedChatStartedOutgoing", R.string.EncryptedChatStartedOutgoing, ""); + } + } else { + messageString = LocaleController.getString("EncryptedChatStartedIncoming", R.string.EncryptedChatStartedIncoming); + } + } + } + } else { + TLRPC.User fromUser = null; + TLRPC.Chat fromChat = null; + if (message.isFromUser()) { + fromUser = MessagesController.getInstance().getUser(message.messageOwner.from_id); + } else { + fromChat = MessagesController.getInstance().getChat(message.messageOwner.to_id.channel_id); + } + if (message.messageOwner instanceof TLRPC.TL_messageService) { + messageString = message.messageText; + currentMessagePaint = messagePrintingPaint; + } else { + if (chat != null && chat.id > 0 && fromChat == null) { + String name; + if (message.isOutOwner()) { + name = LocaleController.getString("FromYou", R.string.FromYou); + } else if (fromUser != null) { + name = UserObject.getFirstName(fromUser).replace("\n", ""); + } else if (fromChat != null) { + name = fromChat.title.replace("\n", ""); + } else { + name = "DELETED"; + } + checkMessage = false; + SpannableStringBuilder stringBuilder; + if (message.caption != null) { + String mess = message.caption.toString(); + if (mess.length() > 150) { + mess = mess.substring(0, 150); + } + stringBuilder = SpannableStringBuilder.valueOf(String.format("%s: %s", name, mess.replace('\n', ' '))); + } else if (message.messageOwner.media != null && !message.isMediaEmpty()) { + currentMessagePaint = messagePrintingPaint; + stringBuilder = SpannableStringBuilder.valueOf(String.format("%s: %s", name, message.messageText)); + stringBuilder.setSpan(new ForegroundColorSpan(Theme.DIALOGS_ATTACH_TEXT_COLOR), name.length() + 2, stringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (message.messageOwner.message != null) { + String mess = message.messageOwner.message; + if (mess.length() > 150) { + mess = mess.substring(0, 150); + } + stringBuilder = SpannableStringBuilder.valueOf(String.format("%s: %s", name, mess.replace('\n', ' '))); + } else { + stringBuilder = SpannableStringBuilder.valueOf(""); + } + if (stringBuilder.length() > 0) { + stringBuilder.setSpan(new ForegroundColorSpan(Theme.DIALOGS_NAME_TEXT_COLOR), 0, name.length() + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + messageString = Emoji.replaceEmoji(stringBuilder, messagePaint.getFontMetricsInt(), AndroidUtilities.dp(20), false); } else { - messageString = LocaleController.getString("EncryptedChatStartedIncoming", R.string.EncryptedChatStartedIncoming); + if (message.caption != null) { + messageString = message.caption; + } else { + messageString = message.messageText; + if (message.messageOwner.media != null && !message.isMediaEmpty()) { + currentMessagePaint = messagePrintingPaint; + } + } } } } } - if (lastMessageDate != 0) { - timeString = LocaleController.stringForMessageListDate(lastMessageDate); - } + } + + if (draftMessage != null) { + timeString = LocaleController.stringForMessageListDate(draftMessage.date); + } else if (lastMessageDate != 0) { + timeString = LocaleController.stringForMessageListDate(lastMessageDate); + } else if (message != null) { + timeString = LocaleController.stringForMessageListDate(message.messageOwner.date); + } + + if (message == null) { drawCheck1 = false; drawCheck2 = false; drawClock = false; drawCount = false; drawError = false; } else { - TLRPC.User fromUser = null; - TLRPC.Chat fromChat = null; - if (message.isFromUser()) { - fromUser = MessagesController.getInstance().getUser(message.messageOwner.from_id); - } else { - fromChat = MessagesController.getInstance().getChat(message.messageOwner.to_id.channel_id); - } - - if (lastMessageDate != 0) { - timeString = LocaleController.stringForMessageListDate(lastMessageDate); - } else { - timeString = LocaleController.stringForMessageListDate(message.messageOwner.date); - } - if (printingString != null) { - lastPrintString = messageString = printingString; - currentMessagePaint = messagePrintingPaint; - } else { - lastPrintString = null; - if (message.messageOwner instanceof TLRPC.TL_messageService) { - messageString = message.messageText; - currentMessagePaint = messagePrintingPaint; - } else { - if (chat != null && chat.id > 0 && fromChat == null) { - String name; - if (message.isOutOwner()) { - name = LocaleController.getString("FromYou", R.string.FromYou); - } else if (fromUser != null) { - name = UserObject.getFirstName(fromUser); - } else if (fromChat != null) { - name = fromChat.title; - } else { - name = "DELETED"; - } - checkMessage = false; - if (message.caption != null) { - String mess = message.caption.toString(); - if (mess.length() > 150) { - mess = mess.substring(0, 150); - } - mess = mess.replace('\n', ' '); - messageString = Emoji.replaceEmoji(AndroidUtilities.replaceTags(String.format("%s: %s", name.replace("\n", ""), mess), AndroidUtilities.FLAG_TAG_COLOR), messagePaint.getFontMetricsInt(), AndroidUtilities.dp(20), false); - } else { - if (message.messageOwner.media != null && !message.isMediaEmpty()) { - currentMessagePaint = messagePrintingPaint; - messageString = Emoji.replaceEmoji(AndroidUtilities.replaceTags(String.format("%s: %s", name.replace("\n", ""), message.messageText), AndroidUtilities.FLAG_TAG_COLOR), messagePaint.getFontMetricsInt(), AndroidUtilities.dp(20), false); - } else { - if (message.messageOwner.message != null) { - String mess = message.messageOwner.message; - if (mess.length() > 150) { - mess = mess.substring(0, 150); - } - mess = mess.replace('\n', ' '); - messageString = Emoji.replaceEmoji(AndroidUtilities.replaceTags(String.format("%s: %s", name.replace("\n", ""), mess), AndroidUtilities.FLAG_TAG_COLOR), messagePaint.getFontMetricsInt(), AndroidUtilities.dp(20), false); - } - } - } - } else { - if (message.caption != null) { - messageString = message.caption; - } else { - messageString = message.messageText; - if (message.messageOwner.media != null && !message.isMediaEmpty()) { - currentMessagePaint = messagePrintingPaint; - } - } - } - } - } - if (unreadCount != 0) { drawCount = true; countString = String.format("%d", unreadCount); @@ -432,7 +474,7 @@ public class DialogCell extends BaseCell { drawCount = false; } - if (message.isOut()) { + if (message.isOut() && draftMessage == null) { if (message.isSending()) { drawCheck1 = false; drawCheck2 = false; @@ -445,13 +487,8 @@ public class DialogCell extends BaseCell { drawError = true; drawCount = false; } else if (message.isSent()) { - if (!message.isUnread()) { - drawCheck1 = true; - drawCheck2 = true; - } else { - drawCheck1 = false; - drawCheck2 = true; - } + drawCheck1 = !message.isUnread() || ChatObject.isChannel(chat) && !chat.megagroup; + drawCheck2 = true; drawClock = false; drawError = false; } @@ -678,7 +715,7 @@ public class DialogCell extends BaseCell { isSelected = value; } - private ArrayList getDialogsArray() { + private ArrayList getDialogsArray() { if (dialogsType == 0) { return MessagesController.getInstance().dialogs; } else if (dialogsType == 1) { @@ -691,8 +728,15 @@ public class DialogCell extends BaseCell { public void checkCurrentDialogIndex() { if (index < getDialogsArray().size()) { - TLRPC.Dialog dialog = getDialogsArray().get(index); - if (currentDialogId != dialog.id || message != null && message.getId() != dialog.top_message || unreadCount != dialog.unread_count || message == null && MessagesController.getInstance().dialogMessage.get(dialog.id) != null) { + TLRPC.TL_dialog dialog = getDialogsArray().get(index); + TLRPC.DraftMessage newDraftMessage = DraftQuery.getDraft(currentDialogId); + MessageObject newMessageObject = MessagesController.getInstance().dialogMessage.get(dialog.id); + if (currentDialogId != dialog.id || + message != null && message.getId() != dialog.top_message || + newMessageObject != null && newMessageObject.messageOwner.edit_date != currentEditDate || + unreadCount != dialog.unread_count || + message != newMessageObject || + message == null && newMessageObject != null || newDraftMessage != draftMessage) { currentDialogId = dialog.id; update(0); } @@ -701,11 +745,12 @@ public class DialogCell extends BaseCell { public void update(int mask) { if (isDialogCell) { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(currentDialogId); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(currentDialogId); if (dialog != null && mask == 0) { message = MessagesController.getInstance().dialogMessage.get(dialog.id); lastUnreadState = message != null && message.isUnread(); unreadCount = dialog.unread_count; + currentEditDate = message != null ? message.messageOwner.edit_date : 0; lastMessageDate = dialog.last_message_date; if (message != null) { lastSendState = message.messageOwner.send_state; @@ -748,7 +793,7 @@ public class DialogCell extends BaseCell { lastUnreadState = message.isUnread(); continueUpdate = true; } else if (isDialogCell) { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(currentDialogId); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(currentDialogId); if (dialog != null && unreadCount != dialog.unread_count) { unreadCount = dialog.unread_count; continueUpdate = true; @@ -917,4 +962,9 @@ public class DialogCell extends BaseCell { avatarImage.draw(canvas); } + + @Override + public boolean hasOverlappingRendering() { + return false; + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/DrawerProfileCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/DrawerProfileCell.java index d2219fc22..2bd32cb5c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/DrawerProfileCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/DrawerProfileCell.java @@ -92,6 +92,7 @@ public class DrawerProfileCell extends FrameLayout { try { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(148), MeasureSpec.EXACTLY)); } catch (Exception e) { + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), AndroidUtilities.dp(148)); FileLog.e("tmessages", e); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/GreySectionCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/GreySectionCell.java index cfbe8048f..0915c3f94 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/GreySectionCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/GreySectionCell.java @@ -27,7 +27,7 @@ public class GreySectionCell extends FrameLayout { setBackgroundColor(0xfff2f2f2); textView = new TextView(getContext()); - textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); textView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); textView.setTextColor(0xff8a8a8a); textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); @@ -36,7 +36,7 @@ public class GreySectionCell extends FrameLayout { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(36), MeasureSpec.EXACTLY)); + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(32), MeasureSpec.EXACTLY)); } public void setText(String text) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/HintDialogCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/HintDialogCell.java new file mode 100644 index 000000000..671397db9 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/HintDialogCell.java @@ -0,0 +1,175 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.ui.Cells; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ContactsController; +import org.telegram.messenger.MessagesController; +import org.telegram.messenger.R; +import org.telegram.tgnet.TLRPC; +import org.telegram.ui.Components.AvatarDrawable; +import org.telegram.ui.Components.BackupImageView; +import org.telegram.ui.Components.LayoutHelper; + +public class HintDialogCell extends FrameLayout { + + private BackupImageView imageView; + private TextView nameTextView; + private AvatarDrawable avatarDrawable = new AvatarDrawable(); + + private static Drawable countDrawable; + private static Drawable countDrawableGrey; + private static TextPaint countPaint; + + private int lastUnreadCount; + private int countWidth; + private StaticLayout countLayout; + + private long dialog_id; + + public HintDialogCell(Context context) { + super(context); + setBackgroundResource(R.drawable.list_selector); + + imageView = new BackupImageView(context); + imageView.setRoundRadius(AndroidUtilities.dp(27)); + addView(imageView, LayoutHelper.createFrame(54, 54, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 7, 0, 0)); + + nameTextView = new TextView(context); + nameTextView.setTextColor(0xff212121); + nameTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); + nameTextView.setMaxLines(2); + nameTextView.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL); + nameTextView.setLines(2); + nameTextView.setEllipsize(TextUtils.TruncateAt.END); + addView(nameTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 6, 64, 6, 0)); + + if (countDrawable == null) { + countDrawable = getResources().getDrawable(R.drawable.dialogs_badge); + countDrawableGrey = getResources().getDrawable(R.drawable.dialogs_badge2); + + countPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); + countPaint.setTextSize(AndroidUtilities.dp(13)); + countPaint.setColor(0xffffffff); + countPaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(100), MeasureSpec.EXACTLY)); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (Build.VERSION.SDK_INT >= 21 && getBackground() != null) { + if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) { + getBackground().setHotspot(event.getX(), event.getY()); + } + } + return super.onTouchEvent(event); + } + + public void checkUnreadCounter(int mask) { + if (mask != 0 && (mask & MessagesController.UPDATE_MASK_READ_DIALOG_MESSAGE) == 0 && (mask & MessagesController.UPDATE_MASK_NEW_MESSAGE) == 0) { + return; + } + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + if (dialog != null && dialog.unread_count != 0) { + if (lastUnreadCount != dialog.unread_count) { + lastUnreadCount = dialog.unread_count; + String countString = String.format("%d", dialog.unread_count); + countWidth = Math.max(AndroidUtilities.dp(12), (int) Math.ceil(countPaint.measureText(countString))); + countLayout = new StaticLayout(countString, countPaint, countWidth, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); + if (mask != 0) { + invalidate(); + } + } + } else if (countLayout != null) { + if (mask != 0) { + invalidate(); + } + lastUnreadCount = 0; + countLayout = null; + } + } + + public void setDialog(int uid, boolean checked, CharSequence name) { + dialog_id = uid; + TLRPC.FileLocation photo = null; + if (uid > 0) { + TLRPC.User user = MessagesController.getInstance().getUser(uid); + if (name != null) { + nameTextView.setText(name); + } else if (user != null) { + nameTextView.setText(ContactsController.formatName(user.first_name, user.last_name)); + } else { + nameTextView.setText(""); + } + avatarDrawable.setInfo(user); + if (user != null && user.photo != null) { + photo = user.photo.photo_small; + } + } else { + TLRPC.Chat chat = MessagesController.getInstance().getChat(-uid); + if (name != null) { + nameTextView.setText(name); + } else if (chat != null) { + nameTextView.setText(chat.title); + } else { + nameTextView.setText(""); + } + avatarDrawable.setInfo(chat); + if (chat != null && chat.photo != null) { + photo = chat.photo.photo_small; + } + } + imageView.setImage(photo, "50_50", avatarDrawable); + checkUnreadCounter(0); + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean result = super.drawChild(canvas, child, drawingTime); + if (child == imageView) { + if (countLayout != null) { + int top = AndroidUtilities.dp(6); + int left = AndroidUtilities.dp(54); + int x = left - AndroidUtilities.dp(5.5f); + if (MessagesController.getInstance().isDialogMuted(dialog_id)) { + countDrawableGrey.setBounds(x, top, x + countWidth + AndroidUtilities.dp(11), top + countDrawableGrey.getIntrinsicHeight()); + countDrawableGrey.draw(canvas); + } else { + countDrawable.setBounds(x, top, x + countWidth + AndroidUtilities.dp(11), top + countDrawable.getIntrinsicHeight()); + countDrawable.draw(canvas); + } + canvas.save(); + canvas.translate(left, top + AndroidUtilities.dp(4)); + countLayout.draw(canvas); + canvas.restore(); + } + } + return result; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/MentionCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/MentionCell.java index 94ffcd3c4..a15dcce7f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/MentionCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/MentionCell.java @@ -18,6 +18,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.Emoji; import org.telegram.messenger.R; import org.telegram.messenger.UserObject; import org.telegram.tgnet.TLRPC; @@ -92,7 +93,11 @@ public class MentionCell extends LinearLayout { imageView.setImageDrawable(avatarDrawable); } nameTextView.setText(UserObject.getUserName(user)); - usernameTextView.setText("@" + user.username); + if (user.username != null) { + usernameTextView.setText("@" + user.username); + } else { + usernameTextView.setText(""); + } imageView.setVisibility(VISIBLE); usernameTextView.setVisibility(VISIBLE); } @@ -117,7 +122,7 @@ public class MentionCell extends LinearLayout { } usernameTextView.setVisibility(VISIBLE); nameTextView.setText(command); - usernameTextView.setText(help); + usernameTextView.setText(Emoji.replaceEmoji(help, usernameTextView.getPaint().getFontMetricsInt(), AndroidUtilities.dp(20), false)); } public void setIsDarkTheme(boolean isDarkTheme) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoAttachCameraCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoAttachCameraCell.java index 4d2fc2e67..7930fc4b2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoAttachCameraCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoAttachCameraCell.java @@ -8,13 +8,16 @@ package org.telegram.ui.Cells; +import android.annotation.SuppressLint; import android.content.Context; import android.widget.FrameLayout; import android.widget.ImageView; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.R; import org.telegram.ui.Components.LayoutHelper; +@SuppressLint("NewApi") public class PhotoAttachCameraCell extends FrameLayout { public PhotoAttachCameraCell(Context context) { @@ -22,8 +25,7 @@ public class PhotoAttachCameraCell extends FrameLayout { ImageView imageView = new ImageView(context); imageView.setScaleType(ImageView.ScaleType.CENTER); - //imageView.setImageResource(R.drawable.ic_attach_photobig); - imageView.setBackgroundColor(0xff777777); + imageView.setImageResource(R.drawable.instant_camera); addView(imageView, LayoutHelper.createFrame(80, 80)); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoEditToolCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoEditToolCell.java index 89584407d..d4f4c065a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoEditToolCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoEditToolCell.java @@ -12,14 +12,14 @@ import android.content.Context; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; -public class PhotoEditToolCell extends FrameLayoutFixed { +public class PhotoEditToolCell extends FrameLayout { private ImageView iconImage; private TextView nameTextView; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerAlbumsCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerAlbumsCell.java index 7fc1d5ec8..a559cdc5c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerAlbumsCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerAlbumsCell.java @@ -15,6 +15,7 @@ import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; +import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; @@ -22,10 +23,9 @@ import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.MediaController; import org.telegram.messenger.R; import org.telegram.ui.Components.BackupImageView; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; -public class PhotoPickerAlbumsCell extends FrameLayoutFixed { +public class PhotoPickerAlbumsCell extends FrameLayout { public interface PhotoPickerAlbumsCellDelegate { void didSelectAlbum(MediaController.AlbumEntry albumEntry); @@ -36,7 +36,7 @@ public class PhotoPickerAlbumsCell extends FrameLayoutFixed { private int albumsCount; private PhotoPickerAlbumsCellDelegate delegate; - private class AlbumView extends FrameLayoutFixed { + private class AlbumView extends FrameLayout { private BackupImageView imageView; private TextView nameTextView; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java index c4d28b1d4..01744fa9b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java @@ -8,15 +8,15 @@ package org.telegram.ui.Cells; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.view.Gravity; import android.widget.FrameLayout; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.R; import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.CheckBox; @@ -27,7 +27,7 @@ public class PhotoPickerPhotoCell extends FrameLayout { public BackupImageView photoImage; public FrameLayout checkFrame; public CheckBox checkBox; - private AnimatorSetProxy animator; + private AnimatorSet animator; public int itemWidth; public PhotoPickerPhotoCell(Context context) { @@ -62,13 +62,13 @@ public class PhotoPickerPhotoCell extends FrameLayout { if (checked) { setBackgroundColor(0xff0A0A0A); } - animator = new AnimatorSetProxy(); - animator.playTogether(ObjectAnimatorProxy.ofFloat(photoImage, "scaleX", checked ? 0.85f : 1.0f), - ObjectAnimatorProxy.ofFloat(photoImage, "scaleY", checked ? 0.85f : 1.0f)); + animator = new AnimatorSet(); + animator.playTogether(ObjectAnimator.ofFloat(photoImage, "scaleX", checked ? 0.85f : 1.0f), + ObjectAnimator.ofFloat(photoImage, "scaleY", checked ? 0.85f : 1.0f)); animator.setDuration(200); animator.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (animator != null && animator.equals(animation)) { animator = null; if (!checked) { @@ -78,7 +78,7 @@ public class PhotoPickerPhotoCell extends FrameLayout { } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (animator != null && animator.equals(animation)) { animator = null; } @@ -87,8 +87,8 @@ public class PhotoPickerPhotoCell extends FrameLayout { animator.start(); } else { setBackgroundColor(checked ? 0xff0A0A0A : 0); - ViewProxy.setScaleX(photoImage, checked ? 0.85f : 1.0f); - ViewProxy.setScaleY(photoImage, checked ? 0.85f : 1.0f); + photoImage.setScaleX(checked ? 0.85f : 1.0f); + photoImage.setScaleY(checked ? 0.85f : 1.0f); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerSearchCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerSearchCell.java index bcb61072b..3b1b58b37 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerSearchCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerSearchCell.java @@ -21,7 +21,6 @@ import android.widget.LinearLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.messenger.LocaleController; import org.telegram.messenger.R; import org.telegram.ui.Components.LayoutHelper; @@ -136,7 +135,7 @@ public class PhotoPickerSearchCell extends LinearLayout { } }); } else { - ViewProxy.setAlpha(searchButton, 0.5f); + searchButton.setAlpha(0.5f); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ProfileSearchCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ProfileSearchCell.java index b09451644..aa518b5f6 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ProfileSearchCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ProfileSearchCell.java @@ -297,7 +297,7 @@ public class ProfileSearchCell extends BaseCell { } if (drawCount) { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null && dialog.unread_count != 0) { lastUnreadCount = dialog.unread_count; String countString = String.format("%d", dialog.unread_count); @@ -350,7 +350,7 @@ public class ProfileSearchCell extends BaseCell { CharSequence onlineStringFinal = TextUtils.ellipsize(onlineString, currentOnlinePaint, onlineWidth - AndroidUtilities.dp(12), TextUtils.TruncateAt.END); onlineLayout = new StaticLayout(onlineStringFinal, currentOnlinePaint, onlineWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); nameTop = AndroidUtilities.dp(13); - if (subLabel != null && !drawNameBot) { + if (subLabel != null && chat != null) { nameLockTop -= AndroidUtilities.dp(12); } } else { @@ -454,7 +454,7 @@ public class ProfileSearchCell extends BaseCell { } } if (!continueUpdate && drawCount && (mask & MessagesController.UPDATE_MASK_READ_DIALOG_MESSAGE) != 0) { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null && dialog.unread_count != lastUnreadCount) { continueUpdate = true; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/RadioButtonCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/RadioButtonCell.java index 6a6199f01..3d3525923 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/RadioButtonCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/RadioButtonCell.java @@ -14,15 +14,15 @@ import android.graphics.Paint; import android.util.TypedValue; import android.view.Gravity; import android.view.View; +import android.widget.FrameLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.RadioButton; -public class RadioButtonCell extends FrameLayoutFixed { +public class RadioButtonCell extends FrameLayout { private TextView textView; private TextView valueTextView; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ShareDialogCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ShareDialogCell.java index d9793952e..1d5b16d01 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ShareDialogCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ShareDialogCell.java @@ -74,11 +74,10 @@ public class ShareDialogCell extends FrameLayout { return super.onTouchEvent(event); } - public void setDialog(TLRPC.Dialog dialog, boolean checked, CharSequence name) { - int lower_id = (int) dialog.id; + public void setDialog(int uid, boolean checked, CharSequence name) { TLRPC.FileLocation photo = null; - if (lower_id > 0) { - TLRPC.User user = MessagesController.getInstance().getUser(lower_id); + if (uid > 0) { + TLRPC.User user = MessagesController.getInstance().getUser(uid); if (name != null) { nameTextView.setText(name); } else if (user != null) { @@ -91,7 +90,7 @@ public class ShareDialogCell extends FrameLayout { photo = user.photo.photo_small; } } else { - TLRPC.Chat chat = MessagesController.getInstance().getChat(-lower_id); + TLRPC.Chat chat = MessagesController.getInstance().getChat(-uid); if (name != null) { nameTextView.setText(name); } else if (chat != 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 333a86877..9ac990c88 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedPhotoVideoCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedPhotoVideoCell.java @@ -8,21 +8,22 @@ package org.telegram.ui.Cells; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.os.Build; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.MessageObject; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLoader; @@ -30,11 +31,10 @@ import org.telegram.messenger.R; import org.telegram.tgnet.TLRPC; import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.CheckBox; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.PhotoViewer; -public class SharedPhotoVideoCell extends FrameLayoutFixed { +public class SharedPhotoVideoCell extends FrameLayout { private PhotoVideoView[] photoVideoViews; private MessageObject[] messageObjects; @@ -48,20 +48,20 @@ public class SharedPhotoVideoCell extends FrameLayoutFixed { boolean didLongClickItem(SharedPhotoVideoCell cell, int index, MessageObject messageObject, int a); } - private class PhotoVideoView extends FrameLayoutFixed { + private class PhotoVideoView extends FrameLayout { private BackupImageView imageView; private TextView videoTextView; private LinearLayout videoInfoContainer; private View selector; private CheckBox checkBox; - private FrameLayoutFixed container; - private AnimatorSetProxy animator; + private FrameLayout container; + private AnimatorSet animator; public PhotoVideoView(Context context) { super(context); - container = new FrameLayoutFixed(context); + container = new FrameLayout(context); addView(container, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); imageView = new BackupImageView(context); @@ -116,13 +116,13 @@ public class SharedPhotoVideoCell extends FrameLayoutFixed { if (checked) { setBackgroundColor(0xfff5f5f5); } - animator = new AnimatorSetProxy(); - animator.playTogether(ObjectAnimatorProxy.ofFloat(container, "scaleX", checked ? 0.85f : 1.0f), - ObjectAnimatorProxy.ofFloat(container, "scaleY", checked ? 0.85f : 1.0f)); + animator = new AnimatorSet(); + animator.playTogether(ObjectAnimator.ofFloat(container, "scaleX", checked ? 0.85f : 1.0f), + ObjectAnimator.ofFloat(container, "scaleY", checked ? 0.85f : 1.0f)); animator.setDuration(200); animator.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (animator != null && animator.equals(animation)) { animator = null; if (!checked) { @@ -132,7 +132,7 @@ public class SharedPhotoVideoCell extends FrameLayoutFixed { } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (animator != null && animator.equals(animation)) { animator = null; } @@ -141,8 +141,8 @@ public class SharedPhotoVideoCell extends FrameLayoutFixed { animator.start(); } else { setBackgroundColor(checked ? 0xfff5f5f5 : 0); - ViewProxy.setScaleX(container, checked ? 0.85f : 1.0f); - ViewProxy.setScaleY(container, checked ? 0.85f : 1.0f); + container.setScaleX(checked ? 0.85f : 1.0f); + container.setScaleY(checked ? 0.85f : 1.0f); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerCell.java index 99261561a..fa44fa0b6 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerCell.java @@ -10,19 +10,18 @@ package org.telegram.ui.Cells; import android.content.Context; import android.graphics.Canvas; -import android.os.Build; import android.view.Gravity; import android.view.View; import android.view.animation.AccelerateInterpolator; +import android.widget.FrameLayout; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.R; import org.telegram.tgnet.TLRPC; import org.telegram.ui.Components.BackupImageView; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; -public class StickerCell extends FrameLayoutFixed { +public class StickerCell extends FrameLayout { private BackupImageView imageView; private TLRPC.Document sticker; @@ -109,10 +108,8 @@ public class StickerCell extends FrameLayoutFixed { scale = 1.0f; } } - if (Build.VERSION.SDK_INT >= 11) { - imageView.setScaleX(scale); - imageView.setScaleY(scale); - } + imageView.setScaleX(scale); + imageView.setScaleY(scale); imageView.invalidate(); invalidate(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerEmojiCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerEmojiCell.java index af2825e4a..c18629711 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerEmojiCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerEmojiCell.java @@ -10,7 +10,6 @@ package org.telegram.ui.Cells; import android.content.Context; import android.graphics.Canvas; -import android.os.Build; import android.util.TypedValue; import android.view.Gravity; import android.view.View; @@ -136,10 +135,8 @@ public class StickerEmojiCell extends FrameLayout { scale = 1.0f; } } - if (Build.VERSION.SDK_INT >= 11) { - imageView.setScaleX(scale); - imageView.setScaleY(scale); - } + imageView.setScaleX(scale); + imageView.setScaleY(scale); imageView.invalidate(); invalidate(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerSetCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerSetCell.java index d8dc1d564..99ff06caa 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerSetCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerSetCell.java @@ -22,7 +22,6 @@ import android.widget.ImageView; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.messenger.LocaleController; import org.telegram.messenger.R; import org.telegram.tgnet.TLRPC; @@ -94,13 +93,13 @@ public class StickerSetCell extends FrameLayout { textView.setText(stickersSet.set.title); if (stickersSet.set.disabled) { - ViewProxy.setAlpha(textView, 0.5f); - ViewProxy.setAlpha(valueTextView, 0.5f); - ViewProxy.setAlpha(imageView, 0.5f); + textView.setAlpha(0.5f); + valueTextView.setAlpha(0.5f); + imageView.setAlpha(0.5f); } else { - ViewProxy.setAlpha(textView, 1.0f); - ViewProxy.setAlpha(valueTextView, 1.0f); - ViewProxy.setAlpha(imageView, 1.0f); + textView.setAlpha(1.0f); + valueTextView.setAlpha(1.0f); + imageView.setAlpha(1.0f); } ArrayList documents = set.documents; if (documents != null && !documents.isEmpty()) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java index 37b6f197e..8a25f0efd 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java @@ -13,13 +13,13 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.util.TypedValue; import android.view.Gravity; +import android.widget.FrameLayout; import android.widget.TextView; import org.telegram.messenger.LocaleController; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; -public class TextBlockCell extends FrameLayoutFixed { +public class TextBlockCell extends FrameLayout { private TextView textView; private static Paint paint; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java index 71af0bb96..7456eb30d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java @@ -11,19 +11,18 @@ package org.telegram.ui.Cells; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; -import android.os.Build; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; +import android.widget.FrameLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.Switch; -public class TextCheckCell extends FrameLayoutFixed { +public class TextCheckCell extends FrameLayout { private TextView textView; private TextView valueTextView; @@ -81,10 +80,6 @@ public class TextCheckCell extends FrameLayoutFixed { public void setTextAndCheck(String text, boolean checked, boolean divider) { textView.setText(text); - if (Build.VERSION.SDK_INT < 11) { - checkBox.resetLayout(); - checkBox.requestLayout(); - } isMultiline = false; checkBox.setChecked(checked); needDivider = divider; @@ -99,10 +94,6 @@ public class TextCheckCell extends FrameLayoutFixed { public void setTextAndValueAndCheck(String text, String value, boolean checked, boolean multiline, boolean divider) { textView.setText(text); valueTextView.setText(value); - if (Build.VERSION.SDK_INT < 11) { - checkBox.resetLayout(); - checkBox.requestLayout(); - } checkBox.setChecked(checked); needDivider = divider; valueTextView.setVisibility(VISIBLE); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailSettingsCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailSettingsCell.java index 2bef5ad32..f1a8bd800 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailSettingsCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailSettingsCell.java @@ -13,14 +13,14 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.util.TypedValue; import android.view.Gravity; +import android.widget.FrameLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; -public class TextDetailSettingsCell extends FrameLayoutFixed { +public class TextDetailSettingsCell extends FrameLayout { private TextView textView; private TextView valueTextView; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/UserCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/UserCell.java index 257eb9c58..1da73b4a0 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/UserCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/UserCell.java @@ -40,15 +40,15 @@ public class UserCell extends FrameLayout { private ImageView adminImage; private AvatarDrawable avatarDrawable; - private TLObject currentObject = null; + private TLObject currentObject; private CharSequence currentName; private CharSequence currrntStatus; private int currentDrawable; - private String lastName = null; - private int lastStatus = 0; - private TLRPC.FileLocation lastAvatar = null; + private String lastName; + private int lastStatus; + private TLRPC.FileLocation lastAvatar; private int statusColor = 0xffa8a8a8; private int statusOnlineColor = 0xff3b84c0; @@ -232,7 +232,7 @@ public class UserCell extends FrameLayout { } else if (currentUser != null) { if (currentUser.bot) { statusTextView.setTextColor(statusColor); - if (currentUser.bot_chat_history) { + if (currentUser.bot_chat_history || adminImage != null && adminImage.getVisibility() == VISIBLE) { //TODO fix statusTextView.setText(LocaleController.getString("BotStatusRead", R.string.BotStatusRead)); } else { statusTextView.setText(LocaleController.getString("BotStatusCantRead", R.string.BotStatusCantRead)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChangePhoneActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChangePhoneActivity.java index 4add7d3e1..4ab801a98 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChangePhoneActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChangePhoneActivity.java @@ -9,7 +9,9 @@ package org.telegram.ui; import android.Manifest; -import android.annotation.SuppressLint; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -61,10 +63,7 @@ import org.telegram.messenger.UserConfig; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; import org.telegram.ui.ActionBar.BaseFragment; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.ui.Components.HintEditText; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.SlideView; @@ -241,44 +240,34 @@ public class ChangePhoneActivity extends BaseFragment { } doneButton.setVisibility(View.VISIBLE); } - if(android.os.Build.VERSION.SDK_INT > 10) { - final SlideView outView = views[currentViewNum]; - final SlideView newView = views[page]; - currentViewNum = page; + final SlideView outView = views[currentViewNum]; + final SlideView newView = views[page]; + currentViewNum = page; - newView.setParams(params); - actionBar.setTitle(newView.getHeaderName()); - newView.onShow(); - ViewProxy.setX(newView, back ? -AndroidUtilities.displaySize.x : AndroidUtilities.displaySize.x); + newView.setParams(params); + actionBar.setTitle(newView.getHeaderName()); + newView.onShow(); + newView.setX(back ? -AndroidUtilities.displaySize.x : AndroidUtilities.displaySize.x); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); - animatorSet.setDuration(300); - animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(outView, "translationX", back ? AndroidUtilities.displaySize.x : -AndroidUtilities.displaySize.x), - ObjectAnimatorProxy.ofFloat(newView, "translationX", 0)); - animatorSet.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationStart(Object animation) { - newView.setVisibility(View.VISIBLE); - } + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); + animatorSet.setDuration(300); + animatorSet.playTogether( + ObjectAnimator.ofFloat(outView, "translationX", back ? AndroidUtilities.displaySize.x : -AndroidUtilities.displaySize.x), + ObjectAnimator.ofFloat(newView, "translationX", 0)); + animatorSet.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationStart(Animator animation) { + newView.setVisibility(View.VISIBLE); + } - @SuppressLint("NewApi") - @Override - public void onAnimationEnd(Object animation) { - outView.setVisibility(View.GONE); - outView.setX(0); - } - }); - animatorSet.start(); - } else { - views[currentViewNum].setVisibility(View.GONE); - currentViewNum = page; - views[page].setParams(params); - views[page].setVisibility(View.VISIBLE); - actionBar.setTitle(views[page].getHeaderName()); - views[page].onShow(); - } + @Override + public void onAnimationEnd(Animator animation) { + outView.setVisibility(View.GONE); + outView.setX(0); + } + }); + animatorSet.start(); } private void fillNextCodeParams(Bundle params, TLRPC.TL_auth_sentCode res) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java index 13b2b705b..938228083 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java @@ -17,7 +17,6 @@ import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; -import android.os.Build; import android.os.Bundle; import android.os.Vibrator; import android.text.Editable; @@ -75,7 +74,6 @@ import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.AvatarUpdater; import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.ChipSpan; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.LetterSectionsListView; @@ -125,7 +123,6 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC private int currentStep; private int chatId; - private boolean allowComments = false; private boolean canCreatePublic = true; private TLRPC.InputFile uploadedAvatar; @@ -292,9 +289,6 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC } } } - if (allowComments) { - MessagesController.getInstance().toogleChannelComments(chatId, allowComments); - } Bundle args = new Bundle(); args.putInt("step", 2); args.putInt("chat_id", chatId); @@ -342,7 +336,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC if (currentStep == 0) { actionBar.setTitle(LocaleController.getString("NewChannel", R.string.NewChannel)); fragmentView.setBackgroundColor(0xffffffff); - FrameLayout frameLayout = new FrameLayoutFixed(context); + FrameLayout frameLayout = new FrameLayout(context); linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); avatarImage = new BackupImageView(context); @@ -469,23 +463,6 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC helpTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); helpTextView.setText(LocaleController.getString("DescriptionInfo", R.string.DescriptionInfo)); linearLayout.addView(helpTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 24, 10, 24, 20)); - - /*helpTextView = new TextView(context); - helpTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); - helpTextView.setTextColor(0xff3d93d5); - helpTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); - helpTextView.setText(LocaleController.getString("ChannelAlertTitle", R.string.ChannelAlertTitle)); - linearLayout.addView(helpTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 24, 14, 24, 20)); - helpTextView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - builder.setTitle(LocaleController.getString("ChannelAlertTitle", R.string.ChannelAlertTitle)); - builder.setMessage(LocaleController.getString("ChannelAlertText", R.string.ChannelAlertText)); - builder.setPositiveButton(LocaleController.getString("Close", R.string.Close), null); - showDialog(builder.create()); - } - });*/ } else if (currentStep == 1) { actionBar.setTitle(LocaleController.getString("ChannelSettings", R.string.ChannelSettings)); fragmentView.setBackgroundColor(0xfff0f0f0); @@ -604,14 +581,9 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC return; } try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(invite.link); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); + clipboard.setPrimaryClip(clip); Toast.makeText(getParentActivity(), LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); } catch (Exception e) { FileLog.e("tmessages", e); @@ -626,30 +598,9 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC linkContainer.addView(checkTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 17, 3, 17, 7)); typeInfoCell = new TextInfoPrivacyCell(context); - //typeInfoCell.setBackgroundResource(R.drawable.greydivider); typeInfoCell.setBackgroundResource(R.drawable.greydivider_bottom); linearLayout.addView(typeInfoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - /*FrameLayout frameLayout = new FrameLayoutFixed(context); - frameLayout.setBackgroundColor(0xffffffff); - linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - TextCheckCell commentsCell = new TextCheckCell(context); - commentsCell.setTextAndCheck(LocaleController.getString("Comments", R.string.Comments), allowComments, false); - commentsCell.setBackgroundResource(R.drawable.list_selector); - frameLayout.addView(commentsCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - commentsCell.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - allowComments = !allowComments; - ((TextCheckCell) v).setChecked(allowComments); - } - }); - - TextInfoPrivacyCell infoCell = new TextInfoPrivacyCell(context); - infoCell.setText(LocaleController.getString("CommentsInfo", R.string.CommentsInfo)); - infoCell.setBackgroundResource(R.drawable.greydivider_bottom); - linearLayout.addView(infoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT));*/ updatePrivatePublic(); } else if (currentStep == 2) { actionBar.setTitle(LocaleController.getString("ChannelAddMembers", R.string.ChannelAddMembers)); @@ -677,9 +628,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC nameTextView.setHorizontalScrollBarEnabled(false); nameTextView.setPadding(0, 0, 0, 0); nameTextView.setHint(LocaleController.getString("AddMutual", R.string.AddMutual)); - if (Build.VERSION.SDK_INT >= 11) { - nameTextView.setTextIsSelectable(false); - } + nameTextView.setTextIsSelectable(false); nameTextView.setImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_EXTRACT_UI); nameTextView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); AndroidUtilities.clearCursorDrawable(nameTextView); @@ -739,9 +688,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC if (listView != null) { listView.setAdapter(searchListViewAdapter); searchListViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(false); - } + listView.setFastScrollAlwaysVisible(false); listView.setFastScrollEnabled(false); listView.setVerticalScrollBarEnabled(true); } @@ -755,9 +702,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC searchWas = false; listView.setAdapter(listViewAdapter); listViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); emptyTextView.setText(LocaleController.getString("NoContacts", R.string.NoContacts)); @@ -796,10 +741,8 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC listView.setFastScrollEnabled(true); listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); listView.setAdapter(listViewAdapter); - if (Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setFastScrollAlwaysVisible(true); + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); linearLayout.addView(listView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override @@ -863,9 +806,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC searchWas = false; listView.setAdapter(listViewAdapter); listViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); emptyTextView.setText(LocaleController.getString("NoContacts", R.string.NoContacts)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java index 825fb18e1..47d402c4a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java @@ -50,7 +50,6 @@ import org.telegram.ui.Cells.TextSettingsCell; import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.AvatarUpdater; import org.telegram.ui.Components.BackupImageView; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import java.util.concurrent.Semaphore; @@ -71,7 +70,6 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A private TLRPC.Chat currentChat; private TLRPC.ChatFull info; private int chatId; - private boolean allowComments = true; private TLRPC.InputFile uploadedAvatar; private boolean signMessages; @@ -124,7 +122,6 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A } avatarUpdater.parentFragment = this; avatarUpdater.delegate = this; - allowComments = !currentChat.broadcast; signMessages = currentChat.signatures; NotificationCenter.getInstance().addObserver(this, NotificationCenter.chatInfoDidLoaded); NotificationCenter.getInstance().addObserver(this, NotificationCenter.updateInterfaces); @@ -194,10 +191,6 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A progressDialog.show(); return; } - boolean currentAllowComments = !currentChat.broadcast; - if (allowComments != currentAllowComments) { - MessagesController.getInstance().toogleChannelComments(chatId, allowComments); - } if (!currentChat.title.equals(nameTextView.getText().toString())) { MessagesController.getInstance().changeChatTitle(chatId, nameTextView.getText().toString()); } @@ -239,7 +232,7 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A linearLayout2.setBackgroundColor(0xffffffff); linearLayout.addView(linearLayout2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - FrameLayout frameLayout = new FrameLayoutFixed(context); + FrameLayout frameLayout = new FrameLayout(context); linearLayout2.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); avatarImage = new BackupImageView(context); @@ -374,7 +367,7 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A linearLayout.addView(sectionCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); if (currentChat.megagroup || !currentChat.megagroup) { - frameLayout = new FrameLayoutFixed(context); + frameLayout = new FrameLayout(context); frameLayout.setBackgroundColor(0xffffffff); linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); @@ -382,13 +375,12 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A updateTypeCell(); typeCell.setBackgroundResource(R.drawable.list_selector); frameLayout.addView(typeCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - //TODO lineView = new View(context); lineView.setBackgroundColor(0xffcfcfcf); linearLayout.addView(lineView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); - frameLayout = new FrameLayoutFixed(context); + frameLayout = new FrameLayout(context); frameLayout.setBackgroundColor(0xffffffff); linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); @@ -434,7 +426,7 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A } if (currentChat.creator) { - frameLayout = new FrameLayoutFixed(context); + frameLayout = new FrameLayout(context); frameLayout.setBackgroundColor(0xffffffff); linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); @@ -485,27 +477,6 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A linearLayout.addView(infoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); } - /*frameLayout = new FrameLayoutFixed(context); - frameLayout.setBackgroundColor(0xffffffff); - linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - TextCheckCell commentsCell = new TextCheckCell(context); - commentsCell.setTextAndCheck(LocaleController.getString("Comments", R.string.Comments), allowComments, false); - commentsCell.setBackgroundResource(R.drawable.list_selector); - frameLayout.addView(commentsCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - commentsCell.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - allowComments = !allowComments; - ((TextCheckCell) v).setChecked(allowComments); - } - }); - - infoCell = new TextInfoPrivacyCell(context); - infoCell.setText(LocaleController.getString("CommentsInfo", R.string.CommentsInfo)); - infoCell.setBackgroundResource(R.drawable.greydivider); - linearLayout.addView(infoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT));*/ - nameTextView.setText(currentChat.title); nameTextView.setSelection(nameTextView.length()); if (info != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditTypeActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditTypeActivity.java index a389ec5cf..c317236ac 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditTypeActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditTypeActivity.java @@ -10,7 +10,6 @@ package org.telegram.ui; import android.app.AlertDialog; import android.content.Context; -import android.os.Build; import android.os.Bundle; import android.os.Vibrator; import android.text.Editable; @@ -298,14 +297,9 @@ public class ChannelEditTypeActivity extends BaseFragment implements Notificatio return; } try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(invite.link); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); + clipboard.setPrimaryClip(clip); Toast.makeText(getParentActivity(), LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); } catch (Exception e) { FileLog.e("tmessages", e); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java index 78c2ff5df..958a1e7ad 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java @@ -11,7 +11,6 @@ package org.telegram.ui; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.os.Build; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; @@ -143,9 +142,7 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe listView.setDividerHeight(0); listView.setDrawSelectorOnTop(true); listView.setAdapter(listViewAdapter = new ListAdapter(context)); - if (Build.VERSION.SDK_INT >= 11) { - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index cb3441442..6e5a2581f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -9,10 +9,12 @@ package org.telegram.ui; import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; -import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -30,10 +32,13 @@ import android.os.Build; import android.os.Bundle; import android.provider.ContactsContract; import android.provider.MediaStore; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.TextUtils; import android.text.style.ClickableSpan; import android.text.style.URLSpan; -import android.util.Base64; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.TypedValue; @@ -43,7 +48,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; -import android.webkit.MimeTypeMap; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; @@ -68,16 +72,18 @@ import org.telegram.messenger.Utilities; import org.telegram.messenger.VideoEditedInfo; import org.telegram.messenger.browser.Browser; import org.telegram.messenger.query.BotQuery; +import org.telegram.messenger.query.DraftQuery; import org.telegram.messenger.query.MessagesSearchQuery; import org.telegram.messenger.query.MessagesQuery; +import org.telegram.messenger.query.SearchQuery; 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.RecyclerView; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLoader; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.RequestDelegate; -import org.telegram.tgnet.SerializedData; import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; import org.telegram.messenger.ContactsController; @@ -93,13 +99,8 @@ import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.ActionBar.SimpleTextView; import org.telegram.ui.Adapters.MentionsAdapter; import org.telegram.ui.Adapters.StickersAdapter; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; -import org.telegram.ui.Cells.BotSwitchCell; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.ui.Cells.ChatActionCell; -import org.telegram.ui.Cells.ChatBaseCell; import org.telegram.ui.Cells.ChatLoadingCell; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; @@ -113,20 +114,23 @@ import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.Cells.BotHelpCell; import org.telegram.ui.Components.ChatActivityEnterView; import org.telegram.messenger.ImageReceiver; -import org.telegram.ui.Components.ChatAttachView; +import org.telegram.ui.Components.ChatAttachAlert; import org.telegram.ui.Components.ChatAvatarContainer; +import org.telegram.ui.Components.ContextProgressView; +import org.telegram.ui.Components.ExtendedGridLayoutManager; import org.telegram.ui.Components.PlayerView; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.NumberTextView; import org.telegram.ui.Components.RecyclerListView; import org.telegram.ui.Components.ShareAlert; +import org.telegram.ui.Components.Size; import org.telegram.ui.Components.SizeNotifierFrameLayout; import org.telegram.ui.Components.StickersAlert; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Components.URLSpanBotCommand; import org.telegram.ui.Components.URLSpanNoUnderline; import org.telegram.ui.Components.URLSpanReplacement; +import org.telegram.ui.Components.URLSpanUserMention; import org.telegram.ui.Components.WebFrameLayout; import java.io.File; @@ -157,8 +161,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private ActionBarMenuItem attachItem; private ActionBarMenuItem headerItem; private ActionBarMenuItem searchItem; - private ActionBarMenuItem searchUpItem; - private ActionBarMenuItem searchDownItem; + private ActionBarMenuItem editDoneItem; + private ContextProgressView editDoneItemProgress; + private AnimatorSet editDoneItemAnimation; private TextView addContactItem; private RecyclerListView chatListView; private LinearLayoutManager chatLayoutManager; @@ -171,29 +176,31 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private TextView bottomOverlayText; private TextView secretViewStatusTextView; private NumberTextView selectedMessagesCountTextView; - private TextView actionModeTextView; + private FrameLayout actionModeTitleContainer; + private SimpleTextView actionModeTextView; + private SimpleTextView actionModeSubTextView; private RecyclerListView stickersListView; private RecyclerListView.OnItemClickListener stickersOnItemClickListener; private RecyclerListView.OnItemClickListener mentionsOnItemClickListener; private StickersAdapter stickersAdapter; private FrameLayout stickersPanel; private TextView muteItem; - private ImageView pagedownButton; + private FrameLayout pagedownButton; + private boolean pagedownButtonShowedByScroll; + private TextView pagedownButtonCounter; private BackupImageView replyImageView; - private TextView replyNameTextView; - private TextView replyObjectTextView; + private SimpleTextView replyNameTextView; + private SimpleTextView replyObjectTextView; private ImageView replyIconImageView; private MentionsAdapter mentionsAdapter; - private BotSwitchCell botSwitchCell; - private View botSwitchShadow; private FrameLayout mentionContainer; private RecyclerListView mentionListView; private LinearLayoutManager mentionLayoutManager; - private AnimatorSetProxy mentionListAnimation; - private ChatAttachView chatAttachView; - private BottomSheet chatAttachViewSheet; + private ExtendedGridLayoutManager mentionGridLayoutManager; + private AnimatorSet mentionListAnimation; + private ChatAttachAlert chatAttachAlert; private LinearLayout reportSpamView; - private AnimatorSetProxy reportSpamViewAnimator; + private AnimatorSet reportSpamViewAnimator; private TextView addToContactsButton; private TextView reportSpamButton; private FrameLayout reportSpamContainer; @@ -201,14 +208,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private TextView gifHintTextView; private View emojiButtonRed; private FrameLayout pinnedMessageView; - private AnimatorSetProxy pinnedMessageViewAnimator; + private AnimatorSet pinnedMessageViewAnimator; private SimpleTextView pinnedMessageNameTextView; private SimpleTextView pinnedMessageTextView; private FrameLayout alertView; private Runnable hideAlertViewRunnable; private TextView alertNameTextView; private TextView alertTextView; - private AnimatorSetProxy alertViewAnimator; + private AnimatorSet alertViewAnimator; + private FrameLayout searchContainer; + private ImageView searchUpButton; + private ImageView searchDownButton; + private SimpleTextView searchCountText; private boolean mentionListViewIgnoreLayout; private int mentionListViewScrollOffsetY; @@ -219,25 +230,23 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private MessageObject pinnedMessageObject; private int loadingPinnedMessage; - private ObjectAnimatorProxy pagedownButtonAnimation; - private AnimatorSetProxy replyButtonAnimation; + private ObjectAnimator pagedownButtonAnimation; + private AnimatorSet replyButtonAnimation; private boolean openSearchKeyboard; - private int channelMessagesImportant; - private boolean waitingForImportantLoad; - private boolean waitingForReplyMessageLoad; private boolean allowStickersPanel; private boolean allowContextBotPanel; private boolean allowContextBotPanelSecond = true; - private AnimatorSetProxy runningAnimation; + private AnimatorSet runningAnimation; private MessageObject selectedObject; private ArrayList forwardingMessages; private MessageObject forwaringMessage; private MessageObject replyingMessageObject; + private int editingMessageObjectReqId; private boolean paused = true; private boolean wasPaused = false; private boolean readWhenResume = false; @@ -254,6 +263,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private int readWithDate; private int readWithMid; private boolean scrollToTopOnResume; + private boolean forceScrollToTop; private boolean scrollToTopUnReadOnResume; private long dialog_id; private int lastLoadIndex; @@ -263,6 +273,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private int cantDeleteMessagesCount; private ArrayList waitingForLoad = new ArrayList<>(); + private int newUnreadMessageCount; + private HashMap[] messagesDict = new HashMap[]{new HashMap<>(), new HashMap<>()}; private HashMap> messagesByDays = new HashMap<>(); protected ArrayList messages = new ArrayList<>(); @@ -295,8 +307,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private String currentPicturePath; - private Rect scrollRect = new Rect(); - protected TLRPC.ChatFull info = null; private HashMap botInfo = new HashMap<>(); @@ -340,10 +350,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private final static int attach_location = 6; private final static int search = 40; - private final static int search_up = 41; - private final static int search_down = 42; - - private final static int open_channel_profile = 50; private final static int id_chat_compose_panel = 1000; @@ -413,12 +419,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not dialog_id = AndroidUtilities.makeBroadcastId(chatId); } if (ChatObject.isChannel(currentChat)) { - if (!currentChat.megagroup) { - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - channelMessagesImportant = preferences.getInt("important_" + dialog_id, 2); - } else { - channelMessagesImportant = 1; - } MessagesController.getInstance().startShortPoll(chatId, false); } } else if (userId != 0) { @@ -536,6 +536,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.getInstance().addObserver(this, NotificationCenter.chatInfoCantLoad); NotificationCenter.getInstance().addObserver(this, NotificationCenter.didLoadedPinnedMessage); NotificationCenter.getInstance().addObserver(this, NotificationCenter.peerSettingsDidLoaded); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.newDraftReceived); super.onFragmentCreate(); @@ -551,13 +552,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not waitingForLoad.add(lastLoadIndex); if (migrated_to != 0) { mergeDialogId = migrated_to; - MessagesController.getInstance().loadMessages(mergeDialogId, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, 0, lastLoadIndex++); + MessagesController.getInstance().loadMessages(mergeDialogId, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } else { - MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } } else { waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, 0, true, 0, classGuid, 2, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, 0, true, 0, classGuid, 2, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } if (currentChat != null) { @@ -575,17 +576,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } - URLSpanBotCommand.enabled = false; if (userId != 0 && currentUser.bot) { BotQuery.loadBotInfo(userId, true, classGuid); - URLSpanBotCommand.enabled = true; } else if (info instanceof TLRPC.TL_chatFull) { for (int a = 0; a < info.participants.participants.size(); a++) { TLRPC.ChatParticipant participant = info.participants.participants.get(a); TLRPC.User user = MessagesController.getInstance().getUser(participant.user_id); if (user != null && user.bot) { BotQuery.loadBotInfo(user.id, true, classGuid); - URLSpanBotCommand.enabled = true; } } } @@ -652,6 +650,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.getInstance().removeObserver(this, NotificationCenter.chatInfoCantLoad); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didLoadedPinnedMessage); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.peerSettingsDidLoaded); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.newDraftReceived); if (AndroidUtilities.isTablet()) { NotificationCenter.getInstance().postNotificationName(NotificationCenter.openedChatChanged, dialog_id, true); @@ -659,7 +658,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (currentEncryptedChat != null) { MediaController.getInstance().stopMediaObserver(); try { - if (Build.VERSION.SDK_INT >= 14) { + if (Build.VERSION.SDK_INT >= 23) { getParentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); } } catch (Throwable e) { @@ -673,8 +672,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (stickersAdapter != null) { stickersAdapter.onDestroy(); } - if (chatAttachView != null) { - chatAttachView.onDestroy(); + if (chatAttachAlert != null) { + chatAttachAlert.onDestroy(); } AndroidUtilities.unlockOrientation(getParentActivity()); /*MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); @@ -701,13 +700,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not cantDeleteMessagesCount = 0; hasOwnBackground = true; - if (chatAttachView != null){ - chatAttachView.onDestroy(); - chatAttachView = null; + if (chatAttachAlert != null){ + chatAttachAlert.onDestroy(); + chatAttachAlert = null; } - chatAttachViewSheet = null; Theme.loadRecources(context); + Theme.loadChatResources(context); actionBar.setBackButtonDrawable(new BackDrawable(false)); actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { @@ -720,15 +719,19 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not selectedMessagesCanCopyIds[a].clear(); } cantDeleteMessagesCount = 0; - chatActivityEnterView.setEditinigMessageObject(null, false); - actionBar.hideActionMode(); - updatePinnedMessageView(true); + if (chatActivityEnterView.isEditingMessage()) { + chatActivityEnterView.setEditingMessageObject(null, false); + } else { + actionBar.hideActionMode(); + updatePinnedMessageView(true); + } updateVisibleRows(); } else { finishFragment(); } } else if (id == copy) { String str = ""; + int previousUid = 0; for (int a = 1; a >= 0; a--) { ArrayList ids = new ArrayList<>(selectedMessagesCanCopyIds[a].keySet()); if (currentEncryptedChat == null) { @@ -740,30 +743,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not Integer messageId = ids.get(b); MessageObject messageObject = selectedMessagesCanCopyIds[a].get(messageId); if (str.length() != 0) { - str += "\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; + str += "\n\n"; } + str += getMessageContent(messageObject, previousUid, true); + previousUid = messageObject.messageOwner.from_id; } } if (str.length() != 0) { - try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(str); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", str); - clipboard.setPrimaryClip(clip); - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } + AndroidUtilities.addToClipboard(str); } for (int a = 1; a >= 0; a--) { selectedMessagesIds[a].clear(); @@ -776,8 +763,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else if (id == edit_done) { if (chatActivityEnterView != null && (chatActivityEnterView.isEditingCaption() || chatActivityEnterView.hasText())) { chatActivityEnterView.doneEditingMessage(); - actionBar.hideActionMode(); - updatePinnedMessageView(true); } } else if (id == delete) { if (getParentActivity() == null) { @@ -872,43 +857,24 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } createChatAttachView(); - chatAttachView.loadGalleryPhotos(); + chatAttachAlert.loadGalleryPhotos(); if (Build.VERSION.SDK_INT == 21 || Build.VERSION.SDK_INT == 22) { chatActivityEnterView.closeKeyboard(); } - - chatAttachView.init(ChatActivity.this); - showDialog(chatAttachViewSheet); + chatAttachAlert.init(ChatActivity.this); + showDialog(chatAttachAlert); } else if (id == bot_help) { - SendMessagesHelper.getInstance().sendMessage("/help", dialog_id, null, null, false, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage("/help", dialog_id, null, null, false, null, null, null); } else if (id == bot_settings) { - SendMessagesHelper.getInstance().sendMessage("/settings", dialog_id, null, null, false, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage("/settings", dialog_id, null, null, false, null, null, null); } else if (id == search) { openSearchWithText(null); - } else if (id == search_up) { - MessagesSearchQuery.searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 1); - } else if (id == search_down) { - MessagesSearchQuery.searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 2); - } else if (id == open_channel_profile) { - Bundle args = new Bundle(); - args.putInt("chat_id", currentChat.id); - ProfileActivity fragment = new ProfileActivity(args); - fragment.setChatInfo(info); - fragment.setPlayProfileAnimation(true); - presentFragment(fragment); } } }); - avatarContainer = new ChatAvatarContainer(context, this, ChatObject.isChannel(currentChat) && !currentChat.megagroup && !(currentChat instanceof TLRPC.TL_channelForbidden), currentEncryptedChat != null); - avatarContainer.setRadioChecked(channelMessagesImportant == 1, false); + avatarContainer = new ChatAvatarContainer(context, this, currentEncryptedChat != null); actionBar.addView(avatarContainer, 0, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 56, 0, 40, 0)); - avatarContainer.setDelegate(new ChatAvatarContainer.ChatAvatarContainerDelegate() { - @Override - public void didPressedRadioButton() { - switchImportantMode(null); - } - }); if (currentChat != null) { if (!ChatObject.isChannel(currentChat)) { @@ -925,7 +891,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not ActionBarMenu menu = actionBar.createMenu(); if (currentEncryptedChat == null && !isBroadcast) { - searchItem = menu.addItem(0, R.drawable.ic_ab_search).setIsSearchField(true, false).setActionBarMenuItemSearchListener(new ActionBarMenuItem.ActionBarMenuItemSearchListener() { + searchItem = menu.addItem(0, R.drawable.ic_ab_search).setIsSearchField(true).setActionBarMenuItemSearchListener(new ActionBarMenuItem.ActionBarMenuItemSearchListener() { @Override public void onSearchCollapse() { @@ -946,14 +912,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } searchItem.setVisibility(View.GONE); - //chatActivityEnterView.setVisibility(View.VISIBLE); - searchUpItem.clearAnimation(); - searchDownItem.clearAnimation(); - searchUpItem.setVisibility(View.GONE); - searchDownItem.setVisibility(View.GONE); highlightMessageId = Integer.MAX_VALUE; updateVisibleRows(); scrollToLastMessage(false); + updateBottomOverlay(); } @Override @@ -972,23 +934,15 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void onSearchPressed(EditText editText) { - updateSearchButtons(0); + updateSearchButtons(0, 0, 0); MessagesSearchQuery.searchMessagesInChat(editText.getText().toString(), dialog_id, mergeDialogId, classGuid, 0); } }); searchItem.getSearchField().setHint(LocaleController.getString("Search", R.string.Search)); searchItem.setVisibility(View.GONE); - - searchUpItem = menu.addItem(search_up, R.drawable.search_up); - searchUpItem.setVisibility(View.GONE); - searchDownItem = menu.addItem(search_down, R.drawable.search_down); - searchDownItem.setVisibility(View.GONE); } headerItem = menu.addItem(0, R.drawable.ic_ab_other); - if (channelMessagesImportant != 0 && !currentChat.megagroup) { - headerItem.addSubItem(open_channel_profile, LocaleController.getString("OpenChannelProfile", R.string.OpenChannelProfile), 0); - } if (searchItem != null) { headerItem.addSubItem(search, LocaleController.getString("Search", R.string.Search), 0); } @@ -1001,7 +955,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (currentEncryptedChat != null) { timeItem2 = headerItem.addSubItem(chat_enc_timer, LocaleController.getString("SetTimer", R.string.SetTimer), 0); } - if (channelMessagesImportant == 0) { + if (!ChatObject.isChannel(currentChat)) { headerItem.addSubItem(clear_history, LocaleController.getString("ClearHistory", R.string.ClearHistory), 0); if (currentChat != null && !isBroadcast) { headerItem.addSubItem(delete_chat, LocaleController.getString("DeleteAndExit", R.string.DeleteAndExit), 0); @@ -1042,20 +996,62 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }); - actionModeTextView = new TextView(actionMode.getContext()); - actionModeTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); - actionModeTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - actionModeTextView.setTextColor(Theme.ACTION_BAR_ACTION_MODE_TEXT_COLOR); - actionModeTextView.setVisibility(View.GONE); - actionModeTextView.setGravity(Gravity.CENTER_VERTICAL); - actionModeTextView.setText(LocaleController.getString("Edit", R.string.Edit)); - actionMode.addView(actionModeTextView, LayoutHelper.createLinear(0, LayoutHelper.MATCH_PARENT, 1.0f, 65, 0, 0, 0)); - actionModeTextView.setOnTouchListener(new View.OnTouchListener() { + actionModeTitleContainer = new FrameLayout(context) { + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + + setMeasuredDimension(width, height); + + actionModeTextView.setTextSize(!AndroidUtilities.isTablet() && getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? 18 : 20); + actionModeTextView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(24), MeasureSpec.AT_MOST)); + + if (actionModeSubTextView.getVisibility() != GONE) { + actionModeSubTextView.setTextSize(!AndroidUtilities.isTablet() && getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? 14 : 16); + actionModeSubTextView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(20), MeasureSpec.AT_MOST)); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int height = bottom - top; + + int textTop; + if (actionModeSubTextView.getVisibility() != GONE) { + textTop = (height / 2 - actionModeTextView.getTextHeight()) / 2 + AndroidUtilities.dp(!AndroidUtilities.isTablet() && getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? 2 : 3); + } else { + textTop = (height - actionModeTextView.getTextHeight()) / 2; + } + actionModeTextView.layout(0, textTop, actionModeTextView.getMeasuredWidth(), textTop + actionModeTextView.getTextHeight()); + + if (actionModeSubTextView.getVisibility() != GONE) { + textTop = height / 2 + (height / 2 - actionModeSubTextView.getTextHeight()) / 2 - AndroidUtilities.dp(!AndroidUtilities.isTablet() && getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? 1 : 1); + actionModeSubTextView.layout(0, textTop, actionModeSubTextView.getMeasuredWidth(), textTop + actionModeSubTextView.getTextHeight()); + } + } + }; + actionMode.addView(actionModeTitleContainer, LayoutHelper.createLinear(0, LayoutHelper.MATCH_PARENT, 1.0f, 65, 0, 0, 0)); + actionModeTitleContainer.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return true; } }); + actionModeTitleContainer.setVisibility(View.GONE); + + actionModeTextView = new SimpleTextView(context); + actionModeTextView.setTextSize(18); + actionModeTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + actionModeTextView.setTextColor(Theme.ACTION_BAR_ACTION_MODE_TEXT_COLOR); + actionModeTextView.setText(LocaleController.getString("Edit", R.string.Edit)); + actionModeTitleContainer.addView(actionModeTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + actionModeSubTextView = new SimpleTextView(context); + actionModeSubTextView.setGravity(Gravity.LEFT); + actionModeSubTextView.setTextColor(Theme.ACTION_BAR_ACTION_MODE_TEXT_COLOR); + actionModeTitleContainer.addView(actionModeSubTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); if (currentEncryptedChat == null) { if (!isBroadcast) { @@ -1064,8 +1060,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not actionModeViews.add(actionMode.addItem(copy, R.drawable.ic_ab_fwd_copy, Theme.ACTION_BAR_MODE_SELECTOR_COLOR, null, AndroidUtilities.dp(54))); actionModeViews.add(actionMode.addItem(forward, R.drawable.ic_ab_fwd_forward, Theme.ACTION_BAR_MODE_SELECTOR_COLOR, null, AndroidUtilities.dp(54))); actionModeViews.add(actionMode.addItem(delete, R.drawable.ic_ab_fwd_delete, Theme.ACTION_BAR_MODE_SELECTOR_COLOR, null, AndroidUtilities.dp(54))); - actionModeViews.add(actionMode.addItem(edit_done, R.drawable.check_blue, Theme.ACTION_BAR_MODE_SELECTOR_COLOR, null, AndroidUtilities.dp(54))); - actionMode.getItem(edit_done).setVisibility(View.GONE); + actionModeViews.add(editDoneItem = actionMode.addItem(edit_done, R.drawable.check_blue, Theme.ACTION_BAR_MODE_SELECTOR_COLOR, null, AndroidUtilities.dp(54))); + editDoneItem.setVisibility(View.GONE); + editDoneItemProgress = new ContextProgressView(context); + editDoneItem.addView(editDoneItemProgress, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + editDoneItemProgress.setVisibility(View.INVISIBLE); } else { actionModeViews.add(actionMode.addItem(reply, R.drawable.ic_ab_reply, Theme.ACTION_BAR_MODE_SELECTOR_COLOR, null, AndroidUtilities.dp(54))); actionModeViews.add(actionMode.addItem(copy, R.drawable.ic_ab_fwd_copy, Theme.ACTION_BAR_MODE_SELECTOR_COLOR, null, AndroidUtilities.dp(54))); @@ -1103,67 +1102,55 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (child == null || child.getVisibility() == GONE || child == chatActivityEnterView) { continue; } - try { - if (child == chatListView || child == progressView) { - int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); - int contentHeightSpec = MeasureSpec.makeMeasureSpec(Math.max(AndroidUtilities.dp(10), heightSize - inputFieldHeight + AndroidUtilities.dp(2)), MeasureSpec.EXACTLY); - child.measure(contentWidthSpec, contentHeightSpec); - } else if (child == emptyViewContainer) { - int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); - int contentHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); - child.measure(contentWidthSpec, contentHeightSpec); - } else if (chatActivityEnterView.isPopupView(child)) { - child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, MeasureSpec.EXACTLY)); - } else if (child == mentionContainer) { - int orientation = mentionsAdapter.getOrientation(); - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mentionContainer.getLayoutParams(); - int height; - mentionListViewIgnoreLayout = true; - if (mentionsAdapter.isBotContext()) { - if (orientation == LinearLayoutManager.HORIZONTAL) { - height = AndroidUtilities.dp(90); - if (botSwitchCell.getVisibility() == VISIBLE) { - height += AndroidUtilities.dp(36); - mentionListView.setPadding(0, AndroidUtilities.dp(2 + 36), AndroidUtilities.dp(5), 0); - } else { - mentionListView.setPadding(0, AndroidUtilities.dp(2), AndroidUtilities.dp(5), 0); - } - } else { - int size = mentionsAdapter.getItemCount(); - int maxHeight = 0; - if (mentionsAdapter.getBotContextSwitch() != null) { - maxHeight += 36; - size -= 1; - } - maxHeight += size * 68; + if (child == chatListView || child == progressView) { + int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + int contentHeightSpec = MeasureSpec.makeMeasureSpec(Math.max(AndroidUtilities.dp(10), heightSize - inputFieldHeight + AndroidUtilities.dp(2 + (chatActivityEnterView.isTopViewVisible() ? 48 : 0))), MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); + } else if (child == emptyViewContainer) { + int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + int contentHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); + } else if (chatActivityEnterView.isPopupView(child)) { + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, MeasureSpec.EXACTLY)); + } else if (child == mentionContainer) { + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mentionContainer.getLayoutParams(); + int height; + mentionListViewIgnoreLayout = true; - 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.isBotContext() && mentionsAdapter.isMediaLayout()) { + int size = mentionGridLayoutManager.getRowsCount(widthSize); + int maxHeight = size * 102; + if (mentionsAdapter.isBotContext()) { + if (mentionsAdapter.getBotContextSwitch() != null) { + maxHeight += 34; } - layoutParams.height = height; - layoutParams.topMargin = 0; - } else { - if (orientation == LinearLayoutManager.HORIZONTAL) { - mentionListView.setPadding(0, AndroidUtilities.dp(2), AndroidUtilities.dp(5), 0); - height = 90; - } else { - mentionListView.setPadding(0, AndroidUtilities.dp(2), 0, 0); - if (mentionsAdapter.isBotContext()) { - height = 36 * 3 + 18; - } else { - height = 36 * Math.min(3, mentionsAdapter.getItemCount()) + (mentionsAdapter.getItemCount() > 3 ? 18 : 0); - } - } - layoutParams.height = AndroidUtilities.dp(height + (height != 0 ? 2 : 0)); - layoutParams.topMargin = -AndroidUtilities.dp(height); } - mentionListViewIgnoreLayout = false; - child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY)); + height = heightSize - chatActivityEnterView.getMeasuredHeight() + (maxHeight != 0 ? AndroidUtilities.dp(2) : 0); + mentionListView.setPadding(0, Math.max(0, height - AndroidUtilities.dp(Math.min(maxHeight, 68 * 1.8f))), 0, 0); } else { - measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + int size = mentionsAdapter.getItemCount(); + int maxHeight = 0; + if (mentionsAdapter.isBotContext()) { + if (mentionsAdapter.getBotContextSwitch() != null) { + maxHeight += 36; + size -= 1; + } + maxHeight += size * 68; + } else { + maxHeight += size * 36; + } + height = heightSize - chatActivityEnterView.getMeasuredHeight() + (maxHeight != 0 ? AndroidUtilities.dp(2) : 0); + mentionListView.setPadding(0, Math.max(0, height - AndroidUtilities.dp(Math.min(maxHeight, 68 * 1.8f))), 0, 0); } - } catch (Exception e) { - FileLog.e("tmessages", e); + + layoutParams.height = height; + layoutParams.topMargin = 0; + + mentionListViewIgnoreLayout = false; + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY)); + } else { + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); } } } @@ -1232,6 +1219,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not childTop = chatActivityEnterView.getBottom(); } else if (child == gifHintTextView) { childTop -= inputFieldHeight; + } else if (child == chatListView || child == progressView) { + if (chatActivityEnterView.isTopViewVisible()) { + childTop -= AndroidUtilities.dp(48); + } } child.layout(childLeft, childTop, childLeft + width, childTop + height); } @@ -1348,11 +1339,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); - if (chatAdapter.isBot/* || ChatObject.isChannel(currentChat) && currentChat.megagroup*/) { + forceScrollToTop = false; + if (chatAdapter.isBot) { int childCount = getChildCount(); for (int a = 0; a < childCount; a++) { View child = getChildAt(a); - if (child instanceof BotHelpCell/* || child instanceof ChatMigrateCell*/) { + if (child instanceof BotHelpCell) { int height = b - t; int top = height / 2 - child.getMeasuredHeight() / 2; if (child.getTop() > top) { @@ -1364,6 +1356,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } }; + chatListView.setTag(1); chatListView.setVerticalScrollBarEnabled(true); chatListView.setAdapter(chatAdapter = new ChatActivityAdapter(context)); chatListView.setClipToPadding(false); @@ -1384,6 +1377,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatListView.setOnItemClickListener(onItemClickListener); chatListView.setOnScrollListener(new RecyclerView.OnScrollListener() { + private float totalDy = 0; + private final int scrollValue = AndroidUtilities.dp(100); + @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_DRAGGING && highlightMessageId != Integer.MAX_VALUE) { @@ -1401,6 +1397,25 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int totalItemCount = chatAdapter.getItemCount(); if (firstVisibleItem + visibleItemCount == totalItemCount && forwardEndReached[0]) { showPagedownButton(false, true); + } else { + if (dy > 0) { + if (pagedownButton.getTag() == null) { + totalDy += dy; + if (totalDy > scrollValue) { + totalDy = 0; + showPagedownButton(true, true); + pagedownButtonShowedByScroll = true; + } + } + } else { + if (pagedownButtonShowedByScroll && pagedownButton.getTag() != null) { + totalDy += dy; + if (totalDy < -scrollValue) { + showPagedownButton(false, true); + totalDy = 0; + } + } + } } } updateMessagesVisisblePart(); @@ -1537,10 +1552,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not progressView.addView(progressBar, LayoutHelper.createFrame(32, 32, Gravity.CENTER)); if (ChatObject.isChannel(currentChat)) { - pinnedMessageView = new FrameLayoutFixed(context); + pinnedMessageView = new FrameLayout(context); pinnedMessageView.setTag(1); - ViewProxy.setTranslationY(pinnedMessageView, -AndroidUtilities.dp(50)); - pinnedMessageView.clearAnimation(); + pinnedMessageView.setTranslationY(-AndroidUtilities.dp(50)); pinnedMessageView.setVisibility(View.GONE); pinnedMessageView.setBackgroundResource(R.drawable.blockpanel); contentView.addView(pinnedMessageView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 50, Gravity.TOP | Gravity.LEFT)); @@ -1599,8 +1613,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not reportSpamView = new LinearLayout(context); reportSpamView.setTag(1); - ViewProxy.setTranslationY(reportSpamView, -AndroidUtilities.dp(50)); - reportSpamView.clearAnimation(); + reportSpamView.setTranslationY(-AndroidUtilities.dp(50)); reportSpamView.setVisibility(View.GONE); reportSpamView.setBackgroundResource(R.drawable.blockpanel); contentView.addView(reportSpamView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 50, Gravity.TOP | Gravity.LEFT)); @@ -1695,10 +1708,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }); - alertView = new FrameLayoutFixed(context); + alertView = new FrameLayout(context); alertView.setTag(1); - ViewProxy.setTranslationY(alertView, -AndroidUtilities.dp(50)); - alertView.clearAnimation(); + alertView.setTranslationY(-AndroidUtilities.dp(50)); alertView.setVisibility(View.GONE); alertView.setBackgroundResource(R.drawable.blockpanel); contentView.addView(alertView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 50, Gravity.TOP | Gravity.LEFT)); @@ -1721,16 +1733,19 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not alertView.addView(alertTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 8, 23, 8, 0)); if (!isBroadcast) { - mentionContainer = new FrameLayoutFixed(context) { + mentionContainer = new FrameLayout(context) { private Drawable background; @Override public void onDraw(Canvas canvas) { - if (mentionsAdapter.isBotContext() && mentionsAdapter.getOrientation() == LinearLayoutManager.VERTICAL) { - background.setBounds(0, mentionListViewScrollOffsetY - AndroidUtilities.dp(2), getMeasuredWidth(), getMeasuredHeight()); + if (mentionListView.getChildCount() <= 0) { + return; + } + if (mentionsAdapter.isBotContext() && mentionsAdapter.isMediaLayout() && mentionsAdapter.getBotContextSwitch() == null) { + background.setBounds(0, mentionListViewScrollOffsetY - AndroidUtilities.dp(4), getMeasuredWidth(), getMeasuredHeight()); } else { - background.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight()); + background.setBounds(0, mentionListViewScrollOffsetY - AndroidUtilities.dp(2), getMeasuredWidth(), getMeasuredHeight()); } background.draw(canvas); } @@ -1760,7 +1775,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public boolean onInterceptTouchEvent(MotionEvent event) { - if (!mentionListViewIsScrolling && mentionListViewScrollOffsetY != 0 && event.getY() < mentionListViewScrollOffsetY && mentionsAdapter.isBotContext() && mentionsAdapter.getOrientation() == LinearLayoutManager.VERTICAL) { + if (!mentionListViewIsScrolling && mentionListViewScrollOffsetY != 0 && event.getY() < mentionListViewScrollOffsetY) { return false; } boolean result = StickerPreviewViewer.getInstance().onInterceptTouchEvent(event, mentionListView, 0); @@ -1769,7 +1784,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public boolean onTouchEvent(MotionEvent event) { - if (!mentionListViewIsScrolling && mentionListViewScrollOffsetY != 0 && event.getY() < mentionListViewScrollOffsetY && mentionsAdapter.isBotContext() && mentionsAdapter.getOrientation() == LinearLayoutManager.VERTICAL) { + if (!mentionListViewIsScrolling && mentionListViewScrollOffsetY != 0 && event.getY() < mentionListViewScrollOffsetY) { return false; } //supress warning @@ -1800,7 +1815,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (newPosition != -1) { mentionListViewIgnoreLayout = true; - mentionLayoutManager.scrollToPositionWithOffset(newPosition, newTop); + if (mentionsAdapter.isBotContext() && mentionsAdapter.isMediaLayout()) { + mentionGridLayoutManager.scrollToPositionWithOffset(newPosition, newTop); + } else { + mentionLayoutManager.scrollToPositionWithOffset(newPosition, newTop); + } super.onLayout(false, l, t, r, b); mentionListViewIgnoreLayout = false; } @@ -1816,15 +1835,94 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return StickerPreviewViewer.getInstance().onTouch(event, mentionListView, 0, mentionsOnItemClickListener); } }); + mentionListView.setTag(2); mentionLayoutManager = new LinearLayoutManager(context) { @Override public boolean supportsPredictiveItemAnimations() { return false; } }; + mentionLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); + mentionGridLayoutManager = new ExtendedGridLayoutManager(context, 100) { + + private Size size = new Size(); + + @Override + protected Size getSizeForItem(int i) { + if (mentionsAdapter.getBotContextSwitch() != null) { + i++; + } + Object object = mentionsAdapter.getItem(i); + if (object instanceof TLRPC.BotInlineResult) { + TLRPC.BotInlineResult inlineResult = (TLRPC.BotInlineResult) object; + if (inlineResult.document != null) { + size.width = inlineResult.document.thumb != null ? inlineResult.document.thumb.w : 100; + size.height = inlineResult.document.thumb != null ? inlineResult.document.thumb.h : 100; + for (int b = 0; b < inlineResult.document.attributes.size(); b++) { + TLRPC.DocumentAttribute attribute = inlineResult.document.attributes.get(b); + if (attribute instanceof TLRPC.TL_documentAttributeImageSize || attribute instanceof TLRPC.TL_documentAttributeVideo) { + size.width = attribute.w; + size.height = attribute.h; + break; + } + } + } else { + size.width = inlineResult.w; + size.height = inlineResult.h; + } + } + return size; + } + + @Override + protected int getFlowItemCount() { + if (mentionsAdapter.getBotContextSwitch() != null) { + return getItemCount() - 1; + } + return super.getFlowItemCount(); + } + }; + mentionGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + Object object = mentionsAdapter.getItem(position); + if (object instanceof TLRPC.TL_inlineBotSwitchPM) { + return 100; + } else { + if (mentionsAdapter.getBotContextSwitch() != null) { + position--; + } + return mentionGridLayoutManager.getSpanSizeForItem(position); + } + } + }); + mentionListView.addItemDecoration(new RecyclerView.ItemDecoration() { + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + outRect.left = 0; + outRect.right = 0; + outRect.top = 0; + outRect.bottom = 0; + if (parent.getLayoutManager() == mentionGridLayoutManager) { + int position = parent.getChildAdapterPosition(view); + if (mentionsAdapter.getBotContextSwitch() != null) { + if (position == 0) { + return; + } + position--; + if (!mentionGridLayoutManager.isFirstRow(position)) { + outRect.top = AndroidUtilities.dp(2); + } + } else { + outRect.top = AndroidUtilities.dp(2); + } + outRect.right = mentionGridLayoutManager.isLastInRow(position) ? 0 : AndroidUtilities.dp(2); + } + } + }); mentionListView.setItemAnimator(null); mentionListView.setLayoutAnimation(null); - mentionLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); + mentionListView.setClipToPadding(false); mentionListView.setLayoutManager(mentionLayoutManager); mentionListView.setOverScrollMode(ListView.OVER_SCROLL_NEVER); mentionContainer.addView(mentionListView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); @@ -1832,37 +1930,23 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not mentionListView.setAdapter(mentionsAdapter = new MentionsAdapter(context, false, dialog_id, new MentionsAdapter.MentionsAdapterDelegate() { @Override public void needChangePanelVisibility(boolean show) { + if (mentionsAdapter.isBotContext() && mentionsAdapter.isMediaLayout()) { + mentionListView.setLayoutManager(mentionGridLayoutManager); + } else { + mentionListView.setLayoutManager(mentionLayoutManager); + } if (show) { - int orientation = mentionsAdapter.getOrientation(); - if (orientation == LinearLayoutManager.HORIZONTAL) { - mentionListView.setClipToPadding(false); - mentionListView.setDisallowInterceptTouchEvents(true); - } else { - mentionListView.setClipToPadding(!mentionsAdapter.isBotContext()); - mentionListView.setDisallowInterceptTouchEvents(false); - } - if (mentionsAdapter.isBotContext() && orientation == LinearLayoutManager.HORIZONTAL && mentionsAdapter.getBotContextSwitch() != null) { - botSwitchShadow.setVisibility(View.VISIBLE); - botSwitchCell.setVisibility(View.VISIBLE); - botSwitchCell.setText(mentionsAdapter.getBotContextSwitch().text); - } else { - botSwitchShadow.setVisibility(View.GONE); - botSwitchCell.setVisibility(View.GONE); - } - if (!mentionsAdapter.isBotContext() || orientation == LinearLayoutManager.HORIZONTAL) { - mentionListViewScrollOffsetY = 0; - mentionListViewLastViewPosition = -1; - } - mentionLayoutManager.setOrientation(orientation); - if (mentionListAnimation != null) { mentionListAnimation.cancel(); mentionListAnimation = null; } if (mentionContainer.getVisibility() == View.VISIBLE) { - ViewProxy.setAlpha(mentionContainer, 1.0f); + mentionContainer.setAlpha(1.0f); return; + } + if (mentionsAdapter.isBotContext() && mentionsAdapter.isMediaLayout()) { + mentionGridLayoutManager.scrollToPositionWithOffset(0, 10000); } else { mentionLayoutManager.scrollToPositionWithOffset(0, 10000); } @@ -1880,21 +1964,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } mentionContainer.setVisibility(View.VISIBLE); mentionContainer.setTag(null); - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionContainer, "alpha", 0.0f, 1.0f) + ObjectAnimator.ofFloat(mentionContainer, "alpha", 0.0f, 1.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionContainer.clearAnimation(); mentionListAnimation = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { mentionListAnimation = null; } @@ -1903,8 +1986,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not mentionListAnimation.setDuration(200); mentionListAnimation.start(); } else { - ViewProxy.setAlpha(mentionContainer, 1.0f); - mentionContainer.clearAnimation(); + mentionContainer.setAlpha(1.0f); mentionContainer.setVisibility(View.INVISIBLE); } } else { @@ -1917,15 +1999,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } if (allowStickersPanel) { - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionContainer, "alpha", 0.0f) + ObjectAnimator.ofFloat(mentionContainer, "alpha", 0.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionContainer.clearAnimation(); mentionContainer.setVisibility(View.GONE); mentionContainer.setTag(null); mentionListAnimation = null; @@ -1933,7 +2014,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { mentionListAnimation = null; } @@ -1943,7 +2024,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not mentionListAnimation.start(); } else { mentionContainer.setTag(null); - mentionContainer.clearAnimation(); mentionContainer.setVisibility(View.GONE); } } @@ -1989,30 +2069,40 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (object instanceof TLRPC.User) { TLRPC.User user = (TLRPC.User) object; if (user != null) { - chatActivityEnterView.replaceWithText(start, len, "@" + user.username + " "); + if (user.username != null) { + chatActivityEnterView.replaceWithText(start, len, "@" + user.username + " "); + } else { + String name = user.first_name; + if (name == null || name.length() == 0) { + name = user.last_name; + } + Spannable spannable = new SpannableString(name + " "); + spannable.setSpan(new URLSpanUserMention("" + user.id), 0, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + chatActivityEnterView.replaceWithText(start, len, spannable); + } } } else if (object instanceof String) { if (mentionsAdapter.isBotCommands()) { - SendMessagesHelper.getInstance().sendMessage((String) object, dialog_id, null, null, false, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage((String) object, dialog_id, null, null, false, null, null, null); chatActivityEnterView.setFieldText(""); } else { chatActivityEnterView.replaceWithText(start, len, object + " "); } } else if (object instanceof TLRPC.BotInlineResult) { - String text = chatActivityEnterView.getFieldText(); - if (text == null) { + if (chatActivityEnterView.getFieldText() == null) { return; } + int uid = mentionsAdapter.getContextBotId(); TLRPC.BotInlineResult result = (TLRPC.BotInlineResult) object; HashMap params = new HashMap<>(); params.put("id", result.id); params.put("query_id", "" + result.query_id); - params.put("bot", "" + mentionsAdapter.getContextBotId()); + params.put("bot", "" + uid); params.put("bot_name", mentionsAdapter.getContextBotName()); - mentionsAdapter.addRecentBot(); - SendMessagesHelper.prepareSendingBotContextResult(result, params, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingBotContextResult(result, params, dialog_id, replyingMessageObject); chatActivityEnterView.setFieldText(""); showReplyPanel(false, null, null, null, false, true); + SearchQuery.increaseInlineRaiting(uid); } else if (object instanceof TLRPC.TL_inlineBotSwitchPM) { processInlineBotContextPM((TLRPC.TL_inlineBotSwitchPM) object); } @@ -2022,7 +2112,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not mentionListView.setOnItemLongClickListener(new RecyclerListView.OnItemLongClickListener() { @Override public boolean onItemClick(View view, int position) { - if (!mentionsAdapter.isLongClickEnabled()) { + if (getParentActivity() == null || !mentionsAdapter.isLongClickEnabled()) { return false; } Object object = mentionsAdapter.getItem(position); @@ -2062,37 +2152,24 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - int lastVisibleItem = mentionLayoutManager.findLastVisibleItemPosition(); + int lastVisibleItem; + if (mentionsAdapter.isBotContext() && mentionsAdapter.isMediaLayout()) { + lastVisibleItem = mentionGridLayoutManager.findLastVisibleItemPosition(); + } else { + lastVisibleItem = mentionLayoutManager.findLastVisibleItemPosition(); + } int visibleItemCount = lastVisibleItem == RecyclerView.NO_POSITION ? 0 : lastVisibleItem; if (visibleItemCount > 0 && lastVisibleItem > mentionsAdapter.getItemCount() - 5) { mentionsAdapter.searchForContextBotForNextOffset(); } - mentionListViewUpdateLayout(); } }); - - botSwitchCell = new BotSwitchCell(context); - botSwitchCell.setVisibility(View.GONE); - botSwitchCell.setBackgroundResource(R.drawable.list_selector); - mentionContainer.addView(botSwitchCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 36, Gravity.LEFT | Gravity.TOP, 0, 2, 0, 0)); - botSwitchCell.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - processInlineBotContextPM(mentionsAdapter.getBotContextSwitch()); - } - }); - - botSwitchShadow = new View(context); - botSwitchShadow.setBackgroundResource(R.drawable.header_shadow); - botSwitchShadow.setVisibility(View.GONE); - mentionContainer.addView(botSwitchShadow, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.LEFT | Gravity.TOP, 0, 38, 0, 0)); } - pagedownButton = new ImageView(context); + pagedownButton = new FrameLayout(context); pagedownButton.setVisibility(View.INVISIBLE); - pagedownButton.setImageResource(R.drawable.pagedown); - contentView.addView(pagedownButton, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 6, 4)); + contentView.addView(pagedownButton, LayoutHelper.createFrame(46, 59, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 7, 5)); pagedownButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -2104,6 +2181,21 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }); + ImageView pagedownButtonImage = new ImageView(context); + pagedownButtonImage.setImageResource(R.drawable.pagedown); + pagedownButton.addView(pagedownButtonImage, LayoutHelper.createFrame(46, 46, Gravity.LEFT | Gravity.BOTTOM)); + + pagedownButtonCounter = new TextView(context); + pagedownButtonCounter.setVisibility(View.INVISIBLE); + pagedownButtonCounter.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + pagedownButtonCounter.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); + pagedownButtonCounter.setTextColor(0xffffffff); + pagedownButtonCounter.setGravity(Gravity.CENTER); + pagedownButtonCounter.setBackgroundResource(R.drawable.chat_badge); + pagedownButtonCounter.setMinWidth(AndroidUtilities.dp(23)); + pagedownButtonCounter.setPadding(AndroidUtilities.dp(8), 0, AndroidUtilities.dp(8), AndroidUtilities.dp(1)); + pagedownButton.addView(pagedownButtonCounter, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 23, Gravity.TOP | Gravity.CENTER_HORIZONTAL)); + chatActivityEnterView = new ChatActivityEnterView(getParentActivity(), contentView, this, true); chatActivityEnterView.setDialogId(dialog_id); chatActivityEnterView.addToAttachLayout(menuItem); @@ -2113,7 +2205,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not contentView.addView(chatActivityEnterView, contentView.getChildCount() - 1, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.BOTTOM)); chatActivityEnterView.setDelegate(new ChatActivityEnterView.ChatActivityEnterViewDelegate() { @Override - public void onMessageSend(String message) { + public void onMessageSend(CharSequence message) { moveScrollToLastMessage(); showReplyPanel(false, null, null, null, false, true); if (mentionsAdapter != null) { @@ -2184,15 +2276,27 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void onMessageEditEnd() { - mentionsAdapter.setNeedBotContext(currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 46); - chatListView.setOnItemLongClickListener(onItemLongClickListener); - chatListView.setOnItemClickListener(onItemClickListener); - chatListView.setClickable(true); - chatListView.setLongClickable(true); - actionModeTextView.setVisibility(View.GONE); - selectedMessagesCountTextView.setVisibility(View.VISIBLE); - chatActivityEnterView.setAllowStickersAndGifs(currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 23, currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 46); + public void onMessageEditEnd(boolean loading) { + if (loading) { + showEditDoneProgress(true, true); + } else { + mentionsAdapter.setNeedBotContext(currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 46); + chatListView.setOnItemLongClickListener(onItemLongClickListener); + chatListView.setOnItemClickListener(onItemClickListener); + chatListView.setClickable(true); + chatListView.setLongClickable(true); + mentionsAdapter.setAllowNewMentions(true); + actionModeTitleContainer.setVisibility(View.GONE); + selectedMessagesCountTextView.setVisibility(View.VISIBLE); + chatActivityEnterView.setAllowStickersAndGifs(currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 23, currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 46); + if (editingMessageObjectReqId != 0) { + ConnectionsManager.getInstance().cancelRequest(editingMessageObjectReqId, true); + editingMessageObjectReqId = 0; + } + actionBar.hideActionMode(); + updatePinnedMessageView(true); + updateVisibleRows(); + } } @Override @@ -2200,21 +2304,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (size < AndroidUtilities.dp(72) + ActionBar.getCurrentActionBarHeight()) { allowStickersPanel = false; if (stickersPanel.getVisibility() == View.VISIBLE) { - stickersPanel.clearAnimation(); stickersPanel.setVisibility(View.INVISIBLE); } if (mentionContainer != null && mentionContainer.getVisibility() == View.VISIBLE) { - mentionContainer.clearAnimation(); mentionContainer.setVisibility(View.INVISIBLE); } } else { allowStickersPanel = true; if (stickersPanel.getVisibility() == View.INVISIBLE) { - stickersPanel.clearAnimation(); stickersPanel.setVisibility(View.VISIBLE); } if (mentionContainer != null && mentionContainer.getVisibility() == View.INVISIBLE && (!mentionsAdapter.isBotContext() || (allowContextBotPanel || allowContextBotPanelSecond))) { - mentionContainer.clearAnimation(); mentionContainer.setVisibility(View.VISIBLE); mentionContainer.setTag(null); } @@ -2234,7 +2334,54 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }); - FrameLayout replyLayout = new FrameLayout(context); + FrameLayout replyLayout = new FrameLayout(context) { + @Override + public void setTranslationY(float translationY) { + super.setTranslationY(translationY); + if (chatActivityEnterView != null) { + chatActivityEnterView.invalidate(); + } + if (getVisibility() != GONE) { + int height = getLayoutParams().height; + if (chatListView != null) { + chatListView.setTranslationY(translationY); + } + if (progressView != null) { + progressView.setTranslationY(translationY); + } + if (mentionContainer != null) { + mentionContainer.setTranslationY(translationY); + } + if (pagedownButton != null) { + pagedownButton.setTranslationY(translationY); + } + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (visibility == GONE) { + if (chatListView != null) { + chatListView.setTranslationY(0); + } + if (progressView != null) { + progressView.setTranslationY(0); + } + if (mentionContainer != null) { + mentionContainer.setTranslationY(0); + } + if (pagedownButton != null) { + pagedownButton.setTranslationY(pagedownButton.getTag() == null ? AndroidUtilities.dp(100) : 0); + } + } + } + }; replyLayout.setClickable(true); chatActivityEnterView.addTopView(replyLayout, 48); @@ -2260,22 +2407,16 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }); - replyNameTextView = new TextView(context); - replyNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + replyNameTextView = new SimpleTextView(context); + replyNameTextView.setTextSize(14); replyNameTextView.setTextColor(Theme.REPLY_PANEL_NAME_TEXT_COLOR); replyNameTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - replyNameTextView.setSingleLine(true); - replyNameTextView.setEllipsize(TextUtils.TruncateAt.END); - replyNameTextView.setMaxLines(1); - replyLayout.addView(replyNameTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 52, 4, 52, 0)); + replyLayout.addView(replyNameTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 18, Gravity.TOP | Gravity.LEFT, 52, 6, 52, 0)); - replyObjectTextView = new TextView(context); - replyObjectTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + replyObjectTextView = new SimpleTextView(context); + replyObjectTextView.setTextSize(14); replyObjectTextView.setTextColor(Theme.REPLY_PANEL_MESSAGE_TEXT_COLOR); - replyObjectTextView.setSingleLine(true); - replyObjectTextView.setEllipsize(TextUtils.TruncateAt.END); - replyObjectTextView.setMaxLines(1); - replyLayout.addView(replyObjectTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 52, 22, 52, 0)); + replyLayout.addView(replyObjectTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 18, Gravity.TOP | Gravity.LEFT, 52, 24, 52, 0)); replyImageView = new BackupImageView(context); replyLayout.addView(replyImageView, LayoutHelper.createFrame(34, 34, Gravity.TOP | Gravity.LEFT, 52, 6, 0, 0)); @@ -2291,6 +2432,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return super.onInterceptTouchEvent(event) || result; } }; + stickersListView.setTag(3); stickersListView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { @@ -2302,9 +2444,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); stickersListView.setLayoutManager(layoutManager); stickersListView.setClipToPadding(false); - if (Build.VERSION.SDK_INT >= 9) { - stickersListView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); - } + stickersListView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); stickersPanel.addView(stickersListView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 78)); initStickers(); @@ -2312,6 +2452,44 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not imageView.setImageResource(R.drawable.stickers_back_arrow); stickersPanel.addView(imageView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.BOTTOM | Gravity.LEFT, 53, 0, 0, 0)); + searchContainer = new FrameLayout(context); + searchContainer.setBackgroundResource(R.drawable.compose_panel); + searchContainer.setVisibility(View.INVISIBLE); + searchContainer.setFocusable(true); + searchContainer.setFocusableInTouchMode(true); + searchContainer.setClickable(true); + searchContainer.setBackgroundResource(R.drawable.compose_panel); + searchContainer.setPadding(0, AndroidUtilities.dp(3), 0, 0); + contentView.addView(searchContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 51, Gravity.BOTTOM)); + + searchUpButton = new ImageView(context); + searchUpButton.setScaleType(ImageView.ScaleType.CENTER); + searchUpButton.setImageResource(R.drawable.search_up); + searchContainer.addView(searchUpButton, LayoutHelper.createFrame(48, 48)); + searchUpButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + MessagesSearchQuery.searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 1); + } + }); + + searchDownButton = new ImageView(context); + searchDownButton.setScaleType(ImageView.ScaleType.CENTER); + searchDownButton.setImageResource(R.drawable.search_down); + searchContainer.addView(searchDownButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.TOP, 48, 0, 0, 0)); + searchDownButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + MessagesSearchQuery.searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 2); + } + }); + + searchCountText = new SimpleTextView(context); + searchCountText.setTextColor(Theme.CHAT_SEARCH_COUNT_TEXT_COLOR); + searchCountText.setTextSize(15); + searchCountText.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + searchContainer.addView(searchCountText, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL, 108, 0, 0, 0)); + bottomOverlay = new FrameLayout(context); bottomOverlay.setVisibility(View.INVISIBLE); bottomOverlay.setFocusable(true); @@ -2346,7 +2524,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (botUserLast != null && botUserLast.length() != 0) { MessagesController.getInstance().sendBotStart(currentUser, botUserLast); } else { - SendMessagesHelper.getInstance().sendMessage("/start", dialog_id, null, null, false, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage("/start", dialog_id, null, null, false, null, null, null); } } else { builder = new AlertDialog.Builder(getParentActivity()); @@ -2362,7 +2540,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (botUser.length() != 0) { MessagesController.getInstance().sendBotStart(currentUser, botUser); } else { - SendMessagesHelper.getInstance().sendMessage("/start", dialog_id, null, null, false, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage("/start", dialog_id, null, null, false, null, null, null); } botUser = null; updateBottomOverlay(); @@ -2433,7 +2611,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } private void mentionListViewUpdateLayout() { - if (mentionListView.getChildCount() <= 0 || !mentionsAdapter.isBotContext() || mentionsAdapter.getOrientation() != LinearLayoutManager.VERTICAL) { + if (mentionListView.getChildCount() <= 0) { mentionListViewScrollOffsetY = 0; mentionListViewLastViewPosition = -1; return; @@ -2457,6 +2635,24 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } + private void checkBotCommands() { + URLSpanBotCommand.enabled = false; + if (currentUser != null && currentUser.bot) { + URLSpanBotCommand.enabled = true; + } else if (info instanceof TLRPC.TL_chatFull) { + for (int a = 0; a < info.participants.participants.size(); a++) { + TLRPC.ChatParticipant participant = info.participants.participants.get(a); + TLRPC.User user = MessagesController.getInstance().getUser(participant.user_id); + if (user != null && user.bot) { + URLSpanBotCommand.enabled = true; + break; + } + } + } else if (info instanceof TLRPC.TL_channelFull) { + URLSpanBotCommand.enabled = !info.bot_info.isEmpty(); + } + } + public void processInlineBotContextPM(TLRPC.TL_inlineBotSwitchPM object) { if (object == null) { return; @@ -2482,15 +2678,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } private void createChatAttachView() { - if (chatAttachView == null) { - BottomSheet.Builder builder = new BottomSheet.Builder(getParentActivity()); - chatAttachView = new ChatAttachView(getParentActivity()); - chatAttachView.setDelegate(new ChatAttachView.ChatAttachViewDelegate() { + if (getParentActivity() == null) { + return; + } + if (chatAttachAlert == null) { + chatAttachAlert = new ChatAttachAlert(getParentActivity()); + chatAttachAlert.setDelegate(new ChatAttachAlert.ChatAttachViewDelegate() { @Override public void didPressedButton(int button) { + if (getParentActivity() == null) { + return; + } if (button == 7) { - chatAttachViewSheet.dismiss(); - HashMap selectedPhotos = chatAttachView.getSelectedPhotos(); + chatAttachAlert.dismiss(); + HashMap selectedPhotos = chatAttachAlert.getSelectedPhotos(); if (!selectedPhotos.isEmpty()) { ArrayList photos = new ArrayList<>(); ArrayList captions = new ArrayList<>(); @@ -2507,58 +2708,31 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not photoEntry.thumbPath = null; photoEntry.caption = null; } - SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } return; - } else { - if (chatAttachViewSheet != null) { - chatAttachViewSheet.dismissWithButtonClick(button); - } + } else if (chatAttachAlert != null) { + chatAttachAlert.dismissWithButtonClick(button); } processSelectedAttach(button); } - }); - builder.setDelegate(new BottomSheet.BottomSheetDelegate() { - - @Override - public void onRevealAnimationStart(boolean open) { - if (chatAttachView != null) { - chatAttachView.onRevealAnimationStart(open); - } - } - - @Override - public void onRevealAnimationProgress(boolean open, float radius, int x, int y) { - if (chatAttachView != null) { - chatAttachView.onRevealAnimationProgress(open, radius, x, y); - } - } - - @Override - public void onRevealAnimationEnd(boolean open) { - if (chatAttachView != null) { - chatAttachView.onRevealAnimationEnd(open); - } - } - - @Override - public void onOpenAnimationEnd() { - if (chatAttachView != null) { - chatAttachView.onRevealAnimationEnd(true); - } - } @Override public View getRevealView() { return menuItem; } + + @Override + public void didSelectBot(TLRPC.User user) { + if (chatActivityEnterView == null || user.username == null || user.username.length() == 0) { + return; + } + chatActivityEnterView.setFieldText("@" + user.username + " "); + chatActivityEnterView.openKeyboard(); + } }); - builder.setApplyTopPadding(false); - builder.setApplyBottomPadding(false); - builder.setUseRevealAnimation(); - builder.setCustomView(chatAttachView); - chatAttachViewSheet = builder.create(); } } @@ -2608,7 +2782,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (show) { stickersListView.scrollToPosition(0); - stickersPanel.clearAnimation(); stickersPanel.setVisibility(allowStickersPanel ? View.VISIBLE : View.INVISIBLE); } if (runningAnimation != null) { @@ -2616,18 +2789,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not runningAnimation = null; } if (stickersPanel.getVisibility() != View.INVISIBLE) { - runningAnimation = new AnimatorSetProxy(); + runningAnimation = new AnimatorSet(); runningAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(stickersPanel, "alpha", show ? 0.0f : 1.0f, show ? 1.0f : 0.0f) + ObjectAnimator.ofFloat(stickersPanel, "alpha", show ? 0.0f : 1.0f, show ? 1.0f : 0.0f) ); runningAnimation.setDuration(150); runningAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { if (!show) { stickersAdapter.clearStickers(); - stickersPanel.clearAnimation(); stickersPanel.setVisibility(View.GONE); if (StickerPreviewViewer.getInstance().isVisible()) { StickerPreviewViewer.getInstance().close(); @@ -2639,7 +2811,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { runningAnimation = null; } @@ -2656,8 +2828,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not public void onItemClick(View view, int position) { TLRPC.Document document = stickersAdapter.getItem(position); if (document instanceof TLRPC.TL_document) { - SendMessagesHelper.getInstance().sendSticker(document, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.getInstance().sendSticker(document, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + chatActivityEnterView.addStickerToRecent(document); } chatActivityEnterView.setFieldText(""); } @@ -2679,7 +2852,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { - SendMessagesHelper.getInstance().sendMessage(UserConfig.getCurrentUser(), dialog_id, messageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null); + SendMessagesHelper.getInstance().sendMessage(UserConfig.getCurrentUser(), dialog_id, messageObject, null, null); moveScrollToLastMessage(); showReplyPanel(false, null, null, null, false, true); } @@ -2723,41 +2896,40 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not gifHintTextView.setGravity(Gravity.CENTER_VERTICAL); frameLayout.addView(gifHintTextView, index + 1, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 32, Gravity.LEFT | Gravity.BOTTOM, 5, 0, 0, 3)); - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(gifHintTextView, "alpha", 0.0f, 1.0f), - ObjectAnimatorProxy.ofFloat(emojiButtonRed, "alpha", 0.0f, 1.0f) + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(gifHintTextView, "alpha", 0.0f, 1.0f), + ObjectAnimator.ofFloat(emojiButtonRed, "alpha", 0.0f, 1.0f) ); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + AnimatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { if (gifHintTextView == null) { return; } - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(gifHintTextView, "alpha", 0.0f) + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(gifHintTextView, "alpha", 0.0f) ); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + AnimatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (gifHintTextView != null) { - gifHintTextView.clearAnimation(); gifHintTextView.setVisibility(View.GONE); } } }); - animatorSetProxy.setDuration(300); - animatorSetProxy.start(); + AnimatorSet.setDuration(300); + AnimatorSet.start(); } }, 2000); } }); - animatorSetProxy.setDuration(300); - animatorSetProxy.start(); + AnimatorSet.setDuration(300); + AnimatorSet.start(); } private void checkContextBotPanel() { @@ -2769,22 +2941,21 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } mentionContainer.setTag(1); - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionContainer, "alpha", 0.0f) + ObjectAnimator.ofFloat(mentionContainer, "alpha", 0.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionContainer.clearAnimation(); mentionContainer.setVisibility(View.INVISIBLE); mentionListAnimation = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { mentionListAnimation = null; } @@ -2800,21 +2971,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } mentionContainer.setTag(null); mentionContainer.setVisibility(View.VISIBLE); - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionContainer, "alpha", 0.0f, 1.0f) + ObjectAnimator.ofFloat(mentionContainer, "alpha", 0.0f, 1.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionContainer.clearAnimation(); mentionListAnimation = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { mentionListAnimation = null; } @@ -2846,24 +3016,24 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not loading = true; waitingForLoad.add(lastLoadIndex); if (messagesByDays.size() != 0) { - MessagesController.getInstance().loadMessages(dialog_id, 50, maxMessageId[0], !cacheEndReached[0], minDate[0], classGuid, 0, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, 50, maxMessageId[0], !cacheEndReached[0], minDate[0], classGuid, 0, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } else { - MessagesController.getInstance().loadMessages(dialog_id, 50, 0, !cacheEndReached[0], minDate[0], classGuid, 0, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, 50, 0, !cacheEndReached[0], minDate[0], classGuid, 0, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } } else if (mergeDialogId != 0 && !endReached[1]) { loading = true; waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(mergeDialogId, 50, maxMessageId[1], !cacheEndReached[1], minDate[1], classGuid, 0, 0, 0, lastLoadIndex++); + MessagesController.getInstance().loadMessages(mergeDialogId, 50, maxMessageId[1], !cacheEndReached[1], minDate[1], classGuid, 0, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } } if (!loadingForward && firstVisibleItem + visibleItemCount >= totalItemCount - 10) { if (mergeDialogId != 0 && !forwardEndReached[1]) { waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(mergeDialogId, 50, minMessageId[1], true, maxDate[1], classGuid, 1, 0, 0, lastLoadIndex++); + MessagesController.getInstance().loadMessages(mergeDialogId, 50, minMessageId[1], true, maxDate[1], classGuid, 1, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); loadingForward = true; } else if (!forwardEndReached[0]) { waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(dialog_id, 50, minMessageId[0], true, maxDate[0], classGuid, 1, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, 50, minMessageId[0], true, maxDate[0], classGuid, 1, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); loadingForward = true; } } @@ -2920,9 +3090,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not fragment.setDelegate(new PhotoAlbumPickerActivity.PhotoAlbumPickerActivityDelegate() { @Override public void didSelectPhotos(ArrayList photos, ArrayList captions, ArrayList webPhotos) { - SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); - SendMessagesHelper.prepareSendingPhotosSearch(webPhotos, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions); + SendMessagesHelper.prepareSendingPhotosSearch(webPhotos, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } @Override @@ -2949,8 +3120,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (Build.VERSION.SDK_INT >= 16) { return !openVideoEditor(path, true, true); } else { - SendMessagesHelper.prepareSendingVideo(path, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingVideo(path, 0, 0, 0, 0, null, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); return true; } } @@ -2978,8 +3150,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not 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) { - Toast.makeText(getParentActivity(), "Telegram-FOSS: Disabled for now.\nYou can share locations from OSMAnd.", Toast.LENGTH_LONG).show(); + public void didSelectLocation(TLRPC.MessageMedia location) { + SendMessagesHelper.getInstance().sendMessage(location, dialog_id, replyingMessageObject, null, null); + moveScrollToLastMessage(); + showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); + if (paused) { + scrollToTopOnResume = true; } }); builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); @@ -2995,8 +3172,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void didSelectFiles(DocumentSelectActivity activity, ArrayList files) { activity.finishFragment(); - SendMessagesHelper.prepareSendingDocuments(files, files, null, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingDocuments(files, files, null, null, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } @Override @@ -3020,8 +3198,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not fragment.setDelegate(new AudioSelectActivity.AudioSelectActivityDelegate() { @Override public void didSelectAudio(ArrayList audios) { - SendMessagesHelper.prepareSendingAudioDocuments(audios, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingAudioDocuments(audios, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } }); presentFragment(fragment); @@ -3044,7 +3223,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public boolean dismissDialogOnPause(Dialog dialog) { - return !(dialog == chatAttachViewSheet && PhotoViewer.getInstance().isVisible()) && super.dismissDialogOnPause(dialog); + return !(dialog == chatAttachAlert && PhotoViewer.getInstance().isVisible()) && super.dismissDialogOnPause(dialog); } private void searchLinks(final CharSequence charSequence, final boolean force) { @@ -3215,84 +3394,87 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } if (!fromMyName) { - SendMessagesHelper.getInstance().sendMessage(arrayList, dialog_id, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.getInstance().sendMessage(arrayList, dialog_id); } else { for (MessageObject object : arrayList) { - SendMessagesHelper.getInstance().processForwardFromMyName(object, dialog_id, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.getInstance().processForwardFromMyName(object, dialog_id); } } } - public void showReplyPanel(boolean show, MessageObject messageObject, ArrayList messageObjects, TLRPC.WebPage webPage, boolean cancel, boolean animated) { + public void showReplyPanel(boolean show, MessageObject messageObjectToReply, ArrayList messageObjectsToForward, TLRPC.WebPage webPage, boolean cancel, boolean animated) { if (chatActivityEnterView == null) { return; } if (show) { - if (messageObject == null && messageObjects == null && webPage == null) { + if (messageObjectToReply == null && messageObjectsToForward == null && webPage == null) { return; } + if (searchItem != null && actionBar.isSearchFieldVisible()) { + actionBar.closeSearchField(); + chatActivityEnterView.setFieldFocused(); + } boolean openKeyboard = false; - if (messageObject != null && messageObject.getDialogId() != dialog_id) { - messageObjects = new ArrayList<>(); - messageObjects.add(messageObject); - messageObject = null; + if (messageObjectToReply != null && messageObjectToReply.getDialogId() != dialog_id) { + messageObjectsToForward = new ArrayList<>(); + messageObjectsToForward.add(messageObjectToReply); + messageObjectToReply = null; openKeyboard = true; } - if (messageObject != null) { + if (messageObjectToReply != null) { + forwardingMessages = null; + replyingMessageObject = messageObjectToReply; + chatActivityEnterView.setReplyingMessageObject(messageObjectToReply); + if (foundWebPage != null) { + return; + } String name; - if (messageObject.isFromUser()) { - TLRPC.User user = MessagesController.getInstance().getUser(messageObject.messageOwner.from_id); + if (messageObjectToReply.isFromUser()) { + TLRPC.User user = MessagesController.getInstance().getUser(messageObjectToReply.messageOwner.from_id); if (user == null) { return; } name = UserObject.getUserName(user); } else { - TLRPC.Chat chat = MessagesController.getInstance().getChat(messageObject.messageOwner.to_id.channel_id); + TLRPC.Chat chat = MessagesController.getInstance().getChat(messageObjectToReply.messageOwner.to_id.channel_id); if (chat == null) { return; } name = chat.title; } - - forwardingMessages = null; - replyingMessageObject = messageObject; - chatActivityEnterView.setReplyingMessageObject(messageObject); - if (foundWebPage != null) { - return; - } replyIconImageView.setImageResource(R.drawable.reply); replyNameTextView.setText(name); - if (messageObject.messageText != null) { - String mess = messageObject.messageText.toString(); + + if (messageObjectToReply.messageText != null) { + String mess = messageObjectToReply.messageText.toString(); if (mess.length() > 150) { mess = mess.substring(0, 150); } mess = mess.replace('\n', ' '); replyObjectTextView.setText(Emoji.replaceEmoji(mess, replyObjectTextView.getPaint().getFontMetricsInt(), AndroidUtilities.dp(14), false)); } - } else if (messageObjects != null) { - if (messageObjects.isEmpty()) { + } else if (messageObjectsToForward != null) { + if (messageObjectsToForward.isEmpty()) { return; } replyingMessageObject = null; chatActivityEnterView.setReplyingMessageObject(null); - forwardingMessages = messageObjects; - + forwardingMessages = messageObjectsToForward; if (foundWebPage != null) { return; } chatActivityEnterView.setForceShowSendButton(true, animated); ArrayList uids = new ArrayList<>(); replyIconImageView.setImageResource(R.drawable.forward_blue); - MessageObject object = messageObjects.get(0); + MessageObject object = messageObjectsToForward.get(0); if (object.isFromUser()) { uids.add(object.messageOwner.from_id); } else { uids.add(-object.messageOwner.to_id.channel_id); } - int type = messageObjects.get(0).type; - for (int a = 1; a < messageObjects.size(); a++) { - object = messageObjects.get(a); + int type = messageObjectsToForward.get(0).type; + for (int a = 1; a < messageObjectsToForward.size(); a++) { + object = messageObjectsToForward.get(a); Integer uid; if (object.isFromUser()) { uid = object.messageOwner.from_id; @@ -3302,7 +3484,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (!uids.contains(uid)) { uids.add(uid); } - if (messageObjects.get(a).type != type) { + if (messageObjectsToForward.get(a).type != type) { type = -1; } } @@ -3348,50 +3530,50 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } replyNameTextView.setText(userNames); if (type == -1 || type == 0 || type == 10 || type == 11) { - if (messageObjects.size() == 1 && messageObjects.get(0).messageText != null) { - String mess = messageObjects.get(0).messageText.toString(); + if (messageObjectsToForward.size() == 1 && messageObjectsToForward.get(0).messageText != null) { + String mess = messageObjectsToForward.get(0).messageText.toString(); if (mess.length() > 150) { mess = mess.substring(0, 150); } mess = mess.replace('\n', ' '); replyObjectTextView.setText(Emoji.replaceEmoji(mess, replyObjectTextView.getPaint().getFontMetricsInt(), AndroidUtilities.dp(14), false)); } else { - replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedMessage", messageObjects.size())); + replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedMessage", messageObjectsToForward.size())); } } else { if (type == 1) { - replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedPhoto", messageObjects.size())); - if (messageObjects.size() == 1) { - messageObject = messageObjects.get(0); + replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedPhoto", messageObjectsToForward.size())); + if (messageObjectsToForward.size() == 1) { + messageObjectToReply = messageObjectsToForward.get(0); } } else if (type == 4) { - replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedLocation", messageObjects.size())); + replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedLocation", messageObjectsToForward.size())); } else if (type == 3) { - replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedVideo", messageObjects.size())); - if (messageObjects.size() == 1) { - messageObject = messageObjects.get(0); + replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedVideo", messageObjectsToForward.size())); + if (messageObjectsToForward.size() == 1) { + messageObjectToReply = messageObjectsToForward.get(0); } } else if (type == 12) { - replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedContact", messageObjects.size())); + replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedContact", messageObjectsToForward.size())); } else if (type == 2) { - replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedAudio", messageObjects.size())); + replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedAudio", messageObjectsToForward.size())); } else if (type == 14) { - replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedMusic", messageObjects.size())); + replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedMusic", messageObjectsToForward.size())); } else if (type == 13) { - replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedSticker", messageObjects.size())); + replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedSticker", messageObjectsToForward.size())); } else if (type == 8 || type == 9) { - if (messageObjects.size() == 1) { + if (messageObjectsToForward.size() == 1) { if (type == 8) { replyObjectTextView.setText(LocaleController.getString("AttachGif", R.string.AttachGif)); } else { String name; - if ((name = FileLoader.getDocumentFileName(messageObjects.get(0).getDocument())).length() != 0) { + if ((name = FileLoader.getDocumentFileName(messageObjectsToForward.get(0).getDocument())).length() != 0) { replyObjectTextView.setText(name); } - messageObject = messageObjects.get(0); + messageObjectToReply = messageObjectsToForward.get(0); } } else { - replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedFile", messageObjects.size())); + replyObjectTextView.setText(LocaleController.formatPluralString("ForwardedFile", messageObjectsToForward.size())); } } } @@ -3422,8 +3604,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } FrameLayout.LayoutParams layoutParams1 = (FrameLayout.LayoutParams) replyNameTextView.getLayoutParams(); FrameLayout.LayoutParams layoutParams2 = (FrameLayout.LayoutParams) replyObjectTextView.getLayoutParams(); - TLRPC.PhotoSize photoSize = messageObject != null ? FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, 80) : null; - if (photoSize == null || photoSize instanceof TLRPC.TL_photoSizeEmpty || photoSize.location instanceof TLRPC.TL_fileLocationUnavailable || messageObject.type == 13 || messageObject != null && messageObject.isSecretMedia()) { + TLRPC.PhotoSize photoSize = messageObjectToReply != null ? FileLoader.getClosestPhotoSizeWithSize(messageObjectToReply.photoThumbs, 80) : null; + if (photoSize == null || photoSize instanceof TLRPC.TL_photoSizeEmpty || photoSize.location instanceof TLRPC.TL_fileLocationUnavailable || messageObjectToReply.type == 13 || messageObjectToReply != null && messageObjectToReply.isSecretMedia()) { replyImageView.setImageBitmap(null); replyImageLocation = null; replyImageView.setVisibility(View.INVISIBLE); @@ -3462,13 +3644,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not replyingMessageObject = null; forwardingMessages = null; replyImageLocation = null; - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - preferences.edit().remove("reply_" + dialog_id).commit(); } } private void moveScrollToLastMessage() { - if (chatListView != null) { + if (chatListView != null && !messages.isEmpty()) { chatLayoutManager.scrollToPositionWithOffset(messages.size() - 1, -100000 - chatListView.getPaddingTop()); } } @@ -3507,7 +3687,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not first = true; firstLoading = true; loading = true; - waitingForImportantLoad = false; + loadingForward = false; waitingForReplyMessageLoad = false; startLoadFromMessageId = 0; last_message_id = 0; @@ -3527,7 +3707,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else { clearChatData(); waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(dialog_id, 30, 0, true, 0, classGuid, 0, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, 30, 0, true, 0, classGuid, 0, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } } @@ -3536,12 +3716,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } int count = chatListView.getChildCount(); + int additionalTop = chatActivityEnterView.isTopViewVisible() ? AndroidUtilities.dp(48) : 0; + int height = chatListView.getMeasuredHeight(); for (int a = 0; a < count; a++) { View view = chatListView.getChildAt(a); if (view instanceof ChatMessageCell) { ChatMessageCell messageCell = (ChatMessageCell) view; - messageCell.getLocalVisibleRect(scrollRect); - messageCell.setVisiblePart(scrollRect.top, scrollRect.bottom - scrollRect.top); + int top = messageCell.getTop(); + int bottom = messageCell.getBottom(); + int viewTop = top >= 0 ? 0 : -top; + int viewBottom = messageCell.getMeasuredHeight(); + if (viewBottom > height) { + viewBottom = viewTop + height; + } + messageCell.setVisiblePart(viewTop, viewBottom - viewTop); } } } @@ -3557,7 +3745,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not flags = 1; MessagesStorage.getInstance().setDialogFlags(dialog_id, flags); editor.commit(); - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); dialog.notify_settings.mute_until = Integer.MAX_VALUE; @@ -3573,7 +3761,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not editor.putInt("notify2_" + dialog_id, 0); MessagesStorage.getInstance().setDialogFlags(dialog_id, 0); editor.commit(); - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); } @@ -3603,15 +3791,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int count = chatListView.getChildCount(); for (int a = 0; a < count; a++) { View view = chatListView.getChildAt(a); - if (view instanceof ChatBaseCell) { - ChatBaseCell cell = (ChatBaseCell) view; - if (cell.getMessageObject() != null && cell.getMessageObject().getId() == object.getId()) { + if (view instanceof ChatMessageCell) { + ChatMessageCell cell = (ChatMessageCell) view; + MessageObject messageObject = cell.getMessageObject(); + if (messageObject != null && messageObject.getId() == object.getId()) { found = true; break; } } else if (view instanceof ChatActionCell) { ChatActionCell cell = (ChatActionCell) view; - if (cell.getMessageObject() != null && cell.getMessageObject().getId() == object.getId()) { + MessageObject messageObject = cell.getMessageObject(); + if (messageObject != null && messageObject.getId() == object.getId()) { found = true; break; } @@ -3639,12 +3829,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not unreadMessageObject = null; scrollToMessage = null;*/ + waitingForLoad.clear(); waitingForReplyMessageLoad = true; highlightMessageId = Integer.MAX_VALUE; scrollToMessagePosition = -10000; startLoadFromMessageId = id; waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(loadIndex == 0 ? dialog_id : mergeDialogId, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, loadIndex == 0 ? channelMessagesImportant : 0, lastLoadIndex++); + MessagesController.getInstance().loadMessages(loadIndex == 0 ? dialog_id : mergeDialogId, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); //emptyViewContainer.setVisibility(View.INVISIBLE); } returnToMessageId = fromMessageId; @@ -3657,24 +3848,27 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } if (show) { + pagedownButtonShowedByScroll = false; if (pagedownButton.getTag() == null) { if (pagedownButtonAnimation != null) { pagedownButtonAnimation.cancel(); pagedownButtonAnimation = null; } if (animated) { - if (ViewProxy.getTranslationY(pagedownButton) == 0) { - ViewProxy.setTranslationY(pagedownButton, AndroidUtilities.dp(100)); + if (pagedownButton.getTranslationY() == 0) { + pagedownButton.setTranslationY(AndroidUtilities.dp(100)); } pagedownButton.setVisibility(View.VISIBLE); pagedownButton.setTag(1); - pagedownButtonAnimation = ObjectAnimatorProxy.ofFloatProxy(pagedownButton, "translationY", 0).setDuration(200).start(); + pagedownButtonAnimation = ObjectAnimator.ofFloat(pagedownButton, "translationY", 0).setDuration(200); + pagedownButtonAnimation.start(); } else { pagedownButton.setVisibility(View.VISIBLE); } } } else { returnToMessageId = 0; + newUnreadMessageCount = 0; if (pagedownButton.getTag() != null) { pagedownButton.setTag(null); if (pagedownButtonAnimation != null) { @@ -3682,15 +3876,16 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pagedownButtonAnimation = null; } if (animated) { - pagedownButtonAnimation = ObjectAnimatorProxy.ofFloatProxy(pagedownButton, "translationY", AndroidUtilities.dp(100)).setDuration(200).addListener(new AnimatorListenerAdapterProxy() { + pagedownButtonAnimation = ObjectAnimator.ofFloat(pagedownButton, "translationY", AndroidUtilities.dp(100)).setDuration(200); + pagedownButtonAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { - pagedownButton.clearAnimation(); + public void onAnimationEnd(Animator animation) { + pagedownButtonCounter.setVisibility(View.INVISIBLE); pagedownButton.setVisibility(View.INVISIBLE); } - }).start(); + }); + pagedownButtonAnimation.start(); } else { - pagedownButton.clearAnimation(); pagedownButton.setVisibility(View.INVISIBLE); } } @@ -3718,8 +3913,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not bottomOverlayText.setText(LocaleController.getString("EncryptionRejected", R.string.EncryptionRejected)); bottomOverlay.setVisibility(View.VISIBLE); chatActivityEnterView.setFieldText(""); - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - preferences.edit().remove("dialog_" + dialog_id).commit(); + DraftQuery.cleanDraft(dialog_id, false); hideKeyboard = true; } else if (currentEncryptedChat instanceof TLRPC.TL_encryptedChat) { bottomOverlay.setVisibility(View.INVISIBLE); @@ -3951,35 +4145,33 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (copyVisible != newCopyVisible) { if (newVisibility == View.VISIBLE) { - ViewProxy.setAlpha(replyItem, 1.0f); - ViewProxy.setScaleX(replyItem, 1.0f); + replyItem.setAlpha(1.0f); + replyItem.setScaleX(1.0f); } else { - ViewProxy.setAlpha(replyItem, 0.0f); - ViewProxy.setScaleX(replyItem, 0.0f); + replyItem.setAlpha(0.0f); + replyItem.setScaleX(0.0f); } replyItem.setVisibility(newVisibility); - replyItem.clearAnimation(); } else { - replyButtonAnimation = new AnimatorSetProxy(); - ViewProxy.setPivotX(replyItem, AndroidUtilities.dp(54)); + replyButtonAnimation = new AnimatorSet(); + replyItem.setPivotX(AndroidUtilities.dp(54)); if (newVisibility == View.VISIBLE) { replyItem.setVisibility(newVisibility); replyButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(replyItem, "alpha", 1.0f), - ObjectAnimatorProxy.ofFloat(replyItem, "scaleX", 1.0f) + ObjectAnimator.ofFloat(replyItem, "alpha", 1.0f), + ObjectAnimator.ofFloat(replyItem, "scaleX", 1.0f) ); } else { replyButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(replyItem, "alpha", 0.0f), - ObjectAnimatorProxy.ofFloat(replyItem, "scaleX", 0.0f) + ObjectAnimator.ofFloat(replyItem, "alpha", 0.0f), + ObjectAnimator.ofFloat(replyItem, "scaleX", 0.0f) ); } replyButtonAnimation.setDuration(100); replyButtonAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (replyButtonAnimation != null && replyButtonAnimation.equals(animation)) { - replyItem.clearAnimation(); if (newVisibility == View.GONE) { replyItem.setVisibility(View.GONE); } @@ -3987,7 +4179,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (replyButtonAnimation != null && replyButtonAnimation.equals(animation)) { replyButtonAnimation = null; } @@ -4003,8 +4195,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private void processRowSelect(View view) { MessageObject message = null; - if (view instanceof ChatBaseCell) { - message = ((ChatBaseCell) view).getMessageObject(); + if (view instanceof ChatMessageCell) { + message = ((ChatMessageCell) view).getMessageObject(); } else if (view instanceof ChatActionCell) { message = ((ChatActionCell) view).getMessageObject(); } @@ -4130,14 +4322,16 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not videoEditedInfo.resultWidth = resultWidth; videoEditedInfo.resultHeight = resultHeight; videoEditedInfo.originalPath = videoPath; - SendMessagesHelper.prepareSendingVideo(videoPath, estimatedSize, estimatedDuration, resultWidth, resultHeight, videoEditedInfo, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingVideo(videoPath, estimatedSize, estimatedDuration, resultWidth, resultHeight, videoEditedInfo, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } }); if (parentLayout == null || !fragment.onFragmentCreate()) { - SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); return false; } parentLayout.presentFragment(fragment, removeLast, !animated, true); @@ -4181,14 +4375,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not PhotoViewer.getInstance().openPhotoForSelect(arrayList, 0, 2, new PhotoViewer.EmptyPhotoViewerProvider() { @Override public void sendButtonPressed(int index) { - MediaController.PhotoEntry photoEntry = (MediaController.PhotoEntry) arrayList.get(0); - if (photoEntry.imagePath != null) { - SendMessagesHelper.prepareSendingPhoto(photoEntry.imagePath, null, dialog_id, replyingMessageObject, photoEntry.caption, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); - showReplyPanel(false, null, null, null, false, true); - } else if (photoEntry.path != null) { - SendMessagesHelper.prepareSendingPhoto(photoEntry.path, null, dialog_id, replyingMessageObject, photoEntry.caption, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); - showReplyPanel(false, null, null, null, false, true); - } + sendPhoto((MediaController.PhotoEntry) arrayList.get(0)); } }, this); AndroidUtilities.addMediaToGallery(currentPicturePath); @@ -4216,13 +4403,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not openVideoEditor(videoPath, false, false); } } else { - SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); - showReplyPanel(false, null, null, null, false, true); + SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject); } } else { - SendMessagesHelper.prepareSendingPhoto(null, uri, dialog_id, replyingMessageObject, null, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingPhoto(null, uri, dialog_id, replyingMessageObject, null); } showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } else if (requestCode == 2) { String videoPath = null; FileLog.d("tmessages", "pic path " + currentPicturePath); @@ -4260,8 +4447,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not openVideoEditor(videoPath, false, false); } } else { - SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } } else if (requestCode == 21) { if (data == null || data.getData() == null) { @@ -4294,8 +4482,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not showAttachmentError(); return; } - SendMessagesHelper.prepareSendingDocument(tempPath, originalPath, null, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingDocument(tempPath, originalPath, null, null, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } else if (requestCode == 31) { if (data == null || data.getData() == null) { showAttachmentError(); @@ -4315,10 +4504,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not user.first_name = name; user.last_name = ""; user.phone = number; - SendMessagesHelper.getInstance().sendMessage(user, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null); + SendMessagesHelper.getInstance().sendMessage(user, dialog_id, replyingMessageObject, null, null); } if (sent) { showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } } } finally { @@ -4352,11 +4542,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not first_unread_id = 0; last_message_id = 0; unread_to_load = 0; - if (chatAdapter != null) { - chatAdapter.removeMessageObject(unreadMessageObject); - } else { - messages.remove(unreadMessageObject); - } + removeMessageObject(unreadMessageObject); unreadMessageObject = null; } } @@ -4368,13 +4554,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void didReceivedNotification(int id, final Object... args) { if (id == NotificationCenter.messagesDidLoaded) { - int guid = (Integer) args[11]; + int guid = (Integer) args[10]; if (guid == classGuid) { if (!openAnimationEnded) { NotificationCenter.getInstance().setAllowedNotificationsDutingAnimation(new int[]{NotificationCenter.chatInfoDidLoaded, NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.botKeyboardDidLoaded/*, NotificationCenter.botInfoDidLoaded*/}); } - int queryLoadIndex = (Integer) args[12]; + int queryLoadIndex = (Integer) args[11]; int index = waitingForLoad.indexOf(queryLoadIndex); if (index == -1) { return; @@ -4382,20 +4568,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not waitingForLoad.remove(index); } ArrayList messArr = (ArrayList) args[2]; - if (waitingForImportantLoad || waitingForReplyMessageLoad) { - if (waitingForReplyMessageLoad) { - boolean found = false; - for (int a = 0; a < messArr.size(); a++) { - if (messArr.get(a).getId() == startLoadFromMessageId) { - found = true; - break; - } - } - if (!found) { - startLoadFromMessageId = 0; - return; + if (waitingForReplyMessageLoad) { + boolean found = false; + for (int a = 0; a < messArr.size(); a++) { + if (messArr.get(a).getId() == startLoadFromMessageId) { + found = true; + break; } } + if (!found) { + startLoadFromMessageId = 0; + return; + } int startLoadFrom = startLoadFromMessageId; boolean needSelect = needSelectFromMessageId; clearChatData(); @@ -4419,15 +4603,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else if (startLoadFromMessageId != 0 && load_type == 3) { last_message_id = (Integer) args[5]; } - ArrayList groups = (ArrayList) args[9]; - SparseArray groupsByStart = null; - if (groups != null && !groups.isEmpty()) { - groupsByStart = new SparseArray<>(); - for (int a = 0; a < groups.size(); a++) { - TLRPC.TL_messageGroup group = groups.get(a); - groupsByStart.put(group.min_id, group); - } - } int newRowsCount = 0; forwardEndReached[loadIndex] = startLoadFromMessageId == 0 && last_message_id == 0; @@ -4459,6 +4634,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } firstLoading = false; + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (parentLayout != null) { + parentLayout.resumeDelayedFragmentAnimation(); + } + } + }); } if (load_type == 1) { @@ -4480,7 +4663,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (loadIndex == 1) { obj.setIsRead(); } - if (loadIndex == 0 && channelMessagesImportant != 0 && obj.getId() == 1) { + if (loadIndex == 0 && ChatObject.isChannel(currentChat) && obj.getId() == 1) { endReached[loadIndex] = true; cacheEndReached[loadIndex] = true; } @@ -4514,6 +4697,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not TLRPC.Message dateMsg = new TLRPC.Message(); dateMsg.message = LocaleController.formatDateChat(obj.messageOwner.date); dateMsg.id = 0; + dateMsg.date = obj.messageOwner.date; MessageObject dateObj = new MessageObject(dateMsg, null, false); dateObj.type = 10; dateObj.contentType = 1; @@ -4531,27 +4715,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not messages.add(0, obj); } - if (groupsByStart != null) { - TLRPC.TL_messageGroup group = groupsByStart.get(obj.getId()); - if (group != null) { - TLRPC.Message dateMsg = new TLRPC.Message(); - dateMsg.message = "+" + LocaleController.formatPluralString("comments", group.count); - dateMsg.id = 0; - dateMsg.date = group.min_id; - dateMsg.from_id = group.max_id; - MessageObject dateObj = new MessageObject(dateMsg, null, false); - dateObj.type = 10; - dateObj.contentType = 1; - dayArray.add(dateObj); - if (load_type == 1) { - messages.add(0, dateObj); - } else { - messages.add(messages.size() - 1, dateObj); - } - newRowsCount++; - } - } - if (load_type != 1) { dayArray.add(obj); messages.add(messages.size() - 1, obj); @@ -4588,6 +4751,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } } + if (load_type == 0 && newRowsCount == 0) { + loadsCount--; + } if (forwardEndReached[loadIndex] && loadIndex != 1) { first_unread_id = 0; @@ -4632,7 +4798,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (currentEncryptedChat != null || isBroadcast) { endReached[loadIndex] = true; } - cacheEndReached[loadIndex] = true; + if (load_type != 2) { + cacheEndReached[loadIndex] = true; + } } else if (load_type != 2) { endReached[loadIndex] = true;// TODO if < 7 from unread } @@ -4640,7 +4808,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not loading = false; if (chatListView != null) { - if (first || scrollToTopOnResume) { + if (first || scrollToTopOnResume || forceScrollToTop) { + forceScrollToTop = false; chatAdapter.notifyDataSetChanged(); if (scrollToMessage != null) { int yOffset; @@ -4765,6 +4934,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else if (id == NotificationCenter.updateInterfaces) { int updateMask = (Integer) args[0]; if ((updateMask & MessagesController.UPDATE_MASK_NAME) != 0 || (updateMask & MessagesController.UPDATE_MASK_CHAT_NAME) != 0) { + if (currentChat != null) { + TLRPC.Chat chat = MessagesController.getInstance().getChat(currentChat.id); + if (chat != null) { + currentChat = chat; + } + } else if (currentUser != null) { + TLRPC.User user = MessagesController.getInstance().getUser(currentUser.id); + if (user != null) { + currentUser = user; + } + } updateTitle(); } boolean updateSubtitle = false; @@ -4905,7 +5085,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not scrollToLastMessage(false); return; } - if (messagesDict[0].containsKey(obj.getId())) { + if (obj.type < 0 || messagesDict[0].containsKey(obj.getId())) { continue; } obj.checkLayout(); @@ -4945,15 +5125,46 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int oldCount = messages.size(); int addedCount = 0; HashMap> webpagesToReload = null; + int placeToPaste = -1; for (int a = 0; a < arr.size(); a++) { MessageObject obj = arr.get(a); + if (a == 0) { + if (obj.messageOwner.id < 0) { + placeToPaste = 0; + } else { + if (!messages.isEmpty()) { + int size = messages.size(); + for (int b = 0; b < size; b++) { + MessageObject lastMessage = messages.get(b); + if (lastMessage.type >= 0 && lastMessage.messageOwner.date > 0) { + if (lastMessage.messageOwner.id > 0 && obj.messageOwner.id > 0) { + if (lastMessage.messageOwner.id < obj.messageOwner.id) { + placeToPaste = b; + break; + } + } else { + if (lastMessage.messageOwner.date < obj.messageOwner.date) { + placeToPaste = b; + break; + } + } + } + } + if (placeToPaste == -1 || placeToPaste > messages.size()) { + placeToPaste = messages.size(); + } + } else { + placeToPaste = 0; + } + } + } if (currentUser != null && currentUser.bot && obj.isOut()) { obj.setIsRead(); } if (avatarContainer != null && currentEncryptedChat != null && obj.messageOwner.action != null && obj.messageOwner.action instanceof TLRPC.TL_messageEncryptedAction && obj.messageOwner.action.encryptedAction instanceof TLRPC.TL_decryptedMessageActionSetMessageTTL) { avatarContainer.setTime(((TLRPC.TL_decryptedMessageActionSetMessageTTL) obj.messageOwner.action.encryptedAction).ttl_seconds); } - if (messagesDict[0].containsKey(obj.getId())) { + if (obj.type < 0 || messagesDict[0].containsKey(obj.getId())) { continue; } if (currentEncryptedChat != null && obj.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage && obj.messageOwner.media.webpage instanceof TLRPC.TL_webPageUrlPending) { @@ -5019,20 +5230,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not TLRPC.Message dateMsg = new TLRPC.Message(); dateMsg.message = LocaleController.formatDateChat(obj.messageOwner.date); dateMsg.id = 0; + dateMsg.date = obj.messageOwner.date; MessageObject dateObj = new MessageObject(dateMsg, null, false); dateObj.type = 10; dateObj.contentType = 1; - messages.add(0, dateObj); + messages.add(placeToPaste, dateObj); addedCount++; } if (!obj.isOut()) { - if (paused) { + if (paused && placeToPaste == 0) { if (!scrollToTopUnReadOnResume && unreadMessageObject != null) { - if (chatAdapter != null) { - chatAdapter.removeMessageObject(unreadMessageObject); - } else { - messages.remove(unreadMessageObject); - } + removeMessageObject(unreadMessageObject); unreadMessageObject = null; } if (unreadMessageObject == null) { @@ -5065,8 +5273,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } dayArray.add(0, obj); - messages.add(0, obj); + messages.add(placeToPaste, obj); addedCount++; + newUnreadMessageCount++; if (obj.type == 10 || obj.type == 11) { updateChat = true; } @@ -5083,7 +5292,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatAdapter.updateRowWithMessageObject(unreadMessageObject); } if (addedCount != 0) { - chatAdapter.notifyItemRangeInserted(chatAdapter.getItemCount(), addedCount); + chatAdapter.notifyItemRangeInserted(chatAdapter.getItemCount() - placeToPaste, addedCount); } } else { scrollToTopOnResume = true; @@ -5101,14 +5310,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not oldCount++; } if (lastVisible >= oldCount || hasFromMe) { + newUnreadMessageCount = 0; if (!firstLoading) { if (paused) { scrollToTopOnResume = true; } else { + forceScrollToTop = true; moveScrollToLastMessage(); } } } else { + if (newUnreadMessageCount != 0 && pagedownButtonCounter != null) { + pagedownButtonCounter.setVisibility(View.VISIBLE); + pagedownButtonCounter.setText(String.format("%d", newUnreadMessageCount)); + } showPagedownButton(true, true); } } else { @@ -5148,7 +5363,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } else if (id == NotificationCenter.messagesRead) { SparseArray inbox = (SparseArray) args[0]; - SparseIntArray outbox = (SparseIntArray) args[1]; + SparseArray outbox = (SparseArray) args[1]; boolean updated = false; for (int b = 0; b < inbox.size(); b++) { int key = inbox.keyAt(b); @@ -5170,7 +5385,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } for (int b = 0; b < outbox.size(); b++) { int key = outbox.keyAt(b); - int messageId = outbox.get(key); + int messageId = (int) ((long) outbox.get(key)); if (key != dialog_id) { continue; } @@ -5228,8 +5443,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not messages.remove(index); } } - updated = true; } + updated = true; } } } @@ -5251,7 +5466,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not maxDate[0] = maxDate[1] = Integer.MIN_VALUE; minDate[0] = minDate[1] = 0; waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(dialog_id, 30, 0, !cacheEndReached[0], minDate[0], classGuid, 0, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, 30, 0, !cacheEndReached[0], minDate[0], classGuid, 0, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); loading = true; } else { if (botButtons != null) { @@ -5288,7 +5503,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not messages.remove(index); } } - chatAdapter.notifyDataSetChanged(); + if (chatAdapter != null) { + chatAdapter.notifyDataSetChanged(); + } } return; } @@ -5317,8 +5534,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (chatAdapter != null) { chatAdapter.updateRowWithMessageObject(obj); } - if (mediaUpdated && chatLayoutManager.findLastVisibleItemPosition() >= messages.size() - 1) { - moveScrollToLastMessage(); + if (chatLayoutManager != null) { + if (mediaUpdated && chatLayoutManager.findLastVisibleItemPosition() >= messages.size() - 1) { + moveScrollToLastMessage(); + } } NotificationsController.getInstance().playOutChatSound(); } @@ -5569,11 +5788,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not first = true; firstLoading = true; loading = true; - waitingForImportantLoad = false; startLoadFromMessageId = 0; needSelectFromMessageId = false; waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, 0, true, 0, classGuid, 2, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, 0, true, 0, classGuid, 2, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } else { if (progressView != null) { progressView.setVisibility(View.INVISIBLE); @@ -5670,6 +5888,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (old.replyMessageObject != null) { messageObject.replyMessageObject = old.replyMessageObject; } + messageObject.messageOwner.attachPath = old.messageOwner.attachPath; + messageObject.attachPathExists = old.attachPathExists; + messageObject.mediaExists = old.mediaExists; messagesDict[loadIndex].put(old.getId(), messageObject); int index = messages.indexOf(old); if (index >= 0) { @@ -5810,7 +6031,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (messageId != 0) { scrollToMessageId(messageId, 0, true, did == dialog_id ? 0 : 1); } - updateSearchButtons((Integer) args[2]); + updateSearchButtons((Integer) args[2], (Integer) args[4], (Integer) args[5]); } } else if (id == NotificationCenter.didUpdatedMessagesViews) { SparseArray channelViews = (SparseArray) args[0]; @@ -5837,6 +6058,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (did == dialog_id) { updateSpamView(); } + } else if (id == NotificationCenter.newDraftReceived) { + long did = (Long) args[0]; + if (did == dialog_id) { + applyDraftMaybe(); + } } } @@ -5849,10 +6075,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not inlineReturn = 0; chatActivityEnterView.setFieldText(query); } else { - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("dialog_" + inlineReturn, query); - editor.commit(); + DraftQuery.saveDraft(inlineReturn, query, null, null, false); if (parentLayout.fragmentsStack.size() > 1) { BaseFragment prevFragment = parentLayout.fragmentsStack.get(parentLayout.fragmentsStack.size() - 2); if (prevFragment instanceof ChatActivity && ((ChatActivity) prevFragment).dialog_id == inlineReturn) { @@ -5882,15 +6105,25 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return true; } - private void updateSearchButtons(int mask) { - if (searchUpItem != null) { - searchUpItem.setEnabled((mask & 1) != 0); - searchDownItem.setEnabled((mask & 2) != 0); - ViewProxy.setAlpha(searchUpItem, searchUpItem.isEnabled() ? 1.0f : 0.6f); - ViewProxy.setAlpha(searchDownItem, searchDownItem.isEnabled() ? 1.0f : 0.6f); + private void updateSearchButtons(int mask, int num, int count) { + if (searchUpButton != null) { + searchUpButton.setEnabled((mask & 1) != 0); + searchDownButton.setEnabled((mask & 2) != 0); + searchUpButton.setAlpha(searchUpButton.isEnabled() ? 1.0f : 0.5f); + searchDownButton.setAlpha(searchDownButton.isEnabled() ? 1.0f : 0.5f); + if (count == 0) { + searchCountText.setText(""); + } else { + searchCountText.setText(LocaleController.formatString("Of", R.string.Of, num + 1, count)); + } } } + @Override + public boolean needDelayOpenAnimation() { + return firstLoading; + } + @Override public void onTransitionAnimationStart(boolean isOpen, boolean backward) { NotificationCenter.getInstance().setAllowedNotificationsDutingAnimation(new int[]{NotificationCenter.chatInfoDidLoaded, NotificationCenter.dialogsNeedReload, @@ -5906,14 +6139,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.getInstance().setAnimationInProgress(false); if (isOpen) { openAnimationEnded = true; - int count = chatListView.getChildCount(); - for (int a = 0; a < count; a++) { - View view = chatListView.getChildAt(a); - if (view instanceof ChatMessageCell) { - ((ChatMessageCell) view).setAllowedToSetPhoto(true); - } - } - if (currentUser != null) { MessagesController.getInstance().loadFullUser(currentUser, classGuid, false); } @@ -5982,21 +6207,30 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not bottomOverlayChatText.setText(LocaleController.getString("DeleteThisChat", R.string.DeleteThisChat)); } } - if (currentChat != null && (ChatObject.isNotInChat(currentChat) || !ChatObject.canWriteToChat(currentChat)) || - currentUser != null && (UserObject.isDeleted(currentUser) || userBlocked)) { - bottomOverlayChat.setVisibility(View.VISIBLE); - muteItem.setVisibility(View.GONE); + + if (searchItem != null && searchItem.getVisibility() == View.VISIBLE) { + searchContainer.setVisibility(View.VISIBLE); + bottomOverlayChat.setVisibility(View.INVISIBLE); chatActivityEnterView.setFieldFocused(false); chatActivityEnterView.setVisibility(View.INVISIBLE); } else { - if (botUser != null && currentUser.bot) { + searchContainer.setVisibility(View.INVISIBLE); + if (currentChat != null && (ChatObject.isNotInChat(currentChat) || !ChatObject.canWriteToChat(currentChat)) || + currentUser != null && (UserObject.isDeleted(currentUser) || userBlocked)) { bottomOverlayChat.setVisibility(View.VISIBLE); + muteItem.setVisibility(View.GONE); + chatActivityEnterView.setFieldFocused(false); chatActivityEnterView.setVisibility(View.INVISIBLE); } else { - chatActivityEnterView.setVisibility(View.VISIBLE); - bottomOverlayChat.setVisibility(View.INVISIBLE); + if (botUser != null && currentUser.bot) { + bottomOverlayChat.setVisibility(View.VISIBLE); + chatActivityEnterView.setVisibility(View.INVISIBLE); + } else { + chatActivityEnterView.setVisibility(View.VISIBLE); + bottomOverlayChat.setVisibility(View.INVISIBLE); + } + muteItem.setVisibility(View.VISIBLE); } - muteItem.setVisibility(View.VISIBLE); } checkRaiseSensors(); } @@ -6012,33 +6246,27 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not alertViewAnimator.cancel(); alertViewAnimator = null; } - if (Build.VERSION.SDK_INT >= 11) { - alertView.setVisibility(View.VISIBLE); - alertViewAnimator = new AnimatorSetProxy(); - alertViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(alertView, "translationY", 0)); - alertViewAnimator.setDuration(200); - alertViewAnimator.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { - if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { - alertView.clearAnimation(); - alertViewAnimator = null; - } - } - @Override - public void onAnimationCancel(Object animation) { - if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { - alertViewAnimator = null; - } + alertView.setVisibility(View.VISIBLE); + alertViewAnimator = new AnimatorSet(); + alertViewAnimator.playTogether(ObjectAnimator.ofFloat(alertView, "translationY", 0)); + alertViewAnimator.setDuration(200); + alertViewAnimator.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { + alertViewAnimator = null; } - }); - alertViewAnimator.start(); - } else { - ViewProxy.setTranslationY(alertView, 0); - alertView.clearAnimation(); - alertView.setVisibility(View.VISIBLE); - } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { + alertViewAnimator = null; + } + } + }); + alertViewAnimator.start(); } alertNameTextView.setText(ContactsController.formatName(user.first_name, user.last_name)); alertTextView.setText(Emoji.replaceEmoji(message.replace('\n', ' '), alertTextView.getPaint().getFontMetricsInt(), AndroidUtilities.dp(14), false)); @@ -6057,33 +6285,26 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not alertViewAnimator.cancel(); alertViewAnimator = null; } - if (Build.VERSION.SDK_INT >= 11) { - alertViewAnimator = new AnimatorSetProxy(); - alertViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(alertView, "translationY", -AndroidUtilities.dp(50))); - alertViewAnimator.setDuration(200); - alertViewAnimator.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { - if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { - alertView.clearAnimation(); - alertView.setVisibility(View.GONE); - alertViewAnimator = null; - } + alertViewAnimator = new AnimatorSet(); + alertViewAnimator.playTogether(ObjectAnimator.ofFloat(alertView, "translationY", -AndroidUtilities.dp(50))); + alertViewAnimator.setDuration(200); + alertViewAnimator.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { + alertView.setVisibility(View.GONE); + alertViewAnimator = null; } + } - @Override - public void onAnimationCancel(Object animation) { - if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { - alertViewAnimator = null; - } + @Override + public void onAnimationCancel(Animator animation) { + if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { + alertViewAnimator = null; } - }); - alertViewAnimator.start(); - } else { - ViewProxy.setTranslationY(alertView, -AndroidUtilities.dp(50)); - alertView.clearAnimation(); - alertView.setVisibility(View.GONE); - } + } + }); + alertViewAnimator.start(); } } }, 3000); @@ -6096,22 +6317,21 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pinnedMessageViewAnimator.cancel(); pinnedMessageViewAnimator = null; } - if (Build.VERSION.SDK_INT >= 11 && animated) { - pinnedMessageViewAnimator = new AnimatorSetProxy(); - pinnedMessageViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(pinnedMessageView, "translationY", -AndroidUtilities.dp(50))); + if (animated) { + pinnedMessageViewAnimator = new AnimatorSet(); + pinnedMessageViewAnimator.playTogether(ObjectAnimator.ofFloat(pinnedMessageView, "translationY", -AndroidUtilities.dp(50))); pinnedMessageViewAnimator.setDuration(200); pinnedMessageViewAnimator.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (pinnedMessageViewAnimator != null && pinnedMessageViewAnimator.equals(animation)) { - pinnedMessageView.clearAnimation(); pinnedMessageView.setVisibility(View.GONE); pinnedMessageViewAnimator = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (pinnedMessageViewAnimator != null && pinnedMessageViewAnimator.equals(animation)) { pinnedMessageViewAnimator = null; } @@ -6119,8 +6339,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not }); pinnedMessageViewAnimator.start(); } else { - ViewProxy.setTranslationY(pinnedMessageView, -AndroidUtilities.dp(50)); - pinnedMessageView.clearAnimation(); + pinnedMessageView.setTranslationY(-AndroidUtilities.dp(50)); pinnedMessageView.setVisibility(View.GONE); } } @@ -6149,22 +6368,21 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pinnedMessageViewAnimator.cancel(); pinnedMessageViewAnimator = null; } - if (Build.VERSION.SDK_INT >= 11 && animated) { + if (animated) { pinnedMessageView.setVisibility(View.VISIBLE); - pinnedMessageViewAnimator = new AnimatorSetProxy(); - pinnedMessageViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(pinnedMessageView, "translationY", 0)); + pinnedMessageViewAnimator = new AnimatorSet(); + pinnedMessageViewAnimator.playTogether(ObjectAnimator.ofFloat(pinnedMessageView, "translationY", 0)); pinnedMessageViewAnimator.setDuration(200); pinnedMessageViewAnimator.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (pinnedMessageViewAnimator != null && pinnedMessageViewAnimator.equals(animation)) { - pinnedMessageView.clearAnimation(); pinnedMessageViewAnimator = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (pinnedMessageViewAnimator != null && pinnedMessageViewAnimator.equals(animation)) { pinnedMessageViewAnimator = null; } @@ -6172,8 +6390,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not }); pinnedMessageViewAnimator.start(); } else { - ViewProxy.setTranslationY(pinnedMessageView, 0); - pinnedMessageView.clearAnimation(); + pinnedMessageView.setTranslationY(0); pinnedMessageView.setVisibility(View.VISIBLE); } } @@ -6220,66 +6437,57 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (!show) { if (reportSpamView.getTag() == null) { reportSpamView.setTag(1); - if (Build.VERSION.SDK_INT >= 11) { - if (reportSpamViewAnimator != null) { - reportSpamViewAnimator.cancel(); - } - reportSpamViewAnimator = new AnimatorSetProxy(); - reportSpamViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(reportSpamView, "translationY", -AndroidUtilities.dp(50))); - reportSpamViewAnimator.setDuration(200); - reportSpamViewAnimator.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { - if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { - reportSpamView.clearAnimation(); - reportSpamView.setVisibility(View.GONE); - reportSpamViewAnimator = null; - } - } - @Override - public void onAnimationCancel(Object animation) { - if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { - reportSpamViewAnimator = null; - } - } - }); - reportSpamViewAnimator.start(); - } else { - reportSpamView.setVisibility(View.GONE); + if (reportSpamViewAnimator != null) { + reportSpamViewAnimator.cancel(); } + reportSpamViewAnimator = new AnimatorSet(); + reportSpamViewAnimator.playTogether(ObjectAnimator.ofFloat(reportSpamView, "translationY", -AndroidUtilities.dp(50))); + reportSpamViewAnimator.setDuration(200); + reportSpamViewAnimator.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { + reportSpamView.setVisibility(View.GONE); + reportSpamViewAnimator = null; + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { + reportSpamViewAnimator = null; + } + } + }); + reportSpamViewAnimator.start(); } } else { if (reportSpamView.getTag() != null) { reportSpamView.setTag(null); - if (Build.VERSION.SDK_INT >= 11) { - reportSpamView.setVisibility(View.VISIBLE); - if (reportSpamViewAnimator != null) { - reportSpamViewAnimator.cancel(); - } - reportSpamViewAnimator = new AnimatorSetProxy(); - reportSpamViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(reportSpamView, "translationY", 0)); - reportSpamViewAnimator.setDuration(200); - reportSpamViewAnimator.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { - if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { - reportSpamView.clearAnimation(); - reportSpamViewAnimator = null; - } - } - - @Override - public void onAnimationCancel(Object animation) { - if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { - reportSpamViewAnimator = null; - } - } - }); - reportSpamViewAnimator.start(); - } else { - reportSpamView.setVisibility(View.VISIBLE); + reportSpamView.setVisibility(View.VISIBLE); + if (reportSpamViewAnimator != null) { + reportSpamViewAnimator.cancel(); } + reportSpamViewAnimator = new AnimatorSet(); + reportSpamViewAnimator.playTogether(ObjectAnimator.ofFloat(reportSpamView, "translationY", 0)); + reportSpamViewAnimator.setDuration(200); + reportSpamViewAnimator.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { + reportSpamViewAnimator = null; + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { + reportSpamViewAnimator = null; + } + } + }); + reportSpamViewAnimator.start(); } } checkListViewPaddings(); @@ -6353,7 +6561,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } private void checkRaiseSensors() { - if (!ApplicationLoader.mainInterfacePaused && (bottomOverlayChat == null || bottomOverlayChat.getVisibility() != View.VISIBLE) && (bottomOverlay == null || bottomOverlay.getVisibility() != View.VISIBLE)) { + if (!ApplicationLoader.mainInterfacePaused && (bottomOverlayChat == null || bottomOverlayChat.getVisibility() != View.VISIBLE) && (bottomOverlay == null || bottomOverlay.getVisibility() != View.VISIBLE) && (searchContainer == null || searchContainer.getVisibility() != View.VISIBLE)) { MediaController.getInstance().setAllowStartRecord(true); } else { MediaController.getInstance().setAllowStartRecord(false); @@ -6416,49 +6624,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } fixLayout(); - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - if (chatActivityEnterView.getFieldText() == null) { - String lastMessageText = preferences.getString("dialog_" + dialog_id, null); - if (lastMessageText != null) { - preferences.edit().remove("dialog_" + dialog_id).commit(); - chatActivityEnterView.setFieldText(lastMessageText); - if (getArguments().getBoolean("hasUrl", false)) { - chatActivityEnterView.setSelection(lastMessageText.indexOf('\n') + 1); - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - if (chatActivityEnterView != null) { - chatActivityEnterView.setFieldFocused(true); - chatActivityEnterView.openKeyboard(); - } - } - }, 700); - } - } - } else { - preferences.edit().remove("dialog_" + dialog_id).commit(); - } - if (replyingMessageObject == null) { - String lastReplyMessage = preferences.getString("reply_" + dialog_id, null); - if (lastReplyMessage != null && lastReplyMessage.length() != 0) { - preferences.edit().remove("reply_" + dialog_id).commit(); - try { - byte[] bytes = Base64.decode(lastReplyMessage, Base64.DEFAULT); - if (bytes != null) { - SerializedData data = new SerializedData(bytes); - TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); - if (message != null) { - replyingMessageObject = new MessageObject(message, MessagesController.getInstance().getUsers(), false); - showReplyPanel(true, replyingMessageObject, null, null, false, false); - } - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } - } - } else { - preferences.edit().remove("reply_" + dialog_id).commit(); - } + applyDraftMaybe(); if (bottomOverlayChat.getVisibility() != View.VISIBLE) { chatActivityEnterView.setFieldFocused(true); } @@ -6483,6 +6649,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatListView.setOnItemClickListener(onItemClickListener); chatListView.setLongClickable(true); } + checkBotCommands(); } @Override @@ -6495,35 +6662,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not paused = true; wasPaused = true; NotificationsController.getInstance().setOpenedDialogId(0); + CharSequence draftMessage = null; + boolean searchWebpage = true; if (chatActivityEnterView != null) { chatActivityEnterView.onPause(); if (!chatActivityEnterView.isEditingMessage()) { - String text = chatActivityEnterView.getFieldText(); - if (text != null && !text.equals("@gif ")) { - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("dialog_" + dialog_id, text); - editor.commit(); + CharSequence text = AndroidUtilities.getTrimmedString(chatActivityEnterView.getFieldText()); + if (!TextUtils.isEmpty(text) && !TextUtils.equals(text, "@gif")) { + draftMessage = text; } } + searchWebpage = chatActivityEnterView.isMessageWebPageSearchEnabled(); chatActivityEnterView.setFieldFocused(false); } - if (replyingMessageObject != null) { - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - try { - SerializedData data = new SerializedData(); - replyingMessageObject.messageOwner.serializeToStream(data); - String string = Base64.encodeToString(data.toByteArray(), Base64.DEFAULT); - if (string.length() != 0) { - editor.putString("reply_" + dialog_id, string); - } - } catch (Exception e) { - editor.remove("reply_" + dialog_id); - FileLog.e("tmessages", e); - } - editor.commit(); - } + DraftQuery.saveDraft(dialog_id, draftMessage, MessagesQuery.getEntities(draftMessage), replyingMessageObject != null ? replyingMessageObject.messageOwner : null, !searchWebpage); MessagesController.getInstance().cancelTyping(0, dialog_id); @@ -6533,6 +6685,58 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } + private void applyDraftMaybe() { + if (chatActivityEnterView == null) { + return; + } + TLRPC.DraftMessage draftMessage = DraftQuery.getDraft(dialog_id); + TLRPC.Message draftReplyMessage = draftMessage != null && draftMessage.reply_to_msg_id != 0 ? DraftQuery.getDraftMessage(dialog_id) : null; + if (chatActivityEnterView.getFieldText() == null) { + if (draftMessage != null) { + chatActivityEnterView.setWebPage(null, !draftMessage.no_webpage); + CharSequence message; + if (!draftMessage.entities.isEmpty()) { + SpannableStringBuilder stringBuilder = SpannableStringBuilder.valueOf(draftMessage.message); + for (int a = 0; a < draftMessage.entities.size(); a++) { + TLRPC.MessageEntity entity = draftMessage.entities.get(a); + if (entity instanceof TLRPC.TL_inputMessageEntityMentionName || entity instanceof TLRPC.TL_messageEntityMentionName) { + int user_id; + if (entity instanceof TLRPC.TL_inputMessageEntityMentionName) { + user_id = ((TLRPC.TL_inputMessageEntityMentionName) entity).user_id.user_id; + } else { + user_id = ((TLRPC.TL_messageEntityMentionName) entity).user_id; + } + if (entity.offset + entity.length < stringBuilder.length() && stringBuilder.charAt(entity.offset + entity.length) == ' ') { + entity.length++; + } + stringBuilder.setSpan(new URLSpanUserMention("" + user_id), entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + message = stringBuilder; + } else { + message = draftMessage.message; + } + chatActivityEnterView.setFieldText(message); + if (getArguments().getBoolean("hasUrl", false)) { + chatActivityEnterView.setSelection(draftMessage.message.indexOf('\n') + 1); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (chatActivityEnterView != null) { + chatActivityEnterView.setFieldFocused(true); + chatActivityEnterView.openKeyboard(); + } + } + }, 700); + } + } + } + if (replyingMessageObject == null && draftReplyMessage != null) { + replyingMessageObject = new MessageObject(draftReplyMessage, MessagesController.getInstance().getUsers(), false); + showReplyPanel(true, replyingMessageObject, null, null, false, false); + } + } + private void updateInformationForScreenshotDetector() { if (currentEncryptedChat == null) { return; @@ -6543,8 +6747,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not for (int a = 0; a < count; a++) { View view = chatListView.getChildAt(a); MessageObject object = null; - if (view instanceof ChatBaseCell) { - ChatBaseCell cell = (ChatBaseCell) view; + if (view instanceof ChatMessageCell) { + ChatMessageCell cell = (ChatMessageCell) view; object = cell.getMessageObject(); } if (object != null && object.getId() < 0 && object.messageOwner.random_id != 0) { @@ -6558,10 +6762,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private boolean fixLayoutInternal() { if (!AndroidUtilities.isTablet() && ApplicationLoader.applicationContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { selectedMessagesCountTextView.setTextSize(18); - actionModeTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); } else { selectedMessagesCountTextView.setTextSize(20); - actionModeTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20); } if (AndroidUtilities.isTablet()) { @@ -6601,76 +6803,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not fixLayout(); } - private void switchImportantMode(MessageObject searchBeforeMessage) { - int count = chatListView.getChildCount(); - MessageObject messageObject = null; - if (searchBeforeMessage == null) { - for (int a = 0; a <= count; a++) { - View child = chatListView.getChildAt(a); - MessageObject message = null; - if (child instanceof ChatBaseCell) { - message = ((ChatBaseCell) child).getMessageObject(); - } else if (child instanceof ChatActionCell) { - message = ((ChatActionCell) child).getMessageObject(); - } - if (message != null && message.getId() > 0) { - if (message.isImportant()) { - messageObject = message; - break; - } else if (searchBeforeMessage == null) { - searchBeforeMessage = message; - } - } - } - } - - if (messageObject == null) { - int index = messages.indexOf(searchBeforeMessage); - if (index >= 0) { - for (int a = index + 1; a < messages.size(); a++) { - MessageObject message = messages.get(a); - if (message.getId() > 0 && message.isImportant()) { - messageObject = message; - break; - } - } - } - } - - if (messageObject != null) { - scrollToMessagePosition = -10000; - for (int a = 0; a < count; a++) { - View child = chatListView.getChildAt(a); - MessageObject message = null; - if (child instanceof ChatBaseCell) { - message = ((ChatBaseCell) child).getMessageObject(); - } else if (child instanceof ChatActionCell) { - message = ((ChatActionCell) child).getMessageObject(); - } - if (message == messageObject) { - scrollToMessagePosition = child.getTop() + AndroidUtilities.dp(7); - break; - } - } - if (scrollToMessagePosition == -10000) { - scrollToMessagePosition = chatListView.getPaddingTop(); - } - } - - avatarContainer.setRadioChecked(!avatarContainer.isRadioChecked(), true); - channelMessagesImportant = avatarContainer.isRadioChecked() ? 1 : 2; - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - preferences.edit().putInt("important_" + dialog_id, channelMessagesImportant).commit(); - waitingForImportantLoad = true; - waitingForLoad.add(lastLoadIndex); - if (messageObject != null) { - startLoadFromMessageId = messageObject.getId(); - MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, channelMessagesImportant, lastLoadIndex++); - } else { - MessagesController.getInstance().loadMessages(dialog_id, 30, 0, true, 0, classGuid, 0, 0, channelMessagesImportant, lastLoadIndex++); - } - } - private void createDeleteMessagesAlert(final MessageObject finalSelectedObject) { AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); builder.setMessage(LocaleController.formatString("AreYouSureDeleteMessages", R.string.AreYouSureDeleteMessages, LocaleController.formatPluralString("messages", finalSelectedObject != null ? 1 : selectedMessagesIds[0].size() + selectedMessagesIds[1].size()))); @@ -6714,9 +6846,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not CheckBoxCell cell = new CheckBoxCell(getParentActivity()); cell.setBackgroundResource(R.drawable.list_selector); cell.setTag(a); - if (Build.VERSION.SDK_INT < 11) { - cell.setTextColor(0xffffffff); - } if (a == 0) { cell.setText(LocaleController.getString("DeleteBanUser", R.string.DeleteBanUser), "", false, false); } else if (a == 1) { @@ -6812,8 +6941,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } MessageObject message = null; - if (v instanceof ChatBaseCell) { - message = ((ChatBaseCell) v).getMessageObject(); + if (v instanceof ChatMessageCell) { + message = ((ChatMessageCell) v).getMessageObject(); } else if (v instanceof ChatActionCell) { message = ((ChatActionCell) v).getMessageObject(); } @@ -6821,10 +6950,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } final int type = getMessageType(message); - if (channelMessagesImportant == 2 && message.getId() == 0 && message.contentType == 1 && message.type == 10 && message.messageOwner.from_id != 0) { - switchImportantMode(message); - return; - } if (single && message.messageOwner.action instanceof TLRPC.TL_messageActionPinMessage) { scrollToMessageId(message.messageOwner.reply_to_msg_id, 0, true, 0); return; @@ -6843,7 +6968,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not 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 allowEdit = message.canEditMessage(currentChat) && !chatActivityEnterView.hasAudioToSend(); + 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)) { allowChatActions = false; } @@ -7052,32 +7177,130 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (item != null) { item.setVisibility(View.VISIBLE); } - item = actionMode.getItem(edit_done); - if (item != null) { - item.setVisibility(View.GONE); + if (editDoneItem != null) { + editDoneItem.setVisibility(View.GONE); } actionBar.showActionMode(); updatePinnedMessageView(true); - if (Build.VERSION.SDK_INT >= 11) { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - ArrayList animators = new ArrayList<>(); - for (int a = 0; a < actionModeViews.size(); a++) { - View view = actionModeViews.get(a); - AndroidUtilities.clearDrawableAnimation(view); - animators.add(ObjectAnimatorProxy.ofFloat(view, "scaleY", 0.1f, 1.0f)); - } - animatorSet.playTogether(animators); - animatorSet.setDuration(250); - animatorSet.start(); + AnimatorSet animatorSet = new AnimatorSet(); + ArrayList animators = new ArrayList<>(); + for (int a = 0; a < actionModeViews.size(); a++) { + View view = actionModeViews.get(a); + AndroidUtilities.clearDrawableAnimation(view); + animators.add(ObjectAnimator.ofFloat(view, "scaleY", 0.1f, 1.0f)); } + animatorSet.playTogether(animators); + animatorSet.setDuration(250); + animatorSet.start(); addToSelectedMessages(message); selectedMessagesCountTextView.setNumber(1, false); updateVisibleRows(); } + private void showEditDoneProgress(final boolean show, boolean animated) { + if (editDoneItemAnimation != null) { + editDoneItemAnimation.cancel(); + } + if (!animated) { + if (show) { + editDoneItem.getImageView().setScaleX(0.1f); + editDoneItem.getImageView().setScaleY(0.1f); + editDoneItem.getImageView().setAlpha(0.0f); + editDoneItemProgress.setScaleX(1.0f); + editDoneItemProgress.setScaleY(1.0f); + editDoneItemProgress.setAlpha(1.0f); + editDoneItem.getImageView().setVisibility(View.INVISIBLE); + editDoneItemProgress.setVisibility(View.VISIBLE); + editDoneItem.setEnabled(false); + } else { + editDoneItemProgress.setScaleX(0.1f); + editDoneItemProgress.setScaleY(0.1f); + editDoneItemProgress.setAlpha(0.0f); + editDoneItem.getImageView().setScaleX(1.0f); + editDoneItem.getImageView().setScaleY(1.0f); + editDoneItem.getImageView().setAlpha(1.0f); + editDoneItem.getImageView().setVisibility(View.VISIBLE); + editDoneItemProgress.setVisibility(View.INVISIBLE); + editDoneItem.setEnabled(true); + } + } else { + editDoneItemAnimation = new AnimatorSet(); + if (show) { + editDoneItemProgress.setVisibility(View.VISIBLE); + editDoneItem.setEnabled(false); + editDoneItemAnimation.playTogether( + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "scaleX", 0.1f), + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "scaleY", 0.1f), + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "alpha", 0.0f), + ObjectAnimator.ofFloat(editDoneItemProgress, "scaleX", 1.0f), + ObjectAnimator.ofFloat(editDoneItemProgress, "scaleY", 1.0f), + ObjectAnimator.ofFloat(editDoneItemProgress, "alpha", 1.0f)); + } else { + editDoneItem.getImageView().setVisibility(View.VISIBLE); + editDoneItem.setEnabled(true); + editDoneItemAnimation.playTogether( + ObjectAnimator.ofFloat(editDoneItemProgress, "scaleX", 0.1f), + ObjectAnimator.ofFloat(editDoneItemProgress, "scaleY", 0.1f), + ObjectAnimator.ofFloat(editDoneItemProgress, "alpha", 0.0f), + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "scaleX", 1.0f), + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "scaleY", 1.0f), + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "alpha", 1.0f)); + + } + editDoneItemAnimation.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (editDoneItemAnimation != null && editDoneItemAnimation.equals(animation)) { + if (!show) { + editDoneItemProgress.setVisibility(View.INVISIBLE); + } else { + editDoneItem.getImageView().setVisibility(View.INVISIBLE); + } + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (editDoneItemAnimation != null && editDoneItemAnimation.equals(animation)) { + editDoneItemAnimation = null; + } + } + }); + editDoneItemAnimation.setDuration(150); + editDoneItemAnimation.start(); + } + } + + 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 void processSelectedOption(int option) { if (selectedObject == null) { return; @@ -7108,26 +7331,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not break; } case 3: { - try { - CharSequence str; - if (selectedObject.type == 0 && selectedObject.messageOwner.message != null) { - str = selectedObject.messageOwner.message; - } else if (selectedObject.messageOwner.media != null && selectedObject.messageOwner.media.caption != null) { - str = selectedObject.messageOwner.media.caption; - } else { - str = selectedObject.messageText; - } - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(str); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", str); - clipboard.setPrimaryClip(clip); - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } + AndroidUtilities.addToClipboard(getMessageContent(selectedObject, 0, false)); break; } case 4: { @@ -7260,77 +7464,67 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not selectedObject = null; return; } - final MessageObject editingMessageObject = selectedObject; - final ProgressDialog progressDialog = new ProgressDialog(getParentActivity()); - progressDialog.setMessage(LocaleController.getString("Loading", R.string.Loading)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.setCancelable(false); + if (searchItem != null && actionBar.isSearchFieldVisible()) { + actionBar.closeSearchField(); + chatActivityEnterView.setFieldFocused(); + } + + mentionsAdapter.setNeedBotContext(false); + chatListView.setOnItemLongClickListener(null); + chatListView.setOnItemClickListener(null); + chatListView.setClickable(false); + chatListView.setLongClickable(false); + chatActivityEnterView.setEditingMessageObject(selectedObject, !selectedObject.isMediaEmpty()); + if (chatActivityEnterView.isEditingCaption()) { + mentionsAdapter.setAllowNewMentions(false); + } + actionModeTitleContainer.setVisibility(View.VISIBLE); + selectedMessagesCountTextView.setVisibility(View.GONE); + checkEditTimer(); + + chatActivityEnterView.setAllowStickersAndGifs(false, false); + final ActionBarMenu actionMode = actionBar.createActionMode(); + actionMode.getItem(reply).setVisibility(View.GONE); + actionMode.getItem(copy).setVisibility(View.GONE); + actionMode.getItem(forward).setVisibility(View.GONE); + actionMode.getItem(delete).setVisibility(View.GONE); + if (editDoneItemAnimation != null) { + editDoneItemAnimation.cancel(); + editDoneItemAnimation = null; + } + editDoneItem.setVisibility(View.VISIBLE); + showEditDoneProgress(true, false); + actionBar.showActionMode(); + updatePinnedMessageView(true); + updateVisibleRows(); TLRPC.TL_messages_getMessageEditData req = new TLRPC.TL_messages_getMessageEditData(); req.peer = MessagesController.getInputPeer((int) dialog_id); req.id = selectedObject.getId(); - final int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + editingMessageObjectReqId = 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 { - if (!getParentActivity().isFinishing()) { - progressDialog.dismiss(); - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } - if (response != null) { - TLRPC.TL_messages_messageEditData res = (TLRPC.TL_messages_messageEditData) response; - if (mentionsAdapter != null) { - mentionsAdapter.setNeedBotContext(false); - chatListView.setOnItemLongClickListener(null); - chatListView.setOnItemClickListener(null); - chatListView.setClickable(false); - chatListView.setLongClickable(false); - chatActivityEnterView.setEditinigMessageObject(editingMessageObject, res.caption); - actionModeTextView.setVisibility(View.VISIBLE); - selectedMessagesCountTextView.setVisibility(View.GONE); - - chatActivityEnterView.setAllowStickersAndGifs(false, false); - final ActionBarMenu actionMode = actionBar.createActionMode(); - actionMode.getItem(reply).setVisibility(View.GONE); - actionMode.getItem(copy).setVisibility(View.GONE); - actionMode.getItem(forward).setVisibility(View.GONE); - actionMode.getItem(delete).setVisibility(View.GONE); - actionMode.getItem(edit_done).setVisibility(View.VISIBLE); - actionBar.showActionMode(); - updatePinnedMessageView(true); - } - } else { + editingMessageObjectReqId = 0; + if (response == null) { AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); builder.setMessage(LocaleController.getString("EditMessageError", R.string.EditMessageError)); builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); showDialog(builder.create()); + + if (chatActivityEnterView != null) { + chatActivityEnterView.setEditingMessageObject(null, false); + } + } else { + showEditDoneProgress(false, true); } } }); } }); - progressDialog.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("tmessages", e); - } - } - }); - try { - progressDialog.show(); - } catch (Exception e) { - //don't promt - } break; } case 13: { @@ -7345,9 +7539,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } CheckBoxCell cell = new CheckBoxCell(getParentActivity()); cell.setBackgroundResource(R.drawable.list_selector); - if (Build.VERSION.SDK_INT < 11) { - cell.setTextColor(0xffffffff); - } cell.setText(LocaleController.getString("PinNotify", R.string.PinNotify), "", true, false); cell.setPadding(LocaleController.isRTL ? AndroidUtilities.dp(8) : 0, 0, LocaleController.isRTL ? 0 : AndroidUtilities.dp(8), 0); frameLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.TOP | Gravity.LEFT, 8, 0, 8, 0)); @@ -7394,18 +7585,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not break; } case 16: { - try { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(selectedObject.messageOwner.media.phone_number); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", selectedObject.messageOwner.media.phone_number); - clipboard.setPrimaryClip(clip); - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } + AndroidUtilities.addToClipboard(selectedObject.messageOwner.media.phone_number); break; } case 17: { @@ -7494,7 +7674,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not selectedMessagesIds[a].clear(); selectedMessagesCanCopyIds[a].clear(); } - chatActivityEnterView.setEditinigMessageObject(null, false); + chatActivityEnterView.setEditingMessageObject(null, false); actionBar.hideActionMode(); updatePinnedMessageView(true); cantDeleteMessagesCount = 0; @@ -7512,17 +7692,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } int count = chatListView.getChildCount(); + MessageObject editingMessageObject = chatActivityEnterView != null ? chatActivityEnterView.getEditingMessageObject() : null; for (int a = 0; a < count; a++) { View view = chatListView.getChildAt(a); - if (view instanceof ChatBaseCell) { - ChatBaseCell cell = (ChatBaseCell) view; + if (view instanceof ChatMessageCell) { + ChatMessageCell cell = (ChatMessageCell) view; boolean disableSelection = false; boolean selected = false; if (actionBar.isActionModeShowed()) { MessageObject messageObject = cell.getMessageObject(); - if (selectedMessagesIds[messageObject.getDialogId() == dialog_id ? 0 : 1].containsKey(messageObject.getId())) { - view.setBackgroundColor(0x6633b5e5); + if (messageObject == editingMessageObject || selectedMessagesIds[messageObject.getDialogId() == dialog_id ? 0 : 1].containsKey(messageObject.getId())) { + view.setBackgroundColor(Theme.MSG_SELECTED_BACKGROUND_COLOR); selected = true; } else { view.setBackgroundColor(0); @@ -7535,6 +7716,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not cell.setMessageObject(cell.getMessageObject()); cell.setCheckPressed(!disableSelection, disableSelection && selected); cell.setHighlighted(highlightMessageId != Integer.MAX_VALUE && cell.getMessageObject() != null && cell.getMessageObject().getId() == highlightMessageId); + if (searchContainer != null && searchContainer.getVisibility() == View.VISIBLE && MessagesSearchQuery.getLastSearchQuery() != null) { + cell.setHighlightedText(MessagesSearchQuery.getLastSearchQuery()); + } else { + cell.setHighlightedText(null); + } } else if (view instanceof ChatActionCell) { ChatActionCell cell = (ChatActionCell) view; cell.setMessageObject(cell.getMessageObject()); @@ -7542,6 +7728,38 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } + private void checkEditTimer() { + if (chatActivityEnterView == null) { + return; + } + MessageObject messageObject = chatActivityEnterView.getEditingMessageObject(); + if (messageObject == null) { + return; + } + int dt = MessagesController.getInstance().maxEditTime + 5 * 60 - Math.abs(ConnectionsManager.getInstance().getCurrentTime() - messageObject.messageOwner.date); + if (dt > 0) { + if (dt > 5 * 60) { + if (actionModeSubTextView.getVisibility() != View.GONE) { + actionModeSubTextView.setVisibility(View.GONE); + } + } else { + if (actionModeSubTextView.getVisibility() != View.VISIBLE) { + actionModeSubTextView.setVisibility(View.VISIBLE); + } + actionModeSubTextView.setText(LocaleController.formatString("TimeToEdit", R.string.TimeToEdit, String.format("%d:%02d", dt / 60, dt % 60))); + } + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + checkEditTimer(); + } + }, 1000); + } else { + editDoneItem.setVisibility(View.GONE); + actionModeSubTextView.setText(LocaleController.formatString("TimeToEditExpired", R.string.TimeToEditExpired)); + } + } + private ArrayList createVoiceMessagesPlaylist(MessageObject startMessageObject, boolean playingUnreadMedia) { ArrayList messageObjects = new ArrayList<>(); messageObjects.add(startMessageObject); @@ -7577,12 +7795,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not avatarContainer.setVisibility(View.GONE); headerItem.setVisibility(View.GONE); attachItem.setVisibility(View.GONE); - if (searchUpItem.getVisibility() != View.VISIBLE) { - searchItem.setVisibility(View.VISIBLE); - searchUpItem.setVisibility(View.VISIBLE); - searchDownItem.setVisibility(View.VISIBLE); - } - updateSearchButtons(0); + searchItem.setVisibility(View.VISIBLE); + updateSearchButtons(0, 0, 0); + updateBottomOverlay(); openSearchKeyboard = text == null; searchItem.openSearch(openSearchKeyboard); if (text != null) { @@ -7619,32 +7834,40 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public PhotoViewer.PlaceProviderObject getPlaceForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { - if (messageObject == null) { - return null; - } int count = chatListView.getChildCount(); for (int a = 0; a < count; a++) { - MessageObject messageToOpen = null; ImageReceiver imageReceiver = null; View view = chatListView.getChildAt(a); - if (view instanceof ChatBaseCell) { - ChatBaseCell cell = (ChatBaseCell) view; - MessageObject message = cell.getMessageObject(); - if (message != null && message.getId() == messageObject.getId()) { - messageToOpen = message; - imageReceiver = cell.getPhotoImage(); + 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 && message.getId() == messageObject.getId()) { - messageToOpen = message; - imageReceiver = cell.getPhotoImage(); + if (message != null) { + if (messageObject != null) { + if (message.getId() == messageObject.getId()) { + imageReceiver = cell.getPhotoImage(); + } + } else if (fileLocation != null && message.photoThumbs != null) { + for (int b = 0; b < message.photoThumbs.size(); b++) { + TLRPC.PhotoSize photoSize = message.photoThumbs.get(b); + if (photoSize.location.volume_id == fileLocation.volume_id && photoSize.location.local_id == fileLocation.local_id) { + imageReceiver = cell.getPhotoImage(); + break; + } + } + } } } - if (messageToOpen != null) { + if (imageReceiver != null) { int coords[] = new int[2]; view.getLocationInWindow(coords); PhotoViewer.PlaceProviderObject object = new PhotoViewer.PlaceProviderObject(); @@ -7654,6 +7877,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not object.imageReceiver = imageReceiver; object.thumb = imageReceiver.getBitmap(); object.radius = imageReceiver.getRoundRadius(); + if (view instanceof ChatActionCell && currentChat != null) { + object.dialogId = -currentChat.id; + } if (pinnedMessageView != null && pinnedMessageView.getTag() == null || reportSpamView != null && reportSpamView.getTag() == null) { object.clipTopAddition = AndroidUtilities.dp(48); } @@ -7699,6 +7925,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return 0; } + public void sendPhoto(MediaController.PhotoEntry photoEntry) { + if (photoEntry.imagePath != null) { + SendMessagesHelper.prepareSendingPhoto(photoEntry.imagePath, null, dialog_id, replyingMessageObject, photoEntry.caption); + showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); + } else if (photoEntry.path != null) { + SendMessagesHelper.prepareSendingPhoto(photoEntry.path, null, dialog_id, replyingMessageObject, photoEntry.caption); + showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); + } + } + public void showOpenUrlAlert(final String url) { if (Browser.isInternalUrl(url)) { Browser.openUrl(getParentActivity(), url, inlineReturn == 0); @@ -7717,6 +7955,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } + 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; @@ -7790,9 +8039,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not view = new ChatMessageCell(mContext); } ChatMessageCell chatMessageCell = (ChatMessageCell) view; - chatMessageCell.setDelegate(new ChatBaseCell.ChatBaseCellDelegate() { + chatMessageCell.setDelegate(new ChatMessageCell.ChatMessageCellDelegate() { @Override - public void didPressedShare(ChatBaseCell cell) { + public void didPressedShare(ChatMessageCell cell) { if (getParentActivity() == null) { return; } @@ -7815,7 +8064,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void didPressedChannelAvatar(ChatBaseCell cell, TLRPC.Chat chat, int postId) { + public void didPressedChannelAvatar(ChatMessageCell cell, TLRPC.Chat chat, int postId) { if (actionBar.isActionModeShowed()) { processRowSelect(cell); return; @@ -7833,12 +8082,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void didPressedOther(ChatBaseCell cell) { + public void didPressedOther(ChatMessageCell cell) { createMenu(cell, true); } @Override - public void didPressedUserAvatar(ChatBaseCell cell, TLRPC.User user) { + public void didPressedUserAvatar(ChatMessageCell cell, TLRPC.User user) { if (actionBar.isActionModeShowed()) { processRowSelect(cell); return; @@ -7853,7 +8102,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void didPressedBotButton(ChatBaseCell cell, TLRPC.KeyboardButton button) { + public void didPressedBotButton(ChatMessageCell cell, TLRPC.KeyboardButton button) { if (getParentActivity() == null || bottomOverlayChat.getVisibility() == View.VISIBLE && !(button instanceof TLRPC.TL_keyboardButtonCallback) && !(button instanceof TLRPC.TL_keyboardButtonUrl)) { return; } @@ -7861,7 +8110,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void didPressedCancelSendButton(ChatBaseCell cell) { + public void didPressedCancelSendButton(ChatMessageCell cell) { MessageObject message = cell.getMessageObject(); if (message.messageOwner.send_state != 0) { SendMessagesHelper.getInstance().cancelSendingMessage(message); @@ -7869,7 +8118,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void didLongPressed(ChatBaseCell cell) { + public void didLongPressed(ChatMessageCell cell) { createMenu(cell, false); } @@ -7883,7 +8132,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (url == null) { return; } - if (url instanceof URLSpanNoUnderline) { + if (url instanceof URLSpanUserMention) { + TLRPC.User user = MessagesController.getInstance().getUser(Utilities.parseInt(((URLSpanUserMention) url).getURL())); + if (user != null) { + MessagesController.openChatOrProfileWith(user, null, ChatActivity.this, 0); + } + } else if (url instanceof URLSpanNoUnderline) { String str = ((URLSpanNoUnderline) url).getURL(); if (str.startsWith("@")) { MessagesController.openByUserName(str.substring(1), ChatActivity.this, 0); @@ -7911,19 +8165,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (which == 0) { Browser.openUrl(getParentActivity(), urlFinal, inlineReturn == 0); } else if (which == 1) { - try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(urlFinal); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", urlFinal); - clipboard.setPrimaryClip(clip); - } - Toast.makeText(getParentActivity(), LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - FileLog.e("tmessages", e); - } + AndroidUtilities.addToClipboard(urlFinal); } } }); @@ -7949,17 +8191,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void didPressedReplyMessage(ChatBaseCell cell, int id) { + public void didPressedReplyMessage(ChatMessageCell cell, int id) { MessageObject messageObject = cell.getMessageObject(); - if (messageObject.replyMessageObject != null && !messageObject.replyMessageObject.isImportant() && channelMessagesImportant == 2) { - channelMessagesImportant = 1; - avatarContainer.setRadioChecked(true, false); - } scrollToMessageId(id, messageObject.getId(), true, messageObject.getDialogId() == mergeDialogId ? 1 : 0); } @Override - public void didPressedViaBot(ChatBaseCell cell, String username) { + public void didPressedViaBot(ChatMessageCell cell, String username) { if (bottomOverlayChat != null && bottomOverlayChat.getVisibility() == View.VISIBLE || bottomOverlay != null && bottomOverlay.getVisibility() == View.VISIBLE) { return; } @@ -7970,7 +8208,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void didPressedImage(ChatBaseCell cell) { + public void didPressedImage(ChatMessageCell cell) { MessageObject message = cell.getMessageObject(); if (message.isSendError()) { createMenu(cell, false); @@ -7980,7 +8218,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (message.type == 13) { showDialog(new StickersAlert(getParentActivity(), message.getInputStickerSet(), null, bottomOverlayChat.getVisibility() != View.VISIBLE ? chatActivityEnterView : null)); - } else if (message.type == 1 || message.type == 0 && !message.isWebpageDocument()) { + } else if (Build.VERSION.SDK_INT >= 16 && message.isVideo() || message.type == 1 || message.type == 0 && !message.isWebpageDocument() || message.isGif()) { PhotoViewer.getInstance().setParentActivity(getParentActivity()); PhotoViewer.getInstance().openPhoto(message, message.type != 0 ? dialog_id : 0, message.type != 0 ? mergeDialogId : 0, ChatActivity.this); } else if (message.type == 3) { @@ -8016,57 +8254,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } } else if (message.type == 9 || message.type == 0) { - File f = null; - String fileName = message.getFileName(); - if (message.messageOwner.attachPath != null && message.messageOwner.attachPath.length() != 0) { - f = new File(message.messageOwner.attachPath); - } - if (f == null || !f.exists()) { - f = FileLoader.getPathToMessage(message.messageOwner); - } - if (f != null && f.exists()) { - String realMimeType = null; - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - MimeTypeMap myMime = MimeTypeMap.getSingleton(); - int idx = fileName.lastIndexOf('.'); - if (idx != -1) { - String ext = fileName.substring(idx + 1); - realMimeType = myMime.getMimeTypeFromExtension(ext.toLowerCase()); - if (realMimeType == null) { - if (message.type == 9 || message.type == 0) { - realMimeType = message.getDocument().mime_type; - } - if (realMimeType == null || realMimeType.length() == 0) { - realMimeType = null; - } - } - if (realMimeType != null) { - intent.setDataAndType(Uri.fromFile(f), realMimeType); - } else { - intent.setDataAndType(Uri.fromFile(f), "text/plain"); - } - } else { - intent.setDataAndType(Uri.fromFile(f), "text/plain"); - } - if (realMimeType != null) { - try { - getParentActivity().startActivityForResult(intent, 500); - } catch (Exception e) { - intent.setDataAndType(Uri.fromFile(f), "text/plain"); - getParentActivity().startActivityForResult(intent, 500); - } - } else { - getParentActivity().startActivityForResult(intent, 500); - } - } catch (Exception e) { - alertUserOpenError(message); - } + try { + AndroidUtilities.openForView(message, getParentActivity()); + } catch (Exception e) { + alertUserOpenError(message); } } } }); - chatMessageCell.setAllowedToSetPhoto(openAnimationEnded); if (currentEncryptedChat == null) { chatMessageCell.setAllowAssistant(true); } @@ -8077,7 +8272,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not public void didClickedImage(ChatActionCell cell) { MessageObject message = cell.getMessageObject(); PhotoViewer.getInstance().setParentActivity(getParentActivity()); - PhotoViewer.getInstance().openPhoto(message, 0, 0, ChatActivity.this); + TLRPC.PhotoSize photoSize = FileLoader.getClosestPhotoSizeWithSize(message.photoThumbs, 640); + if (photoSize != null) { + PhotoViewer.getInstance().openPhoto(photoSize.location, ChatActivity.this); + } else { + PhotoViewer.getInstance().openPhoto(message, 0, 0, ChatActivity.this); + } } @Override @@ -8145,8 +8345,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not boolean selected = false; boolean disableSelection = false; if (actionBar.isActionModeShowed()) { - if (selectedMessagesIds[message.getDialogId() == dialog_id ? 0 : 1].containsKey(message.getId())) { - view.setBackgroundColor(0x6633b5e5); + MessageObject messageObject = chatActivityEnterView != null ? chatActivityEnterView.getEditingMessageObject() : null; + if (messageObject == message || selectedMessagesIds[message.getDialogId() == dialog_id ? 0 : 1].containsKey(message.getId())) { + view.setBackgroundColor(Theme.MSG_SELECTED_BACKGROUND_COLOR); selected = true; } else { view.setBackgroundColor(0); @@ -8165,6 +8366,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not ((ChatMessageCell) view).downloadAudioIfNeed(); } messageCell.setHighlighted(highlightMessageId != Integer.MAX_VALUE && message.getId() == highlightMessageId); + if (searchContainer != null && searchContainer.getVisibility() == View.VISIBLE && MessagesSearchQuery.getLastSearchQuery() != null) { + messageCell.setHighlightedText(MessagesSearchQuery.getLastSearchQuery()); + } else { + messageCell.setHighlightedText(null); + } } else if (view instanceof ChatActionCell) { ChatActionCell actionCell = (ChatActionCell) view; actionCell.setMessageObject(message); @@ -8193,8 +8399,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public boolean onPreDraw() { messageCell.getViewTreeObserver().removeOnPreDrawListener(this); - messageCell.getLocalVisibleRect(scrollRect); - messageCell.setVisiblePart(scrollRect.top, scrollRect.bottom - scrollRect.top); + + int height = chatListView.getMeasuredHeight(); + int top = messageCell.getTop(); + int bottom = messageCell.getBottom(); + int viewTop = top >= 0 ? 0 : -top; + int viewBottom = messageCell.getMeasuredHeight(); + if (viewBottom > height) { + viewBottom = viewTop + height; + } + messageCell.setVisiblePart(viewTop, viewBottom - viewTop); + return true; } }); @@ -8210,15 +8425,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not notifyItemChanged(messagesStartRow + messages.size() - index - 1); } - public void removeMessageObject(MessageObject messageObject) { - int index = messages.indexOf(messageObject); - if (index == -1) { - return; - } - messages.remove(index); - notifyItemRemoved(messagesStartRow + messages.size() - index - 1); - } - @Override public void notifyDataSetChanged() { updateRows(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java index c9c17d729..93ffa940c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java @@ -74,7 +74,7 @@ public class AlertsCreator { NotificationsController.getInstance().removeNotificationsForDialog(dialog_id); MessagesStorage.getInstance().setDialogFlags(dialog_id, flags); editor.commit(); - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); dialog.notify_settings.mute_until = untilTime; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java index 649694d7e..90a639fb4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java @@ -47,6 +47,7 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private boolean destroyWhenDone; private boolean decoderCreated; private File path; + private boolean recycleWithSecond; private BitmapShader renderingShader; private BitmapShader nextRenderingShader; @@ -68,11 +69,14 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2, new ThreadPoolExecutor.DiscardPolicy()); private View parentView = null; + private View secondParentView = null; protected final Runnable mInvalidateTask = new Runnable() { @Override public void run() { - if (parentView != null) { + if (secondParentView != null) { + secondParentView.invalidate(); + } else if (parentView != null) { parentView.invalidate(); } } @@ -102,7 +106,9 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { invalidateAfter = metaData[2] - lastTimeStamp; } lastTimeStamp = metaData[2]; - if (parentView != null) { + if (secondParentView != null) { + secondParentView.invalidate(); + } else if (parentView != null) { parentView.invalidate(); } } @@ -141,7 +147,9 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private final Runnable mStartTask = new Runnable() { @Override public void run() { - if (parentView != null) { + if (secondParentView != null) { + secondParentView.invalidate(); + } else if (parentView != null) { parentView.invalidate(); } } @@ -163,7 +171,18 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { parentView = view; } + public void setSecondParentView(View view) { + secondParentView = view; + if (view == null && recycleWithSecond) { + recycle(); + } + } + public void recycle() { + if (secondParentView != null) { + recycleWithSecond = true; + return; + } isRunning = false; isRecycled = true; if (loadFrameTask == null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java index 92884aa38..0da8100e2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java @@ -9,6 +9,9 @@ package org.telegram.ui.Components; import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; @@ -28,6 +31,7 @@ import android.text.Editable; import android.text.InputFilter; import android.text.Layout; import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; @@ -62,14 +66,13 @@ import org.telegram.messenger.SendMessagesHelper; import org.telegram.messenger.FileLog; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; +import org.telegram.messenger.query.DraftQuery; +import org.telegram.messenger.query.MessagesQuery; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.TLRPC; import org.telegram.messenger.UserConfig; import org.telegram.ui.ActionBar.ActionBar; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.ApplicationLoader; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ChatActivity; @@ -81,17 +84,17 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Locale; -public class ChatActivityEnterView extends FrameLayoutFixed implements NotificationCenter.NotificationCenterDelegate, SizeNotifierFrameLayout.SizeNotifierFrameLayoutDelegate, StickersAlert.StickersAlertDelegate { +public class ChatActivityEnterView extends FrameLayout implements NotificationCenter.NotificationCenterDelegate, SizeNotifierFrameLayout.SizeNotifierFrameLayoutDelegate, StickersAlert.StickersAlertDelegate { public interface ChatActivityEnterViewDelegate { - void onMessageSend(String message); + void onMessageSend(CharSequence message); void needSendTyping(); void onTextChanged(CharSequence text, boolean bigChange); void onAttachButtonHidden(); void onAttachButtonShow(); void onWindowSizeChanged(int size); void onStickersTab(boolean opened); - void onMessageEditEnd(); + void onMessageEditEnd(boolean loading); } private class SeekBarWaveformView extends View { @@ -105,8 +108,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat seekBarWaveform.setDelegate(new SeekBar.SeekBarDelegate() { @Override public void onSeekBarDrag(float progress) { - audioToSendMessageObject.audioProgress = progress; - MediaController.getInstance().seekToProgress(audioToSendMessageObject, progress); + if (audioToSendMessageObject != null) { + audioToSendMessageObject.audioProgress = progress; + MediaController.getInstance().seekToProgress(audioToSendMessageObject, progress); + } } }); } @@ -177,7 +182,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat mCursorDrawableField.setAccessible(true); mCursorDrawable = (Drawable[]) mCursorDrawableField.get(editor); } catch (Throwable e) { - FileLog.e("tmessages", e); + // } } @@ -195,7 +200,14 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat @SuppressLint("DrawAllocation") @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); + try { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } catch (Exception e) { + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), AndroidUtilities.dp(51)); + FileLog.e("tmessages", e); + } + + captionLayout = null; if (caption != null && caption.length() > 0) { @@ -264,7 +276,12 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat showPopup(AndroidUtilities.usingHardwareInput ? 0 : 2, 0); openKeyboardInternal(); } - return super.onTouchEvent(event); + try { + return super.onTouchEvent(event); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + return false; } } @@ -290,13 +307,13 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat private View topView; private PopupWindow botKeyboardPopup; private BotKeyboardView botKeyboardView; - private ImageView asAdminButton; private ImageView notifyButton; private RecordCircle recordCircle; - private ContextProgressView contextProgressView; private CloseProgressDrawable2 progressDrawable; + private Drawable backgroundDrawable; private MessageObject editingMessageObject; + private int editingMessageReqId; private boolean editingCaption; private int currentPopupContentType = -1; @@ -304,10 +321,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat private boolean silent; private boolean canWriteToChannel; - private boolean isAsAdmin; - private boolean adminModeAvailable; - - private boolean isPaused; + private boolean isPaused = true; private boolean showKeyboardOnResume; private MessageObject botButtonsMessageObject; @@ -316,9 +330,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat private boolean hasBotCommands; private PowerManager.WakeLock mWakeLock; - private AnimatorSetProxy runningAnimation; - private AnimatorSetProxy runningAnimation2; - private AnimatorSetProxy runningAnimationAudio; + private AnimatorSet runningAnimation; + private AnimatorSet runningAnimation2; + private AnimatorSet runningAnimationAudio; private int runningAnimationType; private int audioInterfaceState; @@ -354,11 +368,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat private String audioToSendPath; private MessageObject audioToSendMessageObject; - private float topViewAnimation; private boolean topViewShowed; private boolean needShowTopView; private boolean allowShowTopView; - private AnimatorSetProxy currentTopViewAnimation; + private AnimatorSet currentTopViewAnimation; private MessageObject pendingMessageObject; private TLRPC.KeyboardButton pendingLocationButton; @@ -496,9 +509,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat public ChatActivityEnterView(Activity context, SizeNotifierFrameLayout parent, ChatActivity fragment, boolean isChat) { super(context); - setBackgroundResource(R.drawable.compose_panel); + backgroundDrawable = context.getResources().getDrawable(R.drawable.compose_panel); setFocusable(true); setFocusableInTouchMode(true); + setWillNotDraw(false); NotificationCenter.getInstance().addObserver(this, NotificationCenter.recordStarted); NotificationCenter.getInstance().addObserver(this, NotificationCenter.recordStartError); @@ -518,11 +532,11 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat sendByEnter = preferences.getBoolean("send_by_enter", false); textFieldContainer = new LinearLayout(context); - textFieldContainer.setBackgroundColor(0xffffffff); + //textFieldContainer.setBackgroundColor(0xffffffff); textFieldContainer.setOrientation(LinearLayout.HORIZONTAL); addView(textFieldContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 0, 2, 0, 0)); - FrameLayoutFixed frameLayout = new FrameLayoutFixed(context); + FrameLayout frameLayout = new FrameLayout(context); textFieldContainer.addView(frameLayout, LayoutHelper.createLinear(0, LayoutHelper.WRAP_CONTENT, 1.0f)); emojiButton = new ImageView(context); @@ -619,7 +633,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat return; } checkSendButton(true); - String message = getTrimmedString(charSequence.toString()); + CharSequence message = AndroidUtilities.getTrimmedString(charSequence.toString()); if (delegate != null) { if (count > 2 || charSequence == null || charSequence.length() == 0) { messageWebPageSearch = true; @@ -631,7 +645,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if (innerTextChange != 2 && before != count && (count - before) > 1) { processChange = true; } - if (editingMessageObject == null && !isAsAdmin && message.length() != 0 && lastTypingTimeSend < System.currentTimeMillis() - 5000 && !ignoreTextChange) { + if (editingMessageObject == null && !canWriteToChannel && message.length() != 0 && lastTypingTimeSend < System.currentTimeMillis() - 5000 && !ignoreTextChange) { int currentTime = ConnectionsManager.getInstance().getCurrentTime(); TLRPC.User currentUser = null; if ((int) dialog_id > 0) { @@ -674,14 +688,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } if (isChat) { - contextProgressView = new ContextProgressView(context); - contextProgressView.setVisibility(INVISIBLE); - frameLayout.addView(contextProgressView, LayoutHelper.createFrame(38, 48, Gravity.BOTTOM | Gravity.RIGHT)); - attachButton = new LinearLayout(context); attachButton.setOrientation(LinearLayout.HORIZONTAL); attachButton.setEnabled(false); - ViewProxy.setPivotX(attachButton, AndroidUtilities.dp(48)); + attachButton.setPivotX(AndroidUtilities.dp(48)); frameLayout.addView(attachButton, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 48, Gravity.BOTTOM | Gravity.RIGHT)); botButton = new ImageView(context); @@ -714,25 +724,6 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } }); - asAdminButton = new ImageView(context); - asAdminButton.setImageResource(isAsAdmin ? R.drawable.publish_active : R.drawable.publish); - asAdminButton.setScaleType(ImageView.ScaleType.CENTER); - asAdminButton.setVisibility(adminModeAvailable ? VISIBLE : GONE); - if (Build.VERSION.SDK_INT >= 21) { - asAdminButton.setBackgroundDrawable(Theme.createBarSelectorDrawable(Theme.INPUT_FIELD_SELECTOR_COLOR)); - } - attachButton.addView(asAdminButton, LayoutHelper.createLinear(48, 48)); - asAdminButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - isAsAdmin = !isAsAdmin; - asAdminButton.setImageResource(isAsAdmin ? R.drawable.publish_active : R.drawable.publish); - updateFieldHint(); - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - preferences.edit().putBoolean("asadmin_" + dialog_id, isAsAdmin).commit(); - } - }); - notifyButton = new ImageView(context); notifyButton.setImageResource(silent ? R.drawable.notify_members_off : R.drawable.notify_members_on); notifyButton.setScaleType(ImageView.ScaleType.CENTER); @@ -758,7 +749,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat }); } - recordedAudioPanel = new FrameLayoutFixed(context); + recordedAudioPanel = new FrameLayout(context); recordedAudioPanel.setVisibility(audioToSend == null ? GONE : VISIBLE); recordedAudioPanel.setBackgroundColor(0xffffffff); recordedAudioPanel.setFocusable(true); @@ -818,7 +809,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat recordedAudioTimeTextView.setText("0:13"); recordedAudioPanel.addView(recordedAudioTimeTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.RIGHT | Gravity.CENTER_VERTICAL, 0, 0, 13, 0)); - recordPanel = new FrameLayoutFixed(context); + recordPanel = new FrameLayout(context); recordPanel.setVisibility(GONE); recordPanel.setBackgroundColor(0xffffffff); frameLayout.addView(recordPanel, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.BOTTOM)); @@ -889,7 +880,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } } startedDraggingX = -1; - MediaController.getInstance().startRecording(dialog_id, replyingMessageObject, asAdmin()); + MediaController.getInstance().startRecording(dialog_id, replyingMessageObject); updateAudioRecordIntefrace(); audioSendButton.getParent().requestDisallowInterceptTouchEvent(true); } else if (motionEvent.getAction() == MotionEvent.ACTION_UP || motionEvent.getAction() == MotionEvent.ACTION_CANCEL) { @@ -905,11 +896,11 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat updateAudioRecordIntefrace(); } - x = x + ViewProxy.getX(audioSendButton); + x = x + audioSendButton.getX(); FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) slideText.getLayoutParams(); if (startedDraggingX != -1) { float dist = (x - startedDraggingX); - ViewProxy.setTranslationX(recordCircle, dist); + recordCircle.setTranslationX(dist); params.leftMargin = AndroidUtilities.dp(30) + (int) dist; slideText.setLayoutParams(params); float alpha = 1.0f + dist / distCanMove; @@ -918,9 +909,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } else if (alpha < 0) { alpha = 0; } - ViewProxy.setAlpha(slideText, alpha); + slideText.setAlpha(alpha); } - if (x <= ViewProxy.getX(slideText) + slideText.getWidth() + AndroidUtilities.dp(30)) { + if (x <= slideText.getX() + slideText.getWidth() + AndroidUtilities.dp(30)) { if (startedDraggingX == -1) { startedDraggingX = x; distCanMove = (recordPanel.getMeasuredWidth() - slideText.getMeasuredWidth() - AndroidUtilities.dp(48)) / 2.0f; @@ -933,9 +924,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } if (params.leftMargin > AndroidUtilities.dp(30)) { params.leftMargin = AndroidUtilities.dp(30); - ViewProxy.setTranslationX(recordCircle, 0); + recordCircle.setTranslationX(0); slideText.setLayoutParams(params); - ViewProxy.setAlpha(slideText, 1); + slideText.setAlpha(1); startedDraggingX = -1; } } @@ -954,10 +945,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat //cancelBotButton.setImageResource(R.drawable.delete_reply); cancelBotButton.setImageDrawable(progressDrawable = new CloseProgressDrawable2()); cancelBotButton.setSoundEffectsEnabled(false); - ViewProxy.setScaleX(cancelBotButton, 0.1f); - ViewProxy.setScaleY(cancelBotButton, 0.1f); - ViewProxy.setAlpha(cancelBotButton, 0.0f); - cancelBotButton.clearAnimation(); + cancelBotButton.setScaleX(0.1f); + cancelBotButton.setScaleY(0.1f); + cancelBotButton.setAlpha(0.0f); sendButtonContainer.addView(cancelBotButton, LayoutHelper.createFrame(48, 48)); cancelBotButton.setOnClickListener(new OnClickListener() { @Override @@ -977,10 +967,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat sendButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE); sendButton.setImageResource(R.drawable.ic_send); sendButton.setSoundEffectsEnabled(false); - ViewProxy.setScaleX(sendButton, 0.1f); - ViewProxy.setScaleY(sendButton, 0.1f); - ViewProxy.setAlpha(sendButton, 0.0f); - sendButton.clearAnimation(); + sendButton.setScaleX(0.1f); + sendButton.setScaleY(0.1f); + sendButton.setAlpha(0.0f); sendButtonContainer.addView(sendButton, LayoutHelper.createFrame(48, 48)); sendButton.setOnClickListener(new OnClickListener() { @Override @@ -996,16 +985,32 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat checkSendButton(false); } - public void showContextProgress(boolean show) { - /*if (contextProgressView == null) { - return; + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (child == topView) { + canvas.save(); + canvas.clipRect(0, 0, getMeasuredWidth(), child.getLayoutParams().height + AndroidUtilities.dp(2)); } - contextProgressView.setVisibility(show ? VISIBLE : INVISIBLE); - try { - messageEditText.setPadding(0, AndroidUtilities.dp(11), show ? AndroidUtilities.dp(38) : 0, AndroidUtilities.dp(12)); - } catch (Exception e) { - FileLog.e("tmessages", e); - }*/ + boolean result = super.drawChild(canvas, child, drawingTime); + if (child == topView) { + canvas.restore(); + } + return result; + } + + @Override + protected void onDraw(Canvas canvas) { + int top = topView != null && topView.getVisibility() == VISIBLE ? (int) topView.getTranslationY() : 0; + backgroundDrawable.setBounds(0, top, getMeasuredWidth(), getMeasuredHeight()); + backgroundDrawable.draw(canvas); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + public void showContextProgress(boolean show) { if (progressDrawable == null) { return; } @@ -1029,21 +1034,11 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } topView = view; topView.setVisibility(GONE); + topView.setTranslationY(height); addView(topView, 0, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, height, Gravity.TOP | Gravity.LEFT, 0, 2, 0, 0)); needShowTopView = false; } - public void setTopViewAnimation(float progress) { - topViewAnimation = progress; - LayoutParams layoutParams2 = (LayoutParams) textFieldContainer.getLayoutParams(); - layoutParams2.topMargin = AndroidUtilities.dp(2) + (int) (topView.getLayoutParams().height * progress); - textFieldContainer.setLayoutParams(layoutParams2); - } - - public float getTopViewAnimation() { - return topViewAnimation; - } - public void setForceShowSendButton(boolean value, boolean animated) { forceShowSendButton = value; checkSendButton(animated); @@ -1067,10 +1062,6 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat emojiView.switchToGifRecent(); } - public boolean asAdmin() { - return isAsAdmin; - } - public void showTopView(boolean animated, final boolean openKeyboard) { if (topView == null || topViewShowed || getVisibility() != VISIBLE) { return; @@ -1083,17 +1074,15 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat currentTopViewAnimation.cancel(); currentTopViewAnimation = null; } + resizeForTopView(true); if (animated) { if (keyboardVisible || isPopupShowing()) { - currentTopViewAnimation = new AnimatorSetProxy(); - currentTopViewAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(ChatActivityEnterView.this, "topViewAnimation", 1.0f) - ); + currentTopViewAnimation = new AnimatorSet(); + currentTopViewAnimation.playTogether(ObjectAnimator.ofFloat(topView, "translationY", 0)); currentTopViewAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentTopViewAnimation != null && currentTopViewAnimation.equals(animation)) { - setTopViewAnimation(1.0f); if (recordedAudioPanel.getVisibility() != VISIBLE && (!forceShowSendButton || openKeyboard)) { openKeyboard(); } @@ -1102,7 +1091,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (currentTopViewAnimation != null && currentTopViewAnimation.equals(animation)) { currentTopViewAnimation = null; } @@ -1111,13 +1100,13 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat currentTopViewAnimation.setDuration(200); currentTopViewAnimation.start(); } else { - setTopViewAnimation(1.0f); + topView.setTranslationY(0); if (recordedAudioPanel.getVisibility() != VISIBLE && (!forceShowSendButton || openKeyboard)) { openKeyboard(); } } } else { - setTopViewAnimation(1.0f); + topView.setTranslationY(0); } } } @@ -1130,29 +1119,25 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat topViewShowed = false; needShowTopView = false; if (allowShowTopView) { - float resumeValue = 1.0f; if (currentTopViewAnimation != null) { - resumeValue = topViewAnimation; currentTopViewAnimation.cancel(); currentTopViewAnimation = null; } if (animated) { - currentTopViewAnimation = new AnimatorSetProxy(); - currentTopViewAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(ChatActivityEnterView.this, "topViewAnimation", resumeValue, 0.0f) - ); + currentTopViewAnimation = new AnimatorSet(); + currentTopViewAnimation.playTogether(ObjectAnimator.ofFloat(topView, "translationY", topView.getLayoutParams().height)); currentTopViewAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentTopViewAnimation != null && currentTopViewAnimation.equals(animation)) { topView.setVisibility(GONE); - setTopViewAnimation(0.0f); + resizeForTopView(false); currentTopViewAnimation = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (currentTopViewAnimation != null && currentTopViewAnimation.equals(animation)) { currentTopViewAnimation = null; } @@ -1162,7 +1147,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat currentTopViewAnimation.start(); } else { topView.setVisibility(GONE); - setTopViewAnimation(0.0f); + topView.setTranslationY(topView.getLayoutParams().height); } } } @@ -1185,7 +1170,8 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat allowShowTopView = false; if (needShowTopView) { topView.setVisibility(GONE); - setTopViewAnimation(0.0f); + resizeForTopView(false); + topView.setTranslationY(topView.getLayoutParams().height); } } } else { @@ -1193,13 +1179,20 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat allowShowTopView = true; if (needShowTopView) { topView.setVisibility(VISIBLE); - setTopViewAnimation(1.0f); + resizeForTopView(true); + topView.setTranslationY(0); } } } } } + private void resizeForTopView(boolean show) { + LayoutParams layoutParams = (LayoutParams) textFieldContainer.getLayoutParams(); + layoutParams.topMargin = AndroidUtilities.dp(2) + (show ? topView.getLayoutParams().height : 0); + textFieldContainer.setLayoutParams(layoutParams); + } + public void onDestroy() { NotificationCenter.getInstance().removeObserver(this, NotificationCenter.recordStarted); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.recordStartError); @@ -1251,22 +1244,14 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if ((int) dialog_id < 0) { TLRPC.Chat currentChat = MessagesController.getInstance().getChat(-(int) dialog_id); silent = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE).getBoolean("silent_" + dialog_id, false); - isAsAdmin = ChatObject.isChannel(currentChat) && (currentChat.creator || currentChat.editor) && !currentChat.megagroup; - adminModeAvailable = isAsAdmin && !currentChat.broadcast; - canWriteToChannel = isAsAdmin; - if (adminModeAvailable) { - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - isAsAdmin = preferences.getBoolean("asadmin_" + dialog_id, true); - } - if (asAdminButton != null) { - asAdminButton.setVisibility(adminModeAvailable ? VISIBLE : GONE); - asAdminButton.setImageResource(isAsAdmin ? R.drawable.publish_active : R.drawable.publish); - updateFieldHint(); - } + canWriteToChannel = ChatObject.isChannel(currentChat) && (currentChat.creator || currentChat.editor) && !currentChat.megagroup; if (notifyButton != null) { notifyButton.setVisibility(canWriteToChannel ? VISIBLE : GONE); notifyButton.setImageResource(silent ? R.drawable.notify_members_off : R.drawable.notify_members_on); - ViewProxy.setPivotX(attachButton, AndroidUtilities.dp((botButton == null || botButton.getVisibility() == GONE) && (notifyButton == null || notifyButton.getVisibility() == GONE) ? 48 : 96)); + attachButton.setPivotX(AndroidUtilities.dp((botButton == null || botButton.getVisibility() == GONE) && (notifyButton == null || notifyButton.getVisibility() == GONE) ? 48 : 96)); + } + if (attachButton != null) { + updateFieldRight(attachButton.getVisibility() == VISIBLE ? 1 : 0); } } } @@ -1281,14 +1266,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if (editingMessageObject != null) { messageEditText.setHint(editingCaption ? LocaleController.getString("Caption", R.string.Caption) : LocaleController.getString("TypeMessage", R.string.TypeMessage)); } else { - if (isAsAdmin) { - if (silent) { - messageEditText.setHint(LocaleController.getString("ChannelSilentBroadcast", R.string.ChannelSilentBroadcast)); - } else { - messageEditText.setHint(LocaleController.getString("ChannelBroadcast", R.string.ChannelBroadcast)); - } + if (silent) { + messageEditText.setHint(LocaleController.getString("ChannelSilentBroadcast", R.string.ChannelSilentBroadcast)); } else { - messageEditText.setHint(LocaleController.getString("ChannelComment", R.string.ChannelComment)); + messageEditText.setHint(LocaleController.getString("ChannelBroadcast", R.string.ChannelBroadcast)); } } } else { @@ -1325,20 +1306,19 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat audioToSendPath = null; audioToSend = null; audioToSendMessageObject = null; - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(recordedAudioPanel, "alpha", 0.0f) + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(recordedAudioPanel, "alpha", 0.0f) ); - animatorSetProxy.setDuration(200); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + AnimatorSet.setDuration(200); + AnimatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { - recordedAudioPanel.clearAnimation(); + public void onAnimationEnd(Animator animation) { recordedAudioPanel.setVisibility(GONE); } }); - animatorSetProxy.start(); + AnimatorSet.start(); } private void sendMessage() { @@ -1364,7 +1344,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if (playing != null && playing == audioToSendMessageObject) { MediaController.getInstance().cleanupPlayer(true, true); } - SendMessagesHelper.getInstance().sendMessage(audioToSend, null, audioToSendPath, dialog_id, replyingMessageObject, isAsAdmin, null, null); + SendMessagesHelper.getInstance().sendMessage(audioToSend, null, audioToSendPath, dialog_id, replyingMessageObject, null, null); if (delegate != null) { delegate.onMessageSend(null); } @@ -1372,7 +1352,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat checkSendButton(true); return; } - String message = messageEditText.getText().toString(); + CharSequence message = messageEditText.getText(); if (processSendingText(message)) { messageEditText.setText(""); lastTypingTimeSend = 0; @@ -1388,43 +1368,38 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat public void doneEditingMessage() { if (editingMessageObject != null) { - SendMessagesHelper.getInstance().editMessage(editingMessageObject, messageEditText.getText().toString(), messageWebPageSearch, parentFragment); - setEditinigMessageObject(null, false); + delegate.onMessageEditEnd(true); + editingMessageReqId = SendMessagesHelper.getInstance().editMessage(editingMessageObject, messageEditText.getText().toString(), messageWebPageSearch, parentFragment, MessagesQuery.getEntities(messageEditText.getText()), new Runnable() { + @Override + public void run() { + editingMessageReqId = 0; + setEditingMessageObject(null, false); + } + }); } } - public boolean processSendingText(String text) { - text = getTrimmedString(text); + public boolean processSendingText(CharSequence text) { + text = AndroidUtilities.getTrimmedString(text); if (text.length() != 0) { int count = (int) Math.ceil(text.length() / 4096.0f); for (int a = 0; a < count; a++) { - String mess = text.substring(a * 4096, Math.min((a + 1) * 4096, text.length())); - SendMessagesHelper.getInstance().sendMessage(mess, dialog_id, replyingMessageObject, messageWebPage, messageWebPageSearch, asAdmin(), null, null, null); + CharSequence mess = text.subSequence(a * 4096, Math.min((a + 1) * 4096, text.length())); + SendMessagesHelper.getInstance().sendMessage(mess.toString(), dialog_id, replyingMessageObject, messageWebPage, messageWebPageSearch, MessagesQuery.getEntities(mess), null, null); } return true; } return false; } - private String getTrimmedString(String src) { - src = src.trim(); - if (src.length() == 0) { - return src; - } - while (src.startsWith("\n")) { - src = src.substring(1); - } - while (src.endsWith("\n")) { - src = src.substring(0, src.length() - 1); - } - return src; - } - - private void checkSendButton(final boolean animated) { + private void checkSendButton(boolean animated) { if (editingMessageObject != null) { return; } - String message = getTrimmedString(messageEditText.getText().toString()); + if (isPaused) { + animated = false; + } + CharSequence message = AndroidUtilities.getTrimmedString(messageEditText.getText()); if (message.length() > 0 || forceShowSendButton || audioToSend != null) { boolean showBotButton = messageEditText.caption != null && sendButton.getVisibility() == VISIBLE; boolean showSendButton = messageEditText.caption == null && cancelBotButton.getVisibility() == VISIBLE; @@ -1443,23 +1418,22 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } if (attachButton != null) { - runningAnimation2 = new AnimatorSetProxy(); + runningAnimation2 = new AnimatorSet(); runningAnimation2.playTogether( - ObjectAnimatorProxy.ofFloat(attachButton, "alpha", 0.0f), - ObjectAnimatorProxy.ofFloat(attachButton, "scaleX", 0.0f) + ObjectAnimator.ofFloat(attachButton, "alpha", 0.0f), + ObjectAnimator.ofFloat(attachButton, "scaleX", 0.0f) ); runningAnimation2.setDuration(100); runningAnimation2.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (runningAnimation2 != null && runningAnimation2.equals(animation)) { attachButton.setVisibility(GONE); - attachButton.clearAnimation(); } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (runningAnimation2 != null && runningAnimation2.equals(animation)) { runningAnimation2 = null; } @@ -1472,34 +1446,34 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } } - runningAnimation = new AnimatorSetProxy(); + runningAnimation = new AnimatorSet(); - ArrayList animators = new ArrayList<>(); + ArrayList animators = new ArrayList<>(); if (audioSendButton.getVisibility() == VISIBLE) { - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "scaleX", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "scaleY", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "scaleX", 0.1f)); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "scaleY", 0.1f)); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "alpha", 0.0f)); } if (showBotButton) { - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleX", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleY", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleX", 0.1f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleY", 0.1f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "alpha", 0.0f)); } else if (showSendButton) { - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleX", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleY", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleX", 0.1f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleY", 0.1f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "alpha", 0.0f)); } if (messageEditText.caption != null) { runningAnimationType = 3; - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleX", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleY", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "alpha", 1.0f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleX", 1.0f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleY", 1.0f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "alpha", 1.0f)); cancelBotButton.setVisibility(VISIBLE); } else { runningAnimationType = 1; - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleX", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleY", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "alpha", 1.0f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleX", 1.0f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleY", 1.0f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "alpha", 1.0f)); sendButton.setVisibility(VISIBLE); } @@ -1507,26 +1481,23 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat runningAnimation.setDuration(150); runningAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { if (messageEditText.caption != null) { cancelBotButton.setVisibility(VISIBLE); sendButton.setVisibility(GONE); - sendButton.clearAnimation(); } else { sendButton.setVisibility(VISIBLE); cancelBotButton.setVisibility(GONE); - cancelBotButton.clearAnimation(); } audioSendButton.setVisibility(GONE); - audioSendButton.clearAnimation(); runningAnimation = null; runningAnimationType = 0; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { runningAnimation = null; } @@ -1534,35 +1505,31 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat }); runningAnimation.start(); } else { - ViewProxy.setScaleX(audioSendButton, 0.1f); - ViewProxy.setScaleY(audioSendButton, 0.1f); - ViewProxy.setAlpha(audioSendButton, 0.0f); + audioSendButton.setScaleX(0.1f); + audioSendButton.setScaleY(0.1f); + audioSendButton.setAlpha(0.0f); if (messageEditText.caption != null) { - ViewProxy.setScaleX(sendButton, 0.1f); - ViewProxy.setScaleY(sendButton, 0.1f); - ViewProxy.setAlpha(sendButton, 0.0f); - ViewProxy.setScaleX(cancelBotButton, 1.0f); - ViewProxy.setScaleY(cancelBotButton, 1.0f); - ViewProxy.setAlpha(cancelBotButton, 1.0f); + sendButton.setScaleX(0.1f); + sendButton.setScaleY(0.1f); + sendButton.setAlpha(0.0f); + cancelBotButton.setScaleX(1.0f); + cancelBotButton.setScaleY(1.0f); + cancelBotButton.setAlpha(1.0f); cancelBotButton.setVisibility(VISIBLE); sendButton.setVisibility(GONE); - sendButton.clearAnimation(); } else { - ViewProxy.setScaleX(cancelBotButton, 0.1f); - ViewProxy.setScaleY(cancelBotButton, 0.1f); - ViewProxy.setAlpha(cancelBotButton, 0.0f); - ViewProxy.setScaleX(sendButton, 1.0f); - ViewProxy.setScaleY(sendButton, 1.0f); - ViewProxy.setAlpha(sendButton, 1.0f); + cancelBotButton.setScaleX(0.1f); + cancelBotButton.setScaleY(0.1f); + cancelBotButton.setAlpha(0.0f); + sendButton.setScaleX(1.0f); + sendButton.setScaleY(1.0f); + sendButton.setAlpha(1.0f); sendButton.setVisibility(VISIBLE); cancelBotButton.setVisibility(GONE); - cancelBotButton.clearAnimation(); } audioSendButton.setVisibility(GONE); - audioSendButton.clearAnimation(); if (attachButton != null) { attachButton.setVisibility(GONE); - attachButton.clearAnimation(); if (delegate != null) { delegate.onAttachButtonHidden(); } @@ -1587,10 +1554,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if (attachButton != null) { attachButton.setVisibility(VISIBLE); - runningAnimation2 = new AnimatorSetProxy(); + runningAnimation2 = new AnimatorSet(); runningAnimation2.playTogether( - ObjectAnimatorProxy.ofFloat(attachButton, "alpha", 1.0f), - ObjectAnimatorProxy.ofFloat(attachButton, "scaleX", 1.0f) + ObjectAnimator.ofFloat(attachButton, "alpha", 1.0f), + ObjectAnimator.ofFloat(attachButton, "scaleX", 1.0f) ); runningAnimation2.setDuration(100); runningAnimation2.start(); @@ -1599,33 +1566,31 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } audioSendButton.setVisibility(VISIBLE); - runningAnimation = new AnimatorSetProxy(); + runningAnimation = new AnimatorSet(); runningAnimationType = 2; - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "scaleX", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "scaleY", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "alpha", 1.0f)); + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "scaleX", 1.0f)); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "scaleY", 1.0f)); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "alpha", 1.0f)); if (cancelBotButton.getVisibility() == VISIBLE) { - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleX", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleY", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleX", 0.1f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleY", 0.1f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "alpha", 0.0f)); } else { - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleX", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleY", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleX", 0.1f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleY", 0.1f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "alpha", 0.0f)); } runningAnimation.playTogether(animators); runningAnimation.setDuration(150); runningAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { sendButton.setVisibility(GONE); - sendButton.clearAnimation(); cancelBotButton.setVisibility(GONE); - cancelBotButton.clearAnimation(); audioSendButton.setVisibility(VISIBLE); runningAnimation = null; runningAnimationType = 0; @@ -1633,7 +1598,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { runningAnimation = null; } @@ -1641,19 +1606,17 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat }); runningAnimation.start(); } else { - ViewProxy.setScaleX(sendButton, 0.1f); - ViewProxy.setScaleY(sendButton, 0.1f); - ViewProxy.setAlpha(sendButton, 0.0f); - ViewProxy.setScaleX(cancelBotButton, 0.1f); - ViewProxy.setScaleY(cancelBotButton, 0.1f); - ViewProxy.setAlpha(cancelBotButton, 0.0f); - ViewProxy.setScaleX(audioSendButton, 1.0f); - ViewProxy.setScaleY(audioSendButton, 1.0f); - ViewProxy.setAlpha(audioSendButton, 1.0f); + sendButton.setScaleX(0.1f); + sendButton.setScaleY(0.1f); + sendButton.setAlpha(0.0f); + cancelBotButton.setScaleX(0.1f); + cancelBotButton.setScaleY(0.1f); + cancelBotButton.setAlpha(0.0f); + audioSendButton.setScaleX(1.0f); + audioSendButton.setScaleY(1.0f); + audioSendButton.setAlpha(1.0f); cancelBotButton.setVisibility(GONE); - cancelBotButton.clearAnimation(); sendButton.setVisibility(GONE); - sendButton.clearAnimation(); audioSendButton.setVisibility(VISIBLE); if (attachButton != null) { delegate.onAttachButtonShow(); @@ -1716,22 +1679,22 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) slideText.getLayoutParams(); params.leftMargin = AndroidUtilities.dp(30); slideText.setLayoutParams(params); - ViewProxy.setAlpha(slideText, 1); - ViewProxy.setX(recordPanel, AndroidUtilities.displaySize.x); - ViewProxy.setTranslationX(recordCircle, 0); + slideText.setAlpha(1); + recordPanel.setX(AndroidUtilities.displaySize.x); + recordCircle.setTranslationX(0); if (runningAnimationAudio != null) { runningAnimationAudio.cancel(); } - runningAnimationAudio = new AnimatorSetProxy(); - runningAnimationAudio.playTogether(ObjectAnimatorProxy.ofFloat(recordPanel, "translationX", 0), - ObjectAnimatorProxy.ofFloat(recordCircle, "scale", 1), - ObjectAnimatorProxy.ofFloat(audioSendButton, "alpha", 0)); + runningAnimationAudio = new AnimatorSet(); + runningAnimationAudio.playTogether(ObjectAnimator.ofFloat(recordPanel, "translationX", 0), + ObjectAnimator.ofFloat(recordCircle, "scale", 1), + ObjectAnimator.ofFloat(audioSendButton, "alpha", 0)); runningAnimationAudio.setDuration(300); runningAnimationAudio.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animator) { + public void onAnimationEnd(Animator animator) { if (runningAnimationAudio != null && runningAnimationAudio.equals(animator)) { - ViewProxy.setX(recordPanel, 0); + recordPanel.setX(0); runningAnimationAudio = null; } } @@ -1756,19 +1719,19 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if (runningAnimationAudio != null) { runningAnimationAudio.cancel(); } - runningAnimationAudio = new AnimatorSetProxy(); - runningAnimationAudio.playTogether(ObjectAnimatorProxy.ofFloat(recordPanel, "translationX", AndroidUtilities.displaySize.x), - ObjectAnimatorProxy.ofFloat(recordCircle, "scale", 0.0f), - ObjectAnimatorProxy.ofFloat(audioSendButton, "alpha", 1.0f)); + runningAnimationAudio = new AnimatorSet(); + runningAnimationAudio.playTogether(ObjectAnimator.ofFloat(recordPanel, "translationX", AndroidUtilities.displaySize.x), + ObjectAnimator.ofFloat(recordCircle, "scale", 0.0f), + ObjectAnimator.ofFloat(audioSendButton, "alpha", 1.0f)); runningAnimationAudio.setDuration(300); runningAnimationAudio.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animator) { + public void onAnimationEnd(Animator animator) { if (runningAnimationAudio != null && runningAnimationAudio.equals(animator)) { FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) slideText.getLayoutParams(); params.leftMargin = AndroidUtilities.dp(30); slideText.setLayoutParams(params); - ViewProxy.setAlpha(slideText, 1); + slideText.setAlpha(1); recordPanel.setVisibility(GONE); recordCircle.setVisibility(GONE); runningAnimationAudio = null; @@ -1809,17 +1772,21 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } else { TLRPC.User user = messageObject != null && (int) dialog_id < 0 ? MessagesController.getInstance().getUser(messageObject.messageOwner.from_id) : null; if ((botCount != 1 || username) && user != null && user.bot && !command.contains("@")) { - SendMessagesHelper.getInstance().sendMessage(String.format(Locale.US, "%s@%s", command, user.username), dialog_id, null, null, false, asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage(String.format(Locale.US, "%s@%s", command, user.username), dialog_id, null, null, false, null, null, null); } else { - SendMessagesHelper.getInstance().sendMessage(command, dialog_id, null, null, false, asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage(command, dialog_id, null, null, false, null, null, null); } } } - public void setEditinigMessageObject(MessageObject messageObject, boolean caption) { + public void setEditingMessageObject(MessageObject messageObject, boolean caption) { if (audioToSend != null || editingMessageObject == messageObject) { return; } + if (editingMessageReqId != 0) { + ConnectionsManager.getInstance().cancelRequest(editingMessageReqId, true); + editingMessageReqId = 0; + } editingMessageObject = messageObject; editingCaption = caption; if (editingMessageObject != null) { @@ -1834,7 +1801,18 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } else { inputFilters[0] = new InputFilter.LengthFilter(4096); if (editingMessageObject.messageText != null) { - setFieldText(Emoji.replaceEmoji(new SpannableStringBuilder(editingMessageObject.messageText.toString()), messageEditText.getPaint().getFontMetricsInt(), AndroidUtilities.dp(20), false)); + SpannableStringBuilder stringBuilder = new SpannableStringBuilder(editingMessageObject.messageText.toString()); + ArrayList entities = MessagesQuery.getEntities(editingMessageObject.messageText); + if (entities != null) { + for (int a = 0; a < entities.size(); a++) { + TLRPC.TL_inputMessageEntityMentionName entity = (TLRPC.TL_inputMessageEntityMentionName) entities.get(a); + if (entity.offset + entity.length < editingMessageObject.messageText.length() && editingMessageObject.messageText.charAt(entity.offset + entity.length) == ' ') { + entity.length++; + } + stringBuilder.setSpan(new URLSpanUserMention("" + entity.user_id.user_id), entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + setFieldText(Emoji.replaceEmoji(stringBuilder, messageEditText.getPaint().getFontMetricsInt(), AndroidUtilities.dp(20), false)); } else { setFieldText(""); } @@ -1844,11 +1822,6 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) messageEditText.getLayoutParams(); layoutParams.rightMargin = AndroidUtilities.dp(4); messageEditText.setLayoutParams(layoutParams); - sendButton.clearAnimation(); - cancelBotButton.clearAnimation(); - audioSendButton.clearAnimation(); - attachButton.clearAnimation(); - sendButtonContainer.clearAnimation(); sendButton.setVisibility(GONE); cancelBotButton.setVisibility(GONE); audioSendButton.setVisibility(GONE); @@ -1856,25 +1829,23 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat sendButtonContainer.setVisibility(GONE); } else { messageEditText.setFilters(new InputFilter[0]); - delegate.onMessageEditEnd(); + delegate.onMessageEditEnd(false); audioSendButton.setVisibility(VISIBLE); attachButton.setVisibility(VISIBLE); sendButtonContainer.setVisibility(VISIBLE); - ViewProxy.setScaleX(attachButton, 1.0f); - ViewProxy.setAlpha(attachButton, 1.0f); - ViewProxy.setScaleX(sendButton, 0.1f); - ViewProxy.setScaleY(sendButton, 0.1f); - ViewProxy.setAlpha(sendButton, 0.0f); - ViewProxy.setScaleX(cancelBotButton, 0.1f); - ViewProxy.setScaleY(cancelBotButton, 0.1f); - ViewProxy.setAlpha(cancelBotButton, 0.0f); - ViewProxy.setScaleX(audioSendButton, 1.0f); - ViewProxy.setScaleY(audioSendButton, 1.0f); - ViewProxy.setAlpha(audioSendButton, 1.0f); + attachButton.setScaleX(1.0f); + attachButton.setAlpha(1.0f); + sendButton.setScaleX(0.1f); + sendButton.setScaleY(0.1f); + sendButton.setAlpha(0.0f); + cancelBotButton.setScaleX(0.1f); + cancelBotButton.setScaleY(0.1f); + cancelBotButton.setAlpha(0.0f); + audioSendButton.setScaleX(1.0f); + audioSendButton.setScaleY(1.0f); + audioSendButton.setAlpha(1.0f); sendButton.setVisibility(GONE); - sendButton.clearAnimation(); cancelBotButton.setVisibility(GONE); - cancelBotButton.clearAnimation(); messageEditText.setText(""); delegate.onAttachButtonShow(); updateFieldRight(1); @@ -1909,9 +1880,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat return messageEditText.getSelectionStart(); } - public void replaceWithText(int start, int len, String text) { + public void replaceWithText(int start, int len, CharSequence text) { try { - StringBuilder builder = new StringBuilder(messageEditText.getText()); + SpannableStringBuilder builder = new SpannableStringBuilder(messageEditText.getText()); builder.replace(start, start + len, text); messageEditText.setText(builder); messageEditText.setSelection(start + text.length()); @@ -1920,6 +1891,16 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } } + public void setFieldFocused() { + if (messageEditText != null) { + try { + messageEditText.requestFocus(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + } + public void setFieldFocused(boolean focus) { if (messageEditText == null) { return; @@ -1950,9 +1931,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat return messageEditText != null && messageEditText.length() > 0; } - public String getFieldText() { + public CharSequence getFieldText() { if (messageEditText != null && messageEditText.length() > 0) { - return messageEditText.getText().toString(); + return messageEditText.getText(); } return null; } @@ -1992,8 +1973,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat botButton.setVisibility(GONE); } updateFieldRight(2); - ViewProxy.setPivotX(attachButton, AndroidUtilities.dp((botButton == null || botButton.getVisibility() == GONE) && (notifyButton == null || notifyButton.getVisibility() == GONE) ? 48 : 96)); - attachButton.clearAnimation(); + attachButton.setPivotX(AndroidUtilities.dp((botButton == null || botButton.getVisibility() == GONE) && (notifyButton == null || notifyButton.getVisibility() == GONE) ? 48 : 96)); } public void setBotsCount(int count, boolean hasCommands) { @@ -2072,7 +2052,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat return; } if (button instanceof TLRPC.TL_keyboardButton) { - SendMessagesHelper.getInstance().sendMessage(button.text, dialog_id, replyMessageObject, null, false, asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage(button.text, dialog_id, replyMessageObject, null, false, null, null, null); } else if (button instanceof TLRPC.TL_keyboardButtonUrl) { parentFragment.showOpenUrlAlert(button.url); } else if (button instanceof TLRPC.TL_keyboardButtonRequestPhone) { @@ -2117,10 +2097,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat fragment.finishFragment(); return; } - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("dialog_" + did, "@" + user.username + " " + button.query); - editor.commit(); + DraftQuery.saveDraft(did, "@" + user.username + " " + button.query, null, null, true); if (did != dialog_id) { int lower_part = (int) did; if (lower_part != 0) { @@ -2207,7 +2184,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat @Override public void onGifSelected(TLRPC.Document gif) { - SendMessagesHelper.getInstance().sendSticker(gif, dialog_id, replyingMessageObject, asAdmin()); + SendMessagesHelper.getInstance().sendSticker(gif, dialog_id, replyingMessageObject); if ((int) dialog_id == 0) { MessagesController.getInstance().saveGif(gif); } @@ -2259,12 +2236,17 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat @Override public void onStickerSelected(TLRPC.Document sticker) { - SendMessagesHelper.getInstance().sendSticker(sticker, dialog_id, replyingMessageObject, asAdmin()); + SendMessagesHelper.getInstance().sendSticker(sticker, dialog_id, replyingMessageObject); if (delegate != null) { delegate.onMessageSend(null); } } + public void addStickerToRecent(TLRPC.Document sticker) { + createEmojiView(); + emojiView.addRecentSticker(sticker); + } + private void showPopup(int show, int contentType) { if (show == 1) { if (contentType == 0 && emojiView == null) { @@ -2378,6 +2360,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat return editingMessageObject != null; } + public MessageObject getEditingMessageObject() { + return editingMessageObject; + } + public boolean isEditingCaption() { return editingCaption; } @@ -2546,8 +2532,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat message.flags |= TLRPC.MESSAGE_FLAG_HAS_MEDIA | TLRPC.MESSAGE_FLAG_HAS_FROM_ID; audioToSendMessageObject = new MessageObject(message, null, false); - ViewProxy.setAlpha(recordedAudioPanel, 1.0f); - recordedAudioPanel.clearAnimation(); + recordedAudioPanel.setAlpha(1.0f); recordedAudioPanel.setVisibility(VISIBLE); int duration = 0; for (int a = 0; a < audioToSend.attributes.size(); a++) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java new file mode 100644 index 000000000..4c52ff340 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java @@ -0,0 +1,1351 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.ui.Components; + +import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.*; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +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.AnimatorListenerAdapterProxy; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.MessagesController; +import org.telegram.messenger.ContactsController; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MediaController; +import org.telegram.messenger.MessageObject; +import org.telegram.messenger.NotificationCenter; +import org.telegram.messenger.query.SearchQuery; +import org.telegram.messenger.support.widget.LinearLayoutManager; +import org.telegram.messenger.R; +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.PhotoAttachCameraCell; +import org.telegram.ui.Cells.PhotoAttachPhotoCell; +import org.telegram.ui.Cells.ShadowSectionCell; +import org.telegram.ui.ChatActivity; +import org.telegram.ui.PhotoViewer; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; + +public class ChatAttachAlert extends BottomSheet implements NotificationCenter.NotificationCenterDelegate, PhotoViewer.PhotoViewerProvider, BottomSheet.BottomSheetDelegateInterface { + + public interface ChatAttachViewDelegate { + void didPressedButton(int button); + View getRevealView(); + void didSelectBot(TLRPC.User user); + } + + private class InnerAnimator { + private AnimatorSet animatorSet; + private float startRadius; + } + + private LinearLayoutManager attachPhotoLayoutManager; + private PhotoAttachAdapter photoAttachAdapter; + private ChatActivity baseFragment; + private AttachButton sendPhotosButton; + private View views[] = new View[20]; + private RecyclerListView attachPhotoRecyclerView; + private View lineView; + private EmptyTextProgressView progressView; + private ArrayList viewsCache = new ArrayList<>(8); + private RecyclerListView listView; + private LinearLayoutManager layoutManager; + private Drawable shadowDrawable; + private ViewGroup attachView; + private ListAdapter adapter; + private TextView hintTextView; + private ArrayList innerAnimators = new ArrayList<>(); + + private AnimatorSet currentHintAnimation; + private boolean hintShowed; + private Runnable hideHintRunnable; + + private boolean deviceHasGoodCamera = false;//Build.VERSION.SDK_INT >= 16; + + private DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator(); + + private boolean loading = true; + + private ChatAttachViewDelegate delegate; + + private int scrollOffsetY; + private boolean ignoreLayout; + + private boolean useRevealAnimation; + private float revealRadius; + private int revealX; + private int revealY; + private boolean revealAnimationInProgress; + + private class AttachButton extends FrameLayout { + + private TextView textView; + private ImageView imageView; + + public AttachButton(Context context) { + super(context); + + imageView = new ImageView(context); + imageView.setScaleType(ImageView.ScaleType.CENTER); + addView(imageView, LayoutHelper.createFrame(64, 64, Gravity.CENTER_HORIZONTAL | Gravity.TOP)); + + textView = new TextView(context); + textView.setLines(1); + textView.setSingleLine(true); + textView.setGravity(Gravity.CENTER_HORIZONTAL); + textView.setEllipsize(TextUtils.TruncateAt.END); + textView.setTextColor(Theme.ATTACH_SHEET_TEXT_COLOR); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); + addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 0, 64, 0, 0)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(85), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(90), MeasureSpec.EXACTLY)); + } + + public void setTextAndIcon(CharSequence text, Drawable drawable) { + textView.setText(text); + imageView.setBackgroundDrawable(drawable); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + } + + private class AttachBotButton extends FrameLayout { + + private BackupImageView imageView; + private TextView nameTextView; + private AvatarDrawable avatarDrawable = new AvatarDrawable(); + private boolean pressed; + + private boolean checkingForLongPress = false; + private CheckForLongPress pendingCheckForLongPress = null; + private int pressCount = 0; + private CheckForTap pendingCheckForTap = null; + + private TLRPC.User currentUser; + + private final class CheckForTap implements Runnable { + public void run() { + if (pendingCheckForLongPress == null) { + pendingCheckForLongPress = new CheckForLongPress(); + } + pendingCheckForLongPress.currentPressCount = ++pressCount; + postDelayed(pendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout()); + } + } + + class CheckForLongPress implements Runnable { + public int currentPressCount; + + public void run() { + if (checkingForLongPress && getParent() != null && currentPressCount == pressCount) { + checkingForLongPress = false; + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + onLongPress(); + MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0); + onTouchEvent(event); + event.recycle(); + } + } + } + + public AttachBotButton(Context context) { + super(context); + + imageView = new BackupImageView(context); + imageView.setRoundRadius(AndroidUtilities.dp(27)); + addView(imageView, LayoutHelper.createFrame(54, 54, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 7, 0, 0)); + + nameTextView = new TextView(context); + nameTextView.setTextColor(Theme.ATTACH_SHEET_TEXT_COLOR); + nameTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); + nameTextView.setMaxLines(2); + nameTextView.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL); + nameTextView.setLines(2); + nameTextView.setEllipsize(TextUtils.TruncateAt.END); + addView(nameTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 6, 65, 6, 0)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(85), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(100), MeasureSpec.EXACTLY)); + } + + private void onLongPress() { + if (baseFragment == null || currentUser == null) { + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setMessage(LocaleController.formatString("ChatHintsDelete", R.string.ChatHintsDelete, ContactsController.formatName(currentUser.first_name, currentUser.last_name))); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + SearchQuery.removeInline(currentUser.id); + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + builder.show(); + } + + public void setUser(TLRPC.User user) { + if (user == null) { + return; + } + currentUser = user; + TLRPC.FileLocation photo = null; + nameTextView.setText(ContactsController.formatName(user.first_name, user.last_name)); + avatarDrawable.setInfo(user); + if (user != null && user.photo != null) { + photo = user.photo.photo_small; + } + imageView.setImage(photo, "50_50", avatarDrawable); + requestLayout(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean result = false; + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + pressed = true; + invalidate(); + result = true; + } else if (pressed) { + if (event.getAction() == MotionEvent.ACTION_UP) { + getParent().requestDisallowInterceptTouchEvent(true); + pressed = false; + playSoundEffect(SoundEffectConstants.CLICK); + delegate.didSelectBot(MessagesController.getInstance().getUser(SearchQuery.inlineBots.get((Integer) getTag()).peer.user_id)); + setUseRevealAnimation(false); + dismiss(); + setUseRevealAnimation(true); + invalidate(); + } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { + pressed = false; + invalidate(); + } + } + if (!result) { + result = super.onTouchEvent(event); + } else { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + startCheckLongPress(); + } + } + if (event.getAction() != MotionEvent.ACTION_DOWN && event.getAction() != MotionEvent.ACTION_MOVE) { + cancelCheckLongPress(); + } + + return result; + } + + protected void startCheckLongPress() { + if (checkingForLongPress) { + return; + } + checkingForLongPress = true; + if (pendingCheckForTap == null) { + pendingCheckForTap = new CheckForTap(); + } + postDelayed(pendingCheckForTap, ViewConfiguration.getTapTimeout()); + } + + protected void cancelCheckLongPress() { + checkingForLongPress = false; + if (pendingCheckForLongPress != null) { + removeCallbacks(pendingCheckForLongPress); + } + if (pendingCheckForTap != null) { + removeCallbacks(pendingCheckForTap); + } + } + } + + public ChatAttachAlert(Context context) { + super(context, false); + setDelegate(this); + setUseRevealAnimation(true); + if (deviceHasGoodCamera) { + //CameraController.getInstance().initCamera(); + } + NotificationCenter.getInstance().addObserver(this, NotificationCenter.albumsDidLoaded); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.reloadInlineHints); + shadowDrawable = context.getResources().getDrawable(R.drawable.sheet_shadow); + + containerView = listView = new RecyclerListView(context) { + @Override + public void requestLayout() { + if (ignoreLayout) { + return; + } + super.requestLayout(); + } + + @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 contentSize = backgroundPaddingTop + AndroidUtilities.dp(294) + (SearchQuery.inlineBots.isEmpty() ? 0 : ((int) Math.ceil(SearchQuery.inlineBots.size() / 4.0f) * AndroidUtilities.dp(100) + AndroidUtilities.dp(12))); + int padding = contentSize == AndroidUtilities.dp(294) ? 0 : (height - AndroidUtilities.dp(294)); + if (padding != 0 && contentSize < height) { + padding -= (height - contentSize); + } + if (padding == 0) { + padding = backgroundPaddingTop; + } + if (getPaddingTop() != padding) { + ignoreLayout = true; + setPadding(backgroundPaddingLeft, padding, backgroundPaddingLeft, 0); + ignoreLayout = false; + } + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateLayout(); + } + + @Override + public void onDraw(Canvas canvas) { + if (useRevealAnimation && Build.VERSION.SDK_INT <= 19) { + canvas.save(); + canvas.clipRect(backgroundPaddingLeft, scrollOffsetY, getMeasuredWidth() - backgroundPaddingLeft, getMeasuredHeight()); + if (revealAnimationInProgress) { + canvas.drawCircle(revealX, revealY, revealRadius, ciclePaint); + } else { + canvas.drawRect(backgroundPaddingLeft, scrollOffsetY, getMeasuredWidth() - backgroundPaddingLeft, getMeasuredHeight(), ciclePaint); + } + canvas.restore(); + } else { + shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); + shadowDrawable.draw(canvas); + } + } + }; + + listView.setTag(10); + containerView.setWillNotDraw(false); + listView.setClipToPadding(false); + listView.setLayoutManager(layoutManager = new LinearLayoutManager(getContext())); + layoutManager.setOrientation(LinearLayoutManager.VERTICAL); + listView.setAdapter(adapter = new ListAdapter(context)); + listView.setVerticalScrollBarEnabled(false); + listView.setEnabled(true); + listView.setGlowColor(0xfff5f6f7); + listView.addItemDecoration(new RecyclerView.ItemDecoration() { + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + outRect.left = 0; + outRect.right = 0; + outRect.top = 0; + outRect.bottom = 0; + } + }); + + listView.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (listView.getChildCount() <= 0) { + return; + } + if (hintShowed) { + if (layoutManager.findLastVisibleItemPosition() > 1) { + hideHint(); + hintShowed = false; + ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).edit().putBoolean("bothint", true).commit(); + } + } + updateLayout(); + } + }); + containerView.setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); + + attachView = new FrameLayout(context) { + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(294), MeasureSpec.EXACTLY)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int width = right - left; + int height = bottom - top; + int t = AndroidUtilities.dp(8); + attachPhotoRecyclerView.layout(0, t, width, t + attachPhotoRecyclerView.getMeasuredHeight()); + 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 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); + views[a].layout(x, y, x + views[a].getMeasuredWidth(), y + views[a].getMeasuredHeight()); + } + } + }; + + views[8] = attachPhotoRecyclerView = new RecyclerListView(context); + attachPhotoRecyclerView.setVerticalScrollBarEnabled(true); + attachPhotoRecyclerView.setAdapter(photoAttachAdapter = new PhotoAttachAdapter(context)); + attachPhotoRecyclerView.setClipToPadding(false); + attachPhotoRecyclerView.setPadding(AndroidUtilities.dp(8), 0, AndroidUtilities.dp(8), 0); + attachPhotoRecyclerView.setItemAnimator(null); + attachPhotoRecyclerView.setLayoutAnimation(null); + attachPhotoRecyclerView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); + attachView.addView(attachPhotoRecyclerView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 80)); + attachPhotoLayoutManager = new LinearLayoutManager(context) { + @Override + public boolean supportsPredictiveItemAnimations() { + return false; + } + }; + attachPhotoLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); + attachPhotoRecyclerView.setLayoutManager(attachPhotoLayoutManager); + attachPhotoRecyclerView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { + @SuppressWarnings("unchecked") + @Override + public void onItemClick(View view, int position) { + if (baseFragment == null || baseFragment.getParentActivity() == null) { + return; + } + if (!deviceHasGoodCamera || position != 0) { + if (deviceHasGoodCamera) { + position--; + } + ArrayList arrayList = (ArrayList) MediaController.allPhotosAlbumEntry.photos; + if (position < 0 || position >= arrayList.size()) { + return; + } + PhotoViewer.getInstance().setParentActivity(baseFragment.getParentActivity()); + PhotoViewer.getInstance().openPhotoForSelect(arrayList, position, 0, ChatAttachAlert.this, baseFragment); + AndroidUtilities.hideKeyboard(baseFragment.getFragmentView().findFocus()); + } else { + final File path = AndroidUtilities.generatePicturePath(); + /*CameraController.getInstance().takePicture(path, ((PhotoAttachCameraCell) view).cameraSession, new Runnable() { + @Override + public void run() { + PhotoViewer.getInstance().setParentActivity(baseFragment.getParentActivity()); + final ArrayList arrayList = new ArrayList<>(); + int orientation = 0; + try { + ExifInterface ei = new ExifInterface(path.getAbsolutePath()); + int exif = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + switch (exif) { + case ExifInterface.ORIENTATION_ROTATE_90: + orientation = 90; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + orientation = 180; + break; + case ExifInterface.ORIENTATION_ROTATE_270: + orientation = 270; + break; + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + arrayList.add(new MediaController.PhotoEntry(0, 0, 0, path.getAbsolutePath(), orientation, false)); + + PhotoViewer.getInstance().openPhotoForSelect(arrayList, 0, 2, new PhotoViewer.EmptyPhotoViewerProvider() { + @Override + public void sendButtonPressed(int index) { + AndroidUtilities.addMediaToGallery(path.getAbsolutePath()); + baseFragment.sendPhoto((MediaController.PhotoEntry) arrayList.get(0)); + } + + @Override + public boolean cancelButtonPressed() { + path.delete(); + return true; + } + }, baseFragment); + } + });*/ + } + } + }); + + 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)); + progressView.setTextSize(16); + } else { + progressView.setText(LocaleController.getString("NoPhotos", R.string.NoPhotos)); + progressView.setTextSize(20); + } + attachView.addView(progressView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 80)); + attachPhotoRecyclerView.setEmptyView(progressView); + + views[10] = lineView = new View(getContext()) { + @Override + public boolean hasOverlappingRendering() { + return false; + } + }; + lineView.setBackgroundColor(0xffd2d2d2); + attachView.addView(lineView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1, Gravity.TOP | Gravity.LEFT)); + CharSequence[] items = new CharSequence[]{ + LocaleController.getString("ChatCamera", R.string.ChatCamera), + LocaleController.getString("ChatGallery", R.string.ChatGallery), + LocaleController.getString("ChatVideo", R.string.ChatVideo), + LocaleController.getString("AttachMusic", R.string.AttachMusic), + LocaleController.getString("ChatDocument", R.string.ChatDocument), + LocaleController.getString("AttachContact", R.string.AttachContact), + LocaleController.getString("ChatLocation", R.string.ChatLocation), + "" + }; + for (int a = 0; a < 8; a++) { + AttachButton attachButton = new AttachButton(context); + attachButton.setTextAndIcon(items[a], Theme.attachButtonDrawables[a]); + attachView.addView(attachButton, LayoutHelper.createFrame(85, 90, Gravity.LEFT | Gravity.TOP)); + attachButton.setTag(a); + views[a] = attachButton; + if (a == 7) { + sendPhotosButton = attachButton; + sendPhotosButton.imageView.setPadding(0, AndroidUtilities.dp(4), 0, 0); + } + attachButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + delegate.didPressedButton((Integer) v.getTag()); + } + }); + } + + hintTextView = new TextView(context); + hintTextView.setBackgroundResource(R.drawable.tooltip); + hintTextView.setTextColor(Theme.CHAT_GIF_HINT_TEXT_COLOR); + hintTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + hintTextView.setPadding(AndroidUtilities.dp(10), 0, AndroidUtilities.dp(10), 0); + hintTextView.setText(LocaleController.getString("AttachBotsHelp", R.string.AttachBotsHelp)); + hintTextView.setGravity(Gravity.CENTER_VERTICAL); + hintTextView.setVisibility(View.INVISIBLE); + hintTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.scroll_tip, 0, 0, 0); + hintTextView.setCompoundDrawablePadding(AndroidUtilities.dp(8)); + attachView.addView(hintTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 32, Gravity.RIGHT | Gravity.BOTTOM, 5, 0, 5, 5)); + + for (int a = 0; a < 8; a++) { + viewsCache.add(photoAttachAdapter.createHolder()); + } + + if (loading) { + progressView.showProgress(); + } else { + progressView.showTextView(); + } + } + + private void hideHint() { + if (hideHintRunnable != null) { + AndroidUtilities.cancelRunOnUIThread(hideHintRunnable); + hideHintRunnable = null; + } + if (hintTextView == null) { + return; + } + currentHintAnimation = new AnimatorSet(); + currentHintAnimation.playTogether( + ObjectAnimator.ofFloat(hintTextView, "alpha", 0.0f) + ); + currentHintAnimation.setInterpolator(decelerateInterpolator); + currentHintAnimation.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (currentHintAnimation == null || !currentHintAnimation.equals(animation)) { + return; + } + currentHintAnimation = null; + if (hintTextView != null) { + hintTextView.setVisibility(View.INVISIBLE); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (currentHintAnimation != null && currentHintAnimation.equals(animation)) { + currentHintAnimation = null; + } + } + }); + currentHintAnimation.setDuration(300); + currentHintAnimation.start(); + } + + private void showHint() { + if (SearchQuery.inlineBots.isEmpty()) { + return; + } + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + if (preferences.getBoolean("bothint", false)) { + return; + } + hintShowed = true; + + hintTextView.setVisibility(View.VISIBLE); + currentHintAnimation = new AnimatorSet(); + currentHintAnimation.playTogether( + ObjectAnimator.ofFloat(hintTextView, "alpha", 0.0f, 1.0f) + ); + currentHintAnimation.setInterpolator(decelerateInterpolator); + currentHintAnimation.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (currentHintAnimation == null || !currentHintAnimation.equals(animation)) { + return; + } + currentHintAnimation = null; + AndroidUtilities.runOnUIThread(hideHintRunnable = new Runnable() { + @Override + public void run() { + if (hideHintRunnable != this) { + return; + } + hideHintRunnable = null; + hideHint(); + } + }, 2000); + } + + @Override + public void onAnimationCancel(Animator animation) { + if (currentHintAnimation != null && currentHintAnimation.equals(animation)) { + currentHintAnimation = null; + } + } + }); + currentHintAnimation.setDuration(300); + currentHintAnimation.start(); + } + + @Override + public void didReceivedNotification(int id, Object... args) { + if (id == NotificationCenter.albumsDidLoaded) { + if (photoAttachAdapter != null) { + loading = false; + progressView.showTextView(); + photoAttachAdapter.notifyDataSetChanged(); + } + } else if (id == NotificationCenter.reloadInlineHints) { + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + } + } + + @SuppressLint("NewApi") + private void updateLayout() { + if (listView.getChildCount() <= 0) { + listView.setTopGlowOffset(scrollOffsetY = listView.getPaddingTop()); + containerView.invalidate(); + return; + } + View child = listView.getChildAt(0); + Holder holder = (Holder) listView.findContainingViewHolder(child); + int top = child.getTop(); + int newOffset = 0; + if (top >= 0 && holder != null && holder.getAdapterPosition() == 0) { + newOffset = top; + } + if (scrollOffsetY != newOffset) { + listView.setTopGlowOffset(scrollOffsetY = newOffset); + containerView.invalidate(); + } + } + + @Override + protected boolean canDismissWithSwipe() { + return false; + } + + public void updatePhotosButton() { + int count = photoAttachAdapter.getSelectedPhotos().size(); + if (count == 0) { + sendPhotosButton.imageView.setPadding(0, AndroidUtilities.dp(4), 0, 0); + sendPhotosButton.imageView.setBackgroundResource(R.drawable.attach_hide_states); + sendPhotosButton.imageView.setImageResource(R.drawable.attach_hide2); + sendPhotosButton.textView.setText(""); + } 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))); + } + + if (Build.VERSION.SDK_INT >= 23 && getContext().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + progressView.setText(LocaleController.getString("PermissionStorage", R.string.PermissionStorage)); + progressView.setTextSize(16); + } else { + progressView.setText(LocaleController.getString("NoPhotos", R.string.NoPhotos)); + progressView.setTextSize(20); + } + } + + public void setDelegate(ChatAttachViewDelegate chatAttachViewDelegate) { + delegate = chatAttachViewDelegate; + } + + public void loadGalleryPhotos() { + if (MediaController.allPhotosAlbumEntry == null && Build.VERSION.SDK_INT >= 21) { + MediaController.loadGalleryPhotosAlbums(0); + } + } + + public void init(ChatActivity parentFragment) { + 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); + photoEntry.caption = null; + photoEntry.imagePath = null; + photoEntry.thumbPath = null; + } + } + if (currentHintAnimation != null) { + currentHintAnimation.cancel(); + currentHintAnimation = null; + } + hintTextView.setAlpha(0.0f); + hintTextView.setVisibility(View.INVISIBLE); + attachPhotoLayoutManager.scrollToPositionWithOffset(0, 1000000); + photoAttachAdapter.clearSelectedPhotos(); + baseFragment = parentFragment; + layoutManager.scrollToPositionWithOffset(0, 1000000); + updatePhotosButton(); + } + + public HashMap getSelectedPhotos() { + return photoAttachAdapter.getSelectedPhotos(); + } + + public void onDestroy() { + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.albumsDidLoaded); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.reloadInlineHints); + baseFragment = null; + } + + private PhotoAttachPhotoCell getCellForIndex(int index) { + if (MediaController.allPhotosAlbumEntry == null) { + return null; + } + int count = attachPhotoRecyclerView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = attachPhotoRecyclerView.getChildAt(a); + if (view instanceof PhotoAttachPhotoCell) { + PhotoAttachPhotoCell cell = (PhotoAttachPhotoCell) view; + int num = (Integer) cell.getImageView().getTag(); + if (num < 0 || num >= MediaController.allPhotosAlbumEntry.photos.size()) { + continue; + } + if (num == index) { + return cell; + } + } + } + return null; + } + + @Override + public PhotoViewer.PlaceProviderObject getPlaceForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { + PhotoAttachPhotoCell cell = getCellForIndex(index); + if (cell != null) { + int coords[] = new int[2]; + cell.getImageView().getLocationInWindow(coords); + PhotoViewer.PlaceProviderObject object = new PhotoViewer.PlaceProviderObject(); + object.viewX = coords[0]; + object.viewY = coords[1] - (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + object.parentView = attachPhotoRecyclerView; + object.imageReceiver = cell.getImageView().getImageReceiver(); + object.thumb = object.imageReceiver.getBitmap(); + object.scale = cell.getImageView().getScaleX(); + object.clipBottomAddition = (Build.VERSION.SDK_INT >= 21 ? 0 : -AndroidUtilities.statusBarHeight); + cell.getCheckBox().setVisibility(View.GONE); + return object; + } + return null; + } + + @Override + public void updatePhotoAtIndex(int index) { + PhotoAttachPhotoCell cell = getCellForIndex(index); + if (cell != null) { + cell.getImageView().setOrientation(0, true); + MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.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) { + cell.getImageView().setOrientation(photoEntry.orientation, true); + cell.getImageView().setImage("thumb://" + photoEntry.imageId + ":" + photoEntry.path, null, cell.getContext().getResources().getDrawable(R.drawable.nophotos)); + } else { + cell.getImageView().setImageResource(R.drawable.nophotos); + } + } + } + + @Override + public Bitmap getThumbForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { + PhotoAttachPhotoCell cell = getCellForIndex(index); + if (cell != null) { + return cell.getImageView().getImageReceiver().getBitmap(); + } + return null; + } + + @Override + public void willSwitchFromPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { + PhotoAttachPhotoCell cell = getCellForIndex(index); + if (cell != null) { + cell.getCheckBox().setVisibility(View.VISIBLE); + } + } + + @Override + public void willHidePhotoViewer() { + int count = attachPhotoRecyclerView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = attachPhotoRecyclerView.getChildAt(a); + if (view instanceof PhotoAttachPhotoCell) { + PhotoAttachPhotoCell cell = (PhotoAttachPhotoCell) view; + if (cell.getCheckBox().getVisibility() != View.VISIBLE) { + cell.getCheckBox().setVisibility(View.VISIBLE); + } + } + } + } + + @Override + public boolean isPhotoChecked(int index) { + return !(index < 0 || index >= MediaController.allPhotosAlbumEntry.photos.size()) && photoAttachAdapter.getSelectedPhotos().containsKey(MediaController.allPhotosAlbumEntry.photos.get(index).imageId); + } + + @Override + public void setPhotoChecked(int index) { + boolean add = true; + if (index < 0 || index >= MediaController.allPhotosAlbumEntry.photos.size()) { + return; + } + MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.photos.get(index); + if (photoAttachAdapter.getSelectedPhotos().containsKey(photoEntry.imageId)) { + photoAttachAdapter.getSelectedPhotos().remove(photoEntry.imageId); + add = false; + } else { + photoAttachAdapter.getSelectedPhotos().put(photoEntry.imageId, photoEntry); + } + int count = attachPhotoRecyclerView.getChildCount(); + for (int a = 0; a < count; a++) { + View view = attachPhotoRecyclerView.getChildAt(a); + int num = (Integer) view.getTag(); + if (num == index) { + ((PhotoAttachPhotoCell) view).setChecked(add, false); + break; + } + } + updatePhotosButton(); + } + + @Override + public boolean cancelButtonPressed() { + return false; + } + + @Override + public void sendButtonPressed(int index) { + if (photoAttachAdapter.getSelectedPhotos().isEmpty()) { + if (index < 0 || index >= MediaController.allPhotosAlbumEntry.photos.size()) { + return; + } + MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.photos.get(index); + photoAttachAdapter.getSelectedPhotos().put(photoEntry.imageId, photoEntry); + } + delegate.didPressedButton(7); + } + + @Override + public int getSelectedCount() { + return photoAttachAdapter.getSelectedPhotos().size(); + } + + private void onRevealAnimationEnd(boolean open) { + NotificationCenter.getInstance().setAnimationInProgress(false); + revealAnimationInProgress = false; + if (open && Build.VERSION.SDK_INT <= 19 && MediaController.allPhotosAlbumEntry == null) { + MediaController.loadGalleryPhotosAlbums(0); + } + if (open) { + showHint(); + } + } + + @Override + public void onOpenAnimationEnd() { + onRevealAnimationEnd(true); + } + + @Override + public void onOpenAnimationStart() { + + } + + private class Holder extends RecyclerView.ViewHolder { + + public Holder(View itemView) { + super(itemView); + } + } + + private class ListAdapter extends RecyclerView.Adapter { + + private Context mContext; + + public ListAdapter(Context context) { + mContext = context; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view; + switch (viewType) { + case 0: + view = attachView; + break; + case 1: + view = new ShadowSectionCell(mContext); + break; + default: + FrameLayout frameLayout = new FrameLayout(mContext) { + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int diff = (right - left - AndroidUtilities.dp(85 * 4 + 20)) / 3; + for (int a = 0; a < 4; a++) { + int x = AndroidUtilities.dp(10) + (a % 4) * (AndroidUtilities.dp(85) + diff); + View child = getChildAt(a); + child.layout(x, 0, x + child.getMeasuredWidth(), child.getMeasuredHeight()); + } + } + }; + for (int a = 0; a < 4; a++) { + frameLayout.addView(new AttachBotButton(mContext)); + } + view = frameLayout; + frameLayout.setLayoutParams(new RecyclerView.LayoutParams(LayoutHelper.MATCH_PARENT, AndroidUtilities.dp(100))); + break; + } + return new Holder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + if (position > 1) { + position -= 2; + position *= 4; + FrameLayout frameLayout = (FrameLayout) holder.itemView; + for (int a = 0; a < 4; a++) { + AttachBotButton child = (AttachBotButton) frameLayout.getChildAt(a); + if (position + a >= SearchQuery.inlineBots.size()) { + child.setVisibility(View.INVISIBLE); + } else { + child.setVisibility(View.VISIBLE); + child.setTag(position + a); + child.setUser(MessagesController.getInstance().getUser(SearchQuery.inlineBots.get(position + a).peer.user_id)); + } + } + } + } + + @Override + public int getItemCount() { + return 1 + (!SearchQuery.inlineBots.isEmpty() ? 1 + (int) Math.ceil(SearchQuery.inlineBots.size() / 4.0f) : 0); + } + + @Override + public int getItemViewType(int position) { + switch (position) { + case 0: + return 0; + case 1: + return 1; + default: + return 2; + } + } + } + + private class PhotoAttachAdapter extends RecyclerView.Adapter { + + private Context mContext; + private HashMap selectedPhotos = new HashMap<>(); + + public PhotoAttachAdapter(Context context) { + mContext = context; + } + + public void clearSelectedPhotos() { + if (!selectedPhotos.isEmpty()) { + for (HashMap.Entry entry : selectedPhotos.entrySet()) { + MediaController.PhotoEntry photoEntry = entry.getValue(); + photoEntry.imagePath = null; + photoEntry.thumbPath = null; + photoEntry.caption = null; + } + selectedPhotos.clear(); + updatePhotosButton(); + notifyDataSetChanged(); + } + } + + public Holder createHolder() { + PhotoAttachPhotoCell cell = new PhotoAttachPhotoCell(mContext); + cell.setDelegate(new PhotoAttachPhotoCell.PhotoAttachPhotoCellDelegate() { + @Override + public void onCheckClick(PhotoAttachPhotoCell v) { + MediaController.PhotoEntry photoEntry = v.getPhotoEntry(); + if (selectedPhotos.containsKey(photoEntry.imageId)) { + selectedPhotos.remove(photoEntry.imageId); + v.setChecked(false, true); + photoEntry.imagePath = null; + photoEntry.thumbPath = null; + v.setPhotoEntry(photoEntry, (Integer) v.getTag() == MediaController.allPhotosAlbumEntry.photos.size() - 1); + } else { + selectedPhotos.put(photoEntry.imageId, photoEntry); + v.setChecked(true, true); + } + updatePhotosButton(); + } + }); + return new Holder(cell); + } + + public HashMap getSelectedPhotos() { + return selectedPhotos; + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + if (!deviceHasGoodCamera || position != 0) { + if (deviceHasGoodCamera) { + position--; + } + PhotoAttachPhotoCell cell = (PhotoAttachPhotoCell) holder.itemView; + MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.photos.get(position); + cell.setPhotoEntry(photoEntry, position == MediaController.allPhotosAlbumEntry.photos.size() - 1); + cell.setChecked(selectedPhotos.containsKey(photoEntry.imageId), false); + cell.getImageView().setTag(position); + cell.setTag(position); + } + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + Holder holder; + switch (viewType) { + case 1: + holder = new Holder(new PhotoAttachCameraCell(mContext)); + break; + default: + if (!viewsCache.isEmpty()) { + holder = viewsCache.get(0); + viewsCache.remove(0); + } else { + holder = createHolder(); + } + break; + } + + return holder; + } + + @Override + public int getItemCount() { + int count = 0; + if (deviceHasGoodCamera) { + count++; + } + if (MediaController.allPhotosAlbumEntry != null) { + count += MediaController.allPhotosAlbumEntry.photos.size(); + } + return count; + } + + @Override + public int getItemViewType(int position) { + if (deviceHasGoodCamera && position == 0) { + return 1; + } + return 0; + } + } + + private void setUseRevealAnimation(boolean value) { + if (!value || value && Build.VERSION.SDK_INT >= 18 && !AndroidUtilities.isTablet()) { + useRevealAnimation = value; + } + } + + @SuppressLint("NewApi") + protected void setRevealRadius(float radius) { + revealRadius = radius; + if (Build.VERSION.SDK_INT <= 19) { + containerView.invalidate(); + } + if (!isDismissed()) { + for (int a = 0; a < innerAnimators.size(); a++) { + InnerAnimator innerAnimator = innerAnimators.get(a); + if (innerAnimator.startRadius > radius) { + continue; + } + innerAnimator.animatorSet.start(); + innerAnimators.remove(a); + a--; + } + } + } + + protected float getRevealRadius() { + return revealRadius; + } + + @SuppressLint("NewApi") + private void startRevealAnimation(final boolean open) { + containerView.setTranslationY(0); + + final AnimatorSet animatorSet = new AnimatorSet(); + + View view = delegate.getRevealView(); + if (view.getVisibility() == View.VISIBLE && ((ViewGroup) view.getParent()).getVisibility() == View.VISIBLE) { + final int coords[] = new int[2]; + view.getLocationInWindow(coords); + float top; + if (Build.VERSION.SDK_INT <= 19) { + top = AndroidUtilities.displaySize.y - containerView.getMeasuredHeight() - AndroidUtilities.statusBarHeight; + } else { + top = containerView.getY(); + } + revealX = coords[0] + view.getMeasuredWidth() / 2; + revealY = (int) (coords[1] + view.getMeasuredHeight() / 2 - top); + if (Build.VERSION.SDK_INT <= 19) { + revealY -= AndroidUtilities.statusBarHeight; + } + } else { + revealX = AndroidUtilities.displaySize.x / 2 + backgroundPaddingLeft; + revealY = (int) (AndroidUtilities.displaySize.y - containerView.getY()); + } + + int corners[][] = new int[][]{ + {0, 0}, + {0, AndroidUtilities.dp(304)}, + {containerView.getMeasuredWidth(), 0}, + {containerView.getMeasuredWidth(), AndroidUtilities.dp(304)} + }; + int finalRevealRadius = 0; + int y = revealY - scrollOffsetY + backgroundPaddingTop; + for (int a = 0; a < 4; a++) { + finalRevealRadius = Math.max(finalRevealRadius, (int) Math.ceil(Math.sqrt((revealX - corners[a][0]) * (revealX - corners[a][0]) + (y - corners[a][1]) * (y - corners[a][1])))); + } + int finalRevealX = revealX <= containerView.getMeasuredWidth() ? revealX : containerView.getMeasuredWidth(); + + ArrayList animators = new ArrayList<>(3); + animators.add(ObjectAnimator.ofFloat(this, "revealRadius", open ? 0 : finalRevealRadius, open ? finalRevealRadius : 0)); + animators.add(ObjectAnimator.ofInt(backDrawable, "alpha", open ? 51 : 0)); + if (Build.VERSION.SDK_INT >= 21) { + containerView.setElevation(AndroidUtilities.dp(10)); + try { + animators.add(ViewAnimationUtils.createCircularReveal(containerView, finalRevealX, revealY, open ? 0 : finalRevealRadius, open ? finalRevealRadius : 0)); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + animatorSet.setDuration(320); + } else { + if (!open) { + animatorSet.setDuration(200); + containerView.setPivotX(revealX <= containerView.getMeasuredWidth() ? revealX : containerView.getMeasuredWidth()); + containerView.setPivotY(revealY); + animators.add(ObjectAnimator.ofFloat(containerView, "scaleX", 0.0f)); + animators.add(ObjectAnimator.ofFloat(containerView, "scaleY", 0.0f)); + animators.add(ObjectAnimator.ofFloat(containerView, "alpha", 0.0f)); + } else { + animatorSet.setDuration(250); + containerView.setScaleX(1); + containerView.setScaleY(1); + containerView.setAlpha(1); + if (Build.VERSION.SDK_INT <= 19) { + animatorSet.setStartDelay(20); + } + } + } + animatorSet.playTogether(animators); + animatorSet.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { + currentSheetAnimation = null; + onRevealAnimationEnd(open); + containerView.invalidate(); + containerView.setLayerType(View.LAYER_TYPE_NONE, null); + if (!open) { + containerView.setVisibility(View.INVISIBLE); + try { + dismissInternal(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (currentSheetAnimation != null && animatorSet.equals(animation)) { + currentSheetAnimation = null; + } + } + }); + + if (open) { + innerAnimators.clear(); + NotificationCenter.getInstance().setAllowedNotificationsDutingAnimation(new int[]{NotificationCenter.dialogsNeedReload}); + NotificationCenter.getInstance().setAnimationInProgress(true); + revealAnimationInProgress = true; + + int count = Build.VERSION.SDK_INT <= 19 ? 11 : 8; + for (int a = 0; a < count; a++) { + if (Build.VERSION.SDK_INT <= 19) { + if (a < 8) { + views[a].setScaleX(0.1f); + views[a].setScaleY(0.1f); + } + views[a].setAlpha(0.0f); + } else { + views[a].setScaleX(0.7f); + views[a].setScaleY(0.7f); + } + + InnerAnimator innerAnimator = new InnerAnimator(); + + int buttonX = views[a].getLeft() + views[a].getMeasuredWidth() / 2; + int buttonY = views[a].getTop() + attachView.getTop() + views[a].getMeasuredHeight() / 2; + float dist = (float) Math.sqrt((revealX - buttonX) * (revealX - buttonX) + (revealY - buttonY) * (revealY - buttonY)); + float vecX = (revealX - buttonX) / dist; + float vecY = (revealY - buttonY) / dist; + views[a].setPivotX(views[a].getMeasuredWidth() / 2 + vecX * AndroidUtilities.dp(20)); + views[a].setPivotY(views[a].getMeasuredHeight() / 2 + vecY * AndroidUtilities.dp(20)); + innerAnimator.startRadius = dist - AndroidUtilities.dp(27 * 3); + + views[a].setTag(R.string.AppName, 1); + animators = new ArrayList<>(); + final AnimatorSet animatorSetInner; + if (a < 8) { + animators.add(ObjectAnimator.ofFloat(views[a], "scaleX", 0.7f, 1.05f)); + animators.add(ObjectAnimator.ofFloat(views[a], "scaleY", 0.7f, 1.05f)); + + animatorSetInner = new AnimatorSet(); + animatorSetInner.playTogether( + ObjectAnimator.ofFloat(views[a], "scaleX", 1.0f), + ObjectAnimator.ofFloat(views[a], "scaleY", 1.0f)); + animatorSetInner.setDuration(100); + animatorSetInner.setInterpolator(decelerateInterpolator); + } else { + animatorSetInner = null; + } + if (Build.VERSION.SDK_INT <= 19) { + animators.add(ObjectAnimator.ofFloat(views[a], "alpha", 1.0f)); + } + innerAnimator.animatorSet = new AnimatorSet(); + innerAnimator.animatorSet.playTogether(animators); + innerAnimator.animatorSet.setDuration(150); + innerAnimator.animatorSet.setInterpolator(decelerateInterpolator); + innerAnimator.animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (animatorSetInner != null) { + animatorSetInner.start(); + } + } + }); + innerAnimators.add(innerAnimator); + } + } + currentSheetAnimation = animatorSet; + animatorSet.start(); + } + + @Override + protected boolean onCustomOpenAnimation() { + if (useRevealAnimation) { + startRevealAnimation(true); + return true; + } + return false; + } + + @Override + protected boolean onCustomCloseAnimation() { + if (useRevealAnimation) { + backDrawable.setAlpha(51); + startRevealAnimation(false); + return true; + } + return false; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachView.java deleted file mode 100644 index 1f31dbf6a..000000000 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachView.java +++ /dev/null @@ -1,611 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.x. - * It is licensed under GNU GPL v. 2 or later. - * You should have received a copy of the license in this archive (see LICENSE). - * - * Copyright Nikolai Kudashov, 2013-2016. - */ - -package org.telegram.ui.Components; - -import android.Manifest; -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.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.text.TextUtils; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.DecelerateInterpolator; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ViewProxy; -import org.telegram.messenger.LocaleController; -import org.telegram.messenger.MediaController; -import org.telegram.messenger.MessageObject; -import org.telegram.messenger.NotificationCenter; -import org.telegram.messenger.support.widget.LinearLayoutManager; -import org.telegram.messenger.R; -import org.telegram.messenger.support.widget.RecyclerView; -import org.telegram.tgnet.TLRPC; -import org.telegram.ui.ActionBar.Theme; -import org.telegram.ui.Cells.PhotoAttachPhotoCell; -import org.telegram.ui.ChatActivity; -import org.telegram.ui.PhotoViewer; - -import java.util.ArrayList; -import java.util.HashMap; - -public class ChatAttachView extends FrameLayout implements NotificationCenter.NotificationCenterDelegate, PhotoViewer.PhotoViewerProvider { - - public interface ChatAttachViewDelegate { - void didPressedButton(int button); - } - - private LinearLayoutManager attachPhotoLayoutManager; - private PhotoAttachAdapter photoAttachAdapter; - private ChatActivity baseFragment; - private AttachButton sendPhotosButton; - private View views[] = new View[20]; - private RecyclerListView attachPhotoRecyclerView; - private View lineView; - private EmptyTextProgressView progressView; - private ArrayList viewsCache = new ArrayList<>(8); - - private float[] distCache = new float[20]; - - private DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator(); - - private boolean loading = true; - - private ChatAttachViewDelegate delegate; - - private static class AttachButton extends FrameLayout { - - private TextView textView; - private ImageView imageView; - - public AttachButton(Context context) { - super(context); - - imageView = new ImageView(context); - imageView.setScaleType(ImageView.ScaleType.CENTER); - addView(imageView, LayoutHelper.createFrame(64, 64, Gravity.CENTER_HORIZONTAL | Gravity.TOP)); - - textView = new TextView(context); - textView.setLines(1); - textView.setSingleLine(true); - textView.setGravity(Gravity.CENTER_HORIZONTAL); - textView.setEllipsize(TextUtils.TruncateAt.END); - textView.setTextColor(0xff757575); - textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); - addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 0, 64, 0, 0)); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(85), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(90), MeasureSpec.EXACTLY)); - } - - public void setTextAndIcon(CharSequence text, Drawable drawable) { - textView.setText(text); - imageView.setBackgroundDrawable(drawable); - } - } - - public ChatAttachView(Context context) { - super(context); - - NotificationCenter.getInstance().addObserver(this, NotificationCenter.albumsDidLoaded); - - views[8] = attachPhotoRecyclerView = new RecyclerListView(context); - attachPhotoRecyclerView.setVerticalScrollBarEnabled(true); - attachPhotoRecyclerView.setAdapter(photoAttachAdapter = new PhotoAttachAdapter(context)); - attachPhotoRecyclerView.setClipToPadding(false); - attachPhotoRecyclerView.setPadding(AndroidUtilities.dp(8), 0, AndroidUtilities.dp(8), 0); - attachPhotoRecyclerView.setItemAnimator(null); - attachPhotoRecyclerView.setLayoutAnimation(null); - if (Build.VERSION.SDK_INT >= 9) { - attachPhotoRecyclerView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); - } - addView(attachPhotoRecyclerView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 80)); - attachPhotoLayoutManager = new LinearLayoutManager(context) { - @Override - public boolean supportsPredictiveItemAnimations() { - return false; - } - }; - attachPhotoLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); - attachPhotoRecyclerView.setLayoutManager(attachPhotoLayoutManager); - attachPhotoRecyclerView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { - @SuppressWarnings("unchecked") - @Override - public void onItemClick(View view, int position) { - if (baseFragment == null || baseFragment.getParentActivity() == null) { - return; - } - ArrayList arrayList = (ArrayList) MediaController.allPhotosAlbumEntry.photos; - if (position < 0 || position >= arrayList.size()) { - return; - } - PhotoViewer.getInstance().setParentActivity(baseFragment.getParentActivity()); - PhotoViewer.getInstance().openPhotoForSelect(arrayList, position, 0, ChatAttachView.this, baseFragment); - AndroidUtilities.hideKeyboard(baseFragment.getFragmentView().findFocus()); - } - }); - - 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)); - progressView.setTextSize(16); - } else { - progressView.setText(LocaleController.getString("NoPhotos", R.string.NoPhotos)); - progressView.setTextSize(20); - } - addView(progressView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 80)); - attachPhotoRecyclerView.setEmptyView(progressView); - - views[10] = lineView = new View(getContext()); - lineView.setBackgroundColor(0xffd2d2d2); - addView(lineView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1, Gravity.TOP | Gravity.LEFT)); - CharSequence[] items = new CharSequence[]{ - LocaleController.getString("ChatCamera", R.string.ChatCamera), - LocaleController.getString("ChatGallery", R.string.ChatGallery), - LocaleController.getString("ChatVideo", R.string.ChatVideo), - LocaleController.getString("AttachMusic", R.string.AttachMusic), - LocaleController.getString("ChatDocument", R.string.ChatDocument), - LocaleController.getString("AttachContact", R.string.AttachContact), - LocaleController.getString("ChatLocation", R.string.ChatLocation), - "" - }; - for (int a = 0; a < 8; a++) { - AttachButton attachButton = new AttachButton(context); - attachButton.setTextAndIcon(items[a], Theme.attachButtonDrawables[a]); - addView(attachButton, LayoutHelper.createFrame(85, 90, Gravity.LEFT | Gravity.TOP)); - attachButton.setTag(a); - views[a] = attachButton; - if (a == 7) { - sendPhotosButton = attachButton; - sendPhotosButton.imageView.setPadding(0, AndroidUtilities.dp(4), 0, 0); - } - attachButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (delegate != null) { - delegate.didPressedButton((Integer) v.getTag()); - } - } - }); - } - setOnTouchListener(new OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return true; - } - }); - - for (int a = 0; a < 8; a++) { - viewsCache.add(photoAttachAdapter.createHolder()); - } - - if (loading) { - progressView.showProgress(); - } else { - progressView.showTextView(); - } - } - - @Override - public void didReceivedNotification(int id, Object... args) { - if (id == NotificationCenter.albumsDidLoaded) { - if (photoAttachAdapter != null) { - loading = false; - progressView.showTextView(); - photoAttachAdapter.notifyDataSetChanged(); - } - } - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) { - super.onMeasure(widthSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(294), MeasureSpec.EXACTLY)); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - int width = right - left; - - int t = AndroidUtilities.dp(8); - attachPhotoRecyclerView.layout(0, t, width, t + attachPhotoRecyclerView.getMeasuredHeight()); - progressView.layout(0, t, width, t + progressView.getMeasuredHeight()); - lineView.layout(0, AndroidUtilities.dp(96), width, AndroidUtilities.dp(96) + lineView.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); - views[a].layout(x, y, x + views[a].getMeasuredWidth(), y + views[a].getMeasuredHeight()); - } - } - - public void updatePhotosButton() { - int count = photoAttachAdapter.getSelectedPhotos().size(); - if (count == 0) { - sendPhotosButton.imageView.setPadding(0, AndroidUtilities.dp(4), 0, 0); - sendPhotosButton.imageView.setBackgroundResource(R.drawable.attach_hide_states); - sendPhotosButton.imageView.setImageResource(R.drawable.attach_hide2); - sendPhotosButton.textView.setText(""); - } 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))); - } - - if (Build.VERSION.SDK_INT >= 23 && getContext().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - progressView.setText(LocaleController.getString("PermissionStorage", R.string.PermissionStorage)); - progressView.setTextSize(16); - } else { - progressView.setText(LocaleController.getString("NoPhotos", R.string.NoPhotos)); - progressView.setTextSize(20); - } - } - - public void setDelegate(ChatAttachViewDelegate chatAttachViewDelegate) { - delegate = chatAttachViewDelegate; - } - - public void onRevealAnimationEnd(boolean open) { - if (open && Build.VERSION.SDK_INT <= 19 && MediaController.allPhotosAlbumEntry == null) { - MediaController.loadGalleryPhotosAlbums(0); - } - } - - public void loadGalleryPhotos() { - if (MediaController.allPhotosAlbumEntry == null && Build.VERSION.SDK_INT >= 21) { - MediaController.loadGalleryPhotosAlbums(0); - } - } - - @SuppressLint("NewApi") - public void onRevealAnimationStart(boolean open) { - if (!open) { - return; - } - int count = Build.VERSION.SDK_INT <= 19 ? 11 : 8; - for (int a = 0; a < count; a++) { - if (Build.VERSION.SDK_INT <= 19) { - if (a < 8) { - views[a].setScaleX(0.1f); - views[a].setScaleY(0.1f); - } - views[a].setAlpha(0.0f); - } else { - views[a].setScaleX(0.7f); - views[a].setScaleY(0.7f); - } - views[a].setTag(R.string.AppName, null); - distCache[a] = 0; - } - } - - @SuppressLint("NewApi") - public void onRevealAnimationProgress(boolean open, float radius, int x, int y) { - if (!open) { - return; - } - int count = Build.VERSION.SDK_INT <= 19 ? 11 : 8; - for (int a = 0; a < count; a++) { - if (views[a].getTag(R.string.AppName) == null) { - if (distCache[a] == 0) { - int buttonX = views[a].getLeft() + views[a].getMeasuredWidth() / 2; - int buttonY = views[a].getTop() + views[a].getMeasuredHeight() / 2; - distCache[a] = (float) Math.sqrt((x - buttonX) * (x - buttonX) + (y - buttonY) * (y - buttonY)); - float vecX = (x - buttonX) / distCache[a]; - float vecY = (y - buttonY) / distCache[a]; - views[a].setPivotX(views[a].getMeasuredWidth() / 2 + vecX * AndroidUtilities.dp(20)); - views[a].setPivotY(views[a].getMeasuredHeight() / 2 + vecY * AndroidUtilities.dp(20)); - } - if (distCache[a] > radius + AndroidUtilities.dp(27)) { - continue; - } - - views[a].setTag(R.string.AppName, 1); - final ArrayList animators = new ArrayList<>(); - final ArrayList animators2 = new ArrayList<>(); - if (a < 8) { - animators.add(ObjectAnimator.ofFloat(views[a], "scaleX", 0.7f, 1.05f)); - animators.add(ObjectAnimator.ofFloat(views[a], "scaleY", 0.7f, 1.05f)); - animators2.add(ObjectAnimator.ofFloat(views[a], "scaleX", 1.0f)); - animators2.add(ObjectAnimator.ofFloat(views[a], "scaleY", 1.0f)); - } - if (Build.VERSION.SDK_INT <= 19) { - animators.add(ObjectAnimator.ofFloat(views[a], "alpha", 1.0f)); - } - AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(animators); - animatorSet.setDuration(150); - animatorSet.setInterpolator(decelerateInterpolator); - animatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(animators2); - animatorSet.setDuration(100); - animatorSet.setInterpolator(decelerateInterpolator); - animatorSet.start(); - } - }); - animatorSet.start(); - } - } - } - - public void init(ChatActivity parentFragment) { - 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); - photoEntry.caption = null; - photoEntry.imagePath = null; - photoEntry.thumbPath = null; - } - } - attachPhotoLayoutManager.scrollToPositionWithOffset(0, 1000000); - photoAttachAdapter.clearSelectedPhotos(); - baseFragment = parentFragment; - updatePhotosButton(); - } - - public HashMap getSelectedPhotos() { - return photoAttachAdapter.getSelectedPhotos(); - } - - public void onDestroy() { - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.albumsDidLoaded); - baseFragment = null; - } - - private PhotoAttachPhotoCell getCellForIndex(int index) { - int count = attachPhotoRecyclerView.getChildCount(); - for (int a = 0; a < count; a++) { - View view = attachPhotoRecyclerView.getChildAt(a); - if (view instanceof PhotoAttachPhotoCell) { - PhotoAttachPhotoCell cell = (PhotoAttachPhotoCell) view; - int num = (Integer) cell.getImageView().getTag(); - if (num < 0 || num >= MediaController.allPhotosAlbumEntry.photos.size()) { - continue; - } - if (num == index) { - return cell; - } - } - } - return null; - } - - @Override - public PhotoViewer.PlaceProviderObject getPlaceForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { - PhotoAttachPhotoCell cell = getCellForIndex(index); - if (cell != null) { - int coords[] = new int[2]; - cell.getImageView().getLocationInWindow(coords); - PhotoViewer.PlaceProviderObject object = new PhotoViewer.PlaceProviderObject(); - object.viewX = coords[0]; - object.viewY = coords[1] - (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); - object.parentView = attachPhotoRecyclerView; - object.imageReceiver = cell.getImageView().getImageReceiver(); - object.thumb = object.imageReceiver.getBitmap(); - object.scale = ViewProxy.getScaleX(cell.getImageView()); - object.clipBottomAddition = (Build.VERSION.SDK_INT >= 21 ? 0 : -AndroidUtilities.statusBarHeight); - cell.getCheckBox().setVisibility(View.GONE); - return object; - } - return null; - } - - @Override - public void updatePhotoAtIndex(int index) { - PhotoAttachPhotoCell cell = getCellForIndex(index); - if (cell != null) { - cell.getImageView().setOrientation(0, true); - MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.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) { - cell.getImageView().setOrientation(photoEntry.orientation, true); - cell.getImageView().setImage("thumb://" + photoEntry.imageId + ":" + photoEntry.path, null, cell.getContext().getResources().getDrawable(R.drawable.nophotos)); - } else { - cell.getImageView().setImageResource(R.drawable.nophotos); - } - } - } - - @Override - public Bitmap getThumbForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { - PhotoAttachPhotoCell cell = getCellForIndex(index); - if (cell != null) { - return cell.getImageView().getImageReceiver().getBitmap(); - } - return null; - } - - @Override - public void willSwitchFromPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { - PhotoAttachPhotoCell cell = getCellForIndex(index); - if (cell != null) { - cell.getCheckBox().setVisibility(View.VISIBLE); - } - } - - @Override - public void willHidePhotoViewer() { - int count = attachPhotoRecyclerView.getChildCount(); - for (int a = 0; a < count; a++) { - View view = attachPhotoRecyclerView.getChildAt(a); - if (view instanceof PhotoAttachPhotoCell) { - PhotoAttachPhotoCell cell = (PhotoAttachPhotoCell) view; - if (cell.getCheckBox().getVisibility() != VISIBLE) { - cell.getCheckBox().setVisibility(VISIBLE); - } - } - } - } - - @Override - public boolean isPhotoChecked(int index) { - return !(index < 0 || index >= MediaController.allPhotosAlbumEntry.photos.size()) && photoAttachAdapter.getSelectedPhotos().containsKey(MediaController.allPhotosAlbumEntry.photos.get(index).imageId); - } - - @Override - public void setPhotoChecked(int index) { - boolean add = true; - if (index < 0 || index >= MediaController.allPhotosAlbumEntry.photos.size()) { - return; - } - MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.photos.get(index); - if (photoAttachAdapter.getSelectedPhotos().containsKey(photoEntry.imageId)) { - photoAttachAdapter.getSelectedPhotos().remove(photoEntry.imageId); - add = false; - } else { - photoAttachAdapter.getSelectedPhotos().put(photoEntry.imageId, photoEntry); - } - int count = attachPhotoRecyclerView.getChildCount(); - for (int a = 0; a < count; a++) { - View view = attachPhotoRecyclerView.getChildAt(a); - int num = (Integer) view.getTag(); - if (num == index) { - ((PhotoAttachPhotoCell) view).setChecked(add, false); - break; - } - } - updatePhotosButton(); - } - - @Override - public boolean cancelButtonPressed() { - return false; - } - - @Override - public void sendButtonPressed(int index) { - if (photoAttachAdapter.getSelectedPhotos().isEmpty()) { - if (index < 0 || index >= MediaController.allPhotosAlbumEntry.photos.size()) { - return; - } - MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.photos.get(index); - photoAttachAdapter.getSelectedPhotos().put(photoEntry.imageId, photoEntry); - } - delegate.didPressedButton(7); - } - - @Override - public int getSelectedCount() { - return photoAttachAdapter.getSelectedPhotos().size(); - } - - @Override - public boolean hasOverlappingRendering() { - return false; - } - - public class PhotoAttachAdapter extends RecyclerView.Adapter { - - private Context mContext; - private HashMap selectedPhotos = new HashMap<>(); - - private class Holder extends RecyclerView.ViewHolder { - - public Holder(View itemView) { - super(itemView); - } - } - - public PhotoAttachAdapter(Context context) { - mContext = context; - } - - public void clearSelectedPhotos() { - if (!selectedPhotos.isEmpty()) { - for (HashMap.Entry entry : selectedPhotos.entrySet()) { - MediaController.PhotoEntry photoEntry = entry.getValue(); - photoEntry.imagePath = null; - photoEntry.thumbPath = null; - photoEntry.caption = null; - } - selectedPhotos.clear(); - updatePhotosButton(); - notifyDataSetChanged(); - } - } - - public Holder createHolder() { - PhotoAttachPhotoCell cell = new PhotoAttachPhotoCell(mContext); - cell.setDelegate(new PhotoAttachPhotoCell.PhotoAttachPhotoCellDelegate() { - @Override - public void onCheckClick(PhotoAttachPhotoCell v) { - MediaController.PhotoEntry photoEntry = v.getPhotoEntry(); - if (selectedPhotos.containsKey(photoEntry.imageId)) { - selectedPhotos.remove(photoEntry.imageId); - v.setChecked(false, true); - photoEntry.imagePath = null; - photoEntry.thumbPath = null; - v.setPhotoEntry(photoEntry, (Integer) v.getTag() == MediaController.allPhotosAlbumEntry.photos.size() - 1); - } else { - selectedPhotos.put(photoEntry.imageId, photoEntry); - v.setChecked(true, true); - } - updatePhotosButton(); - } - }); - return new Holder(cell); - } - - public HashMap getSelectedPhotos() { - return selectedPhotos; - } - - @Override - public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - PhotoAttachPhotoCell cell = (PhotoAttachPhotoCell) holder.itemView; - MediaController.PhotoEntry photoEntry = MediaController.allPhotosAlbumEntry.photos.get(position); - cell.setPhotoEntry(photoEntry, position == MediaController.allPhotosAlbumEntry.photos.size() - 1); - cell.setChecked(selectedPhotos.containsKey(photoEntry.imageId), false); - cell.getImageView().setTag(position); - cell.setTag(position); - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - Holder holder; - if (!viewsCache.isEmpty()) { - holder = viewsCache.get(0); - viewsCache.remove(0); - } else { - holder = createHolder(); - } - return holder; - } - - @Override - public int getItemCount() { - return (MediaController.allPhotosAlbumEntry != null ? MediaController.allPhotosAlbumEntry.photos.size() : 0); - } - - @Override - public int getItemViewType(int position) { - return 0; - } - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java index c851dbf9c..afad12c08 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java @@ -37,7 +37,6 @@ public class ChatAvatarContainer extends FrameLayout { private BackupImageView avatarImageView; private SimpleTextView titleTextView; private SimpleTextView subtitleTextView; - private RadioButton radioButton; private ImageView timeItem; private TimerDrawable timerDrawable; private ChatActivity parentFragment; @@ -45,15 +44,10 @@ public class ChatAvatarContainer extends FrameLayout { private RecordStatusDrawable recordStatusDrawable; private SendingFileExDrawable sendingFileDrawable; private AvatarDrawable avatarDrawable = new AvatarDrawable(); - private ChatAvatarContainerDelegate delegate; private int onlineCount = -1; - public interface ChatAvatarContainerDelegate { - void didPressedRadioButton(); - } - - public ChatAvatarContainer(Context context, ChatActivity chatActivity, boolean needRadio, boolean needTime) { + public ChatAvatarContainer(Context context, ChatActivity chatActivity, boolean needTime) { super(context); parentFragment = chatActivity; @@ -90,37 +84,27 @@ public class ChatAvatarContainer extends FrameLayout { }); } - if (needRadio) { - radioButton = new RadioButton(context); - radioButton.setVisibility(View.GONE); - addView(radioButton); - } - setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (radioButton == null || radioButton.getVisibility() != View.VISIBLE) { - TLRPC.User user = parentFragment.getCurrentUser(); - TLRPC.Chat chat = parentFragment.getCurrentChat(); - if (user != null) { - Bundle args = new Bundle(); - args.putInt("user_id", user.id); - if (timeItem != null) { - args.putLong("dialog_id", parentFragment.getDialogId()); - } - ProfileActivity fragment = new ProfileActivity(args); - fragment.setPlayProfileAnimation(true); - parentFragment.presentFragment(fragment); - } else if (chat != null) { - Bundle args = new Bundle(); - args.putInt("chat_id", chat.id); - ProfileActivity fragment = new ProfileActivity(args); - fragment.setChatInfo(parentFragment.getCurrentChatInfo()); - fragment.setPlayProfileAnimation(true); - parentFragment.presentFragment(fragment); + TLRPC.User user = parentFragment.getCurrentUser(); + TLRPC.Chat chat = parentFragment.getCurrentChat(); + if (user != null) { + Bundle args = new Bundle(); + args.putInt("user_id", user.id); + if (timeItem != null) { + args.putLong("dialog_id", parentFragment.getDialogId()); } - } else { - delegate.didPressedRadioButton(); + ProfileActivity fragment = new ProfileActivity(args); + fragment.setPlayProfileAnimation(true); + parentFragment.presentFragment(fragment); + } else if (chat != null) { + Bundle args = new Bundle(); + args.putInt("chat_id", chat.id); + ProfileActivity fragment = new ProfileActivity(args); + fragment.setChatInfo(parentFragment.getCurrentChatInfo()); + fragment.setPlayProfileAnimation(true); + parentFragment.presentFragment(fragment); } } }); @@ -140,10 +124,6 @@ public class ChatAvatarContainer extends FrameLayout { int availableWidth = width - AndroidUtilities.dp(54 + 16); avatarImageView.measure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(42), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(42), MeasureSpec.EXACTLY)); titleTextView.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(24), MeasureSpec.AT_MOST)); - if (radioButton != null && radioButton.getVisibility() == VISIBLE) { - radioButton.measure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(24), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(24), MeasureSpec.EXACTLY)); - availableWidth -= AndroidUtilities.dp(20); - } subtitleTextView.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(20), MeasureSpec.AT_MOST)); if (timeItem != null) { timeItem.measure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(34), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(34), MeasureSpec.EXACTLY)); @@ -160,24 +140,7 @@ public class ChatAvatarContainer extends FrameLayout { if (timeItem != null) { timeItem.layout(AndroidUtilities.dp(8 + 16), viewTop + AndroidUtilities.dp(15), AndroidUtilities.dp(8 + 16 + 34), viewTop + AndroidUtilities.dp(15 + 34)); } - if (radioButton != null && radioButton.getVisibility() == VISIBLE) { - subtitleTextView.layout(AndroidUtilities.dp(8 + 54 + 20), viewTop + AndroidUtilities.dp(24), AndroidUtilities.dp(8 + 54 + 20) + subtitleTextView.getMeasuredWidth(), viewTop + subtitleTextView.getTextHeight() + AndroidUtilities.dp(24)); - viewTop = viewTop + subtitleTextView.getTextHeight() / 2 + AndroidUtilities.dp(12); - radioButton.layout(AndroidUtilities.dp(8 + 50), viewTop, AndroidUtilities.dp(8 + 50 + 24), viewTop + AndroidUtilities.dp(24)); - } else { - subtitleTextView.layout(AndroidUtilities.dp(8 + 54), viewTop + AndroidUtilities.dp(24), AndroidUtilities.dp(8 + 54) + subtitleTextView.getMeasuredWidth(), viewTop + subtitleTextView.getTextHeight() + AndroidUtilities.dp(24)); - } - } - - public void setRadioChecked(boolean value, boolean animated) { - if (radioButton == null) { - return; - } - radioButton.setChecked(value, animated); - } - - public boolean isRadioChecked() { - return radioButton.isChecked(); + subtitleTextView.layout(AndroidUtilities.dp(8 + 54), viewTop + AndroidUtilities.dp(24), AndroidUtilities.dp(8 + 54) + subtitleTextView.getMeasuredWidth(), viewTop + subtitleTextView.getTextHeight() + AndroidUtilities.dp(24)); } public void showTimeItem() { @@ -210,10 +173,6 @@ public class ChatAvatarContainer extends FrameLayout { titleTextView.setText(value); } - public void setDelegate(ChatAvatarContainerDelegate chatAvatarContainerDelegate) { - delegate = chatAvatarContainerDelegate; - } - private void setTypingAnimation(boolean start) { if (start) { try { @@ -257,38 +216,28 @@ public class ChatAvatarContainer extends FrameLayout { if (chat != null) { TLRPC.ChatFull info = parentFragment.getCurrentChatInfo(); if (ChatObject.isChannel(chat)) { - if (!chat.broadcast && !chat.megagroup && !(chat instanceof TLRPC.TL_channelForbidden)) { - subtitleTextView.setText(LocaleController.getString("ShowDiscussion", R.string.ShowDiscussion)); - if (radioButton != null && radioButton.getVisibility() != VISIBLE) { - radioButton.setVisibility(View.VISIBLE); - } - } else { - if (info != null && info.participants_count != 0) { - if (chat.megagroup && info.participants_count <= 200) { - if (onlineCount > 1 && info.participants_count != 0) { - subtitleTextView.setText(String.format("%s, %s", LocaleController.formatPluralString("Members", info.participants_count), LocaleController.formatPluralString("Online", onlineCount))); - } else { - subtitleTextView.setText(LocaleController.formatPluralString("Members", info.participants_count)); - } + if (info != null && info.participants_count != 0) { + if (chat.megagroup && info.participants_count <= 200) { + if (onlineCount > 1 && info.participants_count != 0) { + subtitleTextView.setText(String.format("%s, %s", LocaleController.formatPluralString("Members", info.participants_count), LocaleController.formatPluralString("Online", onlineCount))); } else { - int result[] = new int[1]; - String shortNumber = LocaleController.formatShortNumber(info.participants_count, result); - String text = LocaleController.formatPluralString("Members", result[0]).replace(String.format("%d", result[0]), shortNumber); - subtitleTextView.setText(text); + subtitleTextView.setText(LocaleController.formatPluralString("Members", info.participants_count)); } } else { - if (chat.megagroup) { - subtitleTextView.setText(LocaleController.getString("Loading", R.string.Loading).toLowerCase()); - } else { - if ((chat.flags & TLRPC.CHAT_FLAG_IS_PUBLIC) != 0) { - subtitleTextView.setText(LocaleController.getString("ChannelPublic", R.string.ChannelPublic).toLowerCase()); - } else { - subtitleTextView.setText(LocaleController.getString("ChannelPrivate", R.string.ChannelPrivate).toLowerCase()); - } - } + int result[] = new int[1]; + String shortNumber = LocaleController.formatShortNumber(info.participants_count, result); + String text = LocaleController.formatPluralString("Members", result[0]).replace(String.format("%d", result[0]), shortNumber); + subtitleTextView.setText(text); } - if (radioButton != null && radioButton.getVisibility() != GONE) { - radioButton.setVisibility(View.GONE); + } else { + if (chat.megagroup) { + subtitleTextView.setText(LocaleController.getString("Loading", R.string.Loading).toLowerCase()); + } else { + if ((chat.flags & TLRPC.CHAT_FLAG_IS_PUBLIC) != 0) { + subtitleTextView.setText(LocaleController.getString("ChannelPublic", R.string.ChannelPublic).toLowerCase()); + } else { + subtitleTextView.setText(LocaleController.getString("ChannelPrivate", R.string.ChannelPrivate).toLowerCase()); + } } } } else { @@ -298,7 +247,7 @@ public class ChatAvatarContainer extends FrameLayout { subtitleTextView.setText(LocaleController.getString("YouLeft", R.string.YouLeft)); } else { int count = chat.participants_count; - if (info != null) { + if (info != null && info.participants != null) { count = info.participants.participants.size(); } if (onlineCount > 1 && count != 0) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox.java index 5a075500f..7b68bc2e1 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox.java @@ -8,6 +8,7 @@ package org.telegram.ui.Components; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -18,7 +19,6 @@ import android.graphics.drawable.Drawable; import android.view.View; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; public class CheckBox extends View { @@ -37,7 +37,7 @@ public class CheckBox extends View { private boolean drawBackground; private float progress; - private ObjectAnimatorProxy checkAnimator; + private ObjectAnimator checkAnimator; private boolean isCheckAnimation = true; private boolean attachedToWindow; @@ -117,7 +117,7 @@ public class CheckBox extends View { private void animateToCheckedState(boolean newCheckedState) { isCheckAnimation = newCheckedState; - checkAnimator = ObjectAnimatorProxy.ofFloatProxy(this, "progress", newCheckedState ? 1 : 0); + checkAnimator = ObjectAnimator.ofFloat(this, "progress", newCheckedState ? 1 : 0); checkAnimator.setDuration(300); checkAnimator.start(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java index eea7e1615..ab89f7ab0 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java @@ -8,6 +8,7 @@ package org.telegram.ui.Components; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -19,7 +20,6 @@ import android.graphics.RectF; import android.view.View; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; public class CheckBoxSquare extends View { @@ -32,7 +32,7 @@ public class CheckBoxSquare extends View { private Canvas drawCanvas; private float progress; - private ObjectAnimatorProxy checkAnimator; + private ObjectAnimator checkAnimator; private boolean attachedToWindow; private boolean isChecked; @@ -91,7 +91,7 @@ public class CheckBoxSquare extends View { } private void animateToCheckedState(boolean newCheckedState) { - checkAnimator = ObjectAnimatorProxy.ofFloatProxy(this, "progress", newCheckedState ? 1 : 0); + checkAnimator = ObjectAnimator.ofFloat(this, "progress", newCheckedState ? 1 : 0); checkAnimator.setDuration(300); checkAnimator.start(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ClippingImageView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ClippingImageView.java index 64790946f..6988823b4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ClippingImageView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ClippingImageView.java @@ -19,7 +19,6 @@ import android.graphics.Shader; import android.view.View; import org.telegram.messenger.FileLog; -import org.telegram.messenger.AnimationCompat.ViewProxy; public class ClippingImageView extends View { @@ -67,10 +66,10 @@ public class ClippingImageView extends View { public void setAnimationProgress(float progress) { animationProgress = progress; - ViewProxy.setScaleX(this, animationValues[0][0] + (animationValues[1][0] - animationValues[0][0]) * animationProgress); - ViewProxy.setScaleY(this, animationValues[0][1] + (animationValues[1][1] - animationValues[0][1]) * animationProgress); - ViewProxy.setTranslationX(this, animationValues[0][2] + (animationValues[1][2] - animationValues[0][2]) * animationProgress); - ViewProxy.setTranslationY(this, animationValues[0][3] + (animationValues[1][3] - animationValues[0][3]) * animationProgress); + setScaleX(animationValues[0][0] + (animationValues[1][0] - animationValues[0][0]) * animationProgress); + setScaleY(animationValues[0][1] + (animationValues[1][1] - animationValues[0][1]) * animationProgress); + setTranslationX(animationValues[0][2] + (animationValues[1][2] - animationValues[0][2]) * animationProgress); + setTranslationY(animationValues[0][3] + (animationValues[1][3] - animationValues[0][3]) * animationProgress); setClipHorizontal((int) (animationValues[0][4] + (animationValues[1][4] - animationValues[0][4]) * animationProgress)); setClipTop((int) (animationValues[0][5] + (animationValues[1][5] - animationValues[0][5]) * animationProgress)); setClipBottom((int) (animationValues[0][6] + (animationValues[1][6] - animationValues[0][6]) * animationProgress)); @@ -108,7 +107,7 @@ public class ClippingImageView extends View { return; } if (bmp != null) { - float scaleY = ViewProxy.getScaleY(this); + float scaleY = getScaleY(); canvas.save(); if (needRadius) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java index 427f69f32..7c93d55a2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java @@ -42,7 +42,6 @@ import android.widget.PopupWindow; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.messenger.Emoji; import org.telegram.messenger.EmojiData; import org.telegram.messenger.LocaleController; @@ -53,6 +52,7 @@ import org.telegram.messenger.Utilities; import org.telegram.messenger.query.StickersQuery; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; +import org.telegram.messenger.support.widget.GridLayoutManager; import org.telegram.messenger.support.widget.RecyclerView; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.RequestDelegate; @@ -538,7 +538,7 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific private GridView stickersGridView; private TextView stickersEmptyView; private RecyclerListView gifsGridView; - private FlowLayoutManager flowLayoutManager; + private ExtendedGridLayoutManager flowLayoutManager; private GifsAdapter gifsAdapter; private AdapterView.OnItemClickListener stickersOnItemClickListener; @@ -631,18 +631,7 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific } cell.disable(); TLRPC.Document document = cell.getSticker(); - int index = newRecentStickers.indexOf(document.id); - if (index == -1) { - newRecentStickers.add(0, document.id); - if (newRecentStickers.size() > 20) { - newRecentStickers.remove(newRecentStickers.size() - 1); - } - } else if (index != 0) { - newRecentStickers.remove(index); - newRecentStickers.add(0, document.id); - } - - saveRecentStickers(); + addRecentSticker(document); if (listener != null) { listener.onStickerSelected(document); } @@ -656,7 +645,8 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific if (needGif) { gifsGridView = new RecyclerListView(context); - gifsGridView.setLayoutManager(flowLayoutManager = new FlowLayoutManager() { + gifsGridView.setTag(11); + gifsGridView.setLayoutManager(flowLayoutManager = new ExtendedGridLayoutManager(context, 100) { private Size size = new Size(); @@ -676,9 +666,26 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific return size; } }); - if (Build.VERSION.SDK_INT >= 9) { - gifsGridView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); - } + flowLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + return flowLayoutManager.getSpanSizeForItem(position); + } + }); + gifsGridView.addItemDecoration(new RecyclerView.ItemDecoration() { + @Override + public void getItemOffsets(android.graphics.Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + outRect.left = 0; + outRect.top = 0; + outRect.bottom = 0; + int position = parent.getChildAdapterPosition(view); + if (!flowLayoutManager.isFirstRow(position)) { + outRect.top = AndroidUtilities.dp(2); + } + outRect.right = flowLayoutManager.isLastInRow(position) ? 0 : AndroidUtilities.dp(2); + } + }); + gifsGridView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); gifsGridView.setAdapter(gifsAdapter = new GifsAdapter(context)); gifsGridView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @Override @@ -757,51 +764,48 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific @Override public boolean onTouchEvent(MotionEvent ev) { - if (Build.VERSION.SDK_INT >= 11) { - if (first) { - first = false; - lastX = ev.getX(); - } - float newTranslationX = ViewProxy.getTranslationX(scrollSlidingTabStrip); - if (scrollSlidingTabStrip.getScrollX() == 0 && newTranslationX == 0) { - if (!startedScroll && lastX - ev.getX() < 0) { - if (pager.beginFakeDrag()) { - startedScroll = true; - lastTranslateX = ViewProxy.getTranslationX(scrollSlidingTabStrip); - } - } else if (startedScroll && lastX - ev.getX() > 0) { - if (pager.isFakeDragging()) { - pager.endFakeDrag(); - startedScroll = false; - } - } - } - if (startedScroll) { - int dx = (int) (ev.getX() - lastX + newTranslationX - lastTranslateX); - try { - pager.fakeDragBy(dx); - lastTranslateX = newTranslationX; - } catch (Exception e) { - try { - pager.endFakeDrag(); - } catch (Exception e2) { - //don't promt - } - startedScroll = false; - FileLog.e("tmessages", e); - } - } + if (first) { + first = false; lastX = ev.getX(); - if (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP) { - first = true; - if (startedScroll) { + } + float newTranslationX = scrollSlidingTabStrip.getTranslationX(); + if (scrollSlidingTabStrip.getScrollX() == 0 && newTranslationX == 0) { + if (!startedScroll && lastX - ev.getX() < 0) { + if (pager.beginFakeDrag()) { + startedScroll = true; + lastTranslateX = scrollSlidingTabStrip.getTranslationX(); + } + } else if (startedScroll && lastX - ev.getX() > 0) { + if (pager.isFakeDragging()) { pager.endFakeDrag(); startedScroll = false; } } - return startedScroll || super.onTouchEvent(ev); } - return super.onTouchEvent(ev); + if (startedScroll) { + int dx = (int) (ev.getX() - lastX + newTranslationX - lastTranslateX); + try { + pager.fakeDragBy(dx); + lastTranslateX = newTranslationX; + } catch (Exception e) { + try { + pager.endFakeDrag(); + } catch (Exception e2) { + //don't promt + } + startedScroll = false; + FileLog.e("tmessages", e); + } + } + lastX = ev.getX(); + if (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP) { + first = true; + if (startedScroll) { + pager.endFakeDrag(); + startedScroll = false; + } + } + return startedScroll || super.onTouchEvent(ev); } }; scrollSlidingTabStrip.setUnderlineHeight(AndroidUtilities.dp(1)); @@ -809,7 +813,7 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific scrollSlidingTabStrip.setUnderlineColor(0xffe2e5e7); scrollSlidingTabStrip.setVisibility(INVISIBLE); addView(scrollSlidingTabStrip, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.TOP)); - ViewProxy.setTranslationX(scrollSlidingTabStrip, AndroidUtilities.displaySize.x); + scrollSlidingTabStrip.setTranslationX(AndroidUtilities.displaySize.x); updateStickerTabs(); scrollSlidingTabStrip.setDelegate(new ScrollSlidingTabStrip.ScrollSlidingTabStripDelegate() { @Override @@ -1048,21 +1052,10 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific } } - if (ViewProxy.getTranslationX(pagerSlidingTabStripContainer) != margin) { - ViewProxy.setTranslationX(pagerSlidingTabStripContainer, margin); - ViewProxy.setTranslationX(scrollSlidingTabStrip, width + margin); + if (pagerSlidingTabStripContainer.getTranslationX() != margin) { + pagerSlidingTabStripContainer.setTranslationX(margin); + scrollSlidingTabStrip.setTranslationX(width + margin); scrollSlidingTabStrip.setVisibility(margin < 0 ? VISIBLE : INVISIBLE); - if (Build.VERSION.SDK_INT < 11) { - if (margin <= -width) { - pagerSlidingTabStripContainer.clearAnimation(); - pagerSlidingTabStripContainer.setVisibility(GONE); - } else { - pagerSlidingTabStripContainer.setVisibility(VISIBLE); - } - } - } else if (Build.VERSION.SDK_INT < 11 && pagerSlidingTabStripContainer.getVisibility() == GONE) { - pagerSlidingTabStripContainer.clearAnimation(); - pagerSlidingTabStripContainer.setVisibility(GONE); } } @@ -1242,8 +1235,8 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific if (switchToGifTab) { if (gifTabBum >= 0 && gifsGridView.getVisibility() != VISIBLE) { showGifTab(); + switchToGifTab = false; } - switchToGifTab = false; } if (gifTabBum == -2 && gifsGridView != null && gifsGridView.getVisibility() == VISIBLE) { listener.onGifTab(false); @@ -1259,6 +1252,23 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific } } + public void addRecentSticker(TLRPC.Document document) { + if (document == null) { + return; + } + int index = newRecentStickers.indexOf(document.id); + if (index == -1) { + newRecentStickers.add(0, document.id); + if (newRecentStickers.size() > 20) { + newRecentStickers.remove(newRecentStickers.size() - 1); + } + } else if (index != 0) { + newRecentStickers.remove(index); + newRecentStickers.add(0, document.id); + } + saveRecentStickers(); + } + public void addRecentGif(MediaController.SearchImage searchImage) { if (searchImage == null || searchImage.document == null || recentImages == null) { return; @@ -1908,7 +1918,7 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) { MediaController.SearchImage photoEntry = recentImages.get(i); if (photoEntry.document != null) { - ((ContextLinkCell) viewHolder.itemView).setGif(photoEntry.document, flowLayoutManager.getRowOfIndex(i) != flowLayoutManager.getRowCount() - 1); + ((ContextLinkCell) viewHolder.itemView).setGif(photoEntry.document, false); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmptyTextProgressView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmptyTextProgressView.java index 3eb99cf9f..f08d7585e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmptyTextProgressView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmptyTextProgressView.java @@ -105,4 +105,9 @@ public class EmptyTextProgressView extends FrameLayout { super.requestLayout(); } } + + @Override + public boolean hasOverlappingRendering() { + return false; + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ExtendedGridLayoutManager.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ExtendedGridLayoutManager.java new file mode 100644 index 000000000..f5cb849a9 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ExtendedGridLayoutManager.java @@ -0,0 +1,225 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.ui.Components; + +import android.content.Context; +import android.util.SparseArray; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.support.widget.GridLayoutManager; + +import java.util.ArrayList; + +public class ExtendedGridLayoutManager extends GridLayoutManager { + + private SparseArray itemSpans = new SparseArray<>(); + private ArrayList> rows; + private SparseArray itemsToRow = new SparseArray<>(); + private int calculatedWidth; + + public ExtendedGridLayoutManager(Context context, int spanCount) { + super(context, spanCount); + } + + @Override + public boolean supportsPredictiveItemAnimations() { + return false; + } + + private void prepareLayout(float viewPortAvailableSize) { + itemSpans.clear(); + itemsToRow.clear(); + int preferredRowSize = AndroidUtilities.dp(100); + + float totalItemSize = 0; + int itemsCount = getFlowItemCount(); + int[] weights = new int[itemsCount]; + for (int a = 0; a < itemsCount; a++) { + Size size = sizeForItem(a); + totalItemSize += (size.width / size.height) * preferredRowSize; + weights[a] = Math.round(size.width / size.height * 100); + } + + int numberOfRows = Math.max(Math.round(totalItemSize / viewPortAvailableSize), 1); + + rows = getLinearPartitionForSequence(weights, numberOfRows); + + int i = 0, a; + float previousItemSize = 0; + for (a = 0; a < rows.size(); a++) { + ArrayList row = rows.get(a); + + float summedRatios = 0; + for (int j = i, n = i + row.size(); j < n; j++) { + Size preferredSize = sizeForItem(j); + summedRatios += preferredSize.width / preferredSize.height; + } + + float rowSize = viewPortAvailableSize; + + if (rows.size() == 1 && a == rows.size() - 1) { + if (row.size() < 2) { + rowSize = (float) Math.floor(viewPortAvailableSize / 3.0f); + } else if (row.size() < 3) { + rowSize = (float) Math.floor(viewPortAvailableSize * 2.0f / 3.0f); + } + } + + int spanLeft = getSpanCount(); + for (int j = i, n = i + row.size(); j < n; j++) { + Size preferredSize = sizeForItem(j); + int width = Math.round(rowSize / summedRatios * (preferredSize.width / preferredSize.height)); + int itemSpan; + if (itemsCount < 3 || j != n - 1) { + itemSpan = (int) (width / viewPortAvailableSize * getSpanCount()); + spanLeft -= itemSpan; + } else { + itemsToRow.put(j, a); + itemSpan = spanLeft; + } + itemSpans.put(j, itemSpan); + } + i += row.size(); + } + } + + private int[] getLinearPartitionTable(int[] sequence, int numPartitions) { + int n = sequence.length; + int i, j, x; + + int tmpTable[] = new int[n * numPartitions]; + int solution[] = new int[(n - 1) * (numPartitions - 1)]; + + for (i = 0; i < n; i++) { + tmpTable[i * numPartitions] = sequence[i] + (i != 0 ? tmpTable[(i - 1) * numPartitions] : 0); + } + + for (j = 0; j < numPartitions; j++) { + tmpTable[j] = sequence[0]; + } + + for (i = 1; i < n; i++) { + for (j = 1; j < numPartitions; j++) { + int currentMin = 0; + int minX = Integer.MAX_VALUE; + + for (x = 0; x < i; x++) { + int cost = Math.max(tmpTable[x * numPartitions + (j - 1)], tmpTable[i * numPartitions] - tmpTable[x * numPartitions]); + if (x == 0 || cost < currentMin) { + currentMin = cost; + minX = x; + } + } + tmpTable[i * numPartitions + j] = currentMin; + solution[(i - 1) * (numPartitions - 1) + (j - 1)] = minX; + } + } + + return solution; + } + + private ArrayList> getLinearPartitionForSequence(int[] sequence, int numberOfPartitions) { + int n = sequence.length; + int k = numberOfPartitions; + + if (k <= 0) { + return new ArrayList<>(); + } + + if (k >= n || n == 1) { + ArrayList> partition = new ArrayList<>(sequence.length); + for (int i = 0; i < sequence.length; i++) { + ArrayList arrayList = new ArrayList<>(1); + arrayList.add(sequence[i]); + partition.add(arrayList); + } + return partition; + } + + int[] solution = getLinearPartitionTable(sequence, numberOfPartitions); + int solutionRowSize = numberOfPartitions - 1; + + k = k - 2; + n = n - 1; + ArrayList> answer = new ArrayList<>(); + + while (k >= 0) { + if (n < 1) { + answer.add(0, new ArrayList()); + } else { + ArrayList currentAnswer = new ArrayList<>(); + for (int i = solution[(n - 1) * solutionRowSize + k] + 1, range = n + 1; i < range; i++) { + currentAnswer.add(sequence[i]); + } + answer.add(0, currentAnswer); + n = solution[(n - 1) * solutionRowSize + k]; + } + k = k - 1; + } + + ArrayList currentAnswer = new ArrayList<>(); + for (int i = 0, range = n + 1; i < range; i++) { + currentAnswer.add(sequence[i]); + } + answer.add(0, currentAnswer); + return answer; + } + + private Size sizeForItem(int i) { + Size size = getSizeForItem(i); + if (size.width == 0) { + size.width = 100; + } + if (size.height == 0) { + size.height = 100; + } + float aspect = size.width / size.height; + if (aspect > 4.0f || aspect < 0.2f) { + size.height = size.width = Math.max(size.width, size.height); + } + return size; + } + + protected Size getSizeForItem(int i) { + return new Size(100, 100); + } + + private void checkLayout() { + if (itemSpans.size() != getFlowItemCount() || calculatedWidth != getWidth()) { + calculatedWidth = getWidth(); + prepareLayout(getWidth()); + } + } + + public int getSpanSizeForItem(int i) { + checkLayout(); + return itemSpans.get(i); + } + + public int getRowsCount(int width) { + if (rows == null) { + prepareLayout(width); + } + return rows != null ? rows.size() : 0; + } + + public boolean isLastInRow(int i) { + checkLayout(); + return itemsToRow.get(i) != null; + } + + public boolean isFirstRow(int i) { + checkLayout(); + return !(rows == null || rows.isEmpty()) && i < rows.get(0).size(); + } + + protected int getFlowItemCount() { + return getItemCount(); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/FlowLayoutManager.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/FlowLayoutManager.java deleted file mode 100644 index 4d5d6c418..000000000 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/FlowLayoutManager.java +++ /dev/null @@ -1,529 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.x. - * It is licensed under GNU GPL v. 2 or later. - * You should have received a copy of the license in this archive (see LICENSE). - * - * Copyright Nikolai Kudashov, 2013-2016. - */ - -package org.telegram.ui.Components; - -import android.util.SparseArray; -import android.view.View; -import android.view.ViewGroup; - -import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.BuildVars; -import org.telegram.messenger.FileLog; -import org.telegram.messenger.support.widget.RecyclerView; - -import java.util.ArrayList; - -public class FlowLayoutManager extends RecyclerView.LayoutManager { - - private SparseArray framesPos; - private SparseArray itemsToRow; - private SparseArray rowToItems; - private ArrayList> rows; - private Size actualSize = new Size(); - private int lastCalculatedWidth; - private int lastCalculatedHeight; - private float minimumInteritemSpacing = AndroidUtilities.dp(2); - private float preferredRowSize; - - private static final int DIRECTION_NONE = -1; - private static final int DIRECTION_UP = 0; - private static final int DIRECTION_DOWN = 1; - - private int firstVisiblePosition; - private boolean forceClearOffsets; - - @Override - public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { - if (getItemCount() == 0) { - detachAndScrapAttachedViews(recycler); - return; - } - - int childTop; - if (framesPos == null || framesPos.size() != getItemCount() || lastCalculatedHeight != getHeight() || lastCalculatedWidth != getWidth()) { - prepareLayout(); - firstVisiblePosition = 0; - childTop = 0; - } else if (getChildCount() == 0) { - firstVisiblePosition = 0; - childTop = 0; - } else if (getVisibleChildCount(0) >= state.getItemCount()) { - firstVisiblePosition = 0; - childTop = 0; - } else { - final View topChild = getChildAt(0); - if (forceClearOffsets) { - childTop = 0; - forceClearOffsets = false; - } else { - childTop = getDecoratedTop(topChild); - } - - Rect lastFrame = framesPos.get(getIndexOfRow(rows.size() - 1)); - if (getHeight() > lastFrame.y + lastFrame.height) { - firstVisiblePosition = 0; - childTop = 0; - } - - int maxFirstRow = rows.size() - (getVisibleRowCount(0) - 1); - boolean isOutOfRowBounds = getFirstVisibleRow() > maxFirstRow; - if (isOutOfRowBounds) { - int firstRow; - if (isOutOfRowBounds) { - firstRow = maxFirstRow; - } else { - firstRow = getFirstVisibleRow(); - } - firstVisiblePosition = getIndexOfRow(firstRow); - - childTop = (int) framesPos.get(getIndexOfRow(rows.size() - 1)).y; - - if (getFirstVisibleRow() == 0) { - childTop = Math.min(childTop, 0); - } - } - } - detachAndScrapAttachedViews(recycler); - fillGrid(DIRECTION_NONE, childTop, 0, recycler, state); - } - - @Override - public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { - removeAllViews(); - } - - private void fillGrid(int direction, int emptyTop, int directionRowCount, RecyclerView.Recycler recycler, RecyclerView.State state) { - if (firstVisiblePosition < 0) { - firstVisiblePosition = 0; - } - if (firstVisiblePosition >= getItemCount()) { - firstVisiblePosition = (getItemCount() - 1); - } - - SparseArray viewCache = null; - int topOffset = emptyTop; - int childCount = getChildCount(); - if (childCount != 0) { - final View topView = getChildAt(0); - topOffset = getDecoratedTop(topView); - switch (direction) { - case DIRECTION_UP: { - //FileLog.d("tmessages", "up topOffset from " + topOffset); - for (int a = 0; a < directionRowCount; a++) { - topOffset -= preferredRowSize; - } - //FileLog.d("tmessages", "up topOffset to " + topOffset); - /*framesPos.get(firstVisiblePosition - 1).height*/ //TODO support various row height - break; - } - case DIRECTION_DOWN: { - //FileLog.d("tmessages", "down topOffset from " + topOffset); - for (int a = 0; a < directionRowCount; a++) { - topOffset += preferredRowSize; - } - //FileLog.d("tmessages", "down topOffset to " + topOffset); - /*framesPos.get(firstVisiblePosition).height*/ //TODO support various row height - break; - } - } - - viewCache = new SparseArray<>(getChildCount()); - for (int i = 0; i < childCount; i++) { - viewCache.put(firstVisiblePosition + i, getChildAt(i)); - } - for (int i = 0; i < childCount; i++) { - detachView(viewCache.valueAt(i)); - } - } - - int firstRow = getRowOfIndex(firstVisiblePosition); - switch (direction) { - case DIRECTION_UP: - //FileLog.d("tmessages", "up first position from " + firstVisiblePosition + " row from " + getRowOfIndex(firstVisiblePosition)); - for (int a = 0; a < directionRowCount; a++) { - firstVisiblePosition -= getCountForRow(firstRow - (a + 1)); - } - //FileLog.d("tmessages", "up first position to " + firstVisiblePosition + " row to " + getRowOfIndex(firstVisiblePosition)); - break; - case DIRECTION_DOWN: - //FileLog.d("tmessages", "down first position from " + firstVisiblePosition + " row from " + getRowOfIndex(firstVisiblePosition)); - for (int a = 0; a < directionRowCount; a++) { - firstVisiblePosition += getCountForRow(firstRow + a); - } - //FileLog.d("tmessages", "down first position to " + firstVisiblePosition + " row to " + getRowOfIndex(firstVisiblePosition)); - break; - } - - int lastHeight = 0; - int lastRow = -1; - //FileLog.d("tmessages", "fill from " + firstVisiblePosition + " to " + getVisibleChildCount(topOffset)); - for (int i = firstVisiblePosition, v = firstVisiblePosition + getVisibleChildCount(topOffset); i < v; i++) { - if (i < 0 || i >= state.getItemCount()) { - continue; - } - int newRow = getRowOfIndex(i); - if (lastRow != -1 && newRow != lastRow) { - topOffset = lastHeight; - } - - Rect rect = framesPos.get(i); - View view = viewCache != null ? viewCache.get(i) : null; - if (view == null) { - view = recycler.getViewForPosition(i); - addView(view); - RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view.getLayoutParams(); - layoutParams.width = (int) rect.width; - layoutParams.height = (int) rect.height; - view.setLayoutParams(layoutParams); - measureChildWithMargins(view, 0, 0); - layoutDecorated(view, (int) rect.x, topOffset, (int) (rect.x + rect.width), (int) (topOffset + rect.height)); - } else { - attachView(view); - viewCache.remove(i); - } - lastHeight = (int) (topOffset + rect.height); - lastRow = newRow; - } - if (viewCache != null) { - for (int i = 0; i < viewCache.size(); i++) { - final View removingView = viewCache.valueAt(i); - recycler.recycleView(removingView); - } - } - } - - @Override - public void scrollToPosition(int position) { - if (position >= getItemCount()) { - return; - } - forceClearOffsets = true; - firstVisiblePosition = position; - requestLayout(); - } - - @Override - public boolean canScrollVertically() { - return true; - } - - @Override - public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { - if (getChildCount() == 0) { - return 0; - } - - final View topView = getChildAt(0); - final View bottomView = getChildAt(getChildCount() - 1); - - int viewSpan = getDecoratedBottom(bottomView) - getDecoratedTop(topView); - if (viewSpan < getHeight()) { - return 0; - } - - int delta; - int rowsCount; - int firstVisibleRow = getFirstVisibleRow(); - int lastVisibleRow = firstVisibleRow + getVisibleRowCount(0) - 1; - boolean topBoundReached = firstVisibleRow == 0; - boolean bottomBoundReached = lastVisibleRow >= rows.size() - 1; - //FileLog.d("tmessages", "first = " + firstVisibleRow + " last = " + lastVisibleRow + " rows = " + rows.size() + " height = " + getHeight() + " preferredRowSize = " + preferredRowSize); - if (dy > 0) { - int bottom = getDecoratedBottom(bottomView); - int amount = getHeight() - bottom + dy; - rowsCount = (int) Math.ceil(Math.abs(amount) / preferredRowSize); - //FileLog.d("tmessages", "scroll bottom = " + dy + " decorated bottom = " + bottom + " amount = " + amount + " rowsCount = " + rowsCount); - if (amount < 0 || lastVisibleRow + rowsCount <= rows.size() - 1) { - delta = -dy; - //FileLog.d("tmessages", "delta1 = " + delta); - } else { - rowsCount = rows.size() - 1 - lastVisibleRow; - delta = getHeight() - bottom - (int) (rowsCount * preferredRowSize); - //FileLog.d("tmessages", "delta2 = " + delta + " rowsCount = " + rowsCount); - } - } else { - int top = getDecoratedTop(topView); - int amount = Math.abs(dy) + top; - rowsCount = (int) Math.ceil(Math.abs(amount) / preferredRowSize); - //FileLog.d("tmessages", "scroll top = " + dy + " decorated top = " + top + " amount = " + amount + " rowsCount = " + rowsCount); - if (amount < 0 || firstVisibleRow - rowsCount >= 0) { - delta = -dy; - //FileLog.d("tmessages", "delta1 = " + delta); - } else { - rowsCount = firstVisibleRow; - delta = -top + (int) (firstVisibleRow * preferredRowSize); - //FileLog.d("tmessages", "delta2 = " + delta + " rowsCount = " + rowsCount); - } - } - - offsetChildrenVertical(delta); - - if (dy > 0) { - if (getDecoratedBottom(topView) < 0 && !bottomBoundReached) { - fillGrid(DIRECTION_DOWN, 0, rowsCount, recycler, state); - } else if (!bottomBoundReached) { - fillGrid(DIRECTION_NONE, 0, 0, recycler, state); - } - } else { - if (getDecoratedTop(topView) > 0 && !topBoundReached) { - fillGrid(DIRECTION_UP, 0, rowsCount, recycler, state); - } else if (!topBoundReached) { - fillGrid(DIRECTION_NONE, 0, 0, recycler, state); - } - } - - return -delta; - } - - @Override - public RecyclerView.LayoutParams generateDefaultLayoutParams() { - return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - } - - private int getCountForRow(int row) { - if (row < 0 || row >= rows.size()) { - return 0; - } - return rows.get(row).size(); - } - - private int getIndexOfRow(int row) { - if (rows == null) { - return 0; - } - Integer value = rowToItems.get(row); - return value == null ? 0 : value; - } - - public int getRowCount() { - return rows != null ? rows.size() : 0; - } - - public int getRowOfIndex(int index) { - if (rows == null) { - return 0; - } - Integer value = itemsToRow.get(index); - return value == null ? 0 : value; - } - - private int getFirstVisibleRow() { - return getRowOfIndex(firstVisiblePosition); - } - - private int getVisibleChildCount(int topOffset) { - int startRow = getFirstVisibleRow(); - int count = 0; - for (int a = startRow, e = startRow + getVisibleRowCount(topOffset); a < e; a++) { - if (a < rows.size()) { - count += rows.get(a).size(); - } else { - break; - } - } - return count; - } - - private int getVisibleRowCount(int topOffset) { - int height = getHeight(); - View topChild = getChildAt(0); - int top = topChild != null ? getDecoratedTop(topChild) : topOffset; - int count = 0; - int startRow = getFirstVisibleRow(); - for (int a = startRow; a < rows.size(); a++) { - if (top > height) { - break; - } - top += framesPos.get(getIndexOfRow(a)).height; - count++; - } - return count; - } - - private void prepareLayout() { - if (framesPos == null) { - framesPos = new SparseArray<>(); - itemsToRow = new SparseArray<>(); - rowToItems = new SparseArray<>(); - } else { - framesPos.clear(); - itemsToRow.clear(); - rowToItems.clear(); - } - preferredRowSize = getHeight() / 2.0f; - float viewPortAvailableSize = getWidth(); - if (BuildVars.DEBUG_VERSION) { - FileLog.d("tmessages", "preferredRowSize = " + preferredRowSize + " width = " + viewPortAvailableSize); - } - - float totalItemSize = 0; - int[] weights = new int[getItemCount()]; - for (int a = 0; a < getItemCount(); a++) { - Size size = sizeForItem(a); - totalItemSize += (size.width / size.height) * preferredRowSize; - weights[a] = Math.round(size.width / size.height * 100); - } - - int numberOfRows = Math.max(Math.round(totalItemSize / viewPortAvailableSize), 1); - - rows = getLinearPartitionForSequence(weights, numberOfRows); - - int i = 0, a; - Point offset = new Point(0, 0); - float previousItemSize = 0; - for (a = 0; a < rows.size(); a++) { - ArrayList row = rows.get(a); - - float summedRatios = 0; - for (int j = i, n = i + row.size(); j < n; j++) { - Size preferredSize = sizeForItem(j); - summedRatios += preferredSize.width / preferredSize.height; - } - - float rowSize = viewPortAvailableSize - ((row.size() - 1) * minimumInteritemSpacing); - - if (rows.size() == 1 && a == rows.size() - 1) { - if (row.size() < 2) { - rowSize = (float) Math.floor(viewPortAvailableSize / 3.0f) - ((row.size() - 1) * minimumInteritemSpacing); - } else if (row.size() < 3) { - rowSize = (float) Math.floor(viewPortAvailableSize * 2.0f / 3.0f) - ((row.size() - 1) * minimumInteritemSpacing); - } - } - - for (int j = i, n = i + row.size(); j < n; j++) { - Size preferredSize = sizeForItem(j); - - actualSize.width = Math.round(rowSize / summedRatios * (preferredSize.width / preferredSize.height)); - actualSize.height = preferredRowSize;//Math.round(rowSize / summedRatios); - if (a == row.size() - 1) { - actualSize.height = preferredRowSize; - } - Rect rect = new Rect(offset.x, offset.y, actualSize.width, actualSize.height); - if (rect.x + rect.width >= viewPortAvailableSize - 2.0f) { - rect.width = Math.max(1.0f, viewPortAvailableSize - rect.x); - } - framesPos.put(j, rect); - itemsToRow.put(j, a); - if (j == i) { - rowToItems.put(a, j); - } - offset.x += actualSize.width + minimumInteritemSpacing; - previousItemSize = actualSize.height; - } - - if (row.size() > 0) { - offset.x = 0; - offset.y += previousItemSize; - } - - i += row.size(); - } - } - - private int[] getLinearPartitionTable(int[] sequence, int numPartitions) { - int n = sequence.length; - int i, j, x; - - int tmpTable[] = new int[n * numPartitions]; - int solution[] = new int[(n - 1) * (numPartitions - 1)]; - - for (i = 0; i < n; i++) { - tmpTable[i * numPartitions] = sequence[i] + (i != 0 ? tmpTable[(i - 1) * numPartitions] : 0); - } - - for (j = 0; j < numPartitions; j++) { - tmpTable[j] = sequence[0]; - } - - for (i = 1; i < n; i++) { - for (j = 1; j < numPartitions; j++) { - int currentMin = 0; - int minX = Integer.MAX_VALUE; - - for (x = 0; x < i; x++) { - int cost = Math.max(tmpTable[x * numPartitions + (j - 1)], tmpTable[i * numPartitions] - tmpTable[x * numPartitions]); - if (x == 0 || cost < currentMin) { - currentMin = cost; - minX = x; - } - } - tmpTable[i * numPartitions + j] = currentMin; - solution[(i - 1) * (numPartitions - 1) + (j - 1)] = minX; - } - } - - return solution; - } - - private ArrayList> getLinearPartitionForSequence(int[] sequence, int numberOfPartitions) { - int n = sequence.length; - int k = numberOfPartitions; - - if (k <= 0) { - return new ArrayList<>(); - } - - if (k >= n || n == 1) { - ArrayList> partition = new ArrayList<>(sequence.length); - for (int i = 0; i < sequence.length; i++) { - ArrayList arrayList = new ArrayList<>(1); - arrayList.add(sequence[i]); - partition.add(arrayList); - } - return partition; - } - - int[] solution = getLinearPartitionTable(sequence, numberOfPartitions); - int solutionRowSize = numberOfPartitions - 1; - - k = k - 2; - n = n - 1; - ArrayList> answer = new ArrayList<>(); - - while (k >= 0) { - if (n < 1) { - answer.add(0, new ArrayList()); - } else { - ArrayList currentAnswer = new ArrayList<>(); - for (int i = solution[(n - 1) * solutionRowSize + k] + 1, range = n + 1; i < range; i++) { - currentAnswer.add(sequence[i]); - } - answer.add(0, currentAnswer); - n = solution[(n - 1) * solutionRowSize + k]; - } - k = k - 1; - } - - ArrayList currentAnswer = new ArrayList<>(); - for (int i = 0, range = n + 1; i < range; i++) { - currentAnswer.add(sequence[i]); - } - answer.add(0, currentAnswer); - return answer; - } - - private Size sizeForItem(int i) { - Size size = getSizeForItem(i); - if (size.width == 0) { - size.width = 100; - } - if (size.height == 0) { - size.height = 100; - } - float aspect = size.width / size.height; - if (aspect > 4.0f || aspect < 0.2f) { - size.height = size.width = Math.max(size.width, size.height); - } - return size; - } - - protected Size getSizeForItem(int i) { - return new Size(100, 100); - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/FrameLayoutFixed.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/FrameLayoutFixed.java deleted file mode 100644 index f558d9d2d..000000000 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/FrameLayoutFixed.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.x. - * It is licensed under GNU GPL v. 2 or later. - * You should have received a copy of the license in this archive (see LICENSE). - * - * Copyright Nikolai Kudashov, 2013-2016. - */ - -package org.telegram.ui.Components; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.View; -import android.widget.FrameLayout; - -import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.FileLog; - -import java.util.ArrayList; - -public class FrameLayoutFixed extends FrameLayout { - - private final ArrayList mMatchParentChildren = new ArrayList<>(1); - - public FrameLayoutFixed(Context context) { - super(context); - } - - public FrameLayoutFixed(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public FrameLayoutFixed(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public final int getMeasuredStateFixed(View view) { - return (view.getMeasuredWidth()&0xff000000) - | ((view.getMeasuredHeight()>>16) - & (0xff000000>>16)); - } - - public static int resolveSizeAndStateFixed(int size, int measureSpec, int childMeasuredState) { - int result = size; - int specMode = MeasureSpec.getMode(measureSpec); - int specSize = MeasureSpec.getSize(measureSpec); - switch (specMode) { - case MeasureSpec.UNSPECIFIED: - result = size; - break; - case MeasureSpec.AT_MOST: - if (specSize < size) { - result = specSize | 0x01000000; - } else { - result = size; - } - break; - case MeasureSpec.EXACTLY: - result = specSize; - break; - } - return result | (childMeasuredState&0xff000000); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - try { - int count = getChildCount(); - - final boolean measureMatchParentChildren = - MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || - MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; - mMatchParentChildren.clear(); - - int maxHeight = 0; - int maxWidth = 0; - int childState = 0; - - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - if (child.getVisibility() != GONE) { - measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - maxWidth = Math.max(maxWidth, - child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); - maxHeight = Math.max(maxHeight, - child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); - childState |= getMeasuredStateFixed(child); - if (measureMatchParentChildren) { - if (lp.width == LayoutHelper.MATCH_PARENT || - lp.height == LayoutHelper.MATCH_PARENT) { - mMatchParentChildren.add(child); - } - } - } - } - - // Account for padding too - maxWidth += getPaddingLeft() + getPaddingRight(); - maxHeight += getPaddingTop() + getPaddingBottom(); - - // Check against our minimum height and width - maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); - maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); - - // Check against our foreground's minimum height and width - final Drawable drawable = getForeground(); - if (drawable != null) { - maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); - maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); - } - - setMeasuredDimension(resolveSizeAndStateFixed(maxWidth, widthMeasureSpec, childState), - resolveSizeAndStateFixed(maxHeight, heightMeasureSpec, childState << 16)); - - count = mMatchParentChildren.size(); - if (count > 1) { - for (int i = 0; i < count; i++) { - final View child = mMatchParentChildren.get(i); - - final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); - int childWidthMeasureSpec; - int childHeightMeasureSpec; - - if (lp.width == LayoutHelper.MATCH_PARENT) { - childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - - getPaddingLeft() - getPaddingRight() - - lp.leftMargin - lp.rightMargin, - MeasureSpec.EXACTLY); - } else { - childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, - getPaddingLeft() + getPaddingRight() + - lp.leftMargin + lp.rightMargin, - lp.width); - } - - if (lp.height == LayoutHelper.MATCH_PARENT) { - childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - - getPaddingTop() - getPaddingBottom() - - lp.topMargin - lp.bottomMargin, - MeasureSpec.EXACTLY); - } else { - childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, - getPaddingTop() + getPaddingBottom() + - lp.topMargin + lp.bottomMargin, - lp.height); - } - - child.measure(childWidthMeasureSpec, childHeightMeasureSpec); - } - } - } catch (Exception e) { - FileLog.e("tmessages", e); - try { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } catch (Exception e2) { - FileLog.e("tmessages", e2); - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(10), MeasureSpec.EXACTLY)); - } - } - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberTextView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberTextView.java index dbc1f0f5b..991b3448c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberTextView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberTextView.java @@ -8,6 +8,8 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; @@ -18,8 +20,7 @@ import android.text.TextPaint; import android.view.View; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import java.util.ArrayList; import java.util.Locale; @@ -29,7 +30,7 @@ public class NumberTextView extends View { private ArrayList letters = new ArrayList<>(); private ArrayList oldLetters = new ArrayList<>(); private TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - private ObjectAnimatorProxy animator; + private ObjectAnimator animator; private float progress = 0.0f; private int currentNumber = 1; @@ -77,11 +78,11 @@ public class NumberTextView extends View { } } if (animated && !oldLetters.isEmpty()) { - animator = ObjectAnimatorProxy.ofFloatProxy(this, "progress", forwardAnimation ? -1 : 1, 0); + animator = ObjectAnimator.ofFloat(this, "progress", forwardAnimation ? -1 : 1, 0); animator.setDuration(150); animator.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { animator = null; oldLetters.clear(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java index cf49b2769..3b68e7110 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java @@ -8,6 +8,9 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; @@ -30,7 +33,6 @@ import android.text.TextWatcher; import android.text.method.PasswordTransformationMethod; import android.util.TypedValue; import android.view.ActionMode; -import android.view.ContextMenu; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; @@ -52,10 +54,7 @@ import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; import org.telegram.messenger.UserConfig; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import java.util.ArrayList; import java.util.Locale; @@ -72,7 +71,7 @@ public class PasscodeView extends FrameLayout { private ArrayList dotTextViews; private StringBuilder stringBuilder; private String DOT = "\u2022"; - private AnimatorSetProxy currentAnimation; + private AnimatorSet currentAnimation; private Runnable dotRunnable; public AnimatingTextView(Context context) { @@ -86,9 +85,9 @@ public class PasscodeView extends FrameLayout { textView.setTextColor(0xffffffff); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 36); textView.setGravity(Gravity.CENTER); - ViewProxy.setAlpha(textView, 0); - ViewProxy.setPivotX(textView, AndroidUtilities.dp(25)); - ViewProxy.setPivotY(textView, AndroidUtilities.dp(25)); + textView.setAlpha(0); + textView.setPivotX(AndroidUtilities.dp(25)); + textView.setPivotY(AndroidUtilities.dp(25)); addView(textView); LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams(); layoutParams.width = AndroidUtilities.dp(50); @@ -101,10 +100,10 @@ public class PasscodeView extends FrameLayout { textView.setTextColor(0xffffffff); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 36); textView.setGravity(Gravity.CENTER); - ViewProxy.setAlpha(textView, 0); + textView.setAlpha(0); textView.setText(DOT); - ViewProxy.setPivotX(textView, AndroidUtilities.dp(25)); - ViewProxy.setPivotY(textView, AndroidUtilities.dp(25)); + textView.setPivotX(AndroidUtilities.dp(25)); + textView.setPivotY(AndroidUtilities.dp(25)); addView(textView); layoutParams = (LayoutParams) textView.getLayoutParams(); layoutParams.width = AndroidUtilities.dp(50); @@ -130,37 +129,37 @@ public class PasscodeView extends FrameLayout { } - ArrayList animators = new ArrayList<>(); + ArrayList animators = new ArrayList<>(); final int newPos = stringBuilder.length(); stringBuilder.append(c); TextView textView = characterTextViews.get(newPos); textView.setText(c); - ViewProxy.setTranslationX(textView, getXForTextView(newPos)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0, 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0, 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0, 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", AndroidUtilities.dp(20), 0)); + textView.setTranslationX(getXForTextView(newPos)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0, 1)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0, 1)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0, 1)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", AndroidUtilities.dp(20), 0)); textView = dotTextViews.get(newPos); - ViewProxy.setTranslationX(textView, getXForTextView(newPos)); - ViewProxy.setAlpha(textView, 0); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0, 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0, 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", AndroidUtilities.dp(20), 0)); + textView.setTranslationX(getXForTextView(newPos)); + textView.setAlpha(0); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0, 1)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0, 1)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", AndroidUtilities.dp(20), 0)); for (int a = newPos + 1; a < 4; a++) { textView = characterTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); } textView = dotTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); } } @@ -173,23 +172,23 @@ public class PasscodeView extends FrameLayout { if (dotRunnable != this) { return; } - ArrayList animators = new ArrayList<>(); + ArrayList animators = new ArrayList<>(); TextView textView = characterTextViews.get(newPos); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); textView = dotTextViews.get(newPos); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 1)); - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.setDuration(150); currentAnimation.playTogether(animators); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentAnimation != null && currentAnimation.equals(animation)) { currentAnimation = null; } @@ -202,28 +201,28 @@ public class PasscodeView extends FrameLayout { for (int a = 0; a < newPos; a++) { textView = characterTextViews.get(a); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", 0)); textView = dotTextViews.get(a); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", 0)); } if (currentAnimation != null) { currentAnimation.cancel(); } - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.setDuration(150); currentAnimation.playTogether(animators); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentAnimation != null && currentAnimation.equals(animation)) { currentAnimation = null; } @@ -250,7 +249,7 @@ public class PasscodeView extends FrameLayout { FileLog.e("tmessages", e); } - ArrayList animators = new ArrayList<>(); + ArrayList animators = new ArrayList<>(); int deletingPos = stringBuilder.length() - 1; if (deletingPos != 0) { stringBuilder.deleteCharAt(deletingPos); @@ -258,21 +257,21 @@ public class PasscodeView extends FrameLayout { for (int a = deletingPos; a < 4; a++) { TextView textView = characterTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); } textView = dotTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); } } @@ -282,9 +281,9 @@ public class PasscodeView extends FrameLayout { for (int a = 0; a < deletingPos; a++) { TextView textView = characterTextViews.get(a); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); textView = dotTextViews.get(a); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); } if (dotRunnable != null) { @@ -295,12 +294,12 @@ public class PasscodeView extends FrameLayout { if (currentAnimation != null) { currentAnimation.cancel(); } - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.setDuration(150); currentAnimation.playTogether(animators); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentAnimation != null && currentAnimation.equals(animation)) { currentAnimation = null; } @@ -323,30 +322,30 @@ public class PasscodeView extends FrameLayout { } stringBuilder.delete(0, stringBuilder.length()); if (animated) { - ArrayList animators = new ArrayList<>(); + ArrayList animators = new ArrayList<>(); for (int a = 0; a < 4; a++) { TextView textView = characterTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); } textView = dotTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); } } - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.setDuration(150); currentAnimation.playTogether(animators); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentAnimation != null && currentAnimation.equals(animation)) { currentAnimation = null; } @@ -355,10 +354,8 @@ public class PasscodeView extends FrameLayout { currentAnimation.start(); } else { for (int a = 0; a < 4; a++) { - TextView textView = characterTextViews.get(a); - ViewProxy.setAlpha(textView, 0); - textView = dotTextViews.get(a); - ViewProxy.setAlpha(textView, 0); + characterTextViews.get(a).setAlpha(0); + dotTextViews.get(a).setAlpha(0); } } } @@ -377,23 +374,21 @@ public class PasscodeView extends FrameLayout { for (int a = 0; a < 4; a++) { if (a < stringBuilder.length()) { TextView textView = characterTextViews.get(a); - ViewProxy.setAlpha(textView, 0); - ViewProxy.setScaleX(textView, 1); - ViewProxy.setScaleY(textView, 1); - ViewProxy.setTranslationY(textView, 0); - ViewProxy.setTranslationX(textView, getXForTextView(a)); + textView.setAlpha(0); + textView.setScaleX(1); + textView.setScaleY(1); + textView.setTranslationY(0); + textView.setTranslationX(getXForTextView(a)); textView = dotTextViews.get(a); - ViewProxy.setAlpha(textView, 1); - ViewProxy.setScaleX(textView, 1); - ViewProxy.setScaleY(textView, 1); - ViewProxy.setTranslationY(textView, 0); - ViewProxy.setTranslationX(textView, getXForTextView(a)); + textView.setAlpha(1); + textView.setScaleX(1); + textView.setScaleY(1); + textView.setTranslationY(0); + textView.setTranslationX(getXForTextView(a)); } else { - TextView textView = characterTextViews.get(a); - ViewProxy.setAlpha(textView, 0); - textView = dotTextViews.get(a); - ViewProxy.setAlpha(textView, 0); + characterTextViews.get(a).setAlpha(0); + dotTextViews.get(a).setAlpha(0); } } super.onLayout(changed, left, top, right, bottom); @@ -535,30 +530,22 @@ public class PasscodeView extends FrameLayout { } } }); - if (android.os.Build.VERSION.SDK_INT < 11) { - passwordEditText.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - menu.clear(); - } - }); - } else { - passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } + passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } - public void onDestroyActionMode(ActionMode mode) { - } + public void onDestroyActionMode(ActionMode mode) { + } - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return false; - } + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - }); - } + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + }); checkImage = new ImageView(context); checkImage.setImageResource(R.drawable.passcode_check); @@ -764,23 +751,18 @@ public class PasscodeView extends FrameLayout { passwordEditText.clearFocus(); AndroidUtilities.hideKeyboard(passwordEditText); - if (Build.VERSION.SDK_INT >= 14) { - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.setDuration(200); - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(this, "translationY", AndroidUtilities.dp(20)), - ObjectAnimatorProxy.ofFloat(this, "alpha", AndroidUtilities.dp(0.0f))); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { - PasscodeView.this.clearAnimation(); - setVisibility(View.GONE); - } - }); - animatorSetProxy.start(); - } else { - setVisibility(View.GONE); - } + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.setDuration(200); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(this, "translationY", AndroidUtilities.dp(20)), + ObjectAnimator.ofFloat(this, "alpha", AndroidUtilities.dp(0.0f))); + AnimatorSet.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + setVisibility(View.GONE); + } + }); + AnimatorSet.start(); UserConfig.appLocked = false; UserConfig.saveConfig(false); @@ -793,19 +775,18 @@ public class PasscodeView extends FrameLayout { private void shakeTextView(final float x, final int num) { if (num == 6) { - passcodeTextView.clearAnimation(); return; } - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether(ObjectAnimatorProxy.ofFloat(passcodeTextView, "translationX", AndroidUtilities.dp(x))); - animatorSetProxy.setDuration(50); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether(ObjectAnimator.ofFloat(passcodeTextView, "translationX", AndroidUtilities.dp(x))); + AnimatorSet.setDuration(50); + AnimatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { shakeTextView(num == 5 ? 0 : -x, num + 1); } }); - animatorSetProxy.start(); + AnimatorSet.start(); } private void onPasscodeError() { @@ -986,11 +967,8 @@ public class PasscodeView extends FrameLayout { if (getVisibility() == View.VISIBLE) { return; } - if (Build.VERSION.SDK_INT >= 14) { - ViewProxy.setAlpha(this, 1.0f); - ViewProxy.setTranslationY(this, 0); - this.clearAnimation(); - } + setAlpha(1.0f); + setTranslationY(0); SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); int selectedBackground = preferences.getInt("selectedBackground", 1000001); if (selectedBackground == 1000001) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterView.java index c5a162982..5158eeede 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterView.java @@ -8,6 +8,9 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; @@ -36,10 +39,7 @@ import org.telegram.messenger.DispatchQueue; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; import org.telegram.messenger.Utilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Cells.PhotoEditToolCell; @@ -2090,9 +2090,8 @@ public class PhotoFilterView extends FrameLayout { layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); recyclerListView.setLayoutManager(layoutManager); recyclerListView.setClipToPadding(false); - if (Build.VERSION.SDK_INT >= 9) { - recyclerListView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); - } + recyclerListView.setTag(12); + recyclerListView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); recyclerListView.setAdapter(toolsAdapter = new ToolsAdapter(context)); toolsView.addView(recyclerListView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 60, Gravity.LEFT | Gravity.TOP)); recyclerListView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @@ -2595,26 +2594,24 @@ public class PhotoFilterView extends FrameLayout { curvesControl.setVisibility(INVISIBLE); } - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(viewFrom, "translationY", 0, AndroidUtilities.dp(126)) + ObjectAnimator.ofFloat(viewFrom, "translationY", 0, AndroidUtilities.dp(126)) ); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { - viewFrom.clearAnimation(); + public void onAnimationEnd(Animator animation) { viewFrom.setVisibility(GONE); viewTo.setVisibility(VISIBLE); - ViewProxy.setTranslationY(viewTo, AndroidUtilities.dp(126)); + viewTo.setTranslationY(AndroidUtilities.dp(126)); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(viewTo, "translationY", 0) + ObjectAnimator.ofFloat(viewTo, "translationY", 0) ); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { - viewTo.clearAnimation(); + public void onAnimationEnd(Animator animation) { if (selectedTool == enhanceTool) { checkEnhance(); } @@ -2799,10 +2796,10 @@ public class PhotoFilterView extends FrameLayout { private void checkEnhance() { if (enhanceValue == 0) { - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.setDuration(200); - animatorSetProxy.playTogether(ObjectAnimatorProxy.ofInt(valueSeekBar, "progress", 50)); - animatorSetProxy.start(); + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.setDuration(200); + AnimatorSet.playTogether(ObjectAnimator.ofInt(valueSeekBar, "progress", 50)); + AnimatorSet.start(); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoViewerCaptionEnterView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoViewerCaptionEnterView.java index 3da5c1ac1..4d5808c6b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoViewerCaptionEnterView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoViewerCaptionEnterView.java @@ -8,6 +8,8 @@ package org.telegram.ui.Components; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.os.Build; import android.text.Editable; @@ -35,13 +37,11 @@ import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; import org.telegram.tgnet.TLRPC; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; import java.lang.reflect.Field; import java.lang.reflect.Method; -public class PhotoViewerCaptionEnterView extends FrameLayoutFixed implements NotificationCenter.NotificationCenterDelegate, SizeNotifierFrameLayoutPhoto.SizeNotifierFrameLayoutPhotoDelegate { +public class PhotoViewerCaptionEnterView extends FrameLayout implements NotificationCenter.NotificationCenterDelegate, SizeNotifierFrameLayoutPhoto.SizeNotifierFrameLayoutPhotoDelegate { public interface PhotoViewerCaptionEnterViewDelegate { void onCaptionEnter(); @@ -56,9 +56,9 @@ public class PhotoViewerCaptionEnterView extends FrameLayoutFixed implements Not private ActionMode currentActionMode; - private AnimatorSetProxy runningAnimation; - private AnimatorSetProxy runningAnimation2; - private ObjectAnimatorProxy runningAnimationAudio; + private AnimatorSet runningAnimation; + private AnimatorSet runningAnimation2; + private ObjectAnimator runningAnimationAudio; private int runningAnimationType; private int audioInterfaceState; @@ -89,7 +89,7 @@ public class PhotoViewerCaptionEnterView extends FrameLayoutFixed implements Not textFieldContainer.setOrientation(LinearLayout.HORIZONTAL); addView(textFieldContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 2, 0, 0, 0)); - FrameLayoutFixed frameLayout = new FrameLayoutFixed(context); + FrameLayout frameLayout = new FrameLayout(context); textFieldContainer.addView(frameLayout, LayoutHelper.createLinear(0, LayoutHelper.WRAP_CONTENT, 1.0f)); emojiButton = new ImageView(context); @@ -108,7 +108,17 @@ public class PhotoViewerCaptionEnterView extends FrameLayoutFixed implements Not } }); - messageEditText = new EditText(context); + messageEditText = new EditText(context) { + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + try { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } catch (Exception e) { + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), AndroidUtilities.dp(51)); + FileLog.e("tmessages", e); + } + } + }; if (Build.VERSION.SDK_INT >= 23) { messageEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { @Override @@ -512,11 +522,6 @@ public class PhotoViewerCaptionEnterView extends FrameLayoutFixed implements Not private void openKeyboardInternal() { showPopup(AndroidUtilities.usingHardwareInput ? 0 : 2); - /*int selection = messageEditText.getSelectionStart(); - MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0); - messageEditText.onTouchEvent(event); - event.recycle(); - messageEditText.setSelection(selection);*/ AndroidUtilities.showKeyboard(messageEditText); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayerView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayerView.java index 18cbeaf85..c25a465aa 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayerView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayerView.java @@ -8,9 +8,11 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; -import android.os.Build; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -23,10 +25,7 @@ import android.widget.ImageView; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.MediaController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.NotificationCenter; @@ -40,7 +39,7 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi private ImageView playButton; private TextView titleTextView; private MessageObject lastMessageObject; - private AnimatorSetProxy animatorSet; + private AnimatorSet animatorSet; private float yPosition; private BaseFragment fragment; private float topPadding; @@ -165,7 +164,6 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi if (visible) { visible = false; if (create) { - clearAnimation(); if (getVisibility() != GONE) { setVisibility(GONE); } @@ -175,15 +173,14 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi animatorSet.cancel(); animatorSet = null; } - animatorSet = new AnimatorSetProxy(); - animatorSet.playTogether(ObjectAnimatorProxy.ofFloat(this, "translationY", -AndroidUtilities.dp(36)), - ObjectAnimatorProxy.ofFloat(this, "topPadding", 0)); + animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(this, "translationY", -AndroidUtilities.dp(36)), + ObjectAnimator.ofFloat(this, "topPadding", 0)); animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (animatorSet != null && animatorSet.equals(animation)) { - clearAnimation(); setVisibility(GONE); animatorSet = null; } @@ -195,7 +192,7 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi } else { if (create && topPadding == 0) { setTopPadding(AndroidUtilities.dp(36)); - ViewProxy.setTranslationY(this, 0); + setTranslationY(0); yPosition = 0; } if (!visible) { @@ -204,13 +201,13 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi animatorSet.cancel(); animatorSet = null; } - animatorSet = new AnimatorSetProxy(); - animatorSet.playTogether(ObjectAnimatorProxy.ofFloat(this, "translationY", -AndroidUtilities.dp(36), 0), - ObjectAnimatorProxy.ofFloat(this, "topPadding", AndroidUtilities.dp(36))); + animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(this, "translationY", -AndroidUtilities.dp(36), 0), + ObjectAnimator.ofFloat(this, "topPadding", AndroidUtilities.dp(36))); animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (animatorSet != null && animatorSet.equals(animation)) { animatorSet = null; } @@ -252,17 +249,12 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - int restoreToCount = 0; - if (Build.VERSION.SDK_INT >= 11) { - restoreToCount = canvas.save(); - if (yPosition < 0) { - canvas.clipRect(0, (int) -yPosition, child.getMeasuredWidth(), AndroidUtilities.dp(39)); - } + int restoreToCount = canvas.save(); + if (yPosition < 0) { + canvas.clipRect(0, (int) -yPosition, child.getMeasuredWidth(), AndroidUtilities.dp(39)); } final boolean result = super.drawChild(canvas, child, drawingTime); - if (Build.VERSION.SDK_INT >= 11) { - canvas.restoreToCount(restoreToCount); - } + canvas.restoreToCount(restoreToCount); return result; } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ProgressCircleView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ProgressCircleView.java new file mode 100644 index 000000000..6080201c3 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ProgressCircleView.java @@ -0,0 +1,28 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.ui.Components; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.View; + +public class ProgressCircleView extends View { + + private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + + public ProgressCircleView(Context context) { + super(context); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RadioButton.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RadioButton.java index 93bd1efff..8299a6fd7 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RadioButton.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RadioButton.java @@ -8,6 +8,7 @@ package org.telegram.ui.Components; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -18,9 +19,7 @@ import android.graphics.PorterDuffXfermode; import android.view.View; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; import org.telegram.messenger.FileLog; -import org.telegram.messenger.ImageLoader; import org.telegram.ui.ActionBar.Theme; public class RadioButton extends View { @@ -35,7 +34,7 @@ public class RadioButton extends View { private int color = Theme.ACTION_BAR_SUBTITLE_COLOR; private float progress; - private ObjectAnimatorProxy checkAnimator; + private ObjectAnimator checkAnimator; private boolean attachedToWindow; private boolean isChecked; @@ -55,9 +54,6 @@ public class RadioButton extends View { try { bitmap = Bitmap.createBitmap(AndroidUtilities.dp(size), AndroidUtilities.dp(size), Bitmap.Config.ARGB_4444); - if (ImageLoader.getInstance().runtimeHack != null) { - ImageLoader.getInstance().runtimeHack.trackFree(bitmap.getRowBytes() * bitmap.getHeight()); - } bitmapCanvas = new Canvas(bitmap); } catch (Throwable e) { FileLog.e("tmessages", e); @@ -96,7 +92,7 @@ public class RadioButton extends View { } private void animateToCheckedState(boolean newCheckedState) { - checkAnimator = ObjectAnimatorProxy.ofFloatProxy(this, "progress", newCheckedState ? 1 : 0); + checkAnimator = ObjectAnimator.ofFloat(this, "progress", newCheckedState ? 1 : 0); checkAnimator.setDuration(200); checkAnimator.start(); } @@ -111,11 +107,6 @@ public class RadioButton extends View { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); attachedToWindow = false; - if (bitmap != null && ImageLoader.getInstance().runtimeHack != null) { - ImageLoader.getInstance().runtimeHack.trackAlloc(bitmap.getRowBytes() * bitmap.getHeight()); - bitmap.recycle(); - bitmap = null; - } } public void setChecked(boolean checked, boolean animated) { @@ -140,16 +131,10 @@ public class RadioButton extends View { protected void onDraw(Canvas canvas) { if (bitmap == null || bitmap.getWidth() != getMeasuredWidth()) { if (bitmap != null) { - if (ImageLoader.getInstance().runtimeHack != null) { - ImageLoader.getInstance().runtimeHack.trackAlloc(bitmap.getRowBytes() * bitmap.getHeight()); - } bitmap.recycle(); } try { bitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); - if (ImageLoader.getInstance().runtimeHack != null) { - ImageLoader.getInstance().runtimeHack.trackFree(bitmap.getRowBytes() * bitmap.getHeight()); - } bitmapCanvas = new Canvas(bitmap); } catch (Throwable e) { FileLog.e("tmessages", e); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerListView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerListView.java index 21fdedfed..0e7ee7e26 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerListView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerListView.java @@ -382,4 +382,9 @@ public class RecyclerListView extends RecyclerView { */ } } + + @Override + public boolean hasOverlappingRendering() { + return false; + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarWaveform.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarWaveform.java index 776fbe48c..35f47941b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarWaveform.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarWaveform.java @@ -103,7 +103,7 @@ public class SeekBarWaveform { } } if (startX != -1 && Math.abs(x - startX) > AndroidUtilities.getPixelsInCM(0.2f, true)) { - if (parentView != null) { + if (parentView != null && parentView.getParent() != null) { parentView.getParent().requestDisallowInterceptTouchEvent(true); } startDraging = true; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java index 440205d3a..c4967829d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java @@ -63,7 +63,6 @@ import java.util.TimerTask; public class ShareAlert extends BottomSheet { private FrameLayout frameLayout; - private FrameLayout container; private TextView doneButtonBadgeTextView; private TextView doneButtonTextView; private LinearLayout doneButton; @@ -76,7 +75,7 @@ public class ShareAlert extends BottomSheet { private MessageObject sendingMessageObject; private EmptyTextProgressView searchEmptyView; private Drawable shadowDrawable; - private HashMap selectedDialogs = new HashMap<>(); + private HashMap selectedDialogs = new HashMap<>(); private TLRPC.TL_exportedMessageLink exportedMessageLink; private boolean loadingLink; @@ -89,11 +88,6 @@ public class ShareAlert extends BottomSheet { public ShareAlert(final Context context, final MessageObject messageObject, boolean publicChannel) { super(context, true); - setApplyTopPadding(false); - setApplyBottomPadding(false); - if (Build.VERSION.SDK_INT >= 11) { - setDisableBackground(true); - } shadowDrawable = context.getResources().getDrawable(R.drawable.sheet_shadow); @@ -121,18 +115,26 @@ public class ShareAlert extends BottomSheet { loadingLink = false; } }); - } }); } - container = new FrameLayout(context) { + containerView = new FrameLayout(context) { private boolean ignoreLayout = false; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - return scrollOffsetY != 0 && ev.getY() < scrollOffsetY || super.onInterceptTouchEvent(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 @@ -143,25 +145,19 @@ public class ShareAlert extends BottomSheet { } int size = Math.max(searchAdapter.getItemCount(), listAdapter.getItemCount()); int contentSize = AndroidUtilities.dp(48) + Math.max(3, (int) Math.ceil(size / 4.0f)) * AndroidUtilities.dp(100) + backgroundPaddingTop; - if (Build.VERSION.SDK_INT < 11) { - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, AndroidUtilities.displaySize.y / 5 * 3), MeasureSpec.EXACTLY)); - } else { - int padding = contentSize < height ? 0 : height - (height / 5 * 3) + AndroidUtilities.dp(8); - if (gridView.getPaddingTop() != padding) { - ignoreLayout = true; - gridView.setPadding(0, padding, 0, AndroidUtilities.dp(8)); - ignoreLayout = false; - } - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); + int padding = contentSize < height ? 0 : height - (height / 5 * 3) + AndroidUtilities.dp(8); + if (gridView.getPaddingTop() != padding) { + ignoreLayout = true; + gridView.setPadding(0, padding, 0, AndroidUtilities.dp(8)); + ignoreLayout = false; } + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (Build.VERSION.SDK_INT >= 11) { - updateLayout(); - } + updateLayout(); } @Override @@ -174,17 +170,12 @@ public class ShareAlert extends BottomSheet { @Override protected void onDraw(Canvas canvas) { - if (Build.VERSION.SDK_INT >= 11) { - shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); - shadowDrawable.draw(canvas); - } + shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); + shadowDrawable.draw(canvas); } }; - if (Build.VERSION.SDK_INT >= 11) { - container.setWillNotDraw(false); - } - container.setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); - setCustomView(container); + containerView.setWillNotDraw(false); + containerView.setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); frameLayout = new FrameLayout(context); frameLayout.setBackgroundColor(0xffffffff); @@ -214,17 +205,8 @@ public class ShareAlert extends BottomSheet { } else { ArrayList arrayList = new ArrayList<>(); arrayList.add(sendingMessageObject); - for (HashMap.Entry entry : selectedDialogs.entrySet()) { - TLRPC.Dialog dialog = entry.getValue(); - boolean asAdmin = true; - int lower_id = (int) dialog.id; - if (lower_id < 0) { - TLRPC.Chat chat = MessagesController.getInstance().getChat(-lower_id); - if (chat.megagroup) { - asAdmin = false; - } - } - SendMessagesHelper.getInstance().sendMessage(arrayList, entry.getKey(), asAdmin); + for (HashMap.Entry entry : selectedDialogs.entrySet()) { + SendMessagesHelper.getInstance().sendMessage(arrayList, entry.getKey()); } dismiss(); } @@ -308,6 +290,7 @@ public class ShareAlert extends BottomSheet { }); gridView = new RecyclerListView(context); + gridView.setTag(13); gridView.setPadding(0, 0, 0, AndroidUtilities.dp(8)); gridView.setClipToPadding(false); gridView.setLayoutManager(layoutManager = new GridLayoutManager(getContext(), 4)); @@ -327,18 +310,21 @@ public class ShareAlert extends BottomSheet { } } }); - container.addView(gridView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 0)); + containerView.addView(gridView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 0)); gridView.setAdapter(listAdapter = new ShareDialogsAdapter(context)); gridView.setGlowColor(0xfff5f6f7); gridView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @Override public void onItemClick(View view, int position) { - TLRPC.Dialog dialog; + TLRPC.TL_dialog dialog; if (gridView.getAdapter() == listAdapter) { dialog = listAdapter.getItem(position); } else { dialog = searchAdapter.getItem(position); } + if (dialog == null) { + return; + } ShareDialogCell cell = (ShareDialogCell) view; if (selectedDialogs.containsKey(dialog.id)) { selectedDialogs.remove(dialog.id); @@ -350,28 +336,25 @@ public class ShareAlert extends BottomSheet { updateSelectedCount(); } }); - if (Build.VERSION.SDK_INT >= 11) { - gridView.setOnScrollListener(new RecyclerView.OnScrollListener() { - @SuppressLint("NewApi") - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - updateLayout(); - } - }); - } + gridView.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + updateLayout(); + } + }); searchEmptyView = new EmptyTextProgressView(context); searchEmptyView.setShowAtCenter(true); searchEmptyView.showTextView(); searchEmptyView.setText(LocaleController.getString("NoChats", R.string.NoChats)); gridView.setEmptyView(searchEmptyView); - container.addView(searchEmptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 0)); + containerView.addView(searchEmptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 0)); - container.addView(frameLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.TOP)); + containerView.addView(frameLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.TOP)); shadow = new View(context); shadow.setBackgroundResource(R.drawable.header_shadow); - container.addView(shadow, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 0)); + containerView.addView(shadow, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 0)); updateSelectedCount(); } @@ -387,6 +370,11 @@ public class ShareAlert extends BottomSheet { return -1000; } + @Override + protected boolean canDismissWithSwipe() { + return false; + } + @SuppressLint("NewApi") private void updateLayout() { if (gridView.getChildCount() <= 0) { @@ -401,7 +389,7 @@ public class ShareAlert extends BottomSheet { frameLayout.setTranslationY(scrollOffsetY); shadow.setTranslationY(scrollOffsetY); searchEmptyView.setTranslationY(scrollOffsetY); - container.invalidate(); + containerView.invalidate(); } } @@ -410,14 +398,9 @@ public class ShareAlert extends BottomSheet { return; } try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(exportedMessageLink.link); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", exportedMessageLink.link); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", exportedMessageLink.link); + clipboard.setPrimaryClip(clip); Toast.makeText(context, LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); } catch (Exception e) { FileLog.e("tmessages", e); @@ -457,12 +440,12 @@ public class ShareAlert extends BottomSheet { private Context context; private int currentCount; - private ArrayList dialogs = new ArrayList<>(); + private ArrayList dialogs = new ArrayList<>(); public ShareDialogsAdapter(Context context) { this.context = context; for (int a = 0; a < MessagesController.getInstance().dialogsServerOnly.size(); a++) { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogsServerOnly.get(a); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogsServerOnly.get(a); int lower_id = (int) dialog.id; int high_id = (int) (dialog.id >> 32); if (lower_id != 0 && high_id != 1) { @@ -483,7 +466,7 @@ public class ShareAlert extends BottomSheet { return dialogs.size(); } - public TLRPC.Dialog getItem(int i) { + public TLRPC.TL_dialog getItem(int i) { if (i < 0 || i >= dialogs.size()) { return null; } @@ -505,8 +488,8 @@ public class ShareAlert extends BottomSheet { @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { ShareDialogCell cell = (ShareDialogCell) holder.itemView; - TLRPC.Dialog dialog = getItem(position); - cell.setDialog(dialog, selectedDialogs.containsKey(dialog.id), null); + TLRPC.TL_dialog dialog = getItem(position); + cell.setDialog((int) dialog.id, selectedDialogs.containsKey(dialog.id), null); } @Override @@ -526,7 +509,7 @@ public class ShareAlert extends BottomSheet { private int lastSearchId = 0; private class DialogSearchResult { - public TLRPC.Dialog dialog = new TLRPC.Dialog(); + public TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); public TLObject object; public int date; public CharSequence name; @@ -606,9 +589,10 @@ public class ShareAlert extends BottomSheet { found = 2; } if (found != 0) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.User user = TLRPC.User.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); DialogSearchResult dialogSearchResult = dialogsResult.get((long) user.id); if (user.status != null) { user.status.expires = cursor.intValue(1); @@ -622,7 +606,6 @@ public class ShareAlert extends BottomSheet { dialogSearchResult.dialog.id = user.id; resultCount++; } - data.reuse(); break; } } @@ -641,9 +624,10 @@ public class ShareAlert extends BottomSheet { for (int a = 0; a < search.length; a++) { String q = search[a]; if (name.startsWith(q) || name.contains(" " + q) || tName != null && (tName.startsWith(q) || tName.contains(" " + q))) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + 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)) { DialogSearchResult dialogSearchResult = dialogsResult.get(-(long) chat.id); dialogSearchResult.name = AndroidUtilities.generateSearchName(chat.title, null, q); @@ -652,7 +636,6 @@ public class ShareAlert extends BottomSheet { resultCount++; } } - data.reuse(); break; } } @@ -691,9 +674,10 @@ public class ShareAlert extends BottomSheet { found = 2; } if (found != 0) { - NativeByteBuffer data = new NativeByteBuffer(cursor.byteArrayLength(0)); - if (data != null && cursor.byteBufferValue(0, data) != 0) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { TLRPC.User user = TLRPC.User.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); DialogSearchResult dialogSearchResult = new DialogSearchResult(); if (user.status != null) { user.status.expires = cursor.intValue(1); @@ -707,7 +691,6 @@ public class ShareAlert extends BottomSheet { } searchResults.add(dialogSearchResult); } - data.reuse(); break; } } @@ -807,7 +790,7 @@ public class ShareAlert extends BottomSheet { return searchResult.size(); } - public TLRPC.Dialog getItem(int i) { + public TLRPC.TL_dialog getItem(int i) { return searchResult.get(i).dialog; } @@ -827,7 +810,7 @@ public class ShareAlert extends BottomSheet { public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { ShareDialogCell cell = (ShareDialogCell) holder.itemView; DialogSearchResult result = searchResult.get(position); - cell.setDialog(result.dialog, selectedDialogs.containsKey(result.dialog.id), result.name); + cell.setDialog((int) result.dialog.id, selectedDialogs.containsKey(result.dialog.id), result.name); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/SizeNotifierFrameLayoutPhoto.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/SizeNotifierFrameLayoutPhoto.java index 79a0c0514..9eda81801 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/SizeNotifierFrameLayoutPhoto.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/SizeNotifierFrameLayoutPhoto.java @@ -46,9 +46,7 @@ public class SizeNotifierFrameLayoutPhoto extends FrameLayout { int usableViewHeight = rootView.getHeight() - AndroidUtilities.getViewInset(rootView); getWindowVisibleDisplayFrame(rect); int top = rect.top; - int size = (rect.bottom - rect.top); - - size = AndroidUtilities.displaySize.y - top - usableViewHeight; + int size = AndroidUtilities.displaySize.y - top - usableViewHeight; if (size <= AndroidUtilities.dp(10)) { size = 0; } 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 eae765ee7..0461ec5b4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/StickersAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/StickersAlert.java @@ -8,6 +8,9 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.*; @@ -27,10 +30,7 @@ import android.widget.TextView; import android.widget.Toast; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.Emoji; import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; @@ -54,7 +54,6 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not void onStickerSelected(TLRPC.Document sticker); } - private FrameLayout container; private RecyclerListView gridView; private GridLayoutManager layoutManager; private GridAdapter adapter; @@ -67,7 +66,7 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not private TextView stickerEmojiTextView; private RecyclerListView.OnItemClickListener stickersOnItemClickListener; private Drawable shadowDrawable; - private AnimatorSetProxy shadowAnimation[] = new AnimatorSetProxy[2]; + private AnimatorSet shadowAnimation[] = new AnimatorSet[2]; private View shadow[] = new View[2]; private FrameLayout emptyView; @@ -79,25 +78,29 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not private int scrollOffsetY; private int reqId; - private boolean ignoreLayout = false; + private boolean ignoreLayout; public StickersAlert(Context context, TLRPC.InputStickerSet set, TLRPC.TL_messages_stickerSet loadedSet, StickersAlertDelegate stickersAlertDelegate) { super(context, false); - setApplyTopPadding(false); - setApplyBottomPadding(false); - if (Build.VERSION.SDK_INT >= 11) { - setDisableBackground(true); - } delegate = stickersAlertDelegate; inputStickerSet = set; stickerSet = loadedSet; shadowDrawable = context.getResources().getDrawable(R.drawable.sheet_shadow); - container = new FrameLayout(context) { + containerView = new FrameLayout(context) { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - return scrollOffsetY != 0 && ev.getY() < scrollOffsetY || super.onInterceptTouchEvent(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 @@ -107,32 +110,26 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not height -= AndroidUtilities.statusBarHeight; } int contentSize = AndroidUtilities.dp(48 + 48) + Math.max(3, (stickerSet != null ? (int) Math.ceil(stickerSet.documents.size() / 5.0f) : 0)) * AndroidUtilities.dp(82) + backgroundPaddingTop; - if (Build.VERSION.SDK_INT < 11) { - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, AndroidUtilities.displaySize.y / 5 * 3), MeasureSpec.EXACTLY)); - } else { - int padding = contentSize < (height / 5 * 3.2) ? 0 : (height / 5 * 2); - if (padding != 0 && contentSize < height) { - padding -= (height - contentSize); - } - if (padding == 0) { - padding = backgroundPaddingTop; - } - if (gridView.getPaddingTop() != padding) { - ignoreLayout = true; - gridView.setPadding(AndroidUtilities.dp(10), padding, AndroidUtilities.dp(10), 0); - emptyView.setPadding(0, padding, 0, 0); - ignoreLayout = false; - } - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); + int padding = contentSize < (height / 5 * 3.2) ? 0 : (height / 5 * 2); + if (padding != 0 && contentSize < height) { + padding -= (height - contentSize); } + if (padding == 0) { + padding = backgroundPaddingTop; + } + if (gridView.getPaddingTop() != padding) { + ignoreLayout = true; + gridView.setPadding(AndroidUtilities.dp(10), padding, AndroidUtilities.dp(10), 0); + emptyView.setPadding(0, padding, 0, 0); + ignoreLayout = false; + } + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (Build.VERSION.SDK_INT >= 11) { - updateLayout(); - } + updateLayout(); } @Override @@ -145,21 +142,14 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not @Override protected void onDraw(Canvas canvas) { - if (Build.VERSION.SDK_INT >= 11) { - shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); - shadowDrawable.draw(canvas); - } + shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); + shadowDrawable.draw(canvas); } }; - if (Build.VERSION.SDK_INT >= 11) { - container.setWillNotDraw(false); - } - container.setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); - setCustomView(container); + containerView.setWillNotDraw(false); + containerView.setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); titleTextView = new TextView(context); - - titleTextView = new TextView(getContext()); titleTextView.setLines(1); titleTextView.setSingleLine(true); titleTextView.setTextColor(Theme.STICKERS_SHEET_TITLE_TEXT_COLOR); @@ -168,7 +158,7 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not titleTextView.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); titleTextView.setGravity(Gravity.CENTER_VERTICAL); titleTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - container.addView(titleTextView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); + containerView.addView(titleTextView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); titleTextView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { @@ -178,11 +168,10 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not shadow[0] = new View(context); shadow[0].setBackgroundResource(R.drawable.header_shadow); - ViewProxy.setAlpha(shadow[0], 0.0f); - shadow[0].clearAnimation(); + shadow[0].setAlpha(0.0f); shadow[0].setVisibility(View.INVISIBLE); shadow[0].setTag(1); - container.addView(shadow[0], LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 0)); + containerView.addView(shadow[0], LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 0)); gridView = new RecyclerListView(context) { @Override @@ -199,6 +188,7 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not super.requestLayout(); } }; + gridView.setTag(14); gridView.setLayoutManager(layoutManager = new GridLayoutManager(getContext(), 5)); gridView.setAdapter(adapter = new GridAdapter(context)); gridView.setVerticalScrollBarEnabled(false); @@ -221,15 +211,12 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not return StickerPreviewViewer.getInstance().onTouch(event, gridView, 0, stickersOnItemClickListener); } }); - if (Build.VERSION.SDK_INT >= 11) { - gridView.setOnScrollListener(new RecyclerView.OnScrollListener() { - @SuppressLint("NewApi") - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - updateLayout(); - } - }); - } + gridView.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + updateLayout(); + } + }); stickersOnItemClickListener = new RecyclerListView.OnItemClickListener() { @Override public void onItemClick(View view, int position) { @@ -258,14 +245,14 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not layoutParams.topMargin = scrollOffsetY; stickerPreviewLayout.setLayoutParams(layoutParams); stickerPreviewLayout.setVisibility(View.VISIBLE); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - animatorSet.playTogether(ObjectAnimatorProxy.ofFloat(stickerPreviewLayout, "alpha", 0.0f, 1.0f)); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(stickerPreviewLayout, "alpha", 0.0f, 1.0f)); animatorSet.setDuration(200); animatorSet.start(); } }; gridView.setOnItemClickListener(stickersOnItemClickListener); - container.addView(gridView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 48)); + containerView.addView(gridView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 48)); emptyView = new FrameLayout(context) { @Override @@ -276,7 +263,7 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not super.requestLayout(); } }; - container.addView(emptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 0, 0, 48)); + containerView.addView(emptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 0, 0, 48)); gridView.setEmptyView(emptyView); emptyView.setOnTouchListener(new View.OnTouchListener() { @Override @@ -290,10 +277,10 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not shadow[1] = new View(context); shadow[1].setBackgroundResource(R.drawable.header_shadow_reverse); - container.addView(shadow[1], LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.BOTTOM | Gravity.LEFT, 0, 0, 0, 48)); + containerView.addView(shadow[1], LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.BOTTOM | Gravity.LEFT, 0, 0, 0, 48)); pickerBottomLayout = new PickerBottomLayout(context, false); - container.addView(pickerBottomLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.BOTTOM)); + containerView.addView(pickerBottomLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.BOTTOM)); pickerBottomLayout.cancelButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); pickerBottomLayout.cancelButton.setTextColor(Theme.STICKERS_SHEET_CLOSE_TEXT_COLOR); pickerBottomLayout.cancelButton.setText(LocaleController.getString("Close", R.string.Close).toUpperCase()); @@ -310,7 +297,7 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not stickerPreviewLayout.setBackgroundColor(0xdfffffff); stickerPreviewLayout.setVisibility(View.GONE); stickerPreviewLayout.setSoundEffectsEnabled(false); - container.addView(stickerPreviewLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + containerView.addView(stickerPreviewLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); stickerPreviewLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -465,13 +452,18 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not } } + @Override + protected boolean canDismissWithSwipe() { + return false; + } + @SuppressLint("NewApi") private void updateLayout() { if (gridView.getChildCount() <= 0) { gridView.setTopGlowOffset(scrollOffsetY = gridView.getPaddingTop()); titleTextView.setTranslationY(scrollOffsetY); shadow[0].setTranslationY(scrollOffsetY); - container.invalidate(); + containerView.invalidate(); return; } View child = gridView.getChildAt(0); @@ -488,17 +480,17 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not gridView.setTopGlowOffset(scrollOffsetY = newOffset); titleTextView.setTranslationY(scrollOffsetY); shadow[0].setTranslationY(scrollOffsetY); - container.invalidate(); + containerView.invalidate(); } } private void hidePreview() { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - animatorSet.playTogether(ObjectAnimatorProxy.ofFloat(stickerPreviewLayout, "alpha", 0.0f)); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(stickerPreviewLayout, "alpha", 0.0f)); animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { stickerPreviewLayout.setVisibility(View.GONE); } }); @@ -514,14 +506,13 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not if (shadowAnimation[num] != null) { shadowAnimation[num].cancel(); } - shadowAnimation[num] = new AnimatorSetProxy(); - shadowAnimation[num].playTogether(ObjectAnimatorProxy.ofFloat(shadow[num], "alpha", show ? 1.0f : 0.0f)); + shadowAnimation[num] = new AnimatorSet(); + shadowAnimation[num].playTogether(ObjectAnimator.ofFloat(shadow[num], "alpha", show ? 1.0f : 0.0f)); shadowAnimation[num].setDuration(150); shadowAnimation[num].addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (shadowAnimation[num] != null && shadowAnimation[num].equals(animation)) { - shadow[num].clearAnimation(); if (!show) { shadow[num].setVisibility(View.INVISIBLE); } @@ -530,7 +521,7 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (shadowAnimation[num] != null && shadowAnimation[num].equals(animation)) { shadowAnimation[num] = null; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java index 217e6b368..4488dc4a8 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java @@ -16,6 +16,8 @@ package org.telegram.ui.Components; +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.PorterDuff; @@ -33,7 +35,6 @@ import android.widget.CompoundButton; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; import org.telegram.messenger.R; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; public class Switch extends CompoundButton { @@ -89,7 +90,7 @@ public class Switch extends CompoundButton { private int mSwitchRight; private int mSwitchBottom; - private ObjectAnimatorProxy mPositionAnimator; + private ObjectAnimator mPositionAnimator; private final Rect mTempRect = new Rect(); @@ -160,10 +161,6 @@ public class Switch extends CompoundButton { requestLayout(); } - public void setTrackResource(int resId) { - setTrackDrawable(getContext().getDrawable(resId)); - } - public Drawable getTrackDrawable() { return mTrackDrawable; } @@ -179,10 +176,6 @@ public class Switch extends CompoundButton { requestLayout(); } - public void setThumbResource(int resId) { - setThumbDrawable(getContext().getDrawable(resId)); - } - public Drawable getThumbDrawable() { return mThumbDrawable; } @@ -358,7 +351,7 @@ public class Switch extends CompoundButton { private void animateThumbToCheckedState(boolean newCheckedState) { final float targetPosition = newCheckedState ? 1 : 0; - mPositionAnimator = ObjectAnimatorProxy.ofFloatProxy(this, "thumbPosition", targetPosition); + mPositionAnimator = ObjectAnimator.ofFloat(this, "thumbPosition", targetPosition); mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION); mPositionAnimator.start(); } @@ -653,6 +646,7 @@ public class Switch extends CompoundButton { invalidate(); } + @SuppressLint("NewApi") @Override public void drawableHotspotChanged(float x, float y) { super.drawableHotspotChanged(x, y); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/TypingDotsDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/TypingDotsDrawable.java index 34a95d0c7..ecdb77c92 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/TypingDotsDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/TypingDotsDrawable.java @@ -15,6 +15,7 @@ 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 { @@ -91,15 +92,28 @@ public class TypingDotsDrawable extends Drawable { public void draw(Canvas canvas) { int y; if (isChat) { - y = AndroidUtilities.dp(8.3f) + getBounds().top; + y = AndroidUtilities.dp(8.5f) + getBounds().top; } else { - y = AndroidUtilities.dp(9) + getBounds().top; + y = AndroidUtilities.dp(9.3f) + getBounds().top; } canvas.drawCircle(AndroidUtilities.dp(3), y, scales[0] * AndroidUtilities.density, paint); canvas.drawCircle(AndroidUtilities.dp(9), y, scales[1] * AndroidUtilities.density, paint); canvas.drawCircle(AndroidUtilities.dp(15), y, scales[2] * AndroidUtilities.density, paint); + checkUpdate(); + } + + private void checkUpdate() { if (started) { - update(); + if (!NotificationCenter.getInstance().isAnimationInProgress()) { + update(); + } else { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + checkUpdate(); + } + }, 100); + } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/URLSpanUserMention.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/URLSpanUserMention.java new file mode 100644 index 000000000..60d8584d8 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/URLSpanUserMention.java @@ -0,0 +1,27 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.ui.Components; + +import android.text.TextPaint; + +import org.telegram.ui.ActionBar.Theme; + +public class URLSpanUserMention extends URLSpanNoUnderline { + + public URLSpanUserMention(String url) { + super(url); + } + + @Override + public void updateDrawState(TextPaint ds) { + super.updateDrawState(ds); + ds.setColor(Theme.MSG_LINK_TEXT_COLOR); + ds.setUnderlineText(false); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java new file mode 100644 index 000000000..2ffd7e31b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java @@ -0,0 +1,353 @@ +/* + * This is the source code of Telegram for Android v. 3.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2016. + */ + +package org.telegram.ui.Components; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaCodec; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.view.Surface; + +import org.telegram.messenger.exoplayer.DummyTrackRenderer; +import org.telegram.messenger.exoplayer.ExoPlaybackException; +import org.telegram.messenger.exoplayer.ExoPlayer; +import org.telegram.messenger.exoplayer.MediaCodecAudioTrackRenderer; +import org.telegram.messenger.exoplayer.MediaCodecSelector; +import org.telegram.messenger.exoplayer.MediaCodecTrackRenderer; +import org.telegram.messenger.exoplayer.MediaCodecVideoTrackRenderer; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.TrackRenderer; +import org.telegram.messenger.exoplayer.audio.AudioCapabilities; +import org.telegram.messenger.exoplayer.extractor.ExtractorSampleSource; +import org.telegram.messenger.exoplayer.upstream.Allocator; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DefaultAllocator; +import org.telegram.messenger.exoplayer.upstream.DefaultUriDataSource; +import org.telegram.messenger.exoplayer.util.PlayerControl; + +import java.util.concurrent.CopyOnWriteArrayList; + +@SuppressLint("NewApi") +public class VideoPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRenderer.EventListener { + + public interface RendererBuilder { + void buildRenderers(VideoPlayer player); + void cancel(); + } + + public interface Listener { + void onStateChanged(boolean playWhenReady, int playbackState); + void onError(Exception e); + void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio); + } + + public static class ExtractorRendererBuilder implements RendererBuilder { + + private static final int BUFFER_SEGMENT_SIZE = 256 * 1024; + private static final int BUFFER_SEGMENT_COUNT = 256; + + private final Context context; + private final String userAgent; + private final Uri uri; + + public ExtractorRendererBuilder(Context context, String userAgent, Uri uri) { + this.context = context; + this.userAgent = userAgent; + this.uri = uri; + } + + @Override + public void buildRenderers(VideoPlayer player) { + Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE); + Handler mainHandler = player.getMainHandler(); + + TrackRenderer[] renderers = new TrackRenderer[RENDERER_COUNT]; + DataSource dataSource = new DefaultUriDataSource(context, userAgent); + ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE, mainHandler, null, 0); + renderers[TYPE_VIDEO] = new MediaCodecVideoTrackRenderer(context, sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, mainHandler, player, 50) { + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) throws ExoPlaybackException { + super.doSomeWork(positionUs, elapsedRealtimeUs, sourceIsReady); + } + }; + renderers[TYPE_AUDIO] = new MediaCodecAudioTrackRenderer(sampleSource, MediaCodecSelector.DEFAULT, null, true, mainHandler, null, AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); + player.onRenderers(renderers); + } + + @Override + public void cancel() { + + } + } + + public static final int STATE_IDLE = ExoPlayer.STATE_IDLE; + public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING; + public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING; + public static final int STATE_READY = ExoPlayer.STATE_READY; + public static final int STATE_ENDED = ExoPlayer.STATE_ENDED; + public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED; + public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT; + + public static final int RENDERER_COUNT = 2; + public static final int TYPE_VIDEO = 0; + public static final int TYPE_AUDIO = 1; + + private static final int RENDERER_BUILDING_STATE_IDLE = 1; + private static final int RENDERER_BUILDING_STATE_BUILDING = 2; + private static final int RENDERER_BUILDING_STATE_BUILT = 3; + + private final RendererBuilder rendererBuilder; + private final ExoPlayer player; + private final Handler mainHandler; + private final CopyOnWriteArrayList listeners; + private final PlayerControl playerControl; + + private int rendererBuildingState; + private int lastReportedPlaybackState; + private boolean lastReportedPlayWhenReady; + + private Surface surface; + private TrackRenderer videoRenderer; + private int videoTrackToRestore; + + private boolean backgrounded; + + public VideoPlayer(RendererBuilder rendererBuilder) { + this.rendererBuilder = rendererBuilder; + player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000); + player.addListener(this); + playerControl = new PlayerControl(player); + mainHandler = new Handler(); + listeners = new CopyOnWriteArrayList<>(); + lastReportedPlaybackState = STATE_IDLE; + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + } + + public PlayerControl getPlayerControl() { + return playerControl; + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public void setSurface(Surface surface) { + this.surface = surface; + pushSurface(false); + } + + public Surface getSurface() { + return surface; + } + + public void blockingClearSurface() { + surface = null; + pushSurface(true); + } + + public int getTrackCount(int type) { + return player.getTrackCount(type); + } + + public MediaFormat getTrackFormat(int type, int index) { + return player.getTrackFormat(type, index); + } + + public int getSelectedTrack(int type) { + return player.getSelectedTrack(type); + } + + public void setSelectedTrack(int type, int index) { + player.setSelectedTrack(type, index); + } + + public boolean getBackgrounded() { + return backgrounded; + } + + public void setBackgrounded(boolean backgrounded) { + if (this.backgrounded == backgrounded) { + return; + } + this.backgrounded = backgrounded; + if (backgrounded) { + videoTrackToRestore = getSelectedTrack(TYPE_VIDEO); + setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED); + blockingClearSurface(); + } else { + setSelectedTrack(TYPE_VIDEO, videoTrackToRestore); + } + } + + public void prepare() { + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { + player.stop(); + } + rendererBuilder.cancel(); + videoRenderer = null; + rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; + maybeReportPlayerState(); + rendererBuilder.buildRenderers(this); + } + + void onRenderers(TrackRenderer[] renderers) { + for (int i = 0; i < RENDERER_COUNT; i++) { + if (renderers[i] == null) { + renderers[i] = new DummyTrackRenderer(); + } + } + videoRenderer = renderers[TYPE_VIDEO]; + pushSurface(false); + player.prepare(renderers); + rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; + } + + void onRenderersError(Exception e) { + for (Listener listener : listeners) { + listener.onError(e); + } + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + maybeReportPlayerState(); + } + + public void setPlayWhenReady(boolean playWhenReady) { + player.setPlayWhenReady(playWhenReady); + } + + public void seekTo(long positionMs) { + player.seekTo(positionMs); + } + + public void release() { + rendererBuilder.cancel(); + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + surface = null; + player.release(); + } + + public int getPlaybackState() { + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { + return STATE_PREPARING; + } + int playerState = player.getPlaybackState(); + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) { + return STATE_PREPARING; + } + return playerState; + } + + public long getCurrentPosition() { + return player.getCurrentPosition(); + } + + public long getDuration() { + return player.getDuration(); + } + + public int getBufferedPercentage() { + return player.getBufferedPercentage(); + } + + public boolean getPlayWhenReady() { + return player.getPlayWhenReady(); + } + + Looper getPlaybackLooper() { + return player.getPlaybackLooper(); + } + + Handler getMainHandler() { + return mainHandler; + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int state) { + maybeReportPlayerState(); + } + + @Override + public void onPlayerError(ExoPlaybackException exception) { + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + for (Listener listener : listeners) { + listener.onError(exception); + } + } + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + for (Listener listener : listeners) { + listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + } + } + + @Override + public void onDroppedFrames(int count, long elapsed) { + + } + + @Override + public void onDecoderInitializationError(MediaCodecTrackRenderer.DecoderInitializationException e) { + for (Listener listener : listeners) { + listener.onError(e); + } + } + + @Override + public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { + + } + + @Override + public void onPlayWhenReadyCommitted() { + // Do nothing. + } + + @Override + public void onDrawnToSurface(Surface surface) { + // Do nothing. + } + + @Override + public void onCryptoError(MediaCodec.CryptoException e) { + for (Listener listener : listeners) { + listener.onError(e); + } + } + + private void maybeReportPlayerState() { + boolean playWhenReady = player.getPlayWhenReady(); + int playbackState = getPlaybackState(); + if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) { + for (Listener listener : listeners) { + listener.onStateChanged(playWhenReady, playbackState); + } + lastReportedPlayWhenReady = playWhenReady; + lastReportedPlaybackState = playbackState; + } + } + + private void pushSurface(boolean blockForSurfacePush) { + if (videoRenderer == null) { + return; + } + + if (blockForSurfacePush) { + player.blockingSendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + } else { + player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + } + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java index 539952d42..3a485c185 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java @@ -223,12 +223,7 @@ public class VideoTimelineView extends View { } } }; - - if (android.os.Build.VERSION.SDK_INT >= 11) { - currentTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, frameNum, null, null); - } else { - currentTask.execute(frameNum, null, null); - } + currentTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, frameNum, null, null); } public void destroy() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/WebFrameLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/WebFrameLayout.java index 41c99a782..9bc32fc42 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/WebFrameLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/WebFrameLayout.java @@ -113,9 +113,6 @@ public class WebFrameLayout extends FrameLayout { public WebFrameLayout(Context context, final BottomSheet parentDialog, String title, String descripton, String originalUrl, final String url, int w, int h) { super(context); embedUrl = url; - if (embedUrl.toLowerCase().contains("youtube")) { - //embedUrl += "&enablejsapi=1"; - } hasDescription = descripton != null && descripton.length() > 0; openUrl = originalUrl; width = w; @@ -136,14 +133,6 @@ public class WebFrameLayout extends FrameLayout { dialog.getContainer().addView(fullscreenVideoContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); fullscreenVideoContainer.setVisibility(INVISIBLE); - /*LinearLayout linearLayout = new LinearLayout(context); - linearLayout.setOrientation(LinearLayout.HORIZONTAL); - addView(linearLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 32, Gravity.LEFT | Gravity.TOP)); - - - - */ - webView = new WebView(context); webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setDomStorageEnabled(true); @@ -288,14 +277,9 @@ public class WebFrameLayout extends FrameLayout { @Override public void onClick(View v) { try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(openUrl); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", openUrl); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", openUrl); + clipboard.setPrimaryClip(clip); } catch (Exception e) { FileLog.e("tmessages", e); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java index 4971ca007..f449d7ae5 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java @@ -15,7 +15,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.text.Editable; import android.text.InputType; @@ -176,9 +175,7 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter searchWas = false; listView.setAdapter(listViewAdapter); listViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); emptyTextView.setText(LocaleController.getString("NoContacts", R.string.NoContacts)); @@ -195,9 +192,7 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter if (listView != null) { listView.setAdapter(searchListViewAdapter); searchListViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(false); - } + listView.setFastScrollAlwaysVisible(false); listView.setFastScrollEnabled(false); listView.setVerticalScrollBarEnabled(true); } @@ -259,10 +254,8 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter listView.setFastScrollEnabled(true); listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); listView.setAdapter(listViewAdapter); - if (Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setFastScrollAlwaysVisible(true); + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); ((FrameLayout) fragmentView).addView(listView); layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams(); layoutParams.width = LayoutHelper.MATCH_PARENT; @@ -447,9 +440,6 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter if (!user.bot && needForwardCount) { message = String.format("%s\n\n%s", message, LocaleController.getString("AddToTheGroupForwardCount", R.string.AddToTheGroupForwardCount)); editText = new EditText(getParentActivity()); - if (android.os.Build.VERSION.SDK_INT < 11) { - editText.setBackgroundResource(android.R.drawable.editbox_background_normal); - } editText.setTextSize(18); editText.setText("50"); editText.setGravity(Gravity.CENTER); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/CountrySelectActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/CountrySelectActivity.java index c7e81ac11..da75104f5 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/CountrySelectActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/CountrySelectActivity.java @@ -9,7 +9,6 @@ package org.telegram.ui; import android.content.Context; -import android.os.Build; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; @@ -88,9 +87,7 @@ public class CountrySelectActivity extends BaseFragment { searching = false; searchWas = false; listView.setAdapter(listViewAdapter); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); @@ -105,9 +102,7 @@ public class CountrySelectActivity extends BaseFragment { searchWas = true; if (listView != null) { listView.setAdapter(searchListViewAdapter); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(false); - } + listView.setFastScrollAlwaysVisible(false); listView.setFastScrollEnabled(false); listView.setVerticalScrollBarEnabled(true); } @@ -171,10 +166,8 @@ public class CountrySelectActivity extends BaseFragment { listView.setFastScrollEnabled(true); listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); listView.setAdapter(listViewAdapter); - if (Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setFastScrollAlwaysVisible(true); + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); ((FrameLayout) fragmentView).addView(listView); layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams(); layoutParams.width = LayoutHelper.MATCH_PARENT; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java index 391d2c52e..f2d5bc0b4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java @@ -41,10 +41,12 @@ import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.BuildVars; import org.telegram.messenger.ChatObject; +import org.telegram.messenger.DialogObject; import org.telegram.messenger.ImageLoader; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.UserObject; +import org.telegram.messenger.query.SearchQuery; import org.telegram.messenger.support.widget.LinearLayoutManager; import org.telegram.messenger.support.widget.RecyclerView; import org.telegram.messenger.FileLog; @@ -58,8 +60,7 @@ import org.telegram.messenger.UserConfig; import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.Adapters.DialogsAdapter; import org.telegram.ui.Adapters.DialogsSearchAdapter; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.ui.Cells.HintDialogCell; import org.telegram.ui.Cells.ProfileSearchCell; import org.telegram.ui.Cells.UserCell; import org.telegram.ui.Cells.DialogCell; @@ -148,6 +149,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. NotificationCenter.getInstance().addObserver(this, NotificationCenter.didSetPasscode); NotificationCenter.getInstance().addObserver(this, NotificationCenter.needReloadRecentDialogsSearch); NotificationCenter.getInstance().addObserver(this, NotificationCenter.didLoadedReplyMessages); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.reloadHints); } @@ -177,6 +179,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didSetPasscode); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.needReloadRecentDialogsSearch); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didLoadedReplyMessages); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.reloadHints); } delegate = null; } @@ -186,7 +189,12 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. searching = false; searchWas = false; - Theme.loadRecources(context); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + Theme.loadRecources(context); + } + }); ActionBarMenu menu = actionBar.createMenu(); if (!onlySelect && searchString == null) { @@ -235,7 +243,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. if (!onlySelect) { floatingButton.setVisibility(View.VISIBLE); floatingHidden = true; - ViewProxy.setTranslationY(floatingButton, AndroidUtilities.dp(100)); + floatingButton.setTranslationY(AndroidUtilities.dp(100)); hideFloatingButton(false); } if (listView.getAdapter() != dialogsAdapter) { @@ -314,6 +322,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. listView.setItemAnimator(null); listView.setInstantClick(true); listView.setLayoutAnimation(null); + listView.setTag(4); layoutManager = new LinearLayoutManager(context) { @Override public boolean supportsPredictiveItemAnimations() { @@ -322,9 +331,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. }; layoutManager.setOrientation(LinearLayoutManager.VERTICAL); listView.setLayoutManager(layoutManager); - if (Build.VERSION.SDK_INT >= 11) { - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @Override @@ -336,7 +343,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. int message_id = 0; RecyclerView.Adapter adapter = listView.getAdapter(); if (adapter == dialogsAdapter) { - TLRPC.Dialog dialog = dialogsAdapter.getItem(position); + TLRPC.TL_dialog dialog = dialogsAdapter.getItem(position); if (dialog == null) { return; } @@ -473,8 +480,8 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. } return false; } - TLRPC.Dialog dialog; - ArrayList dialogs = getDialogsArray(); + TLRPC.TL_dialog dialog; + ArrayList dialogs = getDialogsArray(); if (position < 0 || position >= dialogs.size()) { return false; } @@ -485,7 +492,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. int lower_id = (int) selectedDialog; int high_id = (int) (selectedDialog >> 32); - if (dialog instanceof TLRPC.TL_dialogChannel) { + if (DialogObject.isChannel(dialog)) { final TLRPC.Chat chat = MessagesController.getInstance().getChat(-lower_id); CharSequence items[]; if (chat != null && chat.megagroup) { @@ -732,7 +739,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. type = 1; } dialogsSearchAdapter = new DialogsSearchAdapter(context, type, dialogsType); - dialogsSearchAdapter.setDelegate(new DialogsSearchAdapter.MessagesActivitySearchAdapterDelegate() { + dialogsSearchAdapter.setDelegate(new DialogsSearchAdapter.DialogsSearchAdapterDelegate() { @Override public void searchStateChanged(boolean search) { if (searching && searchWas && searchEmptyView != null) { @@ -743,6 +750,61 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. } } } + + @Override + public void didPressedOnSubDialog(int did) { + if (onlySelect) { + didSelectResult(did, true, false); + } else { + Bundle args = new Bundle(); + if (did > 0) { + args.putInt("user_id", did); + } else { + args.putInt("chat_id", -did); + } + if (actionBar != null) { + actionBar.closeSearchField(); + } + if (AndroidUtilities.isTablet()) { + if (dialogsAdapter != null) { + dialogsAdapter.setOpenedDialogId(openedDialogId = did); + updateVisibleRows(MessagesController.UPDATE_MASK_SELECT_DIALOG); + } + } + if (searchString != null) { + if (MessagesController.checkCanOpenChat(args, DialogsActivity.this)) { + NotificationCenter.getInstance().postNotificationName(NotificationCenter.closeChats); + presentFragment(new ChatActivity(args)); + } + } else { + if (MessagesController.checkCanOpenChat(args, DialogsActivity.this)) { + presentFragment(new ChatActivity(args)); + } + } + } + } + + @Override + public void needRemoveHint(final int did) { + if (getParentActivity() == null) { + return; + } + TLRPC.User user = MessagesController.getInstance().getUser(did); + if (user == null) { + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setMessage(LocaleController.formatString("ChatHintsDelete", R.string.ChatHintsDelete, ContactsController.formatName(user.first_name, user.last_name))); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + SearchQuery.removePeer(did); + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + showDialog(builder.create()); + } }); if (MessagesController.getInstance().loadingDialogs && MessagesController.getInstance().dialogs.isEmpty()) { @@ -834,7 +896,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. floatingButton.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { - ViewProxy.setTranslationY(floatingButton, floatingHidden ? AndroidUtilities.dp(100) : 0); + floatingButton.setTranslationY(floatingHidden ? AndroidUtilities.dp(100) : 0); floatingButton.setClickable(!floatingHidden); if (floatingButton != null) { if (Build.VERSION.SDK_INT < 16) { @@ -939,10 +1001,14 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. } } else if (id == NotificationCenter.didLoadedReplyMessages) { updateVisibleRows(0); + } else if (id == NotificationCenter.reloadHints) { + if (dialogsSearchAdapter != null) { + dialogsSearchAdapter.notifyDataSetChanged(); + } } } - private ArrayList getDialogsArray() { + private ArrayList getDialogsArray() { if (dialogsType == 0) { return MessagesController.getInstance().dialogs; } else if (dialogsType == 1) { @@ -974,7 +1040,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. return; } floatingHidden = hide; - ObjectAnimatorProxy animator = ObjectAnimatorProxy.ofFloatProxy(floatingButton, "translationY", floatingHidden ? AndroidUtilities.dp(100) : 0).setDuration(300); + ObjectAnimator animator = ObjectAnimator.ofFloat(floatingButton, "translationY", floatingHidden ? AndroidUtilities.dp(100) : 0).setDuration(300); animator.setInterpolator(floatingInterpolator); floatingButton.setClickable(!hide); animator.start(); @@ -1007,12 +1073,21 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. ((UserCell) child).update(mask); } else if (child instanceof ProfileSearchCell) { ((ProfileSearchCell) child).update(mask); + } else if (child instanceof RecyclerListView) { + RecyclerListView innerListView = (RecyclerListView) child; + int count2 = innerListView.getChildCount(); + for (int b = 0; b < count2; b++) { + View child2 = innerListView.getChildAt(b); + if (child2 instanceof HintDialogCell) { + ((HintDialogCell) child2).checkUnreadCounter(mask); + } + } } } } - public void setDelegate(DialogsActivityDelegate delegate) { - this.delegate = delegate; + public void setDelegate(DialogsActivityDelegate dialogsActivityDelegate) { + delegate = dialogsActivityDelegate; } public void setSearchString(String string) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DocumentSelectActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DocumentSelectActivity.java index 1e93dd0a3..7a36f9e21 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/DocumentSelectActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/DocumentSelectActivity.java @@ -8,6 +8,9 @@ package org.telegram.ui; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.BroadcastReceiver; @@ -15,7 +18,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; -import android.os.Build; import android.os.Environment; import android.os.StatFs; import android.view.MotionEvent; @@ -37,8 +39,6 @@ import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Adapters.BaseFragmentAdapter; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.Cells.SharedDocumentCell; import org.telegram.ui.Components.LayoutHelper; @@ -238,18 +238,16 @@ public class DocumentSelectActivity extends BaseFragment { } selectedFiles.put(file.toString(), item); selectedMessagesCountTextView.setNumber(1, false); - if (Build.VERSION.SDK_INT >= 11) { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - ArrayList animators = new ArrayList<>(); - for (int a = 0; a < actionModeViews.size(); a++) { - View view2 = actionModeViews.get(a); - AndroidUtilities.clearDrawableAnimation(view2); - animators.add(ObjectAnimatorProxy.ofFloat(view2, "scaleY", 0.1f, 1.0f)); - } - animatorSet.playTogether(animators); - animatorSet.setDuration(250); - animatorSet.start(); + AnimatorSet animatorSet = new AnimatorSet(); + ArrayList animators = new ArrayList<>(); + for (int a = 0; a < actionModeViews.size(); a++) { + View view2 = actionModeViews.get(a); + AndroidUtilities.clearDrawableAnimation(view2); + animators.add(ObjectAnimator.ofFloat(view2, "scaleY", 0.1f, 1.0f)); } + animatorSet.playTogether(animators); + animatorSet.setDuration(250); + animatorSet.start(); scrolling = false; if (view instanceof SharedDocumentCell) { ((SharedDocumentCell) view).setChecked(true, true); @@ -512,7 +510,7 @@ public class DocumentSelectActivity extends BaseFragment { HashSet paths = new HashSet<>(); String defaultPath = Environment.getExternalStorageDirectory().getPath(); - boolean isDefaultPathRemovable = Build.VERSION.SDK_INT >= 9 && Environment.isExternalStorageRemovable(); + boolean isDefaultPathRemovable = Environment.isExternalStorageRemovable(); String defaultPathState = Environment.getExternalStorageState(); if (defaultPathState.equals(Environment.MEDIA_MOUNTED) || defaultPathState.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { ListItem ext = new ListItem(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java index 469f469ff..5d0506f43 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java @@ -14,7 +14,6 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; -import android.os.Build; import android.os.Bundle; import android.text.Editable; import android.text.InputType; @@ -216,9 +215,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen } else { userSelectEditText.setHint(LocaleController.getString("SendMessageTo", R.string.SendMessageTo)); } - if (Build.VERSION.SDK_INT >= 11) { - userSelectEditText.setTextIsSelectable(false); - } + userSelectEditText.setTextIsSelectable(false); userSelectEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { @@ -275,9 +272,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen if (listView != null) { listView.setAdapter(searchListViewAdapter); searchListViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(false); - } + listView.setFastScrollAlwaysVisible(false); listView.setFastScrollEnabled(false); listView.setVerticalScrollBarEnabled(true); } @@ -291,9 +286,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen searchWas = false; listView.setAdapter(listViewAdapter); listViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); emptyTextView.setText(LocaleController.getString("NoContacts", R.string.NoContacts)); @@ -332,10 +325,8 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen listView.setFastScrollEnabled(true); listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); listView.setAdapter(listViewAdapter); - if (Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setFastScrollAlwaysVisible(true); + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); linearLayout.addView(listView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override @@ -407,9 +398,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen searchWas = false; listView.setAdapter(listViewAdapter); listViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); emptyTextView.setText(LocaleController.getString("NoContacts", R.string.NoContacts)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java index 4cde07fd7..3c3cef490 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java @@ -47,7 +47,6 @@ import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.AvatarUpdater; import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.ActionBar.BaseFragment; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import java.util.ArrayList; @@ -202,7 +201,7 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati LinearLayout linearLayout = (LinearLayout) fragmentView; linearLayout.setOrientation(LinearLayout.VERTICAL); - FrameLayout frameLayout = new FrameLayoutFixed(context); + FrameLayout frameLayout = new FrameLayout(context); linearLayout.addView(frameLayout); LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) frameLayout.getLayoutParams(); layoutParams.width = LayoutHelper.MATCH_PARENT; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/GroupInviteActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/GroupInviteActivity.java index 09fea806e..a3ec2c9ad 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/GroupInviteActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/GroupInviteActivity.java @@ -12,7 +12,6 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.os.Build; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -132,14 +131,9 @@ public class GroupInviteActivity extends BaseFragment implements NotificationCen return; } try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(invite.link); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); + clipboard.setPrimaryClip(clip); Toast.makeText(getParentActivity(), LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); } catch (Exception e) { FileLog.e("tmessages", e); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java index 637a57c22..5546b5403 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java @@ -167,7 +167,6 @@ public class IntroActivity extends Activity { fadeinImage.clearAnimation(); fadeoutImage.clearAnimation(); - Animation outAnimation = AnimationUtils.loadAnimation(IntroActivity.this, R.anim.icon_anim_fade_out); outAnimation.setAnimationListener(new Animation.AnimationListener() { @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java index 1a3063b11..f2ca0620e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java @@ -26,6 +26,7 @@ import android.os.Bundle; import android.os.Parcelable; import android.provider.ContactsContract; import android.support.annotation.NonNull; +import android.text.TextUtils; import android.view.ActionMode; import android.view.KeyEvent; import android.view.MotionEvent; @@ -59,6 +60,7 @@ import org.telegram.messenger.LocaleController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; import org.telegram.messenger.browser.Browser; +import org.telegram.messenger.query.DraftQuery; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.RequestDelegate; import org.telegram.tgnet.TLObject; @@ -283,7 +285,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa drawerLayoutContainer.setDrawerLayout(listView); FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams(); Point screenSize = AndroidUtilities.getRealScreenSize(); - layoutParams.width = AndroidUtilities.isTablet() ? AndroidUtilities.dp(320) : Math.min(screenSize.x, screenSize.y) - AndroidUtilities.dp(56); + layoutParams.width = AndroidUtilities.isTablet() ? AndroidUtilities.dp(320) : Math.min(AndroidUtilities.dp(320), Math.min(screenSize.x, screenSize.y) - AndroidUtilities.dp(56)); layoutParams.height = LayoutHelper.MATCH_PARENT; listView.setLayoutParams(layoutParams); @@ -364,9 +366,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa NotificationCenter.getInstance().addObserver(this, NotificationCenter.didUpdatedConnectionState); NotificationCenter.getInstance().addObserver(this, NotificationCenter.needShowAlert); NotificationCenter.getInstance().addObserver(this, NotificationCenter.wasUnableToFindCurrentLocation); - if (Build.VERSION.SDK_INT < 14) { - NotificationCenter.getInstance().addObserver(this, NotificationCenter.screenStateChanged); - } if (actionBarLayout.fragmentsStack.isEmpty()) { if (!UserConfig.isClientActivated()) { @@ -500,6 +499,11 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa }); } + private class VcardData { + String name; + ArrayList phones = new ArrayList<>(); + } + private boolean handleIntent(Intent intent, boolean isNew, boolean restore, boolean fromPassword) { int flags = intent.getFlags(); if (!fromPassword && (AndroidUtilities.needShowPasscode(true) || UserConfig.isWaitingForPasscodeEnter)) { @@ -539,19 +543,28 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa if (uri != null) { ContentResolver cr = getContentResolver(); InputStream stream = cr.openInputStream(uri); + ArrayList vcardDatas = new ArrayList<>(); + VcardData currentData = null; - String name = null; - String nameEncoding = null; - String nameCharset = null; - ArrayList phones = new ArrayList<>(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream, "UTF-8")); String line; while ((line = bufferedReader.readLine()) != null) { + FileLog.e("tmessages", line); String[] args = line.split(":"); if (args.length != 2) { continue; } - if (args[0].startsWith("FN")) { + if (args[0].equals("BEGIN") && args[1].equals("VCARD")) { + vcardDatas.add(currentData = new VcardData()); + } else if (args[0].equals("END") && args[1].equals("VCARD")) { + currentData = null; + } + if (currentData == null) { + continue; + } + if (args[0].startsWith("FN") || args[0].startsWith("ORG") && TextUtils.isEmpty(currentData.name)) { + String nameEncoding = null; + String nameCharset = null; String[] params = args[0].split(";"); for (String param : params) { String[] args2 = param.split("="); @@ -564,28 +577,28 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa nameEncoding = args2[1]; } } - name = args[1]; + currentData.name = args[1]; if (nameEncoding != null && nameEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { - while (name.endsWith("=") && nameEncoding != null) { - name = name.substring(0, name.length() - 1); + while (currentData.name.endsWith("=") && nameEncoding != null) { + currentData.name = currentData.name.substring(0, currentData.name.length() - 1); line = bufferedReader.readLine(); if (line == null) { break; } - name += line; + currentData.name += line; } - byte[] bytes = AndroidUtilities.decodeQuotedPrintable(name.getBytes()); + byte[] bytes = AndroidUtilities.decodeQuotedPrintable(currentData.name.getBytes()); if (bytes != null && bytes.length != 0) { String decodedName = new String(bytes, nameCharset); if (decodedName != null) { - name = decodedName; + currentData.name = decodedName; } } } } else if (args[0].startsWith("TEL")) { String phone = PhoneFormat.stripExceptNumbers(args[1], true); if (phone.length() > 0) { - phones.add(phone); + currentData.phones.add(phone); } } } @@ -595,15 +608,22 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } catch (Exception e) { FileLog.e("tmessages", e); } - if (name != null && !phones.isEmpty()) { - contactsToSend = new ArrayList<>(); - for (String phone : phones) { - TLRPC.User user = new TLRPC.TL_userContact_old2(); - user.phone = phone; - user.first_name = name; - user.last_name = ""; - user.id = 0; - contactsToSend.add(user); + for (int a = 0; a < vcardDatas.size(); a++) { + VcardData vcardData = vcardDatas.get(a); + if (vcardData.name != null && !vcardData.phones.isEmpty()) { + if (contactsToSend == null) { + contactsToSend = new ArrayList<>(); + } + + for (int b = 0; b < vcardData.phones.size(); b++) { + String phone = vcardData.phones.get(b); + TLRPC.User user = new TLRPC.TL_userContact_old2(); + user.phone = phone; + user.first_name = vcardData.name; + user.last_name = ""; + user.id = 0; + contactsToSend.add(user); + } } } } else { @@ -647,7 +667,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } Uri uri = (Uri) parcelable; if (uri != null) { - if (isInternalUri(uri)) { + if (AndroidUtilities.isInternalUri(uri)) { error = true; } } @@ -681,11 +701,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa documentsMimeType = type; } } - if (sendingText != null) { - if (sendingText.contains("WhatsApp")) { //remove unnecessary caption 'sent from WhatsApp' from photos forwarded from WhatsApp - sendingText = null; - } - } } } else if (sendingText == null && sendingLocation == null) { error = true; @@ -707,7 +722,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } Uri uri = (Uri) parcelable; if (uri != null) { - if (isInternalUri(uri)) { + if (AndroidUtilities.isInternalUri(uri)) { uris.remove(a); a--; } @@ -823,6 +838,10 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa username = data.getQueryParameter("domain"); botUser = data.getQueryParameter("start"); botChat = data.getQueryParameter("startgroup"); + messageId = Utilities.parseInt(data.getQueryParameter("post")); + if (messageId == 0) { + messageId = null; + } } else if (url.startsWith("tg:join") || url.startsWith("tg://join")) { url = url.replace("tg:join", "tg://telegram.org").replace("tg://join", "tg://telegram.org"); data = Uri.parse(url); @@ -1040,21 +1059,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa return false; } - private boolean isInternalUri(Uri uri) { - String pathString = uri.getPath(); - if (pathString == null) { - return false; - } - while (true) { - String newPath = Utilities.readlink(pathString); - if (newPath == null || newPath.equals(pathString)) { - break; - } - pathString = newPath; - } - return pathString != null && pathString.toLowerCase().contains("/data/data/" + getPackageName() + "/files"); - } - 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 int state) { final ProgressDialog progressDialog = new ProgressDialog(this); progressDialog.setMessage(LocaleController.getString("Loading", R.string.Loading)); @@ -1306,10 +1310,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } if (MessagesController.checkCanOpenChat(args, fragment)) { NotificationCenter.getInstance().postNotificationName(NotificationCenter.closeChats); - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("dialog_" + did, message); - editor.commit(); + DraftQuery.saveDraft(did, message, null, null, true); actionBarLayout.presentFragment(new ChatActivity(args), true, false, true); } } @@ -1409,26 +1410,31 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } } else { actionBarLayout.presentFragment(fragment, dialogsFragment != null, dialogsFragment == null, true); - SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, null, true); + SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, null); } } else { actionBarLayout.presentFragment(fragment, dialogsFragment != null, dialogsFragment == null, true); - if (sendingText != null) { - SendMessagesHelper.prepareSendingText(sendingText, dialog_id, true); - } - if (sendingLocation != null) { - SendMessagesHelper.prepareSendingLocation(sendingLocation, dialog_id, true); - } if (photoPathsArray != null) { - SendMessagesHelper.prepareSendingPhotos(null, photoPathsArray, dialog_id, null, null, true); + ArrayList captions = null; + if (sendingText != null && photoPathsArray.size() == 1) { + captions = new ArrayList<>(); + captions.add(sendingText); + sendingText = null; + } + SendMessagesHelper.prepareSendingPhotos(null, photoPathsArray, dialog_id, null, captions); } + + if (sendingText != null) { + SendMessagesHelper.prepareSendingText(sendingText, dialog_id); + } + if (documentsPathsArray != null || documentsUrisArray != null) { - SendMessagesHelper.prepareSendingDocuments(documentsPathsArray, documentsOriginalPathsArray, documentsUrisArray, documentsMimeType, dialog_id, null, true); + SendMessagesHelper.prepareSendingDocuments(documentsPathsArray, documentsOriginalPathsArray, documentsUrisArray, documentsMimeType, dialog_id, null); } if (contactsToSend != null && !contactsToSend.isEmpty()) { for (TLRPC.User user : contactsToSend) { - SendMessagesHelper.getInstance().sendMessage(user, dialog_id, null, true, null, null); + SendMessagesHelper.getInstance().sendMessage(user, dialog_id, null, null, null); } } } @@ -1458,9 +1464,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didUpdatedConnectionState); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.needShowAlert); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.wasUnableToFindCurrentLocation); - if (Build.VERSION.SDK_INT < 14) { - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.screenStateChanged); - } } public void presentFragment(BaseFragment fragment) { @@ -1669,6 +1672,9 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa passcodeView.onPause(); } ConnectionsManager.getInstance().setAppPaused(true, false); + if (PhotoViewer.getInstance().isVisible()) { + PhotoViewer.getInstance().onPause(); + } } @Override @@ -1779,14 +1785,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } } else if (id == NotificationCenter.mainUserInfoChanged) { drawerLayoutAdapter.notifyDataSetChanged(); - } else if (id == NotificationCenter.screenStateChanged) { - if (!ApplicationLoader.mainInterfacePaused) { - if (!ApplicationLoader.isScreenOn) { - onPasscodePause(); - } else { - onPasscodeResume(); - } - } } else if (id == NotificationCenter.needShowAlert) { final Integer reason = (Integer) args[0]; AlertDialog.Builder builder = new AlertDialog.Builder(this); @@ -1836,7 +1834,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa public void didSelectLocation(TLRPC.MessageMedia location) { for (HashMap.Entry entry : waitingForLocation.entrySet()) { MessageObject messageObject = entry.getValue(); - SendMessagesHelper.getInstance().sendMessage(location, messageObject.getDialogId(), messageObject, false, null, null); + SendMessagesHelper.getInstance().sendMessage(location, messageObject.getDialogId(), messageObject, null, null); } } }); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java index 0ddae7e2f..dce7b5f89 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java @@ -315,7 +315,7 @@ public class LoginActivity extends BaseFragment { return false; } - public void needShowAlert(String title, String text) { + private void needShowAlert(String title, String text) { if (text == null || getParentActivity() == null) { return; } @@ -326,7 +326,36 @@ public class LoginActivity extends BaseFragment { showDialog(builder.create()); } - public void needShowProgress() { + private void needShowInvalidAlert(final String phoneNumber) { + if (getParentActivity() == null) { + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setMessage(LocaleController.getString("InvalidPhoneNumber", R.string.InvalidPhoneNumber)); + builder.setNeutralButton(LocaleController.getString("BotHelp", R.string.BotHelp), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + PackageInfo pInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0); + String version = String.format(Locale.US, "%s (%d)", pInfo.versionName, pInfo.versionCode); + + Intent mailer = new Intent(Intent.ACTION_SEND); + mailer.setType("message/rfc822"); + mailer.putExtra(Intent.EXTRA_EMAIL, new String[]{"login@stel.com"}); + mailer.putExtra(Intent.EXTRA_SUBJECT, "Invalid phone number: " + phoneNumber); + mailer.putExtra(Intent.EXTRA_TEXT, "I'm trying to use my mobile phone number: " + phoneNumber + "\nBut Telegram says it's invalid. Please help.\n\nApp version: " + version + "\nOS version: SDK " + Build.VERSION.SDK_INT + "\nDevice Name: " + Build.MANUFACTURER + Build.MODEL + "\nLocale: " + Locale.getDefault()); + getParentActivity().startActivity(Intent.createChooser(mailer, "Send email...")); + } catch (Exception e) { + needShowAlert(LocaleController.getString("AppName", R.string.AppName), LocaleController.getString("NoMailInstalled", R.string.NoMailInstalled)); + } + } + }); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); + showDialog(builder.create()); + } + + private void needShowProgress() { if (getParentActivity() == null || getParentActivity().isFinishing() || progressDialog != null) { return; } @@ -358,7 +387,7 @@ public class LoginActivity extends BaseFragment { } doneButton.setVisibility(View.VISIBLE); } - if (android.os.Build.VERSION.SDK_INT > 13 && animated) { + if (animated) { final SlideView outView = views[currentViewNum]; final SlideView newView = views[page]; currentViewNum = page; @@ -899,17 +928,13 @@ public class LoginActivity extends BaseFragment { return; } - ConnectionsManager.getInstance().cleanUp(); - TLRPC.TL_auth_sendCode req = new TLRPC.TL_auth_sendCode(); + ConnectionsManager.getInstance().cleanup(); + final TLRPC.TL_auth_sendCode req = new TLRPC.TL_auth_sendCode(); String phone = PhoneFormat.stripExceptNumbers("" + codeField.getText() + phoneField.getText()); ConnectionsManager.getInstance().applyCountryPortNumber(phone); req.api_hash = BuildVars.APP_HASH; req.api_id = BuildVars.APP_ID; req.phone_number = phone; - req.lang_code = LocaleController.getLocaleStringIso639(); - if (req.lang_code.length() == 0) { - req.lang_code = "en"; - } req.allow_flashcall = simcardAvailable && allowCall; if (req.allow_flashcall) { String number = tm.getLine1Number(); @@ -939,7 +964,7 @@ public class LoginActivity extends BaseFragment { } else { if (error.text != null) { if (error.text.contains("PHONE_NUMBER_INVALID")) { - needShowAlert(LocaleController.getString("AppName", R.string.AppName), LocaleController.getString("InvalidPhoneNumber", R.string.InvalidPhoneNumber)); + needShowInvalidAlert(req.phone_number); } else if (error.text.contains("PHONE_CODE_EMPTY") || error.text.contains("PHONE_CODE_INVALID")) { needShowAlert(LocaleController.getString("AppName", R.string.AppName), LocaleController.getString("InvalidCode", R.string.InvalidCode)); } else if (error.text.contains("PHONE_CODE_EXPIRED")) { @@ -1513,10 +1538,10 @@ public class LoginActivity extends BaseFragment { destroyTimer(); destroyCodeTimer(); UserConfig.clearConfig(); - MessagesController.getInstance().cleanUp(); + MessagesController.getInstance().cleanup(); UserConfig.setCurrentUser(res.user); UserConfig.saveConfig(true); - MessagesStorage.getInstance().cleanUp(true); + MessagesStorage.getInstance().cleanup(true); ArrayList users = new ArrayList<>(); users.add(res.user); MessagesStorage.getInstance().putUsersAndChats(users, null, true, true); @@ -1975,10 +2000,10 @@ public class LoginActivity extends BaseFragment { TLRPC.TL_auth_authorization res = (TLRPC.TL_auth_authorization) response; ConnectionsManager.getInstance().setUserId(res.user.id); UserConfig.clearConfig(); - MessagesController.getInstance().cleanUp(); + MessagesController.getInstance().cleanup(); UserConfig.setCurrentUser(res.user); UserConfig.saveConfig(true); - MessagesStorage.getInstance().cleanUp(true); + MessagesStorage.getInstance().cleanup(true); ArrayList users = new ArrayList<>(); users.add(res.user); MessagesStorage.getInstance().putUsersAndChats(users, null, true, true); @@ -2198,10 +2223,10 @@ public class LoginActivity extends BaseFragment { TLRPC.TL_auth_authorization res = (TLRPC.TL_auth_authorization) response; ConnectionsManager.getInstance().setUserId(res.user.id); UserConfig.clearConfig(); - MessagesController.getInstance().cleanUp(); + MessagesController.getInstance().cleanup(); UserConfig.setCurrentUser(res.user); UserConfig.saveConfig(true); - MessagesStorage.getInstance().cleanUp(true); + MessagesStorage.getInstance().cleanup(true); ArrayList users = new ArrayList<>(); users.add(res.user); MessagesStorage.getInstance().putUsersAndChats(users, null, true, true); @@ -2411,10 +2436,10 @@ public class LoginActivity extends BaseFragment { final TLRPC.TL_auth_authorization res = (TLRPC.TL_auth_authorization) response; ConnectionsManager.getInstance().setUserId(res.user.id); UserConfig.clearConfig(); - MessagesController.getInstance().cleanUp(); + MessagesController.getInstance().cleanup(); UserConfig.setCurrentUser(res.user); UserConfig.saveConfig(true); - MessagesStorage.getInstance().cleanUp(true); + MessagesStorage.getInstance().cleanup(true); ArrayList users = new ArrayList<>(); users.add(res.user); MessagesStorage.getInstance().putUsersAndChats(users, null, true, true); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java index 716dea31e..06e75bc50 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java @@ -8,6 +8,9 @@ package org.telegram.ui; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; @@ -64,8 +67,6 @@ import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Adapters.BaseFragmentAdapter; import org.telegram.ui.Adapters.BaseSectionsAdapter; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; import org.telegram.ui.Cells.GreySectionCell; import org.telegram.ui.Cells.LoadingCell; import org.telegram.ui.Cells.SharedDocumentCell; @@ -256,14 +257,6 @@ public class MediaActivity extends BaseFragment implements NotificationCenter.No actionBar.hideActionMode(); listView.invalidateViews(); } else { - if (Build.VERSION.SDK_INT < 11 && listView != null) { - listView.setAdapter(null); - listView = null; - photoVideoAdapter = null; - documentsAdapter = null; - audioAdapter = null; - linksAdapter = null; - } finishFragment(); } } else if (id == shared_media_item) { @@ -997,18 +990,16 @@ public class MediaActivity extends BaseFragment implements NotificationCenter.No } actionBar.createActionMode().getItem(delete).setVisibility(cantDeleteMessagesCount == 0 ? View.VISIBLE : View.GONE); selectedMessagesCountTextView.setNumber(1, false); - if (Build.VERSION.SDK_INT >= 11) { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - ArrayList animators = new ArrayList<>(); - for (int i = 0; i < actionModeViews.size(); i++) { - View view2 = actionModeViews.get(i); - AndroidUtilities.clearDrawableAnimation(view2); - animators.add(ObjectAnimatorProxy.ofFloat(view2, "scaleY", 0.1f, 1.0f)); - } - animatorSet.playTogether(animators); - animatorSet.setDuration(250); - animatorSet.start(); + AnimatorSet animatorSet = new AnimatorSet(); + ArrayList animators = new ArrayList<>(); + for (int i = 0; i < actionModeViews.size(); i++) { + View view2 = actionModeViews.get(i); + AndroidUtilities.clearDrawableAnimation(view2); + animators.add(ObjectAnimator.ofFloat(view2, "scaleY", 0.1f, 1.0f)); } + animatorSet.playTogether(animators); + animatorSet.setDuration(250); + animatorSet.start(); scrolling = false; if (view instanceof SharedDocumentCell) { ((SharedDocumentCell) view).setChecked(true, true); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/NotificationsSettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/NotificationsSettingsActivity.java index b3089d7e4..c3a73e2e9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/NotificationsSettingsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/NotificationsSettingsActivity.java @@ -709,7 +709,6 @@ public class NotificationsSettingsActivity extends BaseFragment implements Notif } else if (i == androidAutoAlertRow) { checkCell.setTextAndCheck("Android Auto", preferences.getBoolean("EnableAutoNotifications", false), true); } else if (i == notificationsServiceRow) { - //old: checkCell.setTextAndCheck("Smart Push", preferences.getBoolean("pushService", true), false); checkCell.setTextAndValueAndCheck(LocaleController.getString("NotificationsService", R.string.NotificationsService), LocaleController.getString("NotificationsServiceInfo", R.string.NotificationsServiceInfo), preferences.getBoolean("pushService", true), true, true); } else if (i == notificationsServiceConnectionRow) { checkCell.setTextAndValueAndCheck(LocaleController.getString("NotificationsServiceConnection", R.string.NotificationsServiceConnection), LocaleController.getString("NotificationsServiceConnectionInfo", R.string.NotificationsServiceConnectionInfo), preferences.getBoolean("pushConnection", true), true, true); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PasscodeActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PasscodeActivity.java index 64f5deb6c..fa4904cf1 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PasscodeActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PasscodeActivity.java @@ -25,7 +25,6 @@ import android.text.method.DigitsKeyListener; import android.text.method.PasswordTransformationMethod; import android.util.TypedValue; import android.view.ActionMode; -import android.view.ContextMenu; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; @@ -53,7 +52,6 @@ import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; import org.telegram.ui.ActionBar.ActionBarMenuItem; import org.telegram.ui.ActionBar.BaseFragment; -import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Adapters.BaseFragmentAdapter; import org.telegram.ui.Cells.TextCheckCell; import org.telegram.ui.Cells.TextInfoPrivacyCell; @@ -230,30 +228,23 @@ public class PasscodeActivity extends BaseFragment implements NotificationCenter } } }); - if (android.os.Build.VERSION.SDK_INT < 11) { - passwordEditText.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - menu.clear(); - } - }); - } else { - passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - public void onDestroyActionMode(ActionMode mode) { - } + passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return false; - } + public void onDestroyActionMode(ActionMode mode) { + } - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - }); - } + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + }); if (type == 1) { dropDownContainer = new ActionBarMenuItem(context, menu, 0); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java index c4b50be27..1305c1caa 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java @@ -119,11 +119,6 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati @Override public void onItemClick(int id) { if (id == -1) { - if (Build.VERSION.SDK_INT < 11) { - listView.setAdapter(null); - listView = null; - listAdapter = null; - } finishFragment(); } else if (id == 1) { if (delegate != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java index bebf0688a..83ffc37ea 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java @@ -13,7 +13,6 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.graphics.Bitmap; -import android.os.Build; import android.util.Base64; import android.view.Gravity; import android.view.LayoutInflater; @@ -34,7 +33,6 @@ import android.widget.TextView; import org.json.JSONArray; import org.json.JSONObject; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.messenger.FileLoader; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MediaController; @@ -176,11 +174,6 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen @Override public void onItemClick(int id) { if (id == -1) { - if (Build.VERSION.SDK_INT < 11) { - listView.setAdapter(null); - listView = null; - listAdapter = null; - } finishFragment(); } } @@ -449,18 +442,10 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen PhotoViewer.PlaceProviderObject object = new PhotoViewer.PlaceProviderObject(); object.viewX = coords[0]; object.viewY = coords[1] - AndroidUtilities.statusBarHeight; - if (Build.VERSION.SDK_INT < 11) { - float scale = ViewProxy.getScaleX(cell.photoImage); - if (scale != 1) { - int width = cell.photoImage.getMeasuredWidth(); - object.viewX += (width - width * scale) / 2; - object.viewY += (width - width * scale) / 2; - } - } object.parentView = listView; object.imageReceiver = cell.photoImage.getImageReceiver(); object.thumb = object.imageReceiver.getBitmap(); - object.scale = ViewProxy.getScaleX(cell.photoImage); + object.scale = cell.photoImage.getScaleX(); cell.checkBox.setVisibility(View.GONE); return object; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java index 9071f57d8..b4d701d92 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java @@ -9,6 +9,9 @@ package org.telegram.ui; import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; @@ -22,6 +25,7 @@ import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.SurfaceTexture; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -38,6 +42,7 @@ import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; +import android.view.TextureView; import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; @@ -54,6 +59,9 @@ import org.telegram.messenger.Emoji; import org.telegram.messenger.ImageLoader; import org.telegram.messenger.MessagesStorage; import org.telegram.messenger.UserObject; +import org.telegram.messenger.exoplayer.AspectRatioFrameLayout; +import org.telegram.messenger.exoplayer.ExoPlayer; +import org.telegram.messenger.exoplayer.util.PlayerControl; import org.telegram.messenger.query.SharedMediaQuery; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLoader; @@ -71,13 +79,11 @@ import org.telegram.messenger.MessageObject; import org.telegram.messenger.Utilities; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Adapters.MentionsAdapter; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; import org.telegram.ui.ActionBar.ActionBarMenuItem; +import org.telegram.ui.Components.AnimatedFileDrawable; import org.telegram.ui.Components.CheckBox; import org.telegram.ui.Components.ClippingImageView; import org.telegram.messenger.ImageReceiver; @@ -87,7 +93,9 @@ import org.telegram.ui.Components.PhotoFilterView; import org.telegram.ui.Components.PickerBottomLayout; import org.telegram.ui.Components.PhotoViewerCaptionEnterView; import org.telegram.ui.Components.RecyclerListView; +import org.telegram.ui.Components.SeekBar; import org.telegram.ui.Components.SizeNotifierFrameLayoutPhoto; +import org.telegram.ui.Components.VideoPlayer; import java.io.File; import java.util.ArrayList; @@ -129,7 +137,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private ActionBarMenuItem tuneItem; private ActionBarMenuItem captionItem; private ActionBarMenuItem captionDoneItem; - private AnimatorSetProxy currentActionBarAnimation; + private AnimatorSet currentActionBarAnimation; private PhotoCropView photoCropView; private PhotoFilterView photoFilterView; private AlertDialog visibleDialog; @@ -140,6 +148,38 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private boolean canShowBottom = true; private int sendPhotoType = 0; private boolean needCaptionLayout; + private AnimatedFileDrawable currentAnimation; + + private AspectRatioFrameLayout aspectRatioFrameLayout; + private TextureView videoTextureView; + private VideoPlayer videoPlayer; + private FrameLayout videoPlayerControlFrameLayout; + private ImageView videoPlayButton; + private TextView videoPlayerTime; + private SeekBar videoPlayerSeekbar; + private boolean playerNeedsPrepare; + private boolean textureUploaded; + private boolean videoCrossfadeStarted; + private float videoCrossfadeAlpha; + private long videoCrossfadeAlphaLastTime; + private boolean isPlaying; + private Runnable updateProgressRunnable = new Runnable() { + @Override + public void run() { + if (videoPlayer != null && videoPlayerSeekbar != null) { + if (!videoPlayerSeekbar.isDragging()) { + PlayerControl playerControl = videoPlayer.getPlayerControl(); + float progress = playerControl.getCurrentPosition() / (float) playerControl.getDuration(); + videoPlayerSeekbar.setProgress(progress); + videoPlayerControlFrameLayout.invalidate(); + updateVideoPlayerTime(); + } + } + if (isPlaying) { + AndroidUtilities.runOnUIThread(updateProgressRunnable, 100); + } + } + }; private float animationValues[][] = new float[2][8]; @@ -147,7 +187,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private MentionsAdapter mentionsAdapter; private RecyclerListView mentionListView; private LinearLayoutManager mentionLayoutManager; - private AnimatorSetProxy mentionListAnimation; + private AnimatorSet mentionListAnimation; private boolean allowMentions; private int animationInProgress = 0; @@ -172,7 +212,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private String currentPathObject; private Bitmap currentThumb = null; - private int avatarsUserId; + private int avatarsDialogId; private long currentDialogId; private long mergeDialogId; private int totalImagesCount; @@ -194,8 +234,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private float animationValue; private int currentRotation; private long animationStartTime; - private AnimatorSetProxy imageMoveAnimation; - private AnimatorSetProxy changeModeAnimation; + private AnimatorSet imageMoveAnimation; + private AnimatorSet changeModeAnimation; private GestureDetector gestureDetector; private DecelerateInterpolator interpolator = new DecelerateInterpolator(1.5f); private float pinchStartDistance; @@ -241,6 +281,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private final static int gallery_menu_tune = 7; private final static int gallery_menu_caption = 8; private final static int gallery_menu_caption_done = 9; + private final static int gallery_menu_share = 10; + private final static int gallery_menu_openin = 11; private final static int PAGE_SPACING = AndroidUtilities.dp(30); @@ -412,7 +454,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public int viewY; public View parentView; public Bitmap thumb; - public int user_id; + public int dialogId; public int index; public int size; public int radius; @@ -638,6 +680,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat protected void onDraw(Canvas canvas) { getInstance().onDraw(canvas); } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + return child != aspectRatioFrameLayout && super.drawChild(canvas, child, drawingTime); + } } private static volatile PhotoViewer Instance = null; @@ -673,6 +720,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (currentFileNames[a] != null && currentFileNames[a].equals(location)) { radialProgressViews[a].setProgress(1.0f, true); checkProgress(a, true); + if (Build.VERSION.SDK_INT >= 16 && a == 0 && currentMessageObject != null && currentMessageObject.isVideo()) { + onActionClick(false); + } break; } } @@ -684,10 +734,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat radialProgressViews[a].setProgress(progress, true); } } - } else if (id == NotificationCenter.userPhotosLoaded) { + } else if (id == NotificationCenter.dialogPhotosLoaded) { int guid = (Integer) args[4]; - int uid = (Integer) args[0]; - if (avatarsUserId == uid && classGuid == guid) { + int did = (Integer) args[0]; + if (avatarsDialogId == did && classGuid == guid) { boolean fromCache = (Boolean) args[3]; int setToImage = -1; @@ -698,14 +748,16 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat imagesArrLocations.clear(); imagesArrLocationsSizes.clear(); avatarsArr.clear(); - for (TLRPC.Photo photo : photos) { + for (int a = 0; a < photos.size(); a++) { + TLRPC.Photo photo = photos.get(a); if (photo == null || photo instanceof TLRPC.TL_photoEmpty || photo.sizes == null) { continue; } TLRPC.PhotoSize sizeFull = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 640); if (sizeFull != null) { - if (currentFileLocation != null) { - for (TLRPC.PhotoSize size : photo.sizes) { + if (setToImage == -1 && currentFileLocation != null) { + for (int b = 0; b < photo.sizes.size(); b++) { + TLRPC.PhotoSize size = photo.sizes.get(b); if (size.location.local_id == currentFileLocation.local_id && size.location.volume_id == currentFileLocation.volume_id) { setToImage = imagesArrLocations.size(); break; @@ -733,7 +785,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat setImageIndex(0, true); } if (fromCache) { - MessagesController.getInstance().loadUserPhotos(avatarsUserId, 0, 80, 0, false, classGuid); + MessagesController.getInstance().loadDialogPhotos(avatarsDialogId, 0, 80, 0, false, classGuid); } } } else if (id == NotificationCenter.mediaCountDidLoaded) { @@ -885,6 +937,47 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } + private void onSharePressed() { + if (parentActivity == null) { + return; + } + try { + File f = null; + boolean isVideo = false; + + if (currentMessageObject != null) { + isVideo = currentMessageObject.isVideo(); + /*if (currentMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { + AndroidUtilities.openUrl(parentActivity, currentMessageObject.messageOwner.media.webpage.url); + return; + }*/ + f = FileLoader.getPathToMessage(currentMessageObject.messageOwner); + } else if (currentFileLocation != null) { + f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0); + } + + if (f.exists()) { + Intent intent = new Intent(Intent.ACTION_SEND); + if (isVideo) { + intent.setType("video/mp4"); + } else { + intent.setType("image/jpeg"); + } + intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(f)); + + parentActivity.startActivityForResult(Intent.createChooser(intent, LocaleController.getString("ShareFile", R.string.ShareFile)), 500); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(parentActivity); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); + builder.setMessage(LocaleController.getString("PleaseDownload", R.string.PleaseDownload)); + showAlertDialog(builder); + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + public void setParentActivity(final Activity activity) { if (parentActivity == activity) { return; @@ -981,7 +1074,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (currentMessageObject != null) { f = FileLoader.getPathToMessage(currentMessageObject.messageOwner); } else if (currentFileLocation != null) { - f = FileLoader.getPathToAttach(currentFileLocation, avatarsUserId != 0); + f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0); } if (f != null && f.exists()) { @@ -1060,6 +1153,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } MessageObject obj = imagesArr.get(currentIndex); if (obj.isSent()) { + closePhoto(false, false); ArrayList arr = new ArrayList<>(); arr.add(obj.getId()); @@ -1072,7 +1166,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } MessagesController.getInstance().deleteMessages(arr, random_ids, encryptedChat, obj.messageOwner.to_id.channel_id); - closePhoto(false, false); } } else if (!avatarsArr.isEmpty()) { if (currentIndex < 0 || currentIndex >= avatarsArr.size()) { @@ -1104,7 +1197,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat inputPhoto.id = photo.id; inputPhoto.access_hash = photo.access_hash; MessagesController.getInstance().deleteUserPhoto(inputPhoto); - MessagesStorage.getInstance().clearUserPhoto(avatarsUserId, photo.id); + MessagesStorage.getInstance().clearUserPhoto(avatarsDialogId, photo.id); imagesArrLocations.remove(currentIndex); imagesArrLocationsSizes.remove(currentIndex); avatarsArr.remove(currentIndex); @@ -1140,13 +1233,21 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat layoutParams = (FrameLayout.LayoutParams) mentionListView.getLayoutParams(); layoutParams.bottomMargin = 0; mentionListView.setLayoutParams(layoutParams); - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); captionEditText.openKeyboard(); lastTitle = actionBar.getTitle(); actionBar.setTitle(LocaleController.getString("PhotoCaption", R.string.PhotoCaption)); } else if (id == gallery_menu_caption_done) { closeCaptionEnter(true); + } else if (id == gallery_menu_share) { + onSharePressed(); + } else if (id == gallery_menu_openin) { + try { + AndroidUtilities.openForView(currentMessageObject, parentActivity); + closePhoto(false, false); + } catch (Exception e) { + FileLog.e("tmessages", e); + } } } @@ -1158,7 +1259,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return true; } } else if (currentFileLocation != null) { - File f = FileLoader.getPathToAttach(currentFileLocation, avatarsUserId != 0); + File f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0); if (f.exists()) { return true; } @@ -1170,7 +1271,13 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat ActionBarMenu menu = actionBar.createMenu(); menuItem = menu.addItem(0, R.drawable.ic_ab_other); + String str = LocaleController.getString("OpenInBrowser", R.string.OpenInBrowser); + if (!TextUtils.isEmpty(str)) { //TODO add new string later + str = str.substring(0,1).toUpperCase() + str.substring(1).toLowerCase(); + } + menuItem.addSubItem(gallery_menu_openin, str, 0); menuItem.addSubItem(gallery_menu_showall, LocaleController.getString("ShowAllMedia", R.string.ShowAllMedia), 0); + menuItem.addSubItem(gallery_menu_share, LocaleController.getString("ShareFile", R.string.ShareFile), 0); menuItem.addSubItem(gallery_menu_save, LocaleController.getString("SaveToGallery", R.string.SaveToGallery), 0); menuItem.addSubItem(gallery_menu_delete, LocaleController.getString("Delete", R.string.Delete), 0); @@ -1220,44 +1327,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat shareButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (parentActivity == null) { - return; - } - try { - File f = null; - boolean isVideo = false; - - if (currentMessageObject != null) { - isVideo = currentMessageObject.isVideo(); - /*if (currentMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { - AndroidUtilities.openUrl(parentActivity, currentMessageObject.messageOwner.media.webpage.url); - return; - }*/ - f = FileLoader.getPathToMessage(currentMessageObject.messageOwner); - } else if (currentFileLocation != null) { - f = FileLoader.getPathToAttach(currentFileLocation, avatarsUserId != 0); - } - - if (f.exists()) { - Intent intent = new Intent(Intent.ACTION_SEND); - if (isVideo) { - intent.setType("video/mp4"); - } else { - intent.setType("image/jpeg"); - } - intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(f)); - - parentActivity.startActivityForResult(Intent.createChooser(intent, LocaleController.getString("ShareFile", R.string.ShareFile)), 500); - } else { - AlertDialog.Builder builder = new AlertDialog.Builder(parentActivity); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); - builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); - builder.setMessage(LocaleController.getString("PleaseDownload", R.string.PleaseDownload)); - showAlertDialog(builder); - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } + onSharePressed(); } }); @@ -1281,6 +1351,83 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat dateTextView.setGravity(Gravity.LEFT); bottomLayout.addView(dateTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 16, 25, 50, 0)); + if (Build.VERSION.SDK_INT >= 16) { + videoPlayerSeekbar = new SeekBar(containerView.getContext()); + videoPlayerSeekbar.setColors(0x66ffffff, 0xffffffff, 0xffffffff); + videoPlayerSeekbar.setDelegate(new SeekBar.SeekBarDelegate() { + @Override + public void onSeekBarDrag(float progress) { + if (videoPlayer != null) { + videoPlayer.getPlayerControl().seekTo((int) (progress * videoPlayer.getDuration())); + } + } + }); + + videoPlayerControlFrameLayout = new FrameLayout(containerView.getContext()) { + + @Override + public boolean onTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + if (videoPlayerSeekbar.onTouch(event.getAction(), event.getX() - AndroidUtilities.dp(48), event.getY())) { + getParent().requestDisallowInterceptTouchEvent(true); + invalidate(); + return true; + } + return super.onTouchEvent(event); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + long duration; + if (videoPlayer != null) { + duration = videoPlayer.getDuration(); + if (duration == ExoPlayer.UNKNOWN_TIME) { + duration = 0; + } + } else { + duration = 0; + } + duration /= 1000; + int size = (int) Math.ceil(videoPlayerTime.getPaint().measureText(String.format("%02d:%02d / %02d:%02d", duration / 60, duration % 60, duration / 60, duration % 60))); + videoPlayerSeekbar.setSize(getMeasuredWidth() - AndroidUtilities.dp(48 + 16) - size, getMeasuredHeight()); + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.save(); + canvas.translate(AndroidUtilities.dp(48), 0); + videoPlayerSeekbar.draw(canvas); + canvas.restore(); + } + }; + videoPlayerControlFrameLayout.setWillNotDraw(false); + bottomLayout.addView(videoPlayerControlFrameLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); + + videoPlayButton = new ImageView(containerView.getContext()); + videoPlayButton.setScaleType(ImageView.ScaleType.CENTER); + videoPlayerControlFrameLayout.addView(videoPlayButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.TOP)); + videoPlayButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (videoPlayer != null) { + if (isPlaying) { + videoPlayer.getPlayerControl().pause(); + } else { + videoPlayer.getPlayerControl().start(); + } + } + } + }); + + videoPlayerTime = new TextView(containerView.getContext()); + videoPlayerTime.setTextColor(0xffffffff); + videoPlayerTime.setGravity(Gravity.CENTER_VERTICAL); + videoPlayerTime.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); + videoPlayerControlFrameLayout.addView(videoPlayerTime, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.RIGHT | Gravity.TOP, 0, 0, 8, 0)); + } + pickerView = new PickerBottomLayout(actvityContext); pickerView.setBackgroundColor(0x7f000000); containerView.addView(pickerView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.BOTTOM | Gravity.LEFT)); @@ -1398,13 +1545,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (size - ActionBar.getCurrentActionBarHeight() * 2 < height) { allowMentions = false; if (mentionListView != null && mentionListView.getVisibility() == View.VISIBLE) { - mentionListView.clearAnimation(); mentionListView.setVisibility(View.INVISIBLE); } } else { allowMentions = true; if (mentionListView != null && mentionListView.getVisibility() == View.INVISIBLE) { - mentionListView.clearAnimation(); mentionListView.setVisibility(View.VISIBLE); } } @@ -1413,6 +1558,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat containerView.addView(captionEditText, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.BOTTOM | Gravity.LEFT, 0, 0, 0, -400)); mentionListView = new RecyclerListView(actvityContext); + mentionListView.setTag(5); mentionLayoutManager = new LinearLayoutManager(actvityContext) { @Override public boolean supportsPredictiveItemAnimations() { @@ -1443,22 +1589,21 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } if (mentionListView.getVisibility() == View.VISIBLE) { - ViewProxy.setAlpha(mentionListView, 1.0f); + mentionListView.setAlpha(1.0f); return; } else { mentionLayoutManager.scrollToPositionWithOffset(0, 10000); } if (allowMentions) { mentionListView.setVisibility(View.VISIBLE); - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionListView, "alpha", 0.0f, 1.0f) + ObjectAnimator.ofFloat(mentionListView, "alpha", 0.0f, 1.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionListView.clearAnimation(); mentionListAnimation = null; } } @@ -1466,8 +1611,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat mentionListAnimation.setDuration(200); mentionListAnimation.start(); } else { - ViewProxy.setAlpha(mentionListView, 1.0f); - mentionListView.clearAnimation(); + mentionListView.setAlpha(1.0f); mentionListView.setVisibility(View.INVISIBLE); } } else { @@ -1480,15 +1624,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return; } if (allowMentions) { - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionListView, "alpha", 0.0f) + ObjectAnimator.ofFloat(mentionListView, "alpha", 0.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionListView.clearAnimation(); mentionListView.setVisibility(View.GONE); mentionListAnimation = null; } @@ -1497,7 +1640,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat mentionListAnimation.setDuration(200); mentionListAnimation.start(); } else { - mentionListView.clearAnimation(); mentionListView.setVisibility(View.GONE); } } @@ -1513,6 +1655,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } })); + mentionsAdapter.setAllowNewMentions(false); mentionListView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @Override @@ -1554,6 +1697,173 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat }); } + private void updateVideoPlayerTime() { + String newText; + if (videoPlayer == null) { + newText = "00:00 / 00:00"; + } else { + long current = videoPlayer.getCurrentPosition() / 1000; + long total = videoPlayer.getDuration() / 1000; + if (total != ExoPlayer.UNKNOWN_TIME && current != ExoPlayer.UNKNOWN_TIME) { + newText = String.format("%02d:%02d / %02d:%02d", current / 60, current % 60, total / 60, total % 60); + } else { + newText = "00:00 / 00:00"; + } + } + if (!TextUtils.equals(videoPlayerTime.getText(), newText)) { + videoPlayerTime.setText(newText); + } + } + + @SuppressLint("NewApi") + private void preparePlayer(File file, boolean playWhenReady) { + if (parentActivity == null) { + return; + } + releasePlayer(); + if (videoTextureView == null) { + aspectRatioFrameLayout = new AspectRatioFrameLayout(parentActivity); + aspectRatioFrameLayout.setVisibility(View.INVISIBLE); + containerView.addView(aspectRatioFrameLayout, 0, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.CENTER)); + + videoTextureView = new TextureView(parentActivity); + videoTextureView.setOpaque(false); + videoTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + if (videoPlayer != null) { + videoPlayer.setSurface(new Surface(videoTextureView.getSurfaceTexture())); + } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + if (videoPlayer != null) { + videoPlayer.blockingClearSurface(); + } + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + if (!textureUploaded) { + textureUploaded = true; + containerView.invalidate(); + } + } + }); + aspectRatioFrameLayout.addView(videoTextureView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.CENTER)); + } + textureUploaded = false; + videoCrossfadeStarted = false; + videoTextureView.setAlpha(videoCrossfadeAlpha = 0.0f); + if (videoPlayer == null) { + videoPlayer = new VideoPlayer(new VideoPlayer.ExtractorRendererBuilder(parentActivity, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", Uri.fromFile(file))); + videoPlayer.addListener(new VideoPlayer.Listener() { + @Override + public void onStateChanged(boolean playWhenReady, int playbackState) { + if (videoPlayer == null) { + return; + } + if (playbackState == VideoPlayer.STATE_READY && aspectRatioFrameLayout.getVisibility() != View.VISIBLE) { + aspectRatioFrameLayout.setVisibility(View.VISIBLE); + } + if (videoPlayer.getPlayerControl().isPlaying() && playbackState != VideoPlayer.STATE_ENDED) { + if (!isPlaying) { + isPlaying = true; + videoPlayButton.setImageResource(R.drawable.inline_video_pause); + AndroidUtilities.runOnUIThread(updateProgressRunnable); + } + } else if (isPlaying) { + isPlaying = false; + videoPlayButton.setImageResource(R.drawable.inline_video_play); + AndroidUtilities.cancelRunOnUIThread(updateProgressRunnable); + if (playbackState == VideoPlayer.STATE_ENDED) { + if (!videoPlayerSeekbar.isDragging()) { + videoPlayerSeekbar.setProgress(0.0f); + videoPlayerControlFrameLayout.invalidate(); + videoPlayer.seekTo(0); + videoPlayer.getPlayerControl().pause(); + } + } + } + updateVideoPlayerTime(); + } + + @Override + public void onError(Exception e) { + FileLog.e("tmessages", e); + } + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + if (aspectRatioFrameLayout != null) { + aspectRatioFrameLayout.setAspectRatio(height == 0 ? 1 : (width * pixelWidthHeightRatio) / height); + } + } + }); + long duration; + if (videoPlayer != null) { + duration = videoPlayer.getDuration(); + if (duration == ExoPlayer.UNKNOWN_TIME) { + duration = 0; + } + } else { + duration = 0; + } + duration /= 1000; + int size = (int) Math.ceil(videoPlayerTime.getPaint().measureText(String.format("%02d:%02d / %02d:%02d", duration / 60, duration % 60, duration / 60, duration % 60))); + + playerNeedsPrepare = true; + } + if (playerNeedsPrepare) { + videoPlayer.prepare(); + playerNeedsPrepare = false; + } + if (videoPlayerControlFrameLayout != null) { + videoPlayerControlFrameLayout.setVisibility(View.VISIBLE); + dateTextView.setVisibility(View.GONE); + nameTextView.setVisibility(View.GONE); + shareButton.setVisibility(View.GONE); + menuItem.showSubItem(gallery_menu_share); + } + if (videoTextureView.getSurfaceTexture() != null) { + videoPlayer.setSurface(new Surface(videoTextureView.getSurfaceTexture())); + } + videoPlayer.setPlayWhenReady(playWhenReady); + } + + private void releasePlayer() { + if (videoPlayer != null) { + videoPlayer.release(); + videoPlayer = null; + } + if (aspectRatioFrameLayout != null) { + containerView.removeView(aspectRatioFrameLayout); + aspectRatioFrameLayout = null; + } + if (videoTextureView != null) { + videoTextureView = null; + } + if (isPlaying) { + isPlaying = false; + videoPlayButton.setImageResource(R.drawable.inline_video_play); + AndroidUtilities.cancelRunOnUIThread(updateProgressRunnable); + } + if (videoPlayerControlFrameLayout != null) { + videoPlayerControlFrameLayout.setVisibility(View.GONE); + dateTextView.setVisibility(View.VISIBLE); + nameTextView.setVisibility(View.VISIBLE); + shareButton.setVisibility(View.VISIBLE); + menuItem.hideSubItem(gallery_menu_share); + } + } + private void updateCaptionTextForCurrentPhoto(Object object) { CharSequence caption = null; if (object instanceof MediaController.PhotoEntry) { @@ -1726,31 +2036,28 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat zoomAnimation = true; } - imageMoveAnimation = new AnimatorSetProxy(); + imageMoveAnimation = new AnimatorSet(); if (currentEditMode == 1) { imageMoveAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(editorDoneLayout, "translationY", AndroidUtilities.dp(48)), - ObjectAnimatorProxy.ofFloat(PhotoViewer.this, "animationValue", 0, 1), - ObjectAnimatorProxy.ofFloat(photoCropView, "alpha", 0) + ObjectAnimator.ofFloat(editorDoneLayout, "translationY", AndroidUtilities.dp(48)), + ObjectAnimator.ofFloat(PhotoViewer.this, "animationValue", 0, 1), + ObjectAnimator.ofFloat(photoCropView, "alpha", 0) ); } else if (currentEditMode == 2) { photoFilterView.shutdown(); imageMoveAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(photoFilterView.getToolsView(), "translationY", AndroidUtilities.dp(126)), - ObjectAnimatorProxy.ofFloat(PhotoViewer.this, "animationValue", 0, 1) + ObjectAnimator.ofFloat(photoFilterView.getToolsView(), "translationY", AndroidUtilities.dp(126)), + ObjectAnimator.ofFloat(PhotoViewer.this, "animationValue", 0, 1) ); } imageMoveAnimation.setDuration(200); imageMoveAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentEditMode == 1) { - photoCropView.clearAnimation(); - editorDoneLayout.clearAnimation(); editorDoneLayout.setVisibility(View.GONE); photoCropView.setVisibility(View.GONE); } else if (currentEditMode == 2) { - photoFilterView.getToolsView().clearAnimation(); containerView.removeView(photoFilterView); photoFilterView = null; } @@ -1763,21 +2070,21 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat updateMinMax(scale); containerView.invalidate(); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - ArrayList arrayList = new ArrayList<>(); - arrayList.add(ObjectAnimatorProxy.ofFloat(pickerView, "translationY", 0)); - arrayList.add(ObjectAnimatorProxy.ofFloat(actionBar, "translationY", 0)); + AnimatorSet animatorSet = new AnimatorSet(); + ArrayList arrayList = new ArrayList<>(); + arrayList.add(ObjectAnimator.ofFloat(pickerView, "translationY", 0)); + arrayList.add(ObjectAnimator.ofFloat(actionBar, "translationY", 0)); if (needCaptionLayout) { - arrayList.add(ObjectAnimatorProxy.ofFloat(captionTextView, "translationY", 0)); + arrayList.add(ObjectAnimator.ofFloat(captionTextView, "translationY", 0)); } if (sendPhotoType == 0) { - arrayList.add(ObjectAnimatorProxy.ofFloat(checkImageView, "alpha", 1)); + arrayList.add(ObjectAnimator.ofFloat(checkImageView, "alpha", 1)); } animatorSet.playTogether(arrayList); animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationStart(Object animation) { + public void onAnimationStart(Animator animation) { pickerView.setVisibility(View.VISIBLE); actionBar.setVisibility(View.VISIBLE); if (needCaptionLayout) { @@ -1787,18 +2094,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat checkImageView.setVisibility(View.VISIBLE); } } - - @Override - public void onAnimationEnd(Object animation) { - pickerView.clearAnimation(); - actionBar.clearAnimation(); - if (needCaptionLayout) { - captionTextView.clearAnimation(); - } - if (sendPhotoType == 0) { - checkImageView.clearAnimation(); - } - } }); animatorSet.start(); } @@ -1830,32 +2125,28 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } editorDoneLayout.doneButtonTextView.setText(LocaleController.getString("Crop", R.string.Crop)); - changeModeAnimation = new AnimatorSetProxy(); - ArrayList arrayList = new ArrayList<>(); - arrayList.add(ObjectAnimatorProxy.ofFloat(pickerView, "translationY", 0, AndroidUtilities.dp(96))); - arrayList.add(ObjectAnimatorProxy.ofFloat(actionBar, "translationY", 0, -actionBar.getHeight())); + changeModeAnimation = new AnimatorSet(); + ArrayList arrayList = new ArrayList<>(); + arrayList.add(ObjectAnimator.ofFloat(pickerView, "translationY", 0, AndroidUtilities.dp(96))); + arrayList.add(ObjectAnimator.ofFloat(actionBar, "translationY", 0, -actionBar.getHeight())); if (needCaptionLayout) { - arrayList.add(ObjectAnimatorProxy.ofFloat(captionTextView, "translationY", 0, AndroidUtilities.dp(96))); + arrayList.add(ObjectAnimator.ofFloat(captionTextView, "translationY", 0, AndroidUtilities.dp(96))); } if (sendPhotoType == 0) { - arrayList.add(ObjectAnimatorProxy.ofFloat(checkImageView, "alpha", 1, 0)); + arrayList.add(ObjectAnimator.ofFloat(checkImageView, "alpha", 1, 0)); } changeModeAnimation.playTogether(arrayList); changeModeAnimation.setDuration(200); changeModeAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { changeModeAnimation = null; - pickerView.clearAnimation(); - actionBar.clearAnimation(); pickerView.setVisibility(View.GONE); actionBar.setVisibility(View.GONE); if (needCaptionLayout) { - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); } if (sendPhotoType == 0) { - checkImageView.clearAnimation(); checkImageView.setVisibility(View.GONE); } @@ -1879,22 +2170,22 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat zoomAnimation = true; } - imageMoveAnimation = new AnimatorSetProxy(); + imageMoveAnimation = new AnimatorSet(); imageMoveAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(editorDoneLayout, "translationY", AndroidUtilities.dp(48), 0), - ObjectAnimatorProxy.ofFloat(PhotoViewer.this, "animationValue", 0, 1), - ObjectAnimatorProxy.ofFloat(photoCropView, "alpha", 0, 1) + ObjectAnimator.ofFloat(editorDoneLayout, "translationY", AndroidUtilities.dp(48), 0), + ObjectAnimator.ofFloat(PhotoViewer.this, "animationValue", 0, 1), + ObjectAnimator.ofFloat(photoCropView, "alpha", 0, 1) ); imageMoveAnimation.setDuration(200); imageMoveAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationStart(Object animation) { + public void onAnimationStart(Animator animation) { editorDoneLayout.setVisibility(View.VISIBLE); photoCropView.setVisibility(View.VISIBLE); } @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { imageMoveAnimation = null; currentEditMode = mode; animateToScale = 1; @@ -1903,8 +2194,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat scale = 1; updateMinMax(scale); containerView.invalidate(); - editorDoneLayout.clearAnimation(); - photoCropView.clearAnimation(); } }); imageMoveAnimation.start(); @@ -1946,35 +2235,31 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } }); //photoFilterView.setEditViewFirst(); - ViewProxy.setTranslationY(photoFilterView.getToolsView(), AndroidUtilities.dp(126)); + photoFilterView.getToolsView().setTranslationY(AndroidUtilities.dp(126)); } - changeModeAnimation = new AnimatorSetProxy(); - ArrayList arrayList = new ArrayList<>(); - arrayList.add(ObjectAnimatorProxy.ofFloat(pickerView, "translationY", 0, AndroidUtilities.dp(96))); - arrayList.add(ObjectAnimatorProxy.ofFloat(actionBar, "translationY", 0, -actionBar.getHeight())); + changeModeAnimation = new AnimatorSet(); + ArrayList arrayList = new ArrayList<>(); + arrayList.add(ObjectAnimator.ofFloat(pickerView, "translationY", 0, AndroidUtilities.dp(96))); + arrayList.add(ObjectAnimator.ofFloat(actionBar, "translationY", 0, -actionBar.getHeight())); if (needCaptionLayout) { - arrayList.add(ObjectAnimatorProxy.ofFloat(captionTextView, "translationY", 0, AndroidUtilities.dp(96))); + arrayList.add(ObjectAnimator.ofFloat(captionTextView, "translationY", 0, AndroidUtilities.dp(96))); } if (sendPhotoType == 0) { - arrayList.add(ObjectAnimatorProxy.ofFloat(checkImageView, "alpha", 1, 0)); + arrayList.add(ObjectAnimator.ofFloat(checkImageView, "alpha", 1, 0)); } changeModeAnimation.playTogether(arrayList); changeModeAnimation.setDuration(200); changeModeAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { changeModeAnimation = null; - pickerView.clearAnimation(); - actionBar.clearAnimation(); pickerView.setVisibility(View.GONE); actionBar.setVisibility(View.GONE); if (needCaptionLayout) { - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); } if (sendPhotoType == 0) { - checkImageView.clearAnimation(); checkImageView.setVisibility(View.GONE); } @@ -1997,20 +2282,20 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat zoomAnimation = true; } - imageMoveAnimation = new AnimatorSetProxy(); + imageMoveAnimation = new AnimatorSet(); imageMoveAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(PhotoViewer.this, "animationValue", 0, 1), - ObjectAnimatorProxy.ofFloat(photoFilterView.getToolsView(), "translationY", AndroidUtilities.dp(126), 0) + ObjectAnimator.ofFloat(PhotoViewer.this, "animationValue", 0, 1), + ObjectAnimator.ofFloat(photoFilterView.getToolsView(), "translationY", AndroidUtilities.dp(126), 0) ); imageMoveAnimation.setDuration(200); imageMoveAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationStart(Object animation) { + public void onAnimationStart(Animator animation) { } @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { photoFilterView.init(); imageMoveAnimation = null; currentEditMode = mode; @@ -2020,7 +2305,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat scale = 1; updateMinMax(scale); containerView.invalidate(); - photoFilterView.getToolsView().clearAnimation(); } }); imageMoveAnimation.start(); @@ -2031,14 +2315,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } private void toggleCheckImageView(boolean show) { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - ArrayList arrayList = new ArrayList<>(); - arrayList.add(ObjectAnimatorProxy.ofFloat(pickerView, "alpha", show ? 1.0f : 0.0f)); + AnimatorSet animatorSet = new AnimatorSet(); + ArrayList arrayList = new ArrayList<>(); + arrayList.add(ObjectAnimator.ofFloat(pickerView, "alpha", show ? 1.0f : 0.0f)); if (needCaptionLayout) { - arrayList.add(ObjectAnimatorProxy.ofFloat(captionTextView, "alpha", show ? 1.0f : 0.0f)); + arrayList.add(ObjectAnimator.ofFloat(captionTextView, "alpha", show ? 1.0f : 0.0f)); } if (sendPhotoType == 0) { - arrayList.add(ObjectAnimatorProxy.ofFloat(checkImageView, "alpha", show ? 1.0f : 0.0f)); + arrayList.add(ObjectAnimator.ofFloat(checkImageView, "alpha", show ? 1.0f : 0.0f)); } animatorSet.playTogether(arrayList); animatorSet.setDuration(200); @@ -2060,25 +2344,23 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat bottomLayout.setEnabled(show); if (animated) { - ArrayList arrayList = new ArrayList<>(); - arrayList.add(ObjectAnimatorProxy.ofFloat(actionBar, "alpha", show ? 1.0f : 0.0f)); - arrayList.add(ObjectAnimatorProxy.ofFloat(bottomLayout, "alpha", show ? 1.0f : 0.0f)); + ArrayList arrayList = new ArrayList<>(); + arrayList.add(ObjectAnimator.ofFloat(actionBar, "alpha", show ? 1.0f : 0.0f)); + arrayList.add(ObjectAnimator.ofFloat(bottomLayout, "alpha", show ? 1.0f : 0.0f)); if (captionTextView.getTag() != null) { - arrayList.add(ObjectAnimatorProxy.ofFloat(captionTextView, "alpha", show ? 1.0f : 0.0f)); + arrayList.add(ObjectAnimator.ofFloat(captionTextView, "alpha", show ? 1.0f : 0.0f)); } - currentActionBarAnimation = new AnimatorSetProxy(); + currentActionBarAnimation = new AnimatorSet(); currentActionBarAnimation.playTogether(arrayList); if (!show) { currentActionBarAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentActionBarAnimation != null && currentActionBarAnimation.equals(animation)) { actionBar.setVisibility(View.GONE); if (canShowBottom) { - bottomLayout.clearAnimation(); bottomLayout.setVisibility(View.GONE); if (captionTextView.getTag() != null) { - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); } } @@ -2091,18 +2373,16 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat currentActionBarAnimation.setDuration(200); currentActionBarAnimation.start(); } else { - ViewProxy.setAlpha(actionBar, show ? 1.0f : 0.0f); - ViewProxy.setAlpha(bottomLayout, show ? 1.0f : 0.0f); + actionBar.setAlpha(show ? 1.0f : 0.0f); + bottomLayout.setAlpha(show ? 1.0f : 0.0f); if (captionTextView.getTag() != null) { - ViewProxy.setAlpha(captionTextView, show ? 1.0f : 0.0f); + captionTextView.setAlpha(show ? 1.0f : 0.0f); } if (!show) { actionBar.setVisibility(View.GONE); if (canShowBottom) { - bottomLayout.clearAnimation(); bottomLayout.setVisibility(View.GONE); if (captionTextView.getTag() != null) { - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); } } @@ -2218,7 +2498,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat currentFileNames[0] = null; currentFileNames[1] = null; currentFileNames[2] = null; - avatarsUserId = 0; + avatarsDialogId = 0; totalImagesCount = 0; totalImagesCountMerge = 0; currentEditMode = 0; @@ -2247,32 +2527,27 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat bottomLayout.setVisibility(View.VISIBLE); shareButton.setVisibility(View.GONE); menuItem.hideSubItem(gallery_menu_showall); - ViewProxy.setTranslationY(actionBar, 0); - ViewProxy.setTranslationY(pickerView, 0); - ViewProxy.setAlpha(checkImageView, 1.0f); - ViewProxy.setAlpha(pickerView, 1.0f); - checkImageView.clearAnimation(); - pickerView.clearAnimation(); - editorDoneLayout.clearAnimation(); + menuItem.hideSubItem(gallery_menu_share); + menuItem.hideSubItem(gallery_menu_openin); + actionBar.setTranslationY(0); + pickerView.setTranslationY(0); + checkImageView.setAlpha(1.0f); + pickerView.setAlpha(1.0f); checkImageView.setVisibility(View.GONE); pickerView.setVisibility(View.GONE); cropItem.setVisibility(View.GONE); tuneItem.setVisibility(View.GONE); captionItem.setVisibility(View.GONE); captionDoneItem.setVisibility(View.GONE); - captionEditText.clearAnimation(); captionEditText.setVisibility(View.GONE); mentionListView.setVisibility(View.GONE); editorDoneLayout.setVisibility(View.GONE); captionTextView.setTag(null); - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); if (photoCropView != null) { - photoCropView.clearAnimation(); photoCropView.setVisibility(View.GONE); } if (photoFilterView != null) { - photoFilterView.clearAnimation(); photoFilterView.setVisibility(View.GONE); } @@ -2284,22 +2559,26 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (messageObject != null && messages == null) { imagesArr.add(messageObject); - if (!(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) && (messageObject.messageOwner.action == null || messageObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty)) { + if (currentAnimation != null) { + needSearchImageInArr = false; + } else if (!(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) && (messageObject.messageOwner.action == null || messageObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty)) { needSearchImageInArr = true; imagesByIds[0].put(messageObject.getId(), messageObject); menuItem.showSubItem(gallery_menu_showall); - } else { - menuItem.hideSubItem(gallery_menu_showall); } setImageIndex(0, true); } else if (fileLocation != null) { - avatarsUserId = object.user_id; + avatarsDialogId = object.dialogId; imagesArrLocations.add(fileLocation); imagesArrLocationsSizes.add(object.size); avatarsArr.add(new TLRPC.TL_photoEmpty()); - bottomLayout.clearAnimation(); - shareButton.setVisibility(View.VISIBLE); + shareButton.setVisibility(videoPlayerControlFrameLayout == null || videoPlayerControlFrameLayout.getVisibility() != View.VISIBLE ? View.VISIBLE : View.GONE); menuItem.hideSubItem(gallery_menu_showall); + if (shareButton.getVisibility() == View.VISIBLE) { + menuItem.hideSubItem(gallery_menu_share); + } else { + menuItem.showSubItem(gallery_menu_share); + } setImageIndex(0, true); currentUserAvatarLocation = fileLocation; } else if (messages != null) { @@ -2323,7 +2602,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat imagesArrLocals.addAll(photos); setImageIndex(index, true); pickerView.setVisibility(View.VISIBLE); - bottomLayout.clearAnimation(); bottomLayout.setVisibility(View.GONE); canShowBottom = false; Object obj = imagesArrLocals.get(index); @@ -2345,13 +2623,18 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat updateSelectedCount(); } - if (currentDialogId != 0 && totalImagesCount == 0) { - SharedMediaQuery.getMediaCount(currentDialogId, SharedMediaQuery.MEDIA_PHOTOVIDEO, classGuid, true); - if (mergeDialogId != 0) { - SharedMediaQuery.getMediaCount(mergeDialogId, SharedMediaQuery.MEDIA_PHOTOVIDEO, classGuid, true); + if (currentAnimation == null) { + if (currentDialogId != 0 && totalImagesCount == 0) { + SharedMediaQuery.getMediaCount(currentDialogId, SharedMediaQuery.MEDIA_PHOTOVIDEO, classGuid, true); + if (mergeDialogId != 0) { + SharedMediaQuery.getMediaCount(mergeDialogId, SharedMediaQuery.MEDIA_PHOTOVIDEO, classGuid, true); + } + } else if (avatarsDialogId != 0) { + MessagesController.getInstance().loadDialogPhotos(avatarsDialogId, 0, 80, 0, true, classGuid); } - } else if (avatarsUserId != 0) { - MessagesController.getInstance().loadUserPhotos(avatarsUserId, 0, 80, 0, true, classGuid); + } + if (currentMessageObject != null && currentMessageObject.isVideo()) { + onActionClick(false); } } @@ -2364,7 +2647,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } private void setImageIndex(int index, boolean init) { - if (currentIndex == index) { + if (currentIndex == index || placeProvider == null) { return; } if (!init) { @@ -2393,6 +2676,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else { menuItem.hideSubItem(gallery_menu_delete); } + if (isVideo && Build.VERSION.SDK_INT >= 16) { + menuItem.showSubItem(gallery_menu_openin); + } else { + menuItem.hideSubItem(gallery_menu_openin); + } if (currentMessageObject.isFromUser()) { TLRPC.User user = MessagesController.getInstance().getUser(currentMessageObject.messageOwner.from_id); if (user != null) { @@ -2418,52 +2706,72 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat CharSequence caption = currentMessageObject.caption; setCurrentCaption(caption); - if (totalImagesCount + totalImagesCountMerge != 0 && !needSearchImageInArr) { - if (opennedFromMedia) { - if (imagesArr.size() < totalImagesCount + totalImagesCountMerge && !loadingMoreImages && currentIndex > imagesArr.size() - 5) { - int loadFromMaxId = imagesArr.isEmpty() ? 0 : imagesArr.get(imagesArr.size() - 1).getId(); - int loadIndex = 0; - if (endReached[loadIndex] && mergeDialogId != 0) { - loadIndex = 1; - if (!imagesArr.isEmpty() && imagesArr.get(imagesArr.size() - 1).getDialogId() != mergeDialogId) { - loadFromMaxId = 0; - } - } - - SharedMediaQuery.loadMedia(loadIndex == 0 ? currentDialogId : mergeDialogId, 0, 80, loadFromMaxId, SharedMediaQuery.MEDIA_PHOTOVIDEO, true, classGuid); - loadingMoreImages = true; - } - actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, currentIndex + 1, totalImagesCount + totalImagesCountMerge)); - } else { - if (imagesArr.size() < totalImagesCount + totalImagesCountMerge && !loadingMoreImages && currentIndex < 5) { - int loadFromMaxId = imagesArr.isEmpty() ? 0 : imagesArr.get(0).getId(); - int loadIndex = 0; - if (endReached[loadIndex] && mergeDialogId != 0) { - loadIndex = 1; - if (!imagesArr.isEmpty() && imagesArr.get(0).getDialogId() != mergeDialogId) { - loadFromMaxId = 0; - } - } - - SharedMediaQuery.loadMedia(loadIndex == 0 ? currentDialogId : mergeDialogId, 0, 80, loadFromMaxId, SharedMediaQuery.MEDIA_PHOTOVIDEO, true, classGuid); - loadingMoreImages = true; - } - actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, (totalImagesCount + totalImagesCountMerge - imagesArr.size()) + currentIndex + 1, totalImagesCount + totalImagesCountMerge)); - } - } else if (currentMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { - actionBar.setTitle(LocaleController.getString("AttachPhoto", R.string.AttachPhoto)); - } - if (currentMessageObject.messageOwner.ttl != 0) { + if (currentAnimation != null) { menuItem.hideSubItem(gallery_menu_save); - shareButton.setVisibility(View.GONE); - } else { - menuItem.showSubItem(gallery_menu_save); + menuItem.hideSubItem(gallery_menu_share); + if (!currentMessageObject.canDeleteMessage(null)) { + menuItem.setVisibility(View.GONE); + } shareButton.setVisibility(View.VISIBLE); + actionBar.setTitle(LocaleController.getString("AttachGif", R.string.AttachGif)); + } else { + if (totalImagesCount + totalImagesCountMerge != 0 && !needSearchImageInArr) { + if (opennedFromMedia) { + if (imagesArr.size() < totalImagesCount + totalImagesCountMerge && !loadingMoreImages && currentIndex > imagesArr.size() - 5) { + int loadFromMaxId = imagesArr.isEmpty() ? 0 : imagesArr.get(imagesArr.size() - 1).getId(); + int loadIndex = 0; + if (endReached[loadIndex] && mergeDialogId != 0) { + loadIndex = 1; + if (!imagesArr.isEmpty() && imagesArr.get(imagesArr.size() - 1).getDialogId() != mergeDialogId) { + loadFromMaxId = 0; + } + } + + SharedMediaQuery.loadMedia(loadIndex == 0 ? currentDialogId : mergeDialogId, 0, 80, loadFromMaxId, SharedMediaQuery.MEDIA_PHOTOVIDEO, true, classGuid); + loadingMoreImages = true; + } + actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, currentIndex + 1, totalImagesCount + totalImagesCountMerge)); + } else { + if (imagesArr.size() < totalImagesCount + totalImagesCountMerge && !loadingMoreImages && currentIndex < 5) { + int loadFromMaxId = imagesArr.isEmpty() ? 0 : imagesArr.get(0).getId(); + int loadIndex = 0; + if (endReached[loadIndex] && mergeDialogId != 0) { + loadIndex = 1; + if (!imagesArr.isEmpty() && imagesArr.get(0).getDialogId() != mergeDialogId) { + loadFromMaxId = 0; + } + } + + SharedMediaQuery.loadMedia(loadIndex == 0 ? currentDialogId : mergeDialogId, 0, 80, loadFromMaxId, SharedMediaQuery.MEDIA_PHOTOVIDEO, true, classGuid); + loadingMoreImages = true; + } + actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, (totalImagesCount + totalImagesCountMerge - imagesArr.size()) + currentIndex + 1, totalImagesCount + totalImagesCountMerge)); + } + } else if (currentMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { + if (currentMessageObject.isVideo()) { + actionBar.setTitle(LocaleController.getString("AttachVideo", R.string.AttachVideo)); + } else { + actionBar.setTitle(LocaleController.getString("AttachPhoto", R.string.AttachPhoto)); + } + } + if (currentMessageObject.messageOwner.ttl != 0) { + menuItem.hideSubItem(gallery_menu_save); + shareButton.setVisibility(View.GONE); + menuItem.hideSubItem(gallery_menu_share); + } else { + menuItem.showSubItem(gallery_menu_save); + shareButton.setVisibility(videoPlayerControlFrameLayout == null || videoPlayerControlFrameLayout.getVisibility() != View.VISIBLE ? View.VISIBLE : View.GONE); + if (shareButton.getVisibility() == View.VISIBLE) { + menuItem.hideSubItem(gallery_menu_share); + } else { + menuItem.showSubItem(gallery_menu_share); + } + } } } else if (!imagesArrLocations.isEmpty()) { nameTextView.setText(""); dateTextView.setText(""); - if (avatarsUserId == UserConfig.getClientUserId() && !avatarsArr.isEmpty()) { + if (avatarsDialogId == UserConfig.getClientUserId() && !avatarsArr.isEmpty()) { menuItem.showSubItem(gallery_menu_delete); } else { menuItem.hideSubItem(gallery_menu_delete); @@ -2479,7 +2787,12 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, currentIndex + 1, imagesArrLocations.size())); menuItem.showSubItem(gallery_menu_save); - shareButton.setVisibility(View.VISIBLE); + shareButton.setVisibility(videoPlayerControlFrameLayout == null || videoPlayerControlFrameLayout.getVisibility() != View.VISIBLE ? View.VISIBLE : View.GONE); + if (shareButton.getVisibility() == View.VISIBLE) { + menuItem.hideSubItem(gallery_menu_share); + } else { + menuItem.showSubItem(gallery_menu_share); + } } else if (!imagesArrLocals.isEmpty()) { Object object = imagesArrLocals.get(index); if (index < 0 || index >= imagesArrLocals.size()) { @@ -2542,6 +2855,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animationStartTime = 0; imageMoveAnimation = null; changeModeAnimation = null; + if (aspectRatioFrameLayout != null) { + aspectRatioFrameLayout.setVisibility(View.INVISIBLE); + } + releasePlayer(); pinchStartDistance = 0; pinchStartScale = 1; @@ -2610,12 +2927,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat CharSequence str = Emoji.replaceEmoji(new SpannableStringBuilder(caption.toString()), MessageObject.getTextPaint().getFontMetricsInt(), AndroidUtilities.dp(20), false); captionTextView.setTag(str); captionTextView.setText(str); - ViewProxy.setAlpha(captionTextView, bottomLayout.getVisibility() == View.VISIBLE || pickerView.getVisibility() == View.VISIBLE ? 1.0f : 0.0f); + captionTextView.setAlpha(bottomLayout.getVisibility() == View.VISIBLE || pickerView.getVisibility() == View.VISIBLE ? 1.0f : 0.0f); AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { captionTextViewOld.setTag(null); - captionTextViewOld.clearAnimation(); captionTextViewOld.setVisibility(View.INVISIBLE); captionTextViewNew.setVisibility(bottomLayout.getVisibility() == View.VISIBLE || pickerView.getVisibility() == View.VISIBLE ? View.VISIBLE : View.INVISIBLE); } @@ -2623,7 +2939,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else { captionItem.setIcon(R.drawable.photo_text); captionTextView.setTag(null); - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); } } @@ -2640,11 +2955,19 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat boolean isVideo = false; if (currentMessageObject != null) { MessageObject messageObject = imagesArr.get(index); - f = FileLoader.getPathToMessage(messageObject.messageOwner); + if (!TextUtils.isEmpty(messageObject.messageOwner.attachPath)) { + f = new File(messageObject.messageOwner.attachPath); + if (!f.exists()) { + f = null; + } + } + if (f == null) { + f = FileLoader.getPathToMessage(messageObject.messageOwner); + } isVideo = messageObject.isVideo(); } else if (currentFileLocation != null) { TLRPC.FileLocation location = imagesArrLocations.get(index); - f = FileLoader.getPathToAttach(location, avatarsUserId != 0); + f = FileLoader.getPathToAttach(location, avatarsDialogId != 0); } else if (currentPathObject != null) { f = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_DOCUMENT), currentFileNames[a]); if (!f.exists()) { @@ -2755,6 +3078,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else { imageReceiver.setImageBitmap(parentActivity.getResources().getDrawable(R.drawable.photoview_placeholder)); } + } else if (messageObject != null && currentAnimation != null) { + imageReceiver.setImageBitmap(currentAnimation); + currentAnimation.setSecondParentView(containerView); } else { imageReceiver.setNeedsQualityThumb(false); Bitmap placeHolder = null; @@ -2765,7 +3091,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat size[0] = -1; } TLRPC.PhotoSize thumbLocation = messageObject != null ? FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, 100) : null; - imageReceiver.setImage(fileLocation, null, null, placeHolder != null ? new BitmapDrawable(null, placeHolder) : null, thumbLocation != null ? thumbLocation.location : null, "b", size[0], null, avatarsUserId != 0); + imageReceiver.setImage(fileLocation, null, null, placeHolder != null ? new BitmapDrawable(null, placeHolder) : null, thumbLocation != null ? thumbLocation.location : null, "b", size[0], null, avatarsDialogId != 0); } } else { imageReceiver.setNeedsQualityThumb(false); @@ -2863,7 +3189,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat NotificationCenter.getInstance().addObserver(this, NotificationCenter.FileLoadProgressChanged); NotificationCenter.getInstance().addObserver(this, NotificationCenter.mediaCountDidLoaded); NotificationCenter.getInstance().addObserver(this, NotificationCenter.mediaDidLoaded); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.userPhotosLoaded); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.dialogPhotosLoaded); NotificationCenter.getInstance().addObserver(this, NotificationCenter.emojiDidLoaded); placeProvider = provider; @@ -2880,6 +3206,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (object != null) { disableShowCheck = true; animationInProgress = 1; + if (messageObject != null) { + currentAnimation = object.imageReceiver.getAnimation(); + } + onPhotoShow(messageObject, fileLocation, messages, photos, index, object); final Rect drawRegion = object.imageReceiver.getDrawRegion(); @@ -2891,13 +3221,13 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animatingImageView.setNeedRadius(object.radius != 0); animatingImageView.setImageBitmap(object.thumb); - ViewProxy.setAlpha(animatingImageView, 1.0f); - ViewProxy.setPivotX(animatingImageView, 0.0f); - ViewProxy.setPivotY(animatingImageView, 0.0f); - ViewProxy.setScaleX(animatingImageView, object.scale); - ViewProxy.setScaleY(animatingImageView, object.scale); - ViewProxy.setTranslationX(animatingImageView, object.viewX + drawRegion.left * object.scale); - ViewProxy.setTranslationY(animatingImageView, object.viewY + drawRegion.top * object.scale); + animatingImageView.setAlpha(1.0f); + animatingImageView.setPivotX(0.0f); + animatingImageView.setPivotY(0.0f); + animatingImageView.setScaleX(object.scale); + animatingImageView.setScaleY(object.scale); + animatingImageView.setTranslationX(object.viewX + drawRegion.left * object.scale); + animatingImageView.setTranslationY(object.viewY + drawRegion.top * object.scale); final ViewGroup.LayoutParams layoutParams = animatingImageView.getLayoutParams(); layoutParams.width = (drawRegion.right - drawRegion.left); layoutParams.height = (drawRegion.bottom - drawRegion.top); @@ -2926,10 +3256,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat clipTop = Math.max(clipTop, clipVertical); clipBottom = Math.max(clipBottom, clipVertical); - animationValues[0][0] = ViewProxy.getScaleX(animatingImageView); - animationValues[0][1] = ViewProxy.getScaleY(animatingImageView); - animationValues[0][2] = ViewProxy.getTranslationX(animatingImageView); - animationValues[0][3] = ViewProxy.getTranslationY(animatingImageView); + animationValues[0][0] = animatingImageView.getScaleX(); + animationValues[0][1] = animatingImageView.getScaleY(); + animationValues[0][2] = animatingImageView.getTranslationX(); + animationValues[0][3] = animatingImageView.getTranslationY(); animationValues[0][4] = clipHorizontal * object.scale; animationValues[0][5] = clipTop * object.scale; animationValues[0][6] = clipBottom * object.scale; @@ -2946,19 +3276,19 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animatingImageView.setAnimationProgress(0); backgroundDrawable.setAlpha(0); - ViewProxy.setAlpha(containerView, 0); + containerView.setAlpha(0); - final AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + final AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(animatingImageView, "animationProgress", 0.0f, 1.0f), - ObjectAnimatorProxy.ofInt(backgroundDrawable, "alpha", 0, 255), - ObjectAnimatorProxy.ofFloat(containerView, "alpha", 0.0f, 1.0f) + ObjectAnimator.ofFloat(animatingImageView, "animationProgress", 0.0f, 1.0f), + ObjectAnimator.ofInt(backgroundDrawable, "alpha", 0, 255), + ObjectAnimator.ofFloat(containerView, "alpha", 0.0f, 1.0f) ); animationEndRunnable = new Runnable() { @Override public void run() { - if (containerView == null) { + if (containerView == null || windowView == null) { return; } if (Build.VERSION.SDK_INT >= 18) { @@ -2989,7 +3319,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { @@ -3006,7 +3336,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - NotificationCenter.getInstance().setAllowedNotificationsDutingAnimation(new int[]{NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.mediaCountDidLoaded, NotificationCenter.mediaDidLoaded}); + NotificationCenter.getInstance().setAllowedNotificationsDutingAnimation(new int[]{NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.mediaCountDidLoaded, NotificationCenter.mediaDidLoaded, NotificationCenter.dialogPhotosLoaded}); NotificationCenter.getInstance().setAnimationInProgress(true); animatorSet.start(); } @@ -3031,7 +3361,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } backgroundDrawable.setAlpha(255); - ViewProxy.setAlpha(containerView, 1.0f); + containerView.setAlpha(1.0f); onPhotoShow(messageObject, fileLocation, messages, photos, index, object); } } @@ -3072,6 +3402,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return; } + releasePlayer(); captionEditText.onDestroy(); parentChatActivity = null; NotificationCenter.getInstance().removeObserver(this, NotificationCenter.FileDidFailedLoad); @@ -3079,7 +3410,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat NotificationCenter.getInstance().removeObserver(this, NotificationCenter.FileLoadProgressChanged); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.mediaCountDidLoaded); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.mediaDidLoaded); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.userPhotosLoaded); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.dialogPhotosLoaded); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.emojiDidLoaded); ConnectionsManager.getInstance().cancelRequestsForGuid(classGuid); @@ -3098,7 +3429,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animatingImageView.setVisibility(View.VISIBLE); containerView.invalidate(); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + AnimatorSet animatorSet = new AnimatorSet(); final ViewGroup.LayoutParams layoutParams = animatingImageView.getLayoutParams(); Rect drawRegion = null; @@ -3124,10 +3455,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat float height = layoutParams.height * scale * scale2; float xPos = (AndroidUtilities.displaySize.x - width) / 2.0f; float yPos = (AndroidUtilities.displaySize.y - AndroidUtilities.statusBarHeight - height) / 2.0f; - ViewProxy.setTranslationX(animatingImageView, xPos + translationX); - ViewProxy.setTranslationY(animatingImageView, yPos + translationY); - ViewProxy.setScaleX(animatingImageView, scale * scale2); - ViewProxy.setScaleY(animatingImageView, scale * scale2); + animatingImageView.setTranslationX(xPos + translationX); + animatingImageView.setTranslationY(yPos + translationY); + animatingImageView.setScaleX(scale * scale2); + animatingImageView.setScaleY(scale * scale2); if (object != null) { object.imageReceiver.setVisible(false, true); @@ -3148,10 +3479,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat clipTop = Math.max(clipTop, clipVertical); clipBottom = Math.max(clipBottom, clipVertical); - animationValues[0][0] = ViewProxy.getScaleX(animatingImageView); - animationValues[0][1] = ViewProxy.getScaleY(animatingImageView); - animationValues[0][2] = ViewProxy.getTranslationX(animatingImageView); - animationValues[0][3] = ViewProxy.getTranslationY(animatingImageView); + animationValues[0][0] = animatingImageView.getScaleX(); + animationValues[0][1] = animatingImageView.getScaleY(); + animationValues[0][2] = animatingImageView.getTranslationX(); + animationValues[0][3] = animatingImageView.getTranslationY(); animationValues[0][4] = 0; animationValues[0][5] = 0; animationValues[0][6] = 0; @@ -3167,16 +3498,16 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animationValues[1][7] = object.radius; animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(animatingImageView, "animationProgress", 0.0f, 1.0f), - ObjectAnimatorProxy.ofInt(backgroundDrawable, "alpha", 0), - ObjectAnimatorProxy.ofFloat(containerView, "alpha", 0.0f) + ObjectAnimator.ofFloat(animatingImageView, "animationProgress", 0.0f, 1.0f), + ObjectAnimator.ofInt(backgroundDrawable, "alpha", 0), + ObjectAnimator.ofFloat(containerView, "alpha", 0.0f) ); } else { animatorSet.playTogether( - ObjectAnimatorProxy.ofInt(backgroundDrawable, "alpha", 0), - ObjectAnimatorProxy.ofFloat(animatingImageView, "alpha", 0.0f), - ObjectAnimatorProxy.ofFloat(animatingImageView, "translationY", translationY >= 0 ? AndroidUtilities.displaySize.y : -AndroidUtilities.displaySize.y), - ObjectAnimatorProxy.ofFloat(containerView, "alpha", 0.0f) + ObjectAnimator.ofInt(backgroundDrawable, "alpha", 0), + ObjectAnimator.ofFloat(animatingImageView, "alpha", 0.0f), + ObjectAnimator.ofFloat(animatingImageView, "translationY", translationY >= 0 ? AndroidUtilities.displaySize.y : -AndroidUtilities.displaySize.y), + ObjectAnimator.ofFloat(containerView, "alpha", 0.0f) ); } @@ -3194,7 +3525,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { @@ -3212,12 +3543,12 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } animatorSet.start(); } else { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "scaleX", 0.9f), - ObjectAnimatorProxy.ofFloat(containerView, "scaleY", 0.9f), - ObjectAnimatorProxy.ofInt(backgroundDrawable, "alpha", 0), - ObjectAnimatorProxy.ofFloat(containerView, "alpha", 0.0f) + ObjectAnimator.ofFloat(containerView, "scaleX", 0.9f), + ObjectAnimator.ofFloat(containerView, "scaleY", 0.9f), + ObjectAnimator.ofInt(backgroundDrawable, "alpha", 0), + ObjectAnimator.ofFloat(containerView, "alpha", 0.0f) ); animationInProgress = 2; animationEndRunnable = new Runnable() { @@ -3231,15 +3562,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } animationInProgress = 0; onPhotoClosed(object); - ViewProxy.setScaleX(containerView, 1.0f); - ViewProxy.setScaleY(containerView, 1.0f); - containerView.clearAnimation(); + containerView.setScaleX(1.0f); + containerView.setScaleY(1.0f); } }; animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (animationEndRunnable != null) { animationEndRunnable.run(); animationEndRunnable = null; @@ -3252,12 +3582,18 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } animatorSet.start(); } + if (currentAnimation != null) { + currentAnimation.setSecondParentView(null); + currentAnimation = null; + centerImage.setImageBitmap((Drawable) null); + } } public void destroyPhotoViewer() { if (parentActivity == null || windowView == null) { return; } + releasePlayer(); try { if (windowView.getParent() != null) { WindowManager wm = (WindowManager) parentActivity.getSystemService(Context.WINDOW_SERVICE); @@ -3280,6 +3616,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat currentFileLocation = null; currentPathObject = null; currentThumb = null; + if (currentAnimation != null) { + currentAnimation.setSecondParentView(null); + currentAnimation = null; + } for (int a = 0; a < 3; a++) { if (radialProgressViews[a] != null) { radialProgressViews[a].setBackgroundState(-1, false); @@ -3330,6 +3670,16 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat redraw(0); //workaround for camera bug } + public void onPause() { + if (currentAnimation != null) { + closePhoto(false, false); + return; + } + if (captionDoneItem.getVisibility() != View.GONE) { + closeCaptionEnter(true); + } + } + public boolean isVisible() { return isVisible && placeProvider != null; } @@ -3668,15 +4018,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animateToX = newTx; animateToY = newTy; animationStartTime = System.currentTimeMillis(); - imageMoveAnimation = new AnimatorSetProxy(); + imageMoveAnimation = new AnimatorSet(); imageMoveAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(this, "animationValue", 0, 1) + ObjectAnimator.ofFloat(this, "animationValue", 0, 1) ); imageMoveAnimation.setInterpolator(interpolator); imageMoveAnimation.setDuration(duration); imageMoveAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { imageMoveAnimation = null; containerView.invalidate(); } @@ -3693,6 +4043,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return animationValue; } + @SuppressLint("NewApi") private void onDraw(Canvas canvas) { if (animationInProgress == 1 || !isVisible && animationInProgress != 2) { return; @@ -3819,27 +4170,36 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat canvas.restore(); } - float tranlateX = currentTranslationX; + float translateX = currentTranslationX; float scaleDiff = 0; float alpha = 1; - if (!zoomAnimation && tranlateX > maxX && currentEditMode == 0) { - alpha = Math.min(1.0f, (tranlateX - maxX) / canvas.getWidth()); + if (!zoomAnimation && translateX > maxX && currentEditMode == 0) { + alpha = Math.min(1.0f, (translateX - maxX) / canvas.getWidth()); scaleDiff = alpha * 0.3f; alpha = 1.0f - alpha; - tranlateX = maxX; + translateX = maxX; } + boolean drawTextureView = Build.VERSION.SDK_INT >= 16 && aspectRatioFrameLayout != null && aspectRatioFrameLayout.getVisibility() == View.VISIBLE; if (centerImage.hasBitmapImage()) { canvas.save(); canvas.translate(getContainerViewWidth() / 2 + getAdditionX(), getContainerViewHeight() / 2 + getAdditionY()); - canvas.translate(tranlateX, currentTranslationY); + canvas.translate(translateX, currentTranslationY); canvas.scale(currentScale - scaleDiff, currentScale - scaleDiff); if (currentEditMode == 1) { - photoCropView.setBitmapParams(currentScale, tranlateX, currentTranslationY); + photoCropView.setBitmapParams(currentScale, translateX, currentTranslationY); } int bitmapWidth = centerImage.getBitmapWidth(); int bitmapHeight = centerImage.getBitmapHeight(); + if (drawTextureView && textureUploaded) { + float scale1 = bitmapWidth / (float) bitmapHeight; + float scale2 = videoTextureView.getMeasuredWidth() / (float) videoTextureView.getMeasuredHeight(); + if (Math.abs(scale1 - scale2) > 0.01f) { + bitmapWidth = videoTextureView.getMeasuredWidth(); + bitmapHeight = videoTextureView.getMeasuredHeight(); + } + } float scaleX = (float) getContainerViewWidth() / (float) bitmapWidth; float scaleY = (float) getContainerViewHeight() / (float) bitmapHeight; @@ -3847,17 +4207,41 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat int width = (int) (bitmapWidth * scale); int height = (int) (bitmapHeight * scale); - centerImage.setAlpha(alpha); - centerImage.setImageCoords(-width / 2, -height / 2, width, height); - centerImage.draw(canvas); + if (!drawTextureView || !textureUploaded || !videoCrossfadeStarted || videoCrossfadeAlpha != 1.0f) { + centerImage.setAlpha(alpha); + centerImage.setImageCoords(-width / 2, -height / 2, width, height); + centerImage.draw(canvas); + } + if (drawTextureView) { + if (!videoCrossfadeStarted && textureUploaded) { + videoCrossfadeStarted = true; + videoCrossfadeAlpha = 0.0f; + videoCrossfadeAlphaLastTime = System.currentTimeMillis(); + } + canvas.translate(-width / 2, -height / 2); + videoTextureView.setAlpha(alpha * videoCrossfadeAlpha); + aspectRatioFrameLayout.draw(canvas); + if (videoCrossfadeStarted && videoCrossfadeAlpha < 1.0f) { + long newUpdateTime = System.currentTimeMillis(); + long dt = newUpdateTime - videoCrossfadeAlphaLastTime; + videoCrossfadeAlphaLastTime = newUpdateTime; + videoCrossfadeAlpha += dt / 300.0f; + containerView.invalidate(); + if (videoCrossfadeAlpha > 1.0f) { + videoCrossfadeAlpha = 1.0f; + } + } + } + canvas.restore(); + } + if (!drawTextureView && (videoPlayerControlFrameLayout == null || videoPlayerControlFrameLayout.getVisibility() != View.VISIBLE)) { + canvas.save(); + canvas.translate(translateX, currentTranslationY / currentScale); + radialProgressViews[0].setScale(1.0f - scaleDiff); + radialProgressViews[0].setAlpha(alpha); + radialProgressViews[0].onDraw(canvas); canvas.restore(); } - canvas.save(); - canvas.translate(tranlateX, currentTranslationY / currentScale); - radialProgressViews[0].setScale(1.0f - scaleDiff); - radialProgressViews[0].setAlpha(alpha); - radialProgressViews[0].onDraw(canvas); - canvas.restore(); if (sideImage == leftImage) { if (sideImage.hasBitmapImage()) { @@ -3912,35 +4296,38 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } - private void onActionClick() { + private void onActionClick(boolean download) { if (currentMessageObject == null || currentFileNames[0] == null) { return; } - boolean loadFile = false; + File file = null; if (currentMessageObject.messageOwner.attachPath != null && currentMessageObject.messageOwner.attachPath.length() != 0) { - File f = new File(currentMessageObject.messageOwner.attachPath); - if (f.exists()) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.fromFile(f), "video/mp4"); - parentActivity.startActivityForResult(intent, 500); - } else { - loadFile = true; - } - } else { - File cacheFile = FileLoader.getPathToMessage(currentMessageObject.messageOwner); - if (cacheFile.exists()) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.fromFile(cacheFile), "video/mp4"); - parentActivity.startActivityForResult(intent, 500); - } else { - loadFile = true; + file = new File(currentMessageObject.messageOwner.attachPath); + if (!file.exists()) { + file = null; } } - if (loadFile) { - if (!FileLoader.getInstance().isLoadingFile(currentFileNames[0])) { - FileLoader.getInstance().loadFile(currentMessageObject.getDocument(), true, false); + if (file == null) { + file = FileLoader.getPathToMessage(currentMessageObject.messageOwner); + if (!file.exists()) { + file = null; + } + } + if (file == null) { + if (download) { + if (!FileLoader.getInstance().isLoadingFile(currentFileNames[0])) { + FileLoader.getInstance().loadFile(currentMessageObject.getDocument(), true, false); + } else { + FileLoader.getInstance().cancelLoadFile(currentMessageObject.getDocument()); + } + } + } else { + if (Build.VERSION.SDK_INT >= 16) { + preparePlayer(file, true); } else { - FileLoader.getInstance().cancelLoadFile(currentMessageObject.getDocument()); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(file), "video/mp4"); + parentActivity.startActivityForResult(intent, 500); } } } @@ -3986,14 +4373,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return false; } if (canShowBottom) { - if (radialProgressViews[0] != null && containerView != null) { + boolean drawTextureView = Build.VERSION.SDK_INT >= 16 && aspectRatioFrameLayout != null && aspectRatioFrameLayout.getVisibility() == View.VISIBLE; + if (radialProgressViews[0] != null && containerView != null && !drawTextureView) { int state = radialProgressViews[0].backgroundState; if (state > 0 && state <= 3) { float x = e.getX(); float y = e.getY(); - if (x >= (getContainerViewWidth() - AndroidUtilities.dp(64)) / 2.0f && x <= (getContainerViewWidth() + AndroidUtilities.dp(64)) / 2.0f && - y >= (getContainerViewHeight() - AndroidUtilities.dp(64)) / 2.0f && y <= (getContainerViewHeight() + AndroidUtilities.dp(64)) / 2.0f) { - onActionClick(); + if (x >= (getContainerViewWidth() - AndroidUtilities.dp(100)) / 2.0f && x <= (getContainerViewWidth() + AndroidUtilities.dp(100)) / 2.0f && + y >= (getContainerViewHeight() - AndroidUtilities.dp(100)) / 2.0f && y <= (getContainerViewHeight() + AndroidUtilities.dp(100)) / 2.0f) { + onActionClick(true); checkProgress(0, true); return true; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java index 41e6345d3..8573bfb89 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java @@ -58,7 +58,6 @@ import org.telegram.ui.ActionBar.ActionBarMenu; import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.ChatActivityEnterView; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.PopupAudioView; import org.telegram.ui.Components.RecordStatusDrawable; @@ -105,7 +104,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC private boolean startedMoving = false; private Runnable onAnimationEndRunnable = null; - private class FrameLayoutTouch extends FrameLayoutFixed { + private class FrameLayoutTouch extends FrameLayout { public FrameLayoutTouch(Context context) { super(context); } @@ -135,7 +134,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC } } - public class FrameLayoutAnimationListener extends FrameLayoutFixed { + public class FrameLayoutAnimationListener extends FrameLayout { public FrameLayoutAnimationListener(Context context) { super(context); } @@ -293,7 +292,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC popupContainer.addView(chatActivityEnterView, LayoutHelper.createRelative(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, RelativeLayout.ALIGN_PARENT_BOTTOM)); chatActivityEnterView.setDelegate(new ChatActivityEnterView.ChatActivityEnterViewDelegate() { @Override - public void onMessageSend(String message) { + public void onMessageSend(CharSequence message) { if (currentMessageObject == null) { return; } @@ -311,7 +310,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC } @Override - public void onMessageEditEnd() { + public void onMessageEditEnd(boolean loading) { } @@ -360,7 +359,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC View view = menu.addItemResource(2, R.layout.popup_count_layout); countText = (TextView) view.findViewById(R.id.count_text); - avatarContainer = new FrameLayoutFixed(this); + avatarContainer = new FrameLayout(this); avatarContainer.setPadding(AndroidUtilities.dp(4), 0, AndroidUtilities.dp(4), 0); actionBar.addView(avatarContainer); FrameLayout.LayoutParams layoutParams2 = (FrameLayout.LayoutParams) avatarContainer.getLayoutParams(); @@ -844,29 +843,31 @@ public class PopupNotificationActivity extends Activity implements NotificationC if (rightView != null) { int offset = ((FrameLayout.LayoutParams) rightView.getLayoutParams()).leftMargin; reuseView(rightView); - rightView = getViewForMessage(currentMessageNum + 1, false); - int widht = AndroidUtilities.displaySize.x - AndroidUtilities.dp(24); - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) rightView.getLayoutParams(); - layoutParams.gravity = Gravity.TOP | Gravity.LEFT; - layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; - layoutParams.width = widht; - layoutParams.leftMargin = offset; - rightView.setLayoutParams(layoutParams); - rightView.invalidate(); + if ((rightView = getViewForMessage(currentMessageNum + 1, false)) != null) { + int widht = AndroidUtilities.displaySize.x - AndroidUtilities.dp(24); + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) rightView.getLayoutParams(); + layoutParams.gravity = Gravity.TOP | Gravity.LEFT; + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.width = widht; + layoutParams.leftMargin = offset; + rightView.setLayoutParams(layoutParams); + rightView.invalidate(); + } } } else if (move == 4) { if (leftView != null) { int offset = ((FrameLayout.LayoutParams) leftView.getLayoutParams()).leftMargin; reuseView(leftView); - leftView = getViewForMessage(0, false); - int widht = AndroidUtilities.displaySize.x - AndroidUtilities.dp(24); - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) leftView.getLayoutParams(); - layoutParams.gravity = Gravity.TOP | Gravity.LEFT; - layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; - layoutParams.width = widht; - layoutParams.leftMargin = offset; - leftView.setLayoutParams(layoutParams); - leftView.invalidate(); + if ((leftView = getViewForMessage(0, false)) != null) { + int widht = AndroidUtilities.displaySize.x - AndroidUtilities.dp(24); + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) leftView.getLayoutParams(); + layoutParams.gravity = Gravity.TOP | Gravity.LEFT; + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.width = widht; + layoutParams.leftMargin = offset; + leftView.setLayoutParams(layoutParams); + leftView.invalidate(); + } } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PrivacyUsersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PrivacyUsersActivity.java index 2dd66ce53..9c7412527 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PrivacyUsersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PrivacyUsersActivity.java @@ -11,7 +11,6 @@ package org.telegram.ui; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.os.Build; import android.os.Bundle; import android.view.Gravity; import android.view.MotionEvent; @@ -155,9 +154,7 @@ public class PrivacyUsersActivity extends BaseFragment implements NotificationCe listView.setDivider(null); listView.setDividerHeight(0); listView.setAdapter(listViewAdapter = new ListAdapter(context)); - if (Build.VERSION.SDK_INT >= 11) { - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView); layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams(); layoutParams.width = LayoutHelper.MATCH_PARENT; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java index 4ffd7be05..da09dbca9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java @@ -8,6 +8,8 @@ package org.telegram.ui; +import android.animation.Animator; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.StateListAnimator; import android.annotation.SuppressLint; @@ -40,9 +42,7 @@ import android.widget.ImageView; import org.telegram.messenger.AndroidUtilities; import org.telegram.PhoneFormat.PhoneFormat; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.ChatObject; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessagesStorage; @@ -67,7 +67,6 @@ import org.telegram.messenger.MessageObject; import org.telegram.messenger.UserConfig; import org.telegram.messenger.Utilities; import org.telegram.ui.ActionBar.BackDrawable; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.ui.ActionBar.SimpleTextView; import org.telegram.ui.Cells.AboutLinkCell; import org.telegram.ui.Cells.DividerCell; @@ -106,7 +105,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private SimpleTextView nameTextView[] = new SimpleTextView[2]; private SimpleTextView onlineTextView[] = new SimpleTextView[2]; private ImageView writeButton; - private AnimatorSetProxy writeButtonAnimation; + private AnimatorSet writeButtonAnimation; private AvatarDrawable avatarDrawable; private ActionBarMenuItem animatingItem; private TopView topView; @@ -123,6 +122,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private boolean openAnimationInProgress; private boolean playProfileAnimation; + private boolean allowProfileAnimation = true; private int extraHeight; private int initialAnimationExtraHeight; private float animationProgress; @@ -152,6 +152,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private final static int set_admins = 11; private final static int edit_channel = 12; private final static int convert_to_supergroup = 13; + private final static int add_shortcut = 14; private int emptyRow; private int emptyRowChat; @@ -191,7 +192,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), ActionBar.getCurrentActionBarHeight() + (!AndroidUtilities.isTablet() ? AndroidUtilities.statusBarHeight : 0) + AndroidUtilities.dp(91)); + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), ActionBar.getCurrentActionBarHeight() + (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + AndroidUtilities.dp(91)); } @Override @@ -343,21 +344,21 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. ActionBar actionBar = new ActionBar(context) { @Override public boolean onTouchEvent(MotionEvent event) { - return super.onTouchEvent(event); //TODO + return super.onTouchEvent(event); } }; actionBar.setItemsBackgroundColor(AvatarDrawable.getButtonColorForId(user_id != 0 || ChatObject.isChannel(chat_id) && !currentChat.megagroup ? 5 : chat_id)); actionBar.setBackButtonDrawable(new BackDrawable(false)); actionBar.setCastShadows(false); actionBar.setAddToContainer(false); - actionBar.setOccupyStatusBar(!AndroidUtilities.isTablet()); + actionBar.setOccupyStatusBar(Build.VERSION.SDK_INT >= 21 && !AndroidUtilities.isTablet()); return actionBar; } @Override public View createView(Context context) { hasOwnBackground = true; - extraHeight = 88; + extraHeight = AndroidUtilities.dp(88); actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { @Override public void onItemClick(final int id) { @@ -396,7 +397,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. MessagesController.getInstance().blockUser(user_id); } else { MessagesController.getInstance().unblockUser(user_id); - SendMessagesHelper.getInstance().sendMessage("/start", user_id, null, null, false, false, null, null, null); + SendMessagesHelper.getInstance().sendMessage("/start", user_id, null, null, false, null, null, null); finishFragment(); } } @@ -505,6 +506,27 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. Bundle args = new Bundle(); args.putInt("chat_id", chat_id); presentFragment(new ConvertGroupActivity(args)); + } else if (id == add_shortcut) { + try { + long did; + if (currentEncryptedChat != null) { + did = ((long) currentEncryptedChat.id) << 32; + } else if (user_id != 0) { + did = user_id; + } else if (chat_id != 0) { + did = -chat_id; + } else { + return; + } + AndroidUtilities.installShortcut(did); + /*try { + Toast.makeText(getParentActivity(), LocaleController.getString("ShortcutAdded", R.string.ShortcutAdded), Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + FileLog.e("tmessages", e); + }*/ + } catch (Exception e) { + FileLog.e("tmessages", e); + } } } }); @@ -535,6 +557,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. return false; } }; + listView.setTag(6); listView.setPadding(0, AndroidUtilities.dp(88), 0, 0); listView.setBackgroundColor(0xffffffff); listView.setVerticalScrollBarEnabled(false); @@ -771,8 +794,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. avatarImage = new BackupImageView(context); avatarImage.setRoundRadius(AndroidUtilities.dp(21)); - ViewProxy.setPivotX(avatarImage, 0); - ViewProxy.setPivotY(avatarImage, 0); + avatarImage.setPivotX(0); + avatarImage.setPivotY(0); frameLayout.addView(avatarImage, LayoutHelper.createFrame(42, 42, Gravity.TOP | Gravity.LEFT, 64, 0, 0, 0)); avatarImage.setOnClickListener(new View.OnClickListener() { @Override @@ -804,8 +827,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. nameTextView[a].setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); nameTextView[a].setLeftDrawableTopPadding(-AndroidUtilities.dp(1.3f)); nameTextView[a].setRightDrawableTopPadding(-AndroidUtilities.dp(1.3f)); - ViewProxy.setPivotX(nameTextView[a], 0); - ViewProxy.setPivotY(nameTextView[a], 0); + nameTextView[a].setPivotX(0); + nameTextView[a].setPivotY(0); frameLayout.addView(nameTextView[a], LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 118, 0, a == 0 ? 48 : 0, 0)); onlineTextView[a] = new SimpleTextView(context); @@ -942,14 +965,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. public void onClick(DialogInterface dialogInterface, int i) { if (i == 0) { try { - if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText("@" + user.username); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", "@" + user.username); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", "@" + user.username); + clipboard.setPrimaryClip(clip); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -978,14 +996,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } else if (i == 1) { try { - if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText("+" + user.phone); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", "+" + user.phone); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", "+" + user.phone); + clipboard.setPrimaryClip(clip); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -1009,14 +1022,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (about == null) { return; } - if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(about); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", about); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", about); + clipboard.setPrimaryClip(clip); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -1077,7 +1085,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. return; } loadingUsers = true; - final int delay = Build.VERSION.SDK_INT >= 11 && !participantsMap.isEmpty() && reload ? 300 : 0; + final int delay = !participantsMap.isEmpty() && reload ? 300 : 0; final TLRPC.TL_channels_getParticipants req = new TLRPC.TL_channels_getParticipants(); req.channel = MessagesController.getInputChannel(chat_id); @@ -1172,6 +1180,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (extraHeight != newOffset) { extraHeight = newOffset; topView.invalidate(); + if (playProfileAnimation) { + allowProfileAnimation = extraHeight != 0; + } needLayout(); } } @@ -1192,13 +1203,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. listView.setTopGlowOffset(extraHeight); if (writeButton != null) { - if (Build.VERSION.SDK_INT < 11) { - layoutParams = (FrameLayout.LayoutParams) writeButton.getLayoutParams(); - layoutParams.topMargin = (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f); - writeButton.setLayoutParams(layoutParams); - } else { - ViewProxy.setTranslationY(writeButton, (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f)); - } + writeButton.setTranslationY((actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f)); if (!openAnimationInProgress) { final boolean setVisible = diff > 0.2f; @@ -1206,42 +1211,35 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (setVisible != currentVisible) { if (setVisible) { writeButton.setTag(null); - if (Build.VERSION.SDK_INT < 11) { - writeButton.setVisibility(View.VISIBLE); - } } else { writeButton.setTag(0); } if (writeButtonAnimation != null) { - AnimatorSetProxy old = writeButtonAnimation; + AnimatorSet old = writeButtonAnimation; writeButtonAnimation = null; old.cancel(); } - writeButtonAnimation = new AnimatorSetProxy(); + writeButtonAnimation = new AnimatorSet(); if (setVisible) { writeButtonAnimation.setInterpolator(new DecelerateInterpolator()); writeButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 1.0f), - ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 1.0f), - ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 1.0f) + ObjectAnimator.ofFloat(writeButton, "scaleX", 1.0f), + ObjectAnimator.ofFloat(writeButton, "scaleY", 1.0f), + ObjectAnimator.ofFloat(writeButton, "alpha", 1.0f) ); } else { writeButtonAnimation.setInterpolator(new AccelerateInterpolator()); writeButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 0.2f), - ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 0.2f), - ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 0.0f) + ObjectAnimator.ofFloat(writeButton, "scaleX", 0.2f), + ObjectAnimator.ofFloat(writeButton, "scaleY", 0.2f), + ObjectAnimator.ofFloat(writeButton, "alpha", 0.0f) ); } writeButtonAnimation.setDuration(150); writeButtonAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (writeButtonAnimation != null && writeButtonAnimation.equals(animation)) { - writeButton.clearAnimation(); - if (Build.VERSION.SDK_INT < 11) { - writeButton.setVisibility(setVisible ? View.VISIBLE : View.GONE); - } writeButtonAnimation = null; } } @@ -1252,29 +1250,20 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } float avatarY = (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() / 2.0f * (1.0f + diff) - 21 * AndroidUtilities.density + 27 * AndroidUtilities.density * diff; - if (Build.VERSION.SDK_INT < 11) { - layoutParams = (FrameLayout.LayoutParams) avatarImage.getLayoutParams(); - layoutParams.height = layoutParams.width = (int) Math.ceil(AndroidUtilities.dp(42) * (42 + 18 * diff) / 42.0f); - layoutParams.leftMargin = (int) Math.ceil(AndroidUtilities.dp(64) - AndroidUtilities.dp(47) * diff); - layoutParams.topMargin = (int) Math.ceil(avatarY); - avatarImage.setLayoutParams(layoutParams); - avatarImage.setRoundRadius(layoutParams.height / 2); - } else { - ViewProxy.setScaleX(avatarImage, (42 + 18 * diff) / 42.0f); - ViewProxy.setScaleY(avatarImage, (42 + 18 * diff) / 42.0f); - ViewProxy.setTranslationX(avatarImage, -AndroidUtilities.dp(47) * diff); - ViewProxy.setTranslationY(avatarImage, (float) Math.ceil(avatarY)); - } + avatarImage.setScaleX((42 + 18 * diff) / 42.0f); + avatarImage.setScaleY((42 + 18 * diff) / 42.0f); + avatarImage.setTranslationX(-AndroidUtilities.dp(47) * diff); + avatarImage.setTranslationY((float) Math.ceil(avatarY)); for (int a = 0; a < 2; a++) { if (nameTextView[a] == null) { continue; } - ViewProxy.setTranslationX(nameTextView[a], -21 * AndroidUtilities.density * diff); - ViewProxy.setTranslationY(nameTextView[a], (float) Math.floor(avatarY) + AndroidUtilities.dp(1.3f) + AndroidUtilities.dp(7) * diff); - ViewProxy.setTranslationX(onlineTextView[a], -21 * AndroidUtilities.density * diff); - ViewProxy.setTranslationY(onlineTextView[a], (float) Math.floor(avatarY) + AndroidUtilities.dp(24) + (float) Math.floor(11 * AndroidUtilities.density) * diff); - ViewProxy.setScaleX(nameTextView[a], 1.0f + 0.12f * diff); - ViewProxy.setScaleY(nameTextView[a], 1.0f + 0.12f * diff); + nameTextView[a].setTranslationX(-21 * AndroidUtilities.density * diff); + nameTextView[a].setTranslationY((float) Math.floor(avatarY) + AndroidUtilities.dp(1.3f) + AndroidUtilities.dp(7) * diff); + onlineTextView[a].setTranslationX(-21 * AndroidUtilities.density * diff); + onlineTextView[a].setTranslationY((float) Math.floor(avatarY) + AndroidUtilities.dp(24) + (float) Math.floor(11 * AndroidUtilities.density) * diff); + nameTextView[a].setScaleX(1.0f + 0.12f * diff); + nameTextView[a].setScaleY(1.0f + 0.12f * diff); if (a == 1 && !openAnimationInProgress) { int width; if (AndroidUtilities.isTablet()) { @@ -1282,18 +1271,18 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else { width = AndroidUtilities.displaySize.x; } - width = (int) (width - AndroidUtilities.dp(118 + 8 + 40 * (1.0f - diff)) - ViewProxy.getTranslationX(nameTextView[a])); - float width2 = nameTextView[a].getPaint().measureText(nameTextView[a].getText().toString()) * ViewProxy.getScaleX(nameTextView[a]) + nameTextView[a].getSideDrawablesSize(); + width = (int) (width - AndroidUtilities.dp(118 + 8 + 40 * (1.0f - diff)) - nameTextView[a].getTranslationX()); + float width2 = nameTextView[a].getPaint().measureText(nameTextView[a].getText().toString()) * nameTextView[a].getScaleX() + nameTextView[a].getSideDrawablesSize(); layoutParams = (FrameLayout.LayoutParams) nameTextView[a].getLayoutParams(); if (width < width2) { - layoutParams.width = (int) Math.ceil(width / ViewProxy.getScaleX(nameTextView[a])); + layoutParams.width = (int) Math.ceil(width / nameTextView[a].getScaleX()); } else { layoutParams.width = LayoutHelper.WRAP_CONTENT; } nameTextView[a].setLayoutParams(layoutParams); layoutParams = (FrameLayout.LayoutParams) onlineTextView[a].getLayoutParams(); - layoutParams.rightMargin = (int) Math.ceil(ViewProxy.getTranslationX(onlineTextView[a]) + AndroidUtilities.dp(8) + AndroidUtilities.dp(40) * (1.0f - diff)); + layoutParams.rightMargin = (int) Math.ceil(onlineTextView[a].getTranslationX() + AndroidUtilities.dp(8) + AndroidUtilities.dp(40) * (1.0f - diff)); onlineTextView[a].setLayoutParams(layoutParams); } } @@ -1425,7 +1414,6 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. updateRowsIds(); if (listAdapter != null) { listAdapter.notifyDataSetChanged(); - checkListViewScroll(); } } } else if (id == NotificationCenter.blockedUsersDidLoaded) { @@ -1454,7 +1442,6 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. updateRowsIds(); if (listAdapter != null) { listAdapter.notifyDataSetChanged(); - checkListViewScroll(); } TLRPC.Chat newChat = MessagesController.getInstance().getChat(chat_id); if (newChat != null) { @@ -1474,7 +1461,6 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. updateRowsIds(); if (listAdapter != null) { listAdapter.notifyDataSetChanged(); - checkListViewScroll(); } } } else if (id == NotificationCenter.userInfoDidLoaded) { @@ -1495,7 +1481,6 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. TLRPC.TL_decryptedMessageActionSetMessageTTL action = (TLRPC.TL_decryptedMessageActionSetMessageTTL) obj.messageOwner.action.encryptedAction; if (listAdapter != null) { listAdapter.notifyDataSetChanged(); - checkListViewScroll(); } } } @@ -1515,14 +1500,14 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. public void setPlayProfileAnimation(boolean value) { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - if (!AndroidUtilities.isTablet() && Build.VERSION.SDK_INT > 10 && preferences.getBoolean("view_animations", true)) { + if (!AndroidUtilities.isTablet() && preferences.getBoolean("view_animations", true)) { playProfileAnimation = value; } } @Override protected void onTransitionAnimationStart(boolean isOpen, boolean backward) { - if (!backward && playProfileAnimation) { + if (!backward && playProfileAnimation && allowProfileAnimation) { openAnimationInProgress = true; } NotificationCenter.getInstance().setAllowedNotificationsDutingAnimation(new int[]{NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.mediaCountDidLoaded}); @@ -1531,7 +1516,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. @Override protected void onTransitionAnimationEnd(boolean isOpen, boolean backward) { - if (!backward && playProfileAnimation) { + if (!backward && playProfileAnimation && allowProfileAnimation) { openAnimationInProgress = false; } NotificationCenter.getInstance().setAnimationInProgress(false); @@ -1543,9 +1528,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. public void setAnimationProgress(float progress) { animationProgress = progress; - ViewProxy.setAlpha(listView, progress); + listView.setAlpha(progress); - //ViewProxy.setTranslationY(listView, -AndroidUtilities.dp(48) + AndroidUtilities.dp(48) * progress); + listView.setTranslationX(AndroidUtilities.dp(48) - AndroidUtilities.dp(48) * progress); int color = AvatarDrawable.getProfileBackColorForId(user_id != 0 || ChatObject.isChannel(chat_id) && !currentChat.megagroup ? 5 : chat_id); int r = Color.red(Theme.ACTION_BAR_COLOR); @@ -1586,9 +1571,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } @Override - protected AnimatorSetProxy onCustomTransitionAnimation(final boolean isOpen, final Runnable callback) { - if (playProfileAnimation) { - final AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + protected AnimatorSet onCustomTransitionAnimation(final boolean isOpen, final Runnable callback) { + if (playProfileAnimation && allowProfileAnimation) { + final AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setDuration(180); if (Build.VERSION.SDK_INT > 15) { listView.setLayerType(View.LAYER_TYPE_HARDWARE, null); @@ -1617,49 +1602,49 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. initialAnimationExtraHeight = AndroidUtilities.dp(88); fragmentView.setBackgroundColor(0); setAnimationProgress(0); - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(this, "animationProgress", 0.0f, 1.0f)); + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(this, "animationProgress", 0.0f, 1.0f)); if (writeButton != null) { - ViewProxy.setScaleX(writeButton, 0.2f); - ViewProxy.setScaleY(writeButton, 0.2f); - ViewProxy.setAlpha(writeButton, 0.0f); - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 1.0f)); + writeButton.setScaleX(0.2f); + writeButton.setScaleY(0.2f); + writeButton.setAlpha(0.0f); + animators.add(ObjectAnimator.ofFloat(writeButton, "scaleX", 1.0f)); + animators.add(ObjectAnimator.ofFloat(writeButton, "scaleY", 1.0f)); + animators.add(ObjectAnimator.ofFloat(writeButton, "alpha", 1.0f)); } for (int a = 0; a < 2; a++) { - ViewProxy.setAlpha(onlineTextView[a], a == 0 ? 1.0f : 0.0f); - ViewProxy.setAlpha(nameTextView[a], a == 0 ? 1.0f : 0.0f); - animators.add(ObjectAnimatorProxy.ofFloat(onlineTextView[a], "alpha", a == 0 ? 0.0f : 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(nameTextView[a], "alpha", a == 0 ? 0.0f : 1.0f)); + onlineTextView[a].setAlpha(a == 0 ? 1.0f : 0.0f); + nameTextView[a].setAlpha(a == 0 ? 1.0f : 0.0f); + animators.add(ObjectAnimator.ofFloat(onlineTextView[a], "alpha", a == 0 ? 0.0f : 1.0f)); + animators.add(ObjectAnimator.ofFloat(nameTextView[a], "alpha", a == 0 ? 0.0f : 1.0f)); } if (animatingItem != null) { - ViewProxy.setAlpha(animatingItem, 1.0f); - animators.add(ObjectAnimatorProxy.ofFloat(animatingItem, "alpha", 0.0f)); + animatingItem.setAlpha(1.0f); + animators.add(ObjectAnimator.ofFloat(animatingItem, "alpha", 0.0f)); } animatorSet.playTogether(animators); } else { initialAnimationExtraHeight = extraHeight; - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(this, "animationProgress", 1.0f, 0.0f)); + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(this, "animationProgress", 1.0f, 0.0f)); if (writeButton != null) { - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 0.2f)); - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 0.2f)); - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(writeButton, "scaleX", 0.2f)); + animators.add(ObjectAnimator.ofFloat(writeButton, "scaleY", 0.2f)); + animators.add(ObjectAnimator.ofFloat(writeButton, "alpha", 0.0f)); } for (int a = 0; a < 2; a++) { - animators.add(ObjectAnimatorProxy.ofFloat(onlineTextView[a], "alpha", a == 0 ? 1.0f : 0.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(nameTextView[a], "alpha", a == 0 ? 1.0f : 0.0f)); + animators.add(ObjectAnimator.ofFloat(onlineTextView[a], "alpha", a == 0 ? 1.0f : 0.0f)); + animators.add(ObjectAnimator.ofFloat(nameTextView[a], "alpha", a == 0 ? 1.0f : 0.0f)); } if (animatingItem != null) { - ViewProxy.setAlpha(animatingItem, 0.0f); - animators.add(ObjectAnimatorProxy.ofFloat(animatingItem, "alpha", 1.0f)); + animatingItem.setAlpha(0.0f); + animators.add(ObjectAnimator.ofFloat(animatingItem, "alpha", 1.0f)); } animatorSet.playTogether(animators); } animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (Build.VERSION.SDK_INT > 15) { listView.setLayerType(View.LAYER_TYPE_NONE, null); } @@ -1717,11 +1702,15 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. object.viewY = coords[1] - AndroidUtilities.statusBarHeight; object.parentView = avatarImage; object.imageReceiver = avatarImage.getImageReceiver(); - object.user_id = user_id; + if (user_id != 0) { + object.dialogId = user_id; + } else if (chat_id != 0) { + object.dialogId = -chat_id; + } object.thumb = object.imageReceiver.getBitmap(); object.size = -1; object.radius = avatarImage.getImageReceiver().getRoundRadius(); - object.scale = ViewProxy.getScaleX(avatarImage); + object.scale = avatarImage.getScaleX(); return object; } return null; @@ -2169,13 +2158,14 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. menu.clearItems(); animatingItem = null; + ActionBarMenuItem item = null; if (user_id != 0) { if (ContactsController.getInstance().contactsDict.get(user_id) == null) { TLRPC.User user = MessagesController.getInstance().getUser(user_id); if (user == null) { return; } - ActionBarMenuItem item = menu.addItem(10, R.drawable.ic_ab_other); + item = menu.addItem(10, R.drawable.ic_ab_other); if (user.bot) { if (!user.bot_nochats) { item.addSubItem(invite_to_group, LocaleController.getString("BotInvite", R.string.BotInvite), 0); @@ -2194,7 +2184,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } } else { - ActionBarMenuItem item = menu.addItem(10, R.drawable.ic_ab_other); + item = menu.addItem(10, R.drawable.ic_ab_other); item.addSubItem(share_contact, LocaleController.getString("ShareContact", R.string.ShareContact), 0); item.addSubItem(block_contact, !userBlocked ? LocaleController.getString("BlockContact", R.string.BlockContact) : LocaleController.getString("Unblock", R.string.Unblock), 0); item.addSubItem(edit_contact, LocaleController.getString("EditContact", R.string.EditContact), 0); @@ -2214,7 +2204,6 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } if (ChatObject.isChannel(chat)) { - ActionBarMenuItem item = null; 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), 0); @@ -2226,7 +2215,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. item.addSubItem(leave_group, LocaleController.getString("LeaveMegaMenu", R.string.LeaveMegaMenu), 0); } } else { - ActionBarMenuItem item = menu.addItem(10, R.drawable.ic_ab_other); + 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), 0); } @@ -2239,10 +2228,14 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. item.addSubItem(leave_group, LocaleController.getString("DeleteAndExit", R.string.DeleteAndExit), 0); } } else { - ActionBarMenuItem item = menu.addItem(10, R.drawable.ic_ab_other); + item = menu.addItem(10, R.drawable.ic_ab_other); item.addSubItem(edit_name, LocaleController.getString("EditName", R.string.EditName), 0); } } + if (item == null) { + item = menu.addItem(10, R.drawable.ic_ab_other); + } + item.addSubItem(add_shortcut, LocaleController.getString("AddShortcut", R.string.AddShortcut), 0); } @Override @@ -2276,7 +2269,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. presentFragment(new ChatActivity(args), true); removeSelfFromStack(); TLRPC.User user = MessagesController.getInstance().getUser(user_id); - SendMessagesHelper.getInstance().sendMessage(user, dialog_id, null, true, null, null); + SendMessagesHelper.getInstance().sendMessage(user, dialog_id, null, null, null); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java index a1cd46f5e..6331dc07f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java @@ -181,7 +181,7 @@ public class ProfileNotificationsActivity extends BaseFragment implements Notifi } MessagesStorage.getInstance().setDialogFlags(dialog_id, which == 2 ? 1 : 0); editor.commit(); - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); if (which == 2) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/SecretPhotoViewer.java b/TMessagesProj/src/main/java/org/telegram/ui/SecretPhotoViewer.java index b7dbcd7cd..3125447a1 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/SecretPhotoViewer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/SecretPhotoViewer.java @@ -203,7 +203,7 @@ public class SecretPhotoViewer implements NotificationCenter.NotificationCenterD windowView.setFocusable(true); windowView.setFocusableInTouchMode(true); if (Build.VERSION.SDK_INT >= 23) { - windowView.setFitsSystemWindows(true); //TODO ? + windowView.setFitsSystemWindows(true); } containerView = new FrameLayoutDrawer(activity); @@ -263,7 +263,7 @@ public class SecretPhotoViewer implements NotificationCenter.NotificationCenterD File file = FileLoader.getPathToAttach(sizeFull); Bitmap bitmap = null; BitmapFactory.Options options = null; - if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 21) { + if (Build.VERSION.SDK_INT < 21) { options = new BitmapFactory.Options(); options.inDither = true; options.inPreferredConfig = Bitmap.Config.ARGB_8888; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java index 934573e16..f1d2cc52e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java @@ -8,6 +8,8 @@ package org.telegram.ui; +import android.animation.Animator; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.StateListAnimator; import android.annotation.SuppressLint; @@ -49,12 +51,12 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.PhoneFormat.PhoneFormat; +import org.telegram.messenger.ContactsController; import org.telegram.messenger.MediaController; import org.telegram.messenger.UserObject; import org.telegram.messenger.ApplicationLoader; @@ -76,7 +78,6 @@ import org.telegram.messenger.UserConfig; import org.telegram.messenger.MessageObject; import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.Adapters.BaseFragmentAdapter; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.ui.Cells.CheckBoxCell; import org.telegram.ui.Cells.TextInfoCell; import org.telegram.ui.Cells.EmptyCell; @@ -108,7 +109,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter private TextView nameTextView; private TextView onlineTextView; private ImageView writeButton; - private AnimatorSetProxy writeButtonAnimation; + private AnimatorSet writeButtonAnimation; private AvatarUpdater avatarUpdater = new AvatarUpdater(); private View extraHeightView; private View shadowView; @@ -249,9 +250,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter mobileDownloadRow = rowCount++; wifiDownloadRow = rowCount++; roamingDownloadRow = rowCount++; - if (Build.VERSION.SDK_INT >= 11) { - autoplayGifsRow = rowCount++; - } + autoplayGifsRow = rowCount++; saveToGalleryRow = rowCount++; messagesSectionRow = rowCount++; messagesSectionRow2 = rowCount++; @@ -266,9 +265,9 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter sendByEnterRow = rowCount++; supportSectionRow = rowCount++; supportSectionRow2 = rowCount++; - //askQuestionRow = rowCount++; + //askQuestionRow = rowCount++; // EDIT BY ME telegramFaqRow = rowCount++; - //privacyPolicyRow = rowCount++; + //privacyPolicyRow = rowCount++; // EDIT BY ME if (BuildVars.DEBUG_VERSION) { sendLogsRow = rowCount++; clearLogsRow = rowCount++; @@ -415,9 +414,8 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter presentFragment(new NotificationsSettingsActivity()); } else if (i == backgroundRow) { presentFragment(new WallpapersActivity()); - } - /* + /* EDIT BY ME else if (i == askQuestionRow) { if (getParentActivity() == null) { return; @@ -440,7 +438,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); showDialog(builder.create()); } - */ + EDIT BY ME */ else if (i == sendLogsRow) { sendLogs(); } else if (i == clearLogsRow) { @@ -501,11 +499,11 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter } else if (i == telegramFaqRow) { Browser.openUrl(getParentActivity(), LocaleController.getString("TelegramFaqUrl", R.string.TelegramFaqUrl)); } - /* + /* EDIT BY ME else if (i == privacyPolicyRow) { Browser.openUrl(getParentActivity(), LocaleController.getString("PrivacyPolicyUrl", R.string.PrivacyPolicyUrl)); } - */ + EDIT BY ME */ else if (i == contactsReimportRow) { //not implemented } else if (i == contactsSortRow) { @@ -570,12 +568,8 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_MUSIC) != 0; name = LocaleController.getString("AttachMusic", R.string.AttachMusic); } else if (a == 5) { - if (Build.VERSION.SDK_INT >= 11) { - maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_GIF) != 0; - name = LocaleController.getString("AttachGif", R.string.AttachGif); - } else { - continue; - } + maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_GIF) != 0; + name = LocaleController.getString("AttachGif", R.string.AttachGif); } CheckBoxCell checkBoxCell = new CheckBoxCell(getParentActivity()); checkBoxCell.setTag(a); @@ -592,7 +586,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter } }); } - BottomSheet.BottomSheetCell cell = new BottomSheet.BottomSheetCell(getParentActivity(), 2); + BottomSheet.BottomSheetCell cell = new BottomSheet.BottomSheetCell(getParentActivity(), 1); cell.setBackgroundResource(R.drawable.list_selector); cell.setTextAndIcon(LocaleController.getString("Save", R.string.Save).toUpperCase(), 0); cell.setTextColor(Theme.AUTODOWNLOAD_SHEET_SAVE_TEXT_COLOR); @@ -656,10 +650,49 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter } }); + listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + + private int pressCount = 0; + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + if (position == versionRow) { + pressCount++; + if (pressCount >= 2) { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle("Debug Menu"); + builder.setItems(new CharSequence[]{ + "Import Contacts", + "Reload Contacts" + }, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + ContactsController.getInstance().forceImportContacts(); + } else if (which == 1) { + ContactsController.getInstance().loadContacts(false, true); + } + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + showDialog(builder.create()); + } else { + try { + Toast.makeText(getParentActivity(), "¯\\_(ツ)_/¯", Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + return true; + } + return false; + } + }); + frameLayout.addView(actionBar); extraHeightView = new View(context); - ViewProxy.setPivotY(extraHeightView, 0); + extraHeightView.setPivotY(0); extraHeightView.setBackgroundColor(AvatarDrawable.getProfileBackColorForId(5)); frameLayout.addView(extraHeightView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 88)); @@ -669,8 +702,8 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter avatarImage = new BackupImageView(context); avatarImage.setRoundRadius(AndroidUtilities.dp(21)); - ViewProxy.setPivotX(avatarImage, 0); - ViewProxy.setPivotY(avatarImage, 0); + avatarImage.setPivotX(0); + avatarImage.setPivotY(0); frameLayout.addView(avatarImage, LayoutHelper.createFrame(42, 42, Gravity.TOP | Gravity.LEFT, 64, 0, 0, 0)); avatarImage.setOnClickListener(new View.OnClickListener() { @Override @@ -692,8 +725,8 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter nameTextView.setEllipsize(TextUtils.TruncateAt.END); nameTextView.setGravity(Gravity.LEFT); nameTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - ViewProxy.setPivotX(nameTextView, 0); - ViewProxy.setPivotY(nameTextView, 0); + nameTextView.setPivotX(0); + nameTextView.setPivotY(0); frameLayout.addView(nameTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 118, 0, 48, 0)); onlineTextView = new TextView(context); @@ -822,11 +855,11 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter object.viewY = coords[1] - AndroidUtilities.statusBarHeight; object.parentView = avatarImage; object.imageReceiver = avatarImage.getImageReceiver(); - object.user_id = UserConfig.getClientUserId(); + object.dialogId = UserConfig.getClientUserId(); object.thumb = object.imageReceiver.getBitmap(); object.size = -1; object.radius = avatarImage.getImageReceiver().getRoundRadius(); - object.scale = ViewProxy.getScaleX(avatarImage); + object.scale = avatarImage.getScaleX(); return object; } } @@ -870,7 +903,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter return 0; } - public void performAskAQuestion() { + private void performAskAQuestion() { final SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); int uid = preferences.getInt("support_id", 0); TLRPC.User supportUser = null; @@ -1008,22 +1041,17 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter if (layoutParams.topMargin != newTop) { layoutParams.topMargin = newTop; listView.setLayoutParams(layoutParams); - ViewProxy.setTranslationY(extraHeightView, newTop); + extraHeightView.setTranslationY(newTop); } } if (avatarImage != null) { float diff = extraHeight / (float) AndroidUtilities.dp(88); - ViewProxy.setScaleY(extraHeightView, diff); - ViewProxy.setTranslationY(shadowView, newTop + extraHeight); + extraHeightView.setScaleY(diff); + shadowView.setTranslationY(newTop + extraHeight); - if (Build.VERSION.SDK_INT < 11) { - layoutParams = (FrameLayout.LayoutParams) writeButton.getLayoutParams(); - layoutParams.topMargin = (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f); - writeButton.setLayoutParams(layoutParams); - } else { - ViewProxy.setTranslationY(writeButton, (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f)); - } + + writeButton.setTranslationY((actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f)); final boolean setVisible = diff > 0.2f; boolean currentVisible = writeButton.getTag() == null; @@ -1035,32 +1063,31 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter writeButton.setTag(0); } if (writeButtonAnimation != null) { - AnimatorSetProxy old = writeButtonAnimation; + AnimatorSet old = writeButtonAnimation; writeButtonAnimation = null; old.cancel(); } - writeButtonAnimation = new AnimatorSetProxy(); + writeButtonAnimation = new AnimatorSet(); if (setVisible) { writeButtonAnimation.setInterpolator(new DecelerateInterpolator()); writeButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 1.0f), - ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 1.0f), - ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 1.0f) + ObjectAnimator.ofFloat(writeButton, "scaleX", 1.0f), + ObjectAnimator.ofFloat(writeButton, "scaleY", 1.0f), + ObjectAnimator.ofFloat(writeButton, "alpha", 1.0f) ); } else { writeButtonAnimation.setInterpolator(new AccelerateInterpolator()); writeButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 0.2f), - ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 0.2f), - ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 0.0f) + ObjectAnimator.ofFloat(writeButton, "scaleX", 0.2f), + ObjectAnimator.ofFloat(writeButton, "scaleY", 0.2f), + ObjectAnimator.ofFloat(writeButton, "alpha", 0.0f) ); } writeButtonAnimation.setDuration(150); writeButtonAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (writeButtonAnimation != null && writeButtonAnimation.equals(animation)) { - writeButton.clearAnimation(); writeButton.setVisibility(setVisible ? View.VISIBLE : View.GONE); writeButtonAnimation = null; } @@ -1069,17 +1096,17 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter writeButtonAnimation.start(); } - ViewProxy.setScaleX(avatarImage, (42 + 18 * diff) / 42.0f); - ViewProxy.setScaleY(avatarImage, (42 + 18 * diff) / 42.0f); + avatarImage.setScaleX((42 + 18 * diff) / 42.0f); + avatarImage.setScaleY((42 + 18 * diff) / 42.0f); float avatarY = (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() / 2.0f * (1.0f + diff) - 21 * AndroidUtilities.density + 27 * AndroidUtilities.density * diff; - ViewProxy.setTranslationX(avatarImage, -AndroidUtilities.dp(47) * diff); - ViewProxy.setTranslationY(avatarImage, (float) Math.ceil(avatarY)); - ViewProxy.setTranslationX(nameTextView, -21 * AndroidUtilities.density * diff); - ViewProxy.setTranslationY(nameTextView, (float) Math.floor(avatarY) - (float) Math.ceil(AndroidUtilities.density) + (float) Math.floor(7 * AndroidUtilities.density * diff)); - ViewProxy.setTranslationX(onlineTextView, -21 * AndroidUtilities.density * diff); - ViewProxy.setTranslationY(onlineTextView, (float) Math.floor(avatarY) + AndroidUtilities.dp(22) + (float )Math.floor(11 * AndroidUtilities.density) * diff); - ViewProxy.setScaleX(nameTextView, 1.0f + 0.12f * diff); - ViewProxy.setScaleY(nameTextView, 1.0f + 0.12f * diff); + avatarImage.setTranslationX(-AndroidUtilities.dp(47) * diff); + avatarImage.setTranslationY((float) Math.ceil(avatarY)); + nameTextView.setTranslationX(-21 * AndroidUtilities.density * diff); + nameTextView.setTranslationY((float) Math.floor(avatarY) - (float) Math.ceil(AndroidUtilities.density) + (float) Math.floor(7 * AndroidUtilities.density * diff)); + onlineTextView.setTranslationX(-21 * AndroidUtilities.density * diff); + onlineTextView.setTranslationY((float) Math.floor(avatarY) + AndroidUtilities.dp(22) + (float )Math.floor(11 * AndroidUtilities.density) * diff); + nameTextView.setScaleX(1.0f + 0.12f * diff); + nameTextView.setScaleY(1.0f + 0.12f * diff); } } @@ -1160,10 +1187,10 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter @Override public boolean isEnabled(int i) { return i == textSizeRow || i == enableAnimationsRow || i == notificationRow || i == backgroundRow || i == numberRow || - /*i == askQuestionRow ||*/ i == sendLogsRow || i == sendByEnterRow || i == autoplayGifsRow || i == privacyRow || i == wifiDownloadRow || + /* EDIT BY ME i == askQuestionRow ||*/ i == sendLogsRow || i == sendByEnterRow || i == autoplayGifsRow || i == privacyRow || i == wifiDownloadRow || i == mobileDownloadRow || i == clearLogsRow || i == roamingDownloadRow || i == languageRow || i == usernameRow || i == switchBackendButtonRow || i == telegramFaqRow || i == contactsSortRow || i == contactsReimportRow || i == saveToGalleryRow || - i == stickersRow || i == cacheRow || i == raiseToSpeakRow /*|| i == privacyPolicyRow*/ || i == customTabsRow || i == directShareRow; + i == stickersRow || i == cacheRow || i == raiseToSpeakRow /* EDIT BY ME|| i == privacyPolicyRow*/ || i == customTabsRow || i == directShareRow || i == versionRow; } @Override @@ -1234,7 +1261,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter } else if (i == clearLogsRow) { textCell.setText("Clear Logs", true); } - /*else if (i == askQuestionRow) { + /* EDIT BY ME else if (i == askQuestionRow) { textCell.setText(LocaleController.getString("AskAQuestion", R.string.AskAQuestion), true); } */ @@ -1251,7 +1278,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter } else if (i == cacheRow) { textCell.setText(LocaleController.getString("CacheSettings", R.string.CacheSettings), true); } - /* + /* EDIT BY ME else if (i == privacyPolicyRow) { textCell.setText(LocaleController.getString("PrivacyPolicy", R.string.PrivacyPolicy), true); } @@ -1314,7 +1341,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter abi = "universal"; break; } - ((TextInfoCell) view).setText(String.format(Locale.US, "Messenger for Android V%s (%d) %s", pInfo.versionName, code, abi)); + ((TextInfoCell) view).setText(String.format(Locale.US, "Messenger for Android V%s (%d) %s", pInfo.versionName, code, abi)); // EDIT BY ME } catch (Exception e) { FileLog.e("tmessages", e); } @@ -1367,13 +1394,11 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter } text += LocaleController.getString("AttachMusic", R.string.AttachMusic); } - if (Build.VERSION.SDK_INT >= 11) { - if ((mask & MediaController.AUTODOWNLOAD_MASK_GIF) != 0) { - if (text.length() != 0) { - text += ", "; - } - text += LocaleController.getString("AttachGif", R.string.AttachGif); + if ((mask & MediaController.AUTODOWNLOAD_MASK_GIF) != 0) { + if (text.length() != 0) { + text += ", "; } + text += LocaleController.getString("AttachGif", R.string.AttachGif); } if (text.length() == 0) { text = LocaleController.getString("NoMediaAutoDownload", R.string.NoMediaAutoDownload); @@ -1411,7 +1436,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter return 1; } else if (i == enableAnimationsRow || i == sendByEnterRow || i == saveToGalleryRow || i == autoplayGifsRow || i == raiseToSpeakRow || i == customTabsRow || i == directShareRow) { return 3; - } else if (i == notificationRow || i == backgroundRow /*|| i == askQuestionRow*/ || i == sendLogsRow || i == privacyRow || i == clearLogsRow || i == switchBackendButtonRow || i == telegramFaqRow || i == contactsReimportRow || i == textSizeRow || i == languageRow || i == contactsSortRow || i == stickersRow || i == cacheRow /*|| i == privacyPolicyRow */) { + } else if (i == notificationRow || i == backgroundRow /*|| i == askQuestionRow EDIT BY ME*/ || i == sendLogsRow || i == privacyRow || i == clearLogsRow || i == switchBackendButtonRow || i == telegramFaqRow || i == contactsReimportRow || i == textSizeRow || i == languageRow || i == contactsSortRow || i == stickersRow || i == cacheRow /*EDIT BY ME|| i == privacyPolicyRow*/) { return 2; } else if (i == versionRow) { return 5; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/StickerPreviewViewer.java b/TMessagesProj/src/main/java/org/telegram/ui/StickerPreviewViewer.java index 9970ed5e3..2e898ba88 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/StickerPreviewViewer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/StickerPreviewViewer.java @@ -400,6 +400,9 @@ public class StickerPreviewViewer { } private void onDraw(Canvas canvas) { + if (containerView == null || backgroundDrawable == null) { + return; + } backgroundDrawable.setAlpha((int) (180 * showProgress)); backgroundDrawable.setBounds(0, 0, containerView.getWidth(), containerView.getHeight()); backgroundDrawable.draw(canvas); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java index a6644a08a..edff6df2e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java @@ -13,7 +13,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Canvas; -import android.os.Build; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.view.View; @@ -148,6 +147,7 @@ public class StickersActivity extends BaseFragment implements NotificationCenter listView = new RecyclerListView(context); listView.setFocusable(true); + listView.setTag(7); LinearLayoutManager layoutManager = new LinearLayoutManager(context); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); listView.setLayoutManager(layoutManager); @@ -272,14 +272,9 @@ public class StickersActivity extends BaseFragment implements NotificationCenter } } else if (which == 3) { try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(String.format(Locale.US, "https://telegram.me/addstickers/%s", stickerSet.set.short_name)); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", String.format(Locale.US, "https://telegram.me/addstickers/%s", stickerSet.set.short_name)); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", String.format(Locale.US, "https://telegram.me/addstickers/%s", stickerSet.set.short_name)); + clipboard.setPrimaryClip(clip); Toast.makeText(getParentActivity(), LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); } catch (Exception e) { FileLog.e("tmessages", e); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationActivity.java index 25c793b26..63e638ee6 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationActivity.java @@ -19,7 +19,6 @@ import android.text.InputType; import android.text.method.PasswordTransformationMethod; import android.util.TypedValue; import android.view.ActionMode; -import android.view.ContextMenu; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; @@ -219,30 +218,22 @@ public class TwoStepVerificationActivity extends BaseFragment implements Notific return false; } }); - if (android.os.Build.VERSION.SDK_INT < 11) { - passwordEditText.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - menu.clear(); - } - }); - } else { - passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } + passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } - public void onDestroyActionMode(ActionMode mode) { - } + public void onDestroyActionMode(ActionMode mode) { + } - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return false; - } + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - }); - } + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + }); bottomTextView = new TextView(context); bottomTextView.setTextColor(0xff757575); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/WallpapersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/WallpapersActivity.java index e2ee12828..fa90e559d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/WallpapersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/WallpapersActivity.java @@ -20,7 +20,6 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; import android.view.Gravity; @@ -192,14 +191,13 @@ public class WallpapersActivity extends BaseFragment implements NotificationCent RecyclerListView listView = new RecyclerListView(context); listView.setClipToPadding(false); + listView.setTag(8); listView.setPadding(AndroidUtilities.dp(40), 0, AndroidUtilities.dp(40), 0); LinearLayoutManager layoutManager = new LinearLayoutManager(context); layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); listView.setLayoutManager(layoutManager); listView.setDisallowInterceptTouchEvents(true); - if (Build.VERSION.SDK_INT >= 9) { - listView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); - } + listView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); listView.setAdapter(listAdapter = new ListAdapter(context)); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 102, Gravity.LEFT | Gravity.BOTTOM)); listView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @@ -325,6 +323,9 @@ public class WallpapersActivity extends BaseFragment implements NotificationCent height = temp; } TLRPC.PhotoSize size = FileLoader.getClosestPhotoSizeWithSize(wallPaper.sizes, Math.min(width, height)); + if (size == null) { + return; + } String fileName = size.location.volume_id + "_" + size.location.local_id + ".jpg"; File f = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName); if (!f.exists()) { diff --git a/TMessagesProj/src/main/res/drawable-hdpi/book_bot.png b/TMessagesProj/src/main/res/drawable-hdpi/book_bot.png new file mode 100755 index 0000000000000000000000000000000000000000..a46f3ff102f68a846468f9390946f79fb9bade0b GIT binary patch literal 3825 zcmaJ^c|4T++a61HGDMbWjIuSB8H`M}VaAfi*2GB4m>G=BjLa}7p<{^vJkRr8p6j~q`+MKlKTm>_gN?X|f(QTr5Vx~M zxo}6XokvK3`<}SLJj@;B8E72iIL(K_#0QZ8NFvRf1hS*xeMv4PJTdItGm;4azy~H{ zaSR;h7=l2f=-_uSI-!(6E*k(aF%J#I6V8$tAa9Z{nQ97NYH9<6$V5}Hn;}LI6KF;9 zBin`tk&cHuUM_-$u2Ze&*a2=?DAsh~cfM9x1n64i8hHFC&5QcCB%n0<)1?ENz zBKja)P}cv%;;u}=ehfw+LRXi`Wa=>Wb!b7px=>?d;~fncOq+|)riW1(_)u*sUG`2#U=iii~cQl#|o4{Ze$cGh#W#9S_jc6pudld zApbrW%irq#!zKPc7pvcLb-Bst?lks)HTu^U*F8Jae{IWM{A+v?mFxB(uB{(k-Gv7L zM7h;NVng39c+Sy1++@sOeH=}hjJR@LEm&&bon2kWa3uo;7V)wo{Il!~%Xk(|?7NcF zjYrq_XdKZeE0i2_?}D|9IhCaD7IxovBqX5IW8d&%;Jr;+}n4)mOboTf5_f71b@F@lU^z;?3g9vBgv%KU}39)j$} zO-o4eNU%C_tzD}PsA6C6hX)XI;-;xRqfP1^J}ho38?rGx_(`m;xqVfdm0EG9h7(w0 zqAoV!u6QW#Ny{ynjjf!9jY2iRhpk9-5u!AdqxosG(9h}xkOO);7+ITBIMI>s_xLI* zJMmJKdIxT+B7u6lI2YXQhRV*pr_V#hw_nu0u=PVA&u>_?u=4)urEpwdO>?${fKK8N zX+*;GT?FK(u|=2QSjQ?$p3-+c$}xf68ZDu$fIj)b$`!bbhAw0b{E0T~o`>XJ!?X`T z%^{`Cf#U5KoJDb2()0 zr1X@f%|;dmkZ}=-+m>2uDH65bk1gVFw(Ynzkd}N6GH!GQAjk5xNAxP3fqQ4Hy8tl| zt9$13Be1Bj83Ke!U*yqcW#sM6Z(5pq%O~mm^Mym@n`D$9{gAE4Fj8dclZ%1E@m6M2 z&gA;T(%KRGOqtV1q*m}ObCMOHWJ1Te+2Dyw0CkabD^}*@N%x}lUZuniZ~31~Ax*PR z6d6OK!Q(~MtbG#kNnFjapO2>|^>S7}#*t_Efu^^5kD>T7Gu>IpEN|mSQiJ-B96gG^ zmo}oq=ld_0b~z5EWJ}jER=VUUg4_}VZe6si7K}=LA7Dw=k2)9O$GGXUB-?sY5sY%#vt)G!bYGf6*f52h<)jJPsbGllT;cOCrc5}Bf(4?q$ zF~5L*_>JCfr;pdIJA0DMqOi;<&h_cc$jVkIVp_u{wF}CQ^zS-{3R^IpPL?v};4p=PKfcgH*0&jBCbN?B;ufj`y6sKl6U)HC| zw@+o09r|8uJ-t62snun=#(C#_FeO#KkIhKa5OSctyhGB8{Nu;B7s88$M1Jh<)}pK7 zh?{k;W=P#A|I-2E@K}b<^l7FZ+x3 zP+rHEX(O#PKhI&9&+K*6VAtwo$LN>xgM3#`XCYsG+$x#AX3iO}O#0?ikWe(CI$HU* z>e-~y8~z+wM4JsDNTy%F64h{$u$@rknHX)LScx(7$6HmAbPsIs8Z4_m<2QlKcLnaP zH_52FuXeD&Rbwrw>=gzda7^rt$_H|UMIah zt`l(=&D4xl6Y+jI@$J}4g)i>*>qo5jU(%m#mSu)y9C-QlNyduns4i{!<)=-tU8mcO z(FL5ULde=a?cp?w8+f_l)D9hf4Fq zzVxgpeOsQ4Yc|ojhuz7`hcGj(J}@NZced};{&&^Y&OWW_ z=Pf<1IgZMeg34ef69veUkofbu%7A0^5~-Vl2#cukBz^Jmo6c(3^cQ0bcec$Uo*%s1 zzy6+;J9$}d6a0N_1m}3GGV8jE1(dG~T6Z*85V-vQi*;s!R={0BkVTV;LCwr|v$B!6 z^1O5ZLACZJuymIL^|?-orn&M6M6)zDMk+mT8oRVhfUQ+r$YVMiE#2ZiDmpFVp!EDg z&(`$)Pm;pJ=g5AiRvx+RXLW!7F(>$~cUIUeM`dZ3?*LO){D*9h*W~m8bm4M^EZ;}P zJ17E8n$5n)D)~Mc0?H%tnt<}v#_RH%6gM%wyD)%(G@(M`WWgf^Q|OyLltp5QowE*s zw+vIG)_7ZFufo{@gN49fbuE(ITOme-Zg1$P1N0Cjn*45+ttHUcUV7gDL zp1mHS5xMu)7(@kC2sGQ8g1k*AcnLcbg8 zy6o*`i?@I}5aeDcbW$!!3JIkqiv|`P3)McEYoDeHP-LSH;$ja<>N)rSLAoKpV`LxB z7W)8t7gtekyy&b|l>pnc&!ZjmM@~t;ySKUOy%{^;X3;K0o)+wlef^=fep@NX{xw0E zH+4`heKjic&3Pu@vvP7(U4>fJ*Xkb?xDQsh9|vY1o7JXi%>egrvz}+&EBU&1vF@|v zZMQt^iL4DB(?~}(K)H%)V{U0tvC3oGcHx}nGY89}HkOG3T=W`DJl_9v;~vHjYZy=T zg*cUp7MJWwf7%z#;wj;b^CS1w0GlQJsee2=szB>9Rrh(anv=BofU}t!wC3h+mO3x* zY!Ity+as)c{0i@#L1TPRN&)__r6FMHK$6C}&K@_O>6qu^iZbc4e)$)^!ev5}qtaxc zt~^~VHphb=EztVas-U{csD>)z6stnc{Gpg}&-0@HnebAMn~l4dJ)vc>IZrj-gXBql z&ifV1%Z}XkD_F{Jdr*{5ntd~2>zG|}ClWi0y9VgF8ClL@Mt2!xWDluWx8LMRcs%hy zb7~imMLiL<^?bf)AnvtLwr~cdkfB6cNsoFNHB{zha}{9m(MJgxVqLmUtd z7MFSD{{rs4doT&CQ(C3<01UR7prqCgRSB?1<1lEkUh!<$I&$CgQ9EYXhZEXU^Xq{8 zd*m~@)5#M}DP$egt2#?u#`*=jt6}nEN#*TI0!^H&_78g$IB&vQ%;h6ugr_B#o$XY|g7g%9v%8K?vYYn;tdPwO z)1Nas*QU$_lzXm`05tbg zA}&u_+&3|MwRH}@|e`8IO6h%zoiQ8M}v zsCn_|$me`Nt3SP7iQY|$NFf|du;!%m%Gd^5(Yr><#P$)ZnXg

    CF|eHq2^83GftG k<#Zv=zau62dmDfcutgqtD>5aKxAPNfhju_!S$bXiAO1wB_W%F@ literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-hdpi/book_channel.png b/TMessagesProj/src/main/res/drawable-hdpi/book_channel.png new file mode 100755 index 0000000000000000000000000000000000000000..b4589d99ce71b912b32f3275cd27906342cb0d31 GIT binary patch literal 3110 zcmaJ@c|4SB8y+TGmPnRS7^9?PnZ+1pELnyqgY1%J%nUOzi!mD0p>RY<=oFa>MW$>a zWveJlc4Hf5$tg-?IVqK2rEheq^ZoJly}$Q;pXGO5*L~m5ef{yIxH|8X-K@4500783 zI$+&|W6;JYDK7j!zrZ#X4j`7D56hh%!eZl@B!D%M9!vr|Qt_cAHxi!6iRdJm0{}n> z#lwf?gLA?V=u|`e2F5U&$`G;v0Q0@k3_Kx>!~zGCLMb#0$Z}&71WX}XK)j7{FdV~{ zM5Z_#W0KsDIeQR}g%Qw1$X-jZc{D~SKqay8;Am<%EfN!L0r{qj5zaTZp%Cyl2rJA2 z@=sDeI9ISOok;>48yXo9U~o7Xg))RAjZrALJ{SRmBcL$hi!y*CF~%qi!UX*Nfe529 zi6Iy_to`>`!j%Pt%wjPxP$-+tHe?$a(wU)9I2w)K&_Eyzgb0I34vmG6HlRgn|4_h^ zA_+_igGHg!z#EGAV0skG0wT=xpAx8ypR%;b@5dxOFlaQM0figFHcI*d#NqxwluG@H zj%2x!{>}G)3P*Zy7$m40DUu$=BnTT9qP-D{fw5(h@GLshgH8|saf+^FI*T4jrZd2{ z?kKRXH-$!|vm^I>!{cxmM_MEcPa}{Vu@(@af+2-M#Gp}FBWn|T7#xj7!{K%)Q#(_b zJq&Gc1cRYbFqHKVE|yM+qLOH=A6(+UT=*}!8&;q)gpsi%CgmuJXwRfm!QV>8P=1+< zo!u|>zH^Dc%*D?Bmt3eY8R$l1|EtkIwuJ84nEte_aPialB%09eOrfpOU7&aXK$7c- zwf2Y}Tk!wne$G?%xbMtw;Oe+$QBmeWg@Niz*F(5Ei!sioS++41G-*<4P0MBLlU zZ_TIJOS2Xcu}39dZ3A_vwKIuQgGn_A9q`bT$B$bko2Qw_e->L)WcqA5d1mONbcd}#yB0n$UHI;>iqZ6e z89dQN9~5N$G-Dz?MmP1U1Y+86=2ORV)~yg@utL4!8lrkXZ_!!0T7&~YtC!cp)fRg+ z1mLC7s*lYpck*9Exgf_JZp#)N`vUd44uQ#~`sBIXLa6q|g?sVV;}1);+ZpJ!NR&Ax z!Gfeq53o=2(}#_&%k_NR^3v;;ugGP15(eBktwp4{}X4KQj?_w$_qATA7os zwSP6ZS-r!(!w-D4QecA_1bk}Be{7(-rZM_p>qMc(ipk4x`xX}!3tQq;+F()N_mC9L|F@DC|> zIu$aXif6t`_U(qs-(|CW=F&=r<6?sr(3i;sgBA4vao3jW17tNsp8KG#gkNT-U7kc_ z-y#-Rvvha!6U`-+N|#fb3J#66E#(R1Q(qGKu;6fMq`y`uz@W`oaF-rmn{{@8j>quPBr4AAR4L3?TPHUCylvn8S;M}Gv~OIKw;FR*eWCg5T`HNVM1 zfA#p9`~Bl_jXgS_^}Wz4X1_lhZYqbt=Y|Q&%c3{!N0B2L-Nw?Bv9_j}YRwM>Q5K=M z`v`!`_J^sgXE##kw%--Ec>E|oHv**9UBnRwxk~-E{N~Dh5J19m7??tgyQOQXv43;q z7gROx;@Ii;eVH%AIsu*;G6Q~31W%?l0CuS}Mc5{BRaN5>kX6*1CxUMNUZkjKweDLF z>xnkrZ}=Ygo}F^1-R6_B|8`!LxxZ!;W2d_lSa$dIXOV-8x+h0OFV$Y=kCns)6SG7n z3egHv`+$GS)1+=B_+ zb$?$`W6y4_UFd@j_vDK>isce~9Y@S5UROh9Bd((>Gkd4H`^Vvv0Yk?s`tGD-fwP&~ zdHxEItlLg<(%PP*f^(f*w~wX7tSl=Vo3zd=>^65-NDRE)<9LL*b!Jvy_R}(@>Nt*6 zWu^{%W8*k4CQL=EP z*0#3#^fJFdu{-9adDC;3h}`~h^)KfI_0=!l_+Bc6292cJ)PWiw-q_PnmMu3)){K?8 zFo5vpJUK8q=^fC~<0G?u)YIkx!e9M;ewI<3P2JZ5QlCT2d^HoN_(mo|B(s2ta)|2R9D4o)=ZzS{3Ho6%mT;9TuNgS5jhZ z5qIJmnri8)z|)e>rWZVWAj?&07^5c-R7ow2KC+FlJ~O_lqUGSIKJvD{jNo8me~G@q zUu(MD=+f2E-Ywgz!;LOpg;^{V%GD0klN%HZHT~ki<%EEqeHZKtvPJUjbAyBWq0WH>u{LYmAxR zctGA_o?7YDKC6a1(*-H5dpo*ViR_0*76)G0(j@lmUqAz;Pnk7-iBDV(=2VbKuCfD{4H*$z*}`oy>?i|&d7mvXv(zpap8Huq3wUk9%4Uj zwa;NgyhWQB+~E_v>AI#rSkkCTQOmMqo7bIyKt5CbA$Ka0X1XA|VZ)l-M&_OolFMwvFq>@{iAA||aNIAQ$URcXWh5es z2wkKwQAbH4BxMzzInU`l&mT|E@AdorzL(eg{r-GD-_QG>Z<2%EF$pmxF#rG{fwROq z3P$g(PjtKB|7TvfzF<&fnY*&^G+$OYfk6hCkZ3++5ROXlBRi4_q==9{G6nz;22-3| zS+2G=Xd;aYC2V1!Y${#A1^_TdY&wA$NM?b2$bOVyL-69Q4lsy9G6cIIZQ-_bGqOL$ zGLk{YN7^|NBLj&BB(Tv@5QdEw2vEr^0*Flw3TC3&hTy++(SrHbHVh2<3&ILC1phlJ zS6c^=8I3^(A)z`DA{>DLp-@nSE)s=8Xo0lh2yGZ#@S-3HT{IGf*46|4{eT707$je` zBi7>YSb~)y*q_CsqhYY{@Nj6j4wT05gCPtI47N11wIKoogc%XcBCsLBOtl{hSTd8y zpwL+qS}EoMWf_76nf|*3D*YeXVCLV)Bsee_n?Q#lpzy7degJK4|38#U{Rhot zIgzNpsTENW_re;65SQ;^uN)BfI;FA8sMf{YzWd$l-5E)BmP{PP03kHn}`m1C#<)^ur zng3MpZ!YPlxtLk}lnWCi1KVose>M8YmcTt*(|>F$So~vra5++O~q*dT6w94U~JIZX#;#G0(hf=NPM{C;pe$#?w1SM_WLTg`$YML-V=JIhJ zJ!ON=Ngd*MoF3vojM-q;5W^@bT4F{kc`rc?m&YCi$+2|K{Mk@Fps%xEe^A+Pd*1VH z-6w)bDd*>&^hf3KgJ*Cv$16(jFMieT-|fghox$I};ymM(7vlpxS^PLv`wIs;qcJ1b zH4DXc+=AR)L#_aIT5`A1wf7OC3;C7U(zkNc%>#=%F%NB=ssT*{SMt3GlYx&T-fmyn zvC=z`ckRmY9t`uOO`;zpC`vZ;$l5n&zia+-rerABe2=CbCk#=s z5f>4wxvSTFPc&%UNd}04_B`>^ZAm||eSFE_aYgTSEsnd5H%v69ZTykuc$h`wh$Cg%e9&-Yl|Rr2Z71h!>HBt;l4?49V18cas*QFvME<2Ge?T52!&+VUjh*Con~ zK}Eu6Xxl(;-sLn%Cw}0vD=67p#QXdvlOU%7^d99CvhBH%2mMB0J5`CWIeh%LJ{OUo zk2-|X3{kp6a<~NdI-7kh;{8+m^!Dd)YOS3H4AMcFG;%9^BdDl>=aF7O~-{WjFG)rtP=y(pB7N9L*nj zKaEK}#Ed?eZu!wKm{Rlbl>hk(Khyecl``DPHzEZY;LXn{k%OPeakVsycMD?^l=a~T z)$!z>)xp$LhnjAdVD+;h0H2*w{cCUd4x*<>1zbI2B@U3RxiDixG4?YH8QdQEq%6d@XBzW*lK8# zwg_i%UHYX;M&5LsG)Kl;q?)VwxlaAI1@YHC*9SfGft@qmb{SEm%JzHqAC%S~q!bQ4 z!^8w4p6c)}uJ2duEfeRg4Lve$u37X^m?-GIZ|Fg76%I06H;hk!@72#!i7f-#hul77 zkW25Cl8~PtJ|roX_Mi&#sy5u~aA>HdRKJjLB_+lnnt$J6-)iWt+>uGw#o})X36+z0 zb_Q0eg~qO%Eu) zG@fZSYf63b=*es6uE$i!QB5_VGzh!X($I}JT9!Q$dZD*@bkr_-?tSIhA`$~tGj3K+ zrSp$(gvUK%z`W$_rr2RZd4n@Tv`(ET-Zj%3K=QyTjEyF1oU2ZJp}kv-8*bp?7HT`aug`bu>Bx*8KIkDLgb}R@64cB45HfV#R}Xf6i-`AU~Yyl_jA?k5AF>cgl?R68ZOk`dEPbU%a^b$)5O}+ zO;5dsyH)wpB0X{aY^dDs@tp4FET^9C@I{vwYj-X&>|>--ZX?SHAMGAF(a-v_RTSqJ zE7m=zz9+tQygTK9Uz!=jwGO3ZQokSZvHpz^nUZkv)Sgf6XtQVja^vx+i;02K-ZpGb zPx8Jzr+8)SwXkJKYZNk#nduRzj*;`9_V1LJ$)E^Hiz^`=pJ0qa`LY%LLN=|>gKa7| zB+?z!@@BXtgBF+F^b_7W+%;D_U~BWxjgg-;t2r^{p(6Qh-GXa=me<4k9)Ok?`aWWb z3w-|v$=P@t@=H@ikLzwNpSHo62l6j}&uTR}F{N#k-*^oW5L}nhNv`YP&fDSMaY>`I zB%N@d@am61tL!>*-QcD?szdF_*NA#m@weeSBi(>O3ku>{A&(~d4*)VgZx#g_AD-^2 zE-KEv*+VD#3GIm5B=@&Kjd%)cHHEV8-&{sL1sOY%J?UvYveu`0*6in!A>R#qu1@l| zm5?k)Jp1c?@5_uR(ckt}+uX-(NEHWQtw%VSxus_{-Y`$jB|jt@z4D$qYB(Y$x@U8f z;9#J!DKZm2A5p)5&5=u8s1|!Ir0ROgncgdAM+J4K-fQ5t+w?Y!^*kIAe!}cl$Y9e; z`_FdrLXdh>`MoZCZ!GvKSun2O_{evxb(lliqmo}m5Uf|z;?GdLaA(<4(7=m*Hw)Dr zIxo?!sk7a?RunsS3qA3eJ;sF~g;7VmX3g&1zAxcsC0Z=W3xkN&;^$-+S7TLEz~Wew z7H;$;muk{!u@Bm_c5<6#FM`g)4{$>!e_2Hk4r{vu0I(YVe#Oz-*L+^y`JPrMlWoQM zRF-@Mp2-~yPm@RS8=k@DBm$-(O7=WM!rQ)0`ITLfw}bZU34=k^=+=akeKN!i*y>L2 zPszI{<(E~g3J{}#u0>DZKleg7CMqU;TH+pWJO1ILiuTd*uaZ{nS9ALz08X{ovQ;ea$}=itwPV_pfh1sV z?<;WNM0u2B$!<9Mv)+*S_6KARLp<%*`6EVJCvq2kRKP1W!{;_rr(0sKJaw;I-buGs z4rs^#j2Sz=Mclci5!&nlllvt!WK3Cm?a7y4uZd4ZnVd7P$e$i)&U>!jWY!dP&tk)# aBLrCc0*)`pUMbqTe&NjRur;RMvHt=EtUIXy literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-hdpi/book_logo.png b/TMessagesProj/src/main/res/drawable-hdpi/book_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..634d8a342ee48b4a980e3ecd4d2db7ec18bc9fdb GIT binary patch literal 1474 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`W@2t;Waw&OXzAi;Xy|I@XyR(*=e*Lod8?_ZUZaL~|h`%lTAlP!?NznPP^UDs->fIpC(_r^Nb*pScdIATt%uMT&?qt&$ z;)nbesnpyk-}lh~^f*<)xd-dk##p^Coix+%5HzhO2GPTsRU>#45oJHrfm@6x{o^sB8kattH z=;f{V-=?}8^V=a}lk$+y+OThL_``(`Tc<~zx|_mbxLNnD%;{&B&K>H#cFk}9bUE|5 zWvh~$cj^|;cHgwl>Ts2iO`DtZS-V{eR(}8W!^HjJ*)3~+&-&VQ;o*9julw#UJHJwR zsX_Oq^+#fK{+zw_OK;w?1FtQVQj^2NfeJGx?hH@>D6%hWjBU1)JOYTBGr zDKC9}H219N)>1W&a77TgZoZtHzj{{JTvR5n%&!#cYZecZ1}(@ zQMjw{ykYjdzdky?*}G32;al9I_L=>w<_`ULyGpM0r)`^frl%<1wboAGY0o)^j+Kin zI`3azzUdwBg{k^Ko)zz5i~6xlEa0k`h1eqLP2w&0S6ctMVxbx$kzOcZ;%xHj%d?rQ uLjpurN(Ct1+CNL~vL=Vxo2K(D5)73g? literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-hdpi/book_user.png b/TMessagesProj/src/main/res/drawable-hdpi/book_user.png new file mode 100755 index 0000000000000000000000000000000000000000..c39c071232a7708a3e1d55b34280c375c7b00093 GIT binary patch literal 3161 zcmaJ@c{r4N8-B-@C0SB-#*kw-yUf^zktHKp;zTuL%rG>InPG6g%DxvOS<0F%6|$64 z6r~)JN*~!$qOumJ5cx)@I^Q2(-+R69`&+L2zMtp$J@L`C@TS2?`h{6Udx>&10rQn@@5n*!kcn7$OS1D)hgaix&_!h;`D%m6?TLUZ@x zdJ&v3WG3B+w23haqqF#I05G!*W0A-~6fW48;!k6kL*{R`LclaXbBHI3041<&C;>G4 z2sXtn!r7f15kxligIHRC&B8Ez0Xl_C0*BF$GB}tpbI3Pc41d14Z43c_gK&e)A%7?3 zMQ{PzFxeC^$_QadhQeTAG};J;M4{0z127y4gBwHnH`)+}#Gud^xC!|C1>r|y`%y8j z_&wia@mJ=M04|q>F*fG$ct$*g5tHq23^O$~-PC}?4fzN|PB?>03NvJIG=C`IDI7AJ z#^TbL4DhBR$(I?zHHYvs{ksG@>!&P(^Zl6k2WA{bVj07XpqnNA01^oQA4;eHM02>V zlz;O5pTZpXa2CbbmBL|$u*v+!Q8hP1u`o7l3W>{PyEB=q!F@JcUgQrTFb(GwI-OC1Ys6&IR{N zz3*JVU*}@|ORh0L8RN~y{#T=aZ1LT*IsIu{{^Fa!n-QxJY*KPS%P3K{ZCP^ry2M&#z+>Z=@j-W~ z_QQRZNztUF=oxyk@WGq&v)mWX-o5j;uwL=b{^g6K`HL?;BU*)?hJPOUMkCj z`sDOdQ|-xjbWW&K3n*%&uXopy2K9;`2g9y_guCIB7|K#u$|$z7W8hD-woCa=0@rh- z;3f$5y2o$HNgpH^1t)pk#YuWr5RT@e2Q}&O@hJ!g%Q2}RyI9kge4Lu{UYaM4wy0{E zbq^o$wNo2pFl8FHXGsv3FJ zIdjW3SenxQ$pGOnSHg6~lL?ywcF?fAKB&Ndj->r`!WOd_G>JGMQ5&_A1ein%5#u+S zuO{shC>mS|h~_oDes_+P39lEvp1D%B_$Ez!U;;tWbQ8*{cyZIc^Eoc#(oy%L4FGf% z)W3)oHOFA2=Mt2z0N4kk7OK^CsVfEh1&YXn69Y)!76s=Dl|YCXm-EfuUR2QEgGTnF>|qJtsrR&HB+OoL4f|AtE4AVQJ~pe)3*y2sga4qI2)M zMWMxxkej;ZhPiNrEA1cyT~@B;uzmcJQpfV$9l&%kwrpawJ-(IOSfG?Q)V{jjP&J4= z5gLWmXoBb1D! z4>ixwPX?2#Wt7$15MeuMqM)Oz)|Ji*8yh!bho74??zatD0tMNZ-mW^7ELtP{!ptQy z0rG*I6LK^si=FR4zix)r9=A|QDDT~tr!}|oX6%#}f0@|h!hU;HJg`c*8A}^~e z=(Yv2iKqiTM&mF8E6^2j{~TH4ned7`+gBf!ia9|H}$#98RmsO-FRTDsXH=-yaB z!oekVUnn&%oaSG)H!zyGNRhUBmR%g(qN`ng-te-zm!5NE)&#z%kcmvEI=Yr1L6a-_ z;QczfddLPsYD!A!>F_vNJ8N5M;*|!UUD`Kj4i8n@pLkzoHK7G$6LY1(p1zMB`x)e5 zHQ#g7;}r>cxCbGbFKaVRVnTM*<1nAsu`kN3cXN$Xi&j?s~r%mspxx*hLzEoYSDQt1tYI*E7jv%xq zKhMq>Qmv7+%gmHTR(QjELGY(;jT*XNjpRI}Uq)Y02?_n9vg}q0=BC-f*p5ghw86zk z0-WrhCA==QQ(GTF$k&|kqXcNoMBjHh3zKTQv%Ql{=L$Ka8o7ZP5>$bDveWaAoO;xL z-&7B2VDITfud8eAkwL{x#b;jl%GJd5U&|G4uKOrY+G%>P)e^w&Ht?2@#}?lx#{~`T z0&5z^B~uAc1LNZ&d(JMOpGdeY4Ztj?^n;v|)>B0encrIKjSKAh^I^%odkEb-d5tle z$32A>s^4)KX=hKJzbNg$mJtv*TU2_kb55;hN6i-kr>at0G)Q2;(btiyQgS-2z~HyB zm*7Frg7HXWNrw{;y)|RLP%M*er-H7oSotzc8B!RtytF<&v#aNIpzK$7bgKp?qE(T3 z@iJ2}IuB~qL-qghg z9xsyVe6t1P5URM2R>pR9zZYZ-0O~rxquv_iQb?%9?~j&SJoWhBA5b`*Khb8;VhfVT zr%qOk5LTNa9`+BEXg_#75tv2S%>v${#-LZEYHI+I&qr$C5!`0#PF0)QiYzBw=|{P} zIfL;QhiY1ORu+wNE{xeqe+otc+Z0#(XibtJG)_%vR*HPNe?02eso9eIUy1jnJOqKM z$G6``Rw12LtEE20xK#Hqq%%Thb~9ScvOl!Tv>l1F2tBF@Bi0|&n2I1Bo`~-cy6$q; zG*nW;-`=ugX#4wwaglDYhKl#t!~j~0SlZbRJFX?LysnUT+SYSzP)?0fTDwlmh)*4V zmFMqp#FfL+Tp(GLAH`F}Ue0F)*A3Q^7pD~tpjS+vZ0S@+CV3;#7}WUc*$tJ&ln-Vq zglBsAw!tUIG6$ANF7|A&Tg33n=+u13T-n3s&Z69<>3>VvW+uu64_&9rnRK1c!QZ$0 zeI6_rop)0FTn%L1vHjX_VNa(#Myf#$9R9rJGu1) z;Z1n6UJR|)_p{P-oJ?x$pAv^P)&p5pI-cP^MIUDnU*g7Zl%CwF+OJfRXz5gE3Ef%0 z!pxn|R6|l|I!6qI6vWO!PQ_db&SqD|`L@0F8XKrksINWgB>|Q7;vFufo~_QgmL7a3 zN}-jEotwWf7JbsY%~quk>=0>1>>PHzw{oG7o&Z)=sf*Wt7OazgqOi*DWKaGXZy^&a ncZ4x)U*>J=exthd4KXM%sGD<6NNFNw^Jm||)){}rni%~b5e8A> literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-hdpi/chat_badge.9.png b/TMessagesProj/src/main/res/drawable-hdpi/chat_badge.9.png new file mode 100755 index 0000000000000000000000000000000000000000..50fd7f07d8e1fda67027a3a7d74ebaad88762730 GIT binary patch literal 1411 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBBuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFe_z-M3hAM`dB6B=jtVb)aX^@765fKFxc2v6eK2Rr0UTq__OB&@Hb09I0xZL0)vRD^GUf^&XRs)DJWv2L<~p`n7AnVzAE zshOFfj)IYap^?4;5Si&3npl~dSs9rtK!Fm_wxX0Ys~{IQs9ivwtx`rwNr9EVetCJh zUb(Seeo?xx^|#0uTKVr7^KE~&-IMVSR9nfZANAQKal@=Hr> zm4GgVcpgi&u1T;Y}Gc(1?(9z7z*wN9#$kh=T>aJ#vu1*#f&W>h=<}QZD z7Osvky)OC5rManjB{01y2)&j#^@5T@ZUN9{m(-%nveXo}qWoM1u$Qeeal6F~r+HAl zDY)HYj#IBb&@uX;h((HMm=G}afSB-P3*^8Perg^twHE=CwQ|C&as~#*@18D>Ar-gI z%rNw4GL&fJ-n6LOFt2Gf4_}{@jOYjU1)YT{6HgjQGz&gDv_g1w=O0E<$JIfXH&(Z7JRXhF_tF2@wHvw|H_JD8Id z-1HoT?nsw@;G597c;{A)&(?u`Tk||Qj51AF4fNi0%e)p@;>+onCVsY2BKFCRYG=-m zX0lxu)wv(FIZfbPbdvk|q~~*L!(T}JkQMnidt-9c_Vj0)nOofoZ)^#QVyTV$ZIQ}{8Drs51QMo1P$xPMmXw8cYv$vn$ zsWIiXW!-@dXM&Et6tVn1U;WsU!~Z>t*1g^59x>yTlHS7w0Zql<)?0mOZVJ4clKd_5 zJMX&F3R6?t15c&+gsz(~N!HBavfD(p<7XmPy{-KHEVt@cm(1#fpQ+4CS1BAzerFlK zk!QC4H_pvRR`I<`_$71Xm4a4XT*TQ1&bq%`aytX>6?j)k6&!qZJS=*J>X(k^(G83Y XOzL$j0=i$V02Pa#u6{1-oD!M)K*N}#0ibSC8QBV3>3${G^VxBv9H99 zmG78XGKTtH%B?b-n`AqKWe<(wvgfneFbcTey8-uLdkyH{K~H#Ty7 zgyXoe!lG7U>p1%+hYzuTZTI43woK4mg)WmCZ5lr27A#W5px_v*xP%RBv+)_HId1Tz zU9QlIenBybBN$PP5IP=1b6k2Z^bB(iQ&7dLwyW|#UcKdkZK?c~q%P@R7T4{?mXDWP zOJ%dQX37>ncNU~Wg%LQI8X$Dm-9QOdzRjyJJ93LWXot`>l|K+w(TgBUd<>F8B4$bu z0)zxOlSBxf0dWb&MTxyA24|EcQsODld3cu1w`xjB%Xf0IoyylK^%PNTHk(2-ArOC6 zgt9D09PxOJMZ|(lml|Qr4W_yb8V*d~_NYx<5HT86vO!gzDLp8`@p`mw&^ab{U}9)^ zA{3;kq;8-V*bA9VKm<~1e literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-hdpi/inline_video_play.png b/TMessagesProj/src/main/res/drawable-hdpi/inline_video_play.png new file mode 100644 index 0000000000000000000000000000000000000000..981f5fb219493d7fa3ae85ee69fdf6c510940961 GIT binary patch literal 1204 zcmeAS@N?(olHy`uVBq!ia0vp^5DSr z1<%~X^wgl##FWaylc_cg49qH-ArU1JzCKpT`MG+DAT@dwxdlMo3=B5*6$OdO*{LN8 zNvY|XdA3ULckfqH$V{%1*XSQL?vFu&J;D8jzb> zlBiITo0C^;Rbi_HHrEQs1_|pcDS(xfWZNo192Makpx~Tel&WB=XRMoSU}&gdW~OIo zVrph)sH0$HU}&Uo07PcGh9*{~W>!Y#3Q(W~w5=#5%__*n4QdyVXRDM^Qc_^0uU}qX zu2*iXmtT~wZ)j<02{OaTNEfI=x41H|B(Xv_uUHvof=g;~a#3bMNoIbY0?5R~r2Ntn zTP2`NAzsKWfE$}v3=Jk=fazBx7U&!58GyV5Q|Rl9UukYGTy=3tP%6T`SPd=?sVqp< z4@xc0FD*(2MqHXQ$f^P>=c3falKi5O{QMkPC z!8&|>tvvIJOA_;vQ$1a5m4IgGWoD*W8M(Qb85ua47+ShG8XCHqIhmW8TAI0<7#kZ} zSQ?s}!t}c2Czs}?=9R$orXcj1;?xUD47mkBn_W_iGRsm^+=}vZ6~Lah%Eav!Bb??z z^`_uRM;E$&VJ&&3(;;hBo>j}dvb#S)t-SCHBTKM=`SM*i z7T(p;YvnWhr@yx`z^SL|9lJdThvmn6ldBgUGAQwh`D1mcF!?&8n?`5Yh5rX4+UtM1 ziO0R!K3P??<5Qp7l(kHO%QWUk9XPw8%k+$)LtGO7rA-HgizazbnWFm2jN3)>@vA$w z9;aoey*N1eM)Q`M1%LOiRpoY>`7kW2b5hVj9=j=BvSryKt=+NDWUMdx+g@3|xmUHy zRrLD}FR>p-ns-(z++1e=;<3_8SLuTeJk-d1sJYh~5O$5rg| ztkS~1`+u36xh=EX6J?gRDQ~y<%Jp3pv+EKY&n-M>p?2!q^t*8lj10k9ZY%1gq+6s3fsbRlI30a8c?5_DORhlL^{hzD6+ zz_u1_ARw-Yz@ArLY&Sca4JT=yB zSqFo`G`zhifzW6JJy^Ih^!BTfwL!yXkQxdG@nbMi4!Q|ecw3?!foQ!Q_4PtS z(S)p6Vj#uiYbpy`CQbOl!=`0d!c++ z?<<$}T`%s`?{cw_8SF}7|EtieTTu6`Ouw})wD{Ke01xVRA=K8Ck@TG~n6k1r#Wh$m z+#ep{W4uf6wYpV4zV(;;IEuPH ze&%^ z#3v*y9iFh);7khaF+T=awhOcNYhJm0`5x}1&%=%I3FYp_QRRZ5%?IZry~hy03)|Mk znBLf8Z|UUZ)Ei&(Q%knVA4_Hy78aR#)Q7==o9~h;*A8@ERf<7`CMiwP+9-!}*7YmA z$}nD7Sa|AS$PtSdc3SHQ>{nTR#ADL@9(Mfs;;)yX~N`8 zXJ@Csv;<5vC_Yb>9e{08aKWJ5{@v4CM^Qdf?vq6c+K;WQth5zw=#$B0WNSjPmMCW z-gPM$+m@MkvCHpMQr_0-P-J3^3p*t4gj(T+e%7JZ>iFs*c?$8@NTk7*E%D{9o*T{p zMMYte_VN(&)O7ps)2B8^j~*R0x3s)_E!ct|S2G-uC7l}|9|t8Vy2GURwHEKsaipJwdl>ao@V;&+_^K0T|Lsajo3%TJPW~T&KP4* z=H})}W6z!=k1t1?nwaR}%ST2=wkwT&Qc*rGKSr!h@NkhL{g>uuXGt47X!?~$khw}N zi;S}B>gtA8PZpEO{HUd+#ossVY;9c?%V40_UwvmF5{ViZ@8*^A8ZKSps5?4-=u#Qc zEG#WuG#Z(V(X(M^MY zzUQ85f=nMli;C~wxiftD2RM8p{o~V)uEZDf5zc{mQ{pmJRn?w}&tx+B3Yzbk zwj)?Nh5t#({qks4RTXaJ7A#Mxa3rg4LSJ8>WPn6EjkgyL4-X3txFtOvjn(NL94yK8 zRoqduG@_|c+?Pg z*5>ABnToPN27MphU zDqh}i@#auK=$oN#UQQ?DW*%&p%oOpczP=u7XlST%t9vq`(z9S$UBTrh3xA@ltPI{W zW7ixL_~Z`-E1f#iw2BMbHS-t;y>%ByJJsjQanV{`heJ`iI;E3$DV0~H7t>FRJMF> zX@BJys^3V>Nob`RHhXUOm1MiY0vAlgzuvLudy@Csn2??*(OHv{| zx7-ha3Oc9KPVZY@UfxGx%e^K#=-OnPe)oHlwLY&^QO>usRD4tr17#WcrnX@-A6m95 zpB@-D7KvZJ_Q|-Kvz{i&eUe|7IaO6%wr>pHBxcacV-(1bHl087+CXK<}`Vr zb4WuQg_5Mav8PlA4v#v3#bAt# zVUWnc^9~Ls_;Y?20#5{m0eCam7J{JqQ%(NoqW{LtED%QiFMzc9TM-Nr8s>W9qzU!! z0?5B{GYv#=bUT-lA|WBMF96=^pUA;EZ->JwM0vG7U%x{b=hLyW8O{&~dtLlbr^2eL zYh5sMad8MUaWE9N+(ptQxZJUP#gn{to2#oe3uma^w&CsG-2@dCCQDQ#xHN2tN)`hm zq6%$n`je&FO6v2c+E4g~zNmRj)l2K2^7tG&ANqx!4s?a@T`*O@Son{}Fd!Me za9g8;NV1|ndE2garu~${FT?S! z7&!^0q{ha^h#Au6HSVflkR`Kx%e+TL-}uO7^~J;&)z#H0lX2lk((~D+YcAh6*H=Fu z<2Dm;S3!xI`DQe%UvRL1ro67Qu}gB6K?j#J3?V>;w$=!Eb$+8Y)+%-rs0 z+HQAZ(|YdTzi%+p?Kv62Yd{Q$9rCeBQ67z_o(wa&IpvpTTYZyxY~S3luEaTa;l#F% z4xiE0v<6C?;$uKUcJaqdck70l`~aAh6F8AJGc$88{8lYl|9wMDq3RxeXi{8U9G&gI zxVS*`@U=K!epO9DA8z{qURqj8G*j!)Qx_9fbuXjn3n8Ean@nOG;(AFcTZ0!FBZ7d| zCrYxNr$}e9A5Z!F`!5SHv3NnWuy+ITb6PF4X|j!Tfd3;kESGJ)4KcE(NoVLwKrV+< z+xDiyIohG2mQW>11N{8_9`@6}-ejM0jg>KK7G%d>KkVoj)4DHKs3@aa6Q07K_ww@k zdO&gKxtHtr%q|tE0S*qOJUXI#j2v)Dcw7Sv2473I6c;s$b62EG0CoV-GL?*Z)4Y(R zsf8T5!R7w8f~DgGF*?dZGB7P<-{8Mt z^v=#s=8uu6y!W7uKD%XHeZU@1)l-s&Z|}_JCN?ubIAO zqfEQoBVCn>iT2Uv`Cbmk7nha*GWlAH;?rSAHH(;M6VK^u;C?{j{(}cQr^zIp`Ceyk zyxjM_j}Oo&6iTe(>}{(l27@6&0J375H$Q|@*NyAN*OA80L#U9qoBoo8+RG!s7x_G+M$WJ*;z%YkCoVGO=cOKKK`I&3rRd?oYg8 za9?vnDWlFc;=w1!ZK|T;n>ikPns8!~W^E*fmuU=5o1dTm z2y>0}@8|kKvjFRR8g9_4Oq}(pl%#gzkB*BVlNe8S_T>-C-}Y~0h$qVow5X`u)y1*n z12U>(<@~;}<3|?W`*b!mG_Yyuas$%qN>>B*5!sX1`%fEVO-(nJD6p47)jo{jwit3Y z@5<5@7s%bRva$;@i5d0v^?z7QS+a$PGwg0nY3a9T{a-J9@4Chg+PY=cutd81l7`Na zM<4op>cVEEuD2RKY0c)a8xeg9Eih%esMdSbG)x_#r>9rVKieNs*J86-F0#^qIGI10 z-wGr*npC(WX>m2rBL&ArzUi;bSw0jLPXc#}~36llemNfolyF`|g~oxbZH-JAgzY0Y{G>y9vM6)qRTq7=B)6h!%W(zmVOQgt)eBRQ|q7B)p3Vvf}2}*WN(W;8uG)&-lokqJ*~2q z%w%c?%f*Z{qe)5@7c0O_ci!nbvhD6p3N`PYtzCvN+*IO|t@ddWr*sFW7ah4zbM70X hO4}0nz1LSrYuM<$Y{INAZR;Nr0tk-yhdAH(e*s2vJv{&b delta 1699 zcmZ{lX*Ao38pb1}N*SaYIh9mWQ?*o^zrTdU7)itu71Y?OD%Fq)PN=n1+f>82%vBSr zf+BJmiDK+@p;KdR(KxDfMs!r#GWMk-s%qSG?)}&g&-OK)*O;wBqZnuu%Em=6g8e>B|3E%;1Ti^u73I~9v zaNsEbv;?e3I3fwaH|$_lv>Y*}j1X3MRD5(CC(jIu4g#n)P84Sf#ReqO0KmzbLbP(G z093%%mf++_qtT9HlDL-`3=%DxmB0;)il@`;kM21qL&HN!|6{3NV)MjM-2Ve?zjSmI z6Bq9t#wLY+Ie_>jHt(b=mG{9MD}+E`XXrGFPtxxTC2~)cFS1vszSRk4ENiT5^}2KT zPC#ByEwiUP2RYikqGK%ubVoPDbC%P0R@9R!vE7Et?N-Qa;N=00dx_~Z!cVfOu?ViJ z{^(e)H)n49_GQBS(%{nI!opqf;d^@N!pT%(%JsWBn}c_gt0)p)IU?5Xe)iOP%{OD#niTsVe>*o4vunJ|=p3G^2@`R+9SYO? zqg9R3xuC*gvM~~B`e5}(#gU~>t7gYex6k<*8D!`p=6JtLTHpg`55YJesy{RRY_h`_ z@k44^@}w`EW6axvMdf4lv=%Q#GUD{hwY<_u(Rn(V7b5aM*>AOKl5+&=L_e)Y-9LtN zPd$TXJ-(LxFxgT=?m(aVaFBCw3mw}gVeEK(zIFArj%d4oSx~0eQfsJ)PRcxo7X>Un zdOp8&MR!=|>Rf34*lc}RX|=KN=KPhc`6V;2W>$R#Kz_(?+t>@WHKME zyaz#$sim!b?Ja9;Nt{mD+KG0r8kKgH^;~RdFTN82I~eBf>3Qm7P>V^kEGLwwMOa^7 zH|m{P^|uI@`r!-^i0ddMQt9s8A)^}gn<5W6_Y4J^lVN&4t8A3kbk&NWK4y~YuCKYO zv#o=!6J|c?A=J%E)dZ+-j4+3t?Cv%pK%EJZv+-vtz0D>|u<*=oQ5UY<5c2Wo`UZw4 z!3Q+kcQBAr@Vq)c@k#YIPaR@%8z$54>GU}($?8fY+*S+JtnR|akw@Oz@F9CoF9`98 zZ#qwZTRm%VsL=i0H^KiIneC&V-*Nu@a>8t_>M1!RBg4VquLD|fDLoA%)6KGKnzAbT zaDia?Uma=31DE1Qg&Av`gfMG^@dpu)2Gp}qD7&I;R_d!TP;_pKSJ8rM8MXvPGV!~a zfVodP>Esxm5s&+ZB$L#5-tYju$~O^?<$M z%YcjJJbGNyi(?@%ROt(O|7it&sLnNV(AgH2Me{O9sAuYTGxd+7hPElKQ*q>>}Ur*l% zqYO{A_k#D~^xAdfeIk1R2U6#p0CDkqSJuMijuhTrBy9-Y#Zd+2G2hNfYq}z!F!sYPQ zT^9-Z&yb_HZ7hDf{OauA4i}ei7H+sm?Da1|stGx*>oc}fd%XjpJ9*IRs26Vj1AK=A A`Tzg` diff --git a/TMessagesProj/src/main/res/drawable-hdpi/scroll_tip.png b/TMessagesProj/src/main/res/drawable-hdpi/scroll_tip.png new file mode 100755 index 0000000000000000000000000000000000000000..c3a1bb27338d4b9ffec88a59c67d484dfea907b0 GIT binary patch literal 1139 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM4!3HGT`Ez4{lw^r(L`iUdT1k0gQ7VIDN`6wR zf@f}GdTLN=VoGJ<$y6H#24v4 zq}24xJX@vryZ0+8WTx0Eg`4^s_!c;)W@LI)6{QAO`Gq7`WhYyvDB0U7*i={n4aiL` zNmQuF&B-gas<2f8n`;GRgM{^!6u?SKvTcwn`GuBNuFf>#!Gt)CP zF*P$Y)KM@pFf`IP03tJ8LlY}gGbUo-h6WQb!1OB;3-k^33_xCjDfIQluQWFouDZA+C>7yetOgf{R2HP_ z2c;J0mlh=hBQ8xDWL1Hcb5UwyNq$jCetr%t6azByOY(~|@(UE4gUu8)!ZY(y^2>`g z!Rmc|tvvIJOA_;vQ$1a5m4GJbWoD*WnVUNrJ6oDt8X5z!p{tpNiG`D+g}J4fnUSfR zv6Bl-uSMv>2~2MaLa!rEy`ZF!TL84#CABECEH%ZgC_h&L>}9J=+-@<)X&zK> z3U0Sp;MA)Rbc{YIVv!;mCIn19ASOK70y*%6pPC0u?M1+39b0(M8JH!Wd%8G=RNQ(q zc_Y^$2cEX~M-F~3xX|}jX~m87d`%~LM}ctJH9Kw>{hK&v+Kk;|>CEy|y!Ux$M7^G4 z+!)c!cO|MwYC>58=ZkBS8Yu@=lma6*F#HTSK8HK?$7ZM38cUfkPcOb1IN59IXEA0^E@M)5*YHhD&de*xFV4_ZQmDu+@b$Iw%quQQ z%u7!7bg@+eis)r#rdYWe8X7vfI-3|7o4FVox|%teyE(bIn7J7`ni?1w8ktW1#VDfy zRb+}&(E=uo6f_f!a4A+&C@u*~O|weLPcF?(%`1VqCI#UdV_bSC%P>3Dw|r1xVPIek z@pN$vskk*|vaL6>qd=Q^_Yx0f4$UoJ*&~*2>gwIt;j$$t#KqD2iZ0;N&ttHTkSW)8KcBo~ z^N;FDpQCO!8ks0qcW5+4&eeWg-`(fE@uk)CeLrq}XQ{04Utq~}-C*HI`J5BW%0FaD zEqy4|_M5}&?;#GWt2@OniitcpEK*#+H&d@ka_w^;zh}#LUJu)GnSDm1|78=-bi+eJ z*SE^~Jmcc9yTz|s#_amTXs4z{TLi3 delta 479 zcmaFF`I%#aC!@edFLlPrvlvq*XEAC{&S8>JaItVUGdFm!TtH8gZJwKR9KbToA} zu{3lva4|JEo~*+p15;#k;xG(sF*jV&BaT+IzkEsV^JTrJGa z4J_OYTr3=&EzKNVoXnIIic5l0)2veRlS^|`^GZBjY?TP>MvyFo$9pWg$=9h(w4q3&*hT-;aHF?Ru1U8q=@NU-G|f{;xaxETaAs!(IE<*m;Y4dP+FvS)G&JV$5ZI zC*Nsay-CtP$S@!$v3CnZlSzNxqvvJo`Ug;V=W8b>qq=}bGSVI-p zeB5^?=AXu4qoqHYnC$p>Op8>r4SLmBdMbdUYR{=P73Z`}AN8J_)+=&ri3?wj>>=In zE7t@qV&D@~eEU3i^SaZqjOvOuANQ4>E06iWQF37BYvbz|*DFq+BGe>i@kweITbwu( a55t5FVmm&c+dmB$3=E#GelF{r5}E+{k+oz1 diff --git a/TMessagesProj/src/main/res/drawable-hdpi/search_up.png b/TMessagesProj/src/main/res/drawable-hdpi/search_up.png old mode 100755 new mode 100644 index 41839a16ba5b3a2240f33617185a8ba5f079e9a9..e19ca0cd36a09fc554d55fe6abf6b78baa65172a GIT binary patch delta 580 zcmaFNagcL@C!@+nFLlPrcNmo>XEA0^E@M)5*YHhD&de*xFV4_ZQmDu+@b$Iw%quQQ z%u7!7bg@+eis)r#rdT<<7#O&^7+V?{o4FVox|%teyE(bIn7J7`ni?1w8ktW1#VDfy zRpf|M(E=uo6f_gfaVb_(C@u*~O|weLPcF?(%`1VqCI#Ud3tW09%P>3D_cXckFfcG0 zd%8G=RNPuJWxsb*ph%m&uuzl07k`dBFi3!7w;tC0xvuoV!>V(&?+-u!>aKA6*C!cmrnGM#REs|;S3J6?pSI)doXIA< zRu2Ns?owcjyXy1Jdgr(DTmG{aDJ)5|{lGEpLE~P{lckD&?y0MWw_7B7pZ~#fT+8Oc+_h`VmH+B~_dU5&?#OaCspST1oSX~(c3-&C_1oEW@@IL0 zb&BKW)3qC3)=tP`S^VKu|FNPlK)wKiJa4|D;b~SNzF?4cuH8gZJwKOqtv$Qlb zax-x@bu)A^oUFqn15@OTQ_(L*b@uSgyp;U%;>k;xG(sF*jV&BaT+IzkEsV^JTrJGa z4J_OYTr3=&EzKNVoXnIIic5l0)2veRlS^|`^GZBjY?TPF!Nx%O8w`UfNVC&fKoZ0lM(=dRkBmZxELv(N!WFhDPiT9);ME03Lt$p-!{rKWQvB@p4L9UF_zfShEBs_wQ6<4J^D}Q+w&0a% z^Bvy!rc^Q#yW$+FqVV(6lMm4S1_j9p5EQjwBW zBuks!DZV<6_@af1U#O5RaYm;)-ydJ!`+MK_S$@}b-S_?6*B{R@ii`awIW;)|05*{v z2=3z1YWBILSJL9NqCM<9r0~0VgVm1JnTM9UV)Nnc<5=0MSu<+3Ljd!3B1`QAOHYFp-99w!Q z!y%4K_lR@xq{f9)u{5Zq1;ku{6ALiu{6L6+8NuS=1bFCIU7UEnz72;$zC!rncRws0btyf=0uTCZ=dK(hy>dKpMjl;umd%G{KpoamITg-yWzq8kZJ~ zb0^q+izQy+p`m;}2M348#Kgd2P%t((1dhaFvFjSf#zta<5igd-4-^=&c-r3;2y`Bm z%i!=CY!+l)F))Z7#m7U%nf_e@lk-EC#rt+l;sb*V0y%Ib46$C)cOaSk|DjCg4>XVO zPX8z0|0&G#jOEbb?sOhIic1wYE?9d#6bEO^r3doaTu(MT;`=F5LfL#aFORxAIPH8Nd!DptN>#$XgF)EH3DUa!62~&ED}jXW9(1}0(LLT zm>_Nf#?IzDm%ye*G3hM+cP{N8F7~I~bt^DA;>ZL#mk~{;*>TxS$k&o_jGyO%{;A$K zF74;J?ENVhE=~r%-q`tStlm(?0`pwM)sJS_*b{Iu?I@K!M8Y?R~@ymxC&O=q`Z zH~7ix<3*NPDB!pHtI11NxPugi?4pmwv@|d1SikmtM^r#xs~E(jFr)-oC{t%fy?mad zX6L}R2;$lzA#dhbf0V#P{G#lepuAR`% zi9nyuA1>T)Gw5GzgTAQ$M!q&y69KVV@(Cil#Y*VU4~&5)^m`r_el%00=SzD{m9BMM zD+bhQIF+Ioq%AScYn(H^D=63cM#~5KJ%iFAd>@4HC2jYqXGbU}uevm(PobYo+<*T^ zu-brmiQ!GBGWO;?Lz2`}aJ$n$OwOzR(itz2ykV_-+q$dW?%6Mfd;SU)Mh@@lYZDW&QO*nEz-} z$5iXQf}0e^S!!G*s=nnQs3khSv7x%FN6HGa+y#6l4qs{Pk&ch4^6pw#ST1wYRroNv z>XMYBHa?R4N_d~6xB2XYj-A;t2Wx8#iM+PYcw%IEf@^(T4Yo10_PVvhW#-g^56x}$igpt^myaR`Z5ur+DXRV|ieTSewTEY z?5glkQ)TShN&lQWoLZ3^g=W7&u>td>eIsyMn|3>E9~u2k#U<_q*+v)Z?x;u~`r_Hcfi465jO>F7SS-<76WJ3-c$t^42!RHYIrE_h?swnsv7!vuCzk@bIdJ>ZCBn7~51|m^_gcY|6^a zdvw}rN$aM=K=(_NmhgbrjLDKJ(+V2ki_-k4>lAhLUBu)*Z7E3KzPTM)pTnx$IQgQ6 zS6Cd|G@CUCo3R1w9ZC}AzuP5Y^xCt~F+^)2jC4amIkWa!aet20yUN|U_V{jzv^Y6# z#&~7;xKV}cwTk0K&YT^}inT~}@1eId1?tG<15hi3C7g~zqoEiK0=>lwgF%}^El_9+Bue&T;OH$lD-6zJEA*cSCacD0 zgyN`#9sksljcj1yLLm=_M8?F#AYv>LTz(i5jm2VDG%PIOG6Y-@#}U$E;T(bCR|Ntf zp!1nLA(P92t|-z%xY0rzn9S+lOJMW9$#Mk$Y?Exmkg+r#5{*EuEa@xI!{h%$+3as< zfshLP>-T>O3q0d^0Fnv_xY2yN?BGHTS4#14j(mV7>p{O~g15QI7Uj zXlpFN!U2ogO8CkpaOu%(fFu0MW&F#v_%3%P3T&RNG6CQ-_X3O^d@dXMWyv_^cfH8W zeAmn0T*mjkpuWpR%FG~F4)(te`sXm@hEa9%rn3rhjGa?U-IR;pS`hy5sAgTGxLlB}e`8M*7$C+nf6n zxbAP00h9)55E!~vM@%RD8E)O^QmLw$pQ&c;qnLz6HIXE`TvnfNlgSRiLQ z5jMGQQa|k%P<)=gp)Emg%WSd@AqL0DgIL$b)=3_I(Q{q>K zNT@WJJL;5_-jwamuoPkMYU|sLgiK%cX;WzY%L*fDPBa~bPiFA>T7$0q-Oo$yIyh(SDze{;`h8` z-A{5e<(nOv71~wumIfbA%W+}*MfX}B?XC$oEs~_282(`=NOkwwPLuePqCz#x$;g3$t-;Z31q|Ik7<0NMqRuWF!1|g4J4Q}27DYgIo?B7&gNg2@ zS9(fB+Fk?DXFClHhF&o^RQYpo5FLJ{VT&E{TDOvsc})_5a#z0Rwc2)_faDHx;>N?J z$m=C$EP0a~Dx(CWh3LAJ2O(qFGoi*4aaJl-hBkP&;b(rfS>#1%PG<$>0AH$Fp#3ss zF)w1|oUmbtEXK8xW5ML{>yHBy=PfJRq}|3*%N0e!Jk>u3ipBw~HyTeta?Rfg?v`V9(l zQe0=Zg}NLmY{*23CpzFx=py@JP7FoUn#r&SdnOb=Z$E1@M={kktH}&^+zp{ z?3DSN{jU??*EB>c_&_I#+Tw=a;oolJh~9Jf)7&=G_S#^=!L_*?nef8$)5O-~!z8he zkr9#lzQ2Qa8%=GYA;4q?vETvi)Q^QD!;8jvjlwpbTI$k--p~*wG)0gpM?12sewI&u z#Xzi^|HGdVXZ51VK@lmIPrv5dl9$N#b{p8oh%>@6EhZ?Qr&ZH`QZDTse^*N2XHnh! zre@ZpGba&xhVAwB5SkkSa4p&8RV+MwLKX8cj9(N%Kjb=@iqN_SKORIW*goYCxp(7J zk8$fxu=2q!)v7vG2|RZIH`M4UYBmML<>q^!AAgLGihT*+V|B zRUpjtMO+c0_R6(GkkP&btBbwh-pKi7aBu!=p}+KnsvGna_2a_cdZqjFM%EeW37)Pq z9z*Wsdi0me8M9^=u0vEzU0bsCHSd&)YNVDsTd12$@;;QqsJvX3S$%g>uUYQd!7_^XHR%rzN|Fco{a5hbYPc4VQnl`< zw3wa^R)79nJZ2ww<~N%=NM-fNMcG7C6Jmebv zq+8MklAk)udB)oe2?6Oy$5|!- zHPa2bQ(SOv^>bd;plKs_!}(!NVPVoH^K>g>x&8afkx64{sT5r7W|pVZ{{HE5lr{qY zP9w+$cPtBg-)!yJ4HsS=jHTY^{g7Q9aaG~b#c>7ZAYS<~r7V&>7^1qv?WelU<45-X zV(eiGG>oKIuHqOAYD`^S-ksP}D@xWYD{ebp^+b0mc5QT8m-!mWCk|F-|h4yoMoXSiVJ_bvwE_bL*va}=SP&4<)49fGR9H+8?pZW*IbXG-7Cybn3G#522?|Y95 zN+u-)!TZN&44nmb4hvxoMOE2t^PstX1)^NX2NAjk*G1V~sjklNHy(HCceodE$OB52 zh~-I^r|mAwD+w?C;Q&^$UEP=;<30%aB>y2`lL}d|oH%&LBH(#1ex@4+`D`prfT=4g zy<0uMZYYUW>B~FNjb+nHVuD1g7OM>1zB_7VR9o?mwW=?7dTN*ee1-vF19}_lTE$kjk_0HewDmQ+ts41^Utw5rt z5eItm!fo#>mUwF0f!%d>^&5lEoHMbA6ptdjFR0$Htc`%`ztNkI?&}Zzz4~#ILz05x z=lLsYw+CmOf|tVc-Y?>#3oi7P8PawRl!L31R@FVzx6=zOTqcX;K-ve3e{_n@n_an` Nx)4c(OAf&){{c6KrK2xB)`25H)~G^vO~m4G2PQK|$+6alr-kC01#UW5Drz#J`%iIc~%R#AAc2+K8Luqu(n$OZuaK$V2cOM~R76i6TxQ_)XaFQHLF zJ{28HWcjcpK~SnNG*b#iXGX;EGShe<9~~Hg@>fxe0wPGxMX5yTVi`q6MSswx80V&K z92)fjB2S~D|4b^5#YP3eQV2!F61;gncsz5yOmVG=X@w z@Uwa!x%|)PLjNomXG{iXYV6+{J-cOek7@eJw#LOL<3nPj+oeWZXY|AE0ART>j7g7C z4c_Lk*qk-?0Z7-pnJG?A$tCu8XvZ`{wPBE2Z9_H$H$^Ls8%h%!4T|B3Rrik!<0;UW zO*XC4ss?4_@Wk@+LHqD}M7Z_5JU8{PU*(UxjYDQ0#maqUS}lC%bQM>!J!Ivu*@~vLkgZWwNXJ{Bx;VC>6;fnCrN9)N0_ZhU5 znO)lanV>GS-XZ@TGr%h?1NSgMY71UJ9pYft=h}k_q?2gEx?S2r?e+w(GJrC+WXyqY zS27ndc0vyqBlnm+%W-gFyu032pU`dIPra&nRuZn>=l_qTbDyizU+oh`uL3qqVe~G= zo7gT7(}%2v;$slI^iPTx)83xfX`Bk&0f%Rpp5IGzqSMdSvk^ad4b$ca-R1sE)bD`S zE!Hcmq;yTT*VnoseNW}oM&a38u4MprtiN(;)|G&}?&?C!vK`wkBz2K3Sr(O(FBHAb z`V-UB9GkvBGPO$@^96^BLUEP0trsOpr@L2@No^~LbK54J;<`L0lucVZ*6-)6BD_(7 z9Gg1y7Wagk*W1f4c_b^m557wf`RLLPBWBjQH_k~8I zDM)&3$3;P}?G|Tal5yUQ=T;zxm$IUe39n zmK$+!4(D#bb68Gzb&sg_Sfulq_Hrq_`G<;OV-~l~BF;L5hN(k}cK>uUW?65pj&zRx zZc)tX@2!w*L33)bdd(soZn}mfs9tMfRSX2&lC8Y3H2&a&MwGh9VsfWHI52d^a-r5u zmss^v^w}L9Z_6c@hvzw-tBJBa|83ReqZ~%foXvY%@zkgG#|4Y~u)3ui5ca*1_N`-u z;|Hs1zrN&C-Q4Oh-*0~wX6K8*^=>wu1&Nd^*4_7BYcHnN5Ziibi&aSzVegXAGiXaj z9w)r7*MNpwPATJlEiHRpd7=YJDUT2$#E|vm3Tf>Cao}`Qa+7{@b*5a~dcm@Aev2;E z&-3n#{hLFM>iwIl!~5iwtJ7pJ8Z|ZK1SfTm2w_YiVuh_LR0R9?iV>`h?riXbBg=!71OiDI&@MGCT;C5I`=4_ zVG$ZJs&7Uq(oRN6_K8Nw*%ePN8b46ysSTFPvr9!?X-^s5SFMQ03W_Q5i`G_+ay5_8 z`tvmR1_BdPyTjauXbG+io^m?UeD(P9+9Pxic1WjrdPjc3Q*D%4XkyN98z@OiP_}io z89T8;Hq1k*1U~)oH2a%Bugk=Uw|zN1#bB+86`b38_51(m$*u~|`UPy|8{{kRA7-ZH z*wFs*xvmO7R!cyn7cbnlx4v#ocb^65LKnWiRAtdW{@yMba6Fq{xha0#xao%^EI5LB IhLN1}U;FmBFaQ7m literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-mdpi/book_logo.png b/TMessagesProj/src/main/res/drawable-mdpi/book_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..f452a54eb038814223dca316bb44033cd29f6e7b GIT binary patch literal 1279 zcmeAS@N?(olHy`uVBq!ia0vp^{2c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`X6a&J;tUL7OBY8&Lsv5=M<;U^3u8A&MT zz-YdM;fSLH^Si^Q%%)n*H;*?&^fVe&upSXpV3sJIW|zQJ6E&=zTRR1smnlfMY4_RvZ`4a+`Ly1kXZe})#}=GA-?dQi$&&)4Tkh(V4w${4#C6 z-}fbPpWm!-mV4t#oqZoWAKsq-KVi>X#jvxd9T>N9b$DM~{QB_udY)kY+Jpo~ssBH( z+n4k{Z>Uh!Y1L%1RBw!!azZ4H*C(-j#sNkq&(&rD!XFtY?BVrL_Bz&(#y24`@j^&? p@08VjO@BC!WH(4XtYhFXV7OG&5S*J;#0V;qJYD@<);T3K0RT)6wr&6b literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-mdpi/book_user.png b/TMessagesProj/src/main/res/drawable-mdpi/book_user.png new file mode 100755 index 0000000000000000000000000000000000000000..66bf3b1d502369546857367e871a252da3ff54de GIT binary patch literal 2303 zcmaJ@eK=Hk8$Q-3)%qBfUTf$WA7$5kGZ*L&vnlEeO&i_Klk%{o;k>0olL~pmAXlo->ER%S93`8vxurIZiCIo>yQ( zGOko0K?JZEoKxg%fs+wBR_pX*3L?p8S+VTXF=-EsAm>O3Bs_60rFkHm{r{mN(Py*_ z34#9h`#*(cVG0RE2!Uj9vXrYW9N&5_l!WFjg*XT-4TE9f{3!+}zz8f$fF+=JC>2~C zDG>AE6q)@8Jey5piDd{!%!OD?I##QI7YKMXFCQZxwwNh9Fen^=n__7zRfiHRa z$mM<63-ya!g4PUSuCV_o^!%2#d*-H}+g7{y-1v}K+wD?qThDal1_FSdjK%Z{llMM~ zW=lfDmoWtXMUJeAPSeUdQ~O?1Omc5-&gppMkF_z%y&jZIN_)#!&1b~O?$gRx-#w!h z4!q$tV*_51rI#}+{k#r_By^r&snUZk}uoVw~1^{wfItUsn|Eu;1X*H zUQJQU)Y;t*z%vb6W3IkmB<_LL>ou9`qAFuh<=&R|lNmOVc+LH?`;8;#{DFlNP8t^9 z?cNXi6LigpI%nNNgCBLrx6Fc_x9}sZtCq{7Q>#sel&E9zm8n29K1u`=s;2V7_2gMIjBZAt@GMPeqYBiNhj~mh)5x|;Z}N8;7K?^flwkNM&(yb=?v+EG zPp@pz?gA zMk_W7>i@abt@iy)hE@H+;lWUJZ&P*dg4Byh;Y};X_TYpC@pMv%4}jp?8IDugwAb zSS*GVf$|1~QH&E@8~3}TqIhIcjepvvr9;Y7`;EuO5)!{6j4UZF>)E(*W7+T|eR#@V zoXv1F_1~ydqnYe{Zp#yqh5p9eyy9NHPkm!{-_J(5J=p^#cU;%(qqv8oq8cy6 z|DJCg=Q*N(zj!nYdVRUq`N%@hGp`-pU>Qkm#4V5QzuNcUHqO%s^|%X98JV)WH1hJJxVto!e|+D4W8d`*6-6wkhhv^_C1{fbp|_^x^Ae%q^PxF>XZ}wG(=(H^~>e26BFEH(b3p-R{!t#?6M_%9M&-&dt$%?;5_g z6QWhu>}6Sp&^oilK6>qoAJ)CTN2{J4INpCN>f1^Ee@*8ojv5wU0-M&8bc`*M0_-Au z+pjg3=a}gz7is>WnGDrTGx>T%a#?=z(7WgSLh)}aV3*Z1;RbF-eL=#g4a?bQrTb;S zEGS;Nw2v~Z55!H(`n08=3IEPz6}3G3-*@cXn7+RfgSwRy<;{2M3bXwcGkYtMBT`@g z2jP$9F+#UM+ceKI=^Fr@T+w#GwufAC`q!hW@4k99$yh{2n_J{%8oZ@OjU+FMb2n)@ znbctD8#Lm8V7Te4xp^j)@Yj#05z`dr-OBu$Wn05vJrP=gYa80qc1ZV_T-UKY2N!M* zF350iMCWvjybcyWy+57CXlZ_GiLnW22{gzY8yzf+Y}wf{@*W)rG;gb_eZ$UvJ9i~v M`2;dgt%=+DUw;w93IG5A literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-mdpi/chat_badge.9.png b/TMessagesProj/src/main/res/drawable-mdpi/chat_badge.9.png new file mode 100755 index 0000000000000000000000000000000000000000..493995bcd1726b72562e3a2a5390bb73505ed834 GIT binary patch literal 1246 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAnN?apKg7ec#$`gxH85~pclTsBt za}(23gHjVyDhp4h+AuIMD`kd6ltlRYSS9D@>Lr5I=vCwv0Ch7k*w|MTBqnF4mMA2p zrf25aD!t#mUr8Y|#a1cY)Yrhbz&SM|)1#^=HMq(zB)KX(*)m1R-cG@$!U||WZfZ%Q zLPc&)Ua?h$trFN=DVPg@)As;uP=V3xw&xF#U(+h2=`(&xHzP;AXPso zwK%`DC>a=WY04n03ap%qQWHz^i$e1Ab6}wukda@KU!0L&px_*Arl1j?nU|7ZUaSdL z@9S&jnO9trn3tUD>0+w{G(j&jGX)rwmd-Aw&Mro-j=)fNHFI({b8~icb2Bw}G&XQ_ zbAsu0$xklLP0cHT=}kfCb;YR{loWCcfHu3N7G;*DrnnX5=PH1`Y?X=IElxPigX&Ge z?G|U8di8;h(Fa8=QbfaqfT;(>geO}d2cGa#^MI+n2$-yQHk$=7Ffayrx;TbZ+)7GP zko}_`%FJ!&dEZrkM#B_g0d~fXssh>$OYbp%WLB`_JMy}r;{G#>&iWlPM>~!)$tzst zJQC$%@l8r#I4@+TMl z_sQT|7LdWn9P&RTh0#qVzJhtBF3%O_eHSd6Sj0_4W0{h2wW5k-J4zeG{jm7W+8;eqOlB=?}&P3gb%O|RKZAf}*&-|9b{*3wpk#CJ|(`sD!*0q~# zdSbUp^>}8=NzN4SYnM(;`g&s8o91T$Y+BEM*cNy%5cQLss;k)^n(`q?B5KCjg_Br| z8=vL26<+B6m6K8HzKk-Q$g0 z!sbkj(q6EsgTKSx(O7ED)=krAT;tL`t?$;rz|8PoD&Ak`i^^+IiR0<&=d#Wzp$PyT Ch@OZ5 literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-mdpi/inline_video_pause.png b/TMessagesProj/src/main/res/drawable-mdpi/inline_video_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..27d8ea66e1188b1c8454e9c12745cd01ffeb516e GIT binary patch literal 977 zcmaJ=J#W)M7&fZ-suYA65M?@?R)UJyKBsnSQxlRnAwjrm(;&hC>cze!Zfc*guf&ZQ zDh4E^{suP0g4mcCm=OXAi7t%%0aga2a!!+!fneFbyI0Tiyr1`CZS{6)_VO&paj9C> zth4nz`!1Y0#s2jhkG9w{N6SsRLE5yBLd+E$(!!wTp$@KNZCD$q+IMwEmNLbk_MT3=%`8AzR*N}R(bV!%HVc@XR9?6Y|f*6@)e>XNegja!`KK7A3t&c8(K`}b1mS0`t0?VaEj*B_*wFCV;4E?KR- KYQ8OQKKld1dn`l% literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-mdpi/inline_video_play.png b/TMessagesProj/src/main/res/drawable-mdpi/inline_video_play.png new file mode 100644 index 0000000000000000000000000000000000000000..e3b8d462ac65f5d6f6c96aa9333e5733917a1580 GIT binary patch literal 1043 zcmaJ=U2D@&7|vGhbaOgHadV1?Ol1R`H0_$Mv2p!KyH(8?UBO8*(hEUBL>y8OLGX5Xp&}v(f(Tv;^Q3F57po!3Ip^hlp7(h_&f56cNN4-; zc7h-}GowNluU+^%yw8jO+4GaLc_xF_?ZCh$1m$+2DuQ+1;U{AkT{y9OmkTwU5kZJJXu7@BFE>q@a9f__nwRoyiVR1u=cWpo=!x(qV{Pht*ARTVCf7TAD4kz^u5oMBRI zBFu&&Y-%tVWQHP4I8?_9hCHu99o4Z4#)gK5!s$S&Io2Hp&BT!fXsHWONn3^ns!4Nd zb1m`aT6VC?E?R1NB${GrEQWSl`%kOuCOkjxwlTQ)&=`HF<9WC6VE=l0%DrK?GD1A( zyf6IT-X8XxS>C_);^gX+QMTnvYrl8pV=Q`fHFc-*vw%EPpZKn09;Wx1cXLw%2RAB2 z_tv4x?fczC>-80f>bi8VL>##0{X7f4?)fGQ7uz<@u8zb`yspH0rq;JQP>1k%{qFah s!b0|8e^6^H_zIy{{=Nr$+hfZu#K7BkM|wXznRKsdCN(BJN=)7O1FRQDMgRZ+ literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-mdpi/instant_camera.png b/TMessagesProj/src/main/res/drawable-mdpi/instant_camera.png new file mode 100755 index 0000000000000000000000000000000000000000..3dc347e88da8c14a246f26f16defe26013064f12 GIT binary patch literal 1639 zcmeAS@N?(olHy`uVBq!ia0vp^N8U}fi7AzZCsS=07?_nZLn2Bde0{8v^Kf6`()~Xj@TAnpKdC8`Lf!&sHg;q@=(~U%$M( zT(8_%FTW^V-_X+15@d#vkuFe$ZgFK^Nn(X=Ua>OF1ees}+T7#d8#0MoBXEYLU9GXQxBrqI_HztY@Xxa#7Ppj3o=u^L<)Qdy9y zACy|0Us{w5jJPyqkW~d%&PAz-CHX}m`T04pPz=b(FUc>?$S+WE4mMNJ2+zz*$uBR~ z1grP;werj>E=kNwPW5!LRRWrzmzkMjryA&s6|>*(wvaTU>CO z2i2Q`(=Dd1IQ8lS9itD5Sfq%C2?0|NhzU=&Kn^_Nr{)1udl4{Mw^+^u<}s$*o-U3d z6}QfW`*sH+u+Lm-{DGp!DD`d4yHhtQ&YKF z>m6peZuQt^?mS`_pTg3e|-=S5H2< zB+WzWXkG8|$KeyQMLWFSDf3UtpJgl5`G%+caDSWY;yK?R7F47c+OB)}c-HZT6&oYo zByGQ4Ydin^?_;xivt$;8nMmEWV>;|m^W$*AjeyDJ$CesoH?@44zG_vp9=q@LueCtc zGxgdRxv#pcu;azFjNbIA%O!c(-gm6B&ot*Z_1!JT!zO=8IB$No@_(8`kjct+`)Ic4IO+n61#f2`cw>-L!Q?1>b^ws*a+ zzy3;Qd%W1pcC#plOJDk-=D8M}Eu3doMCjae)G5jcue;5*Gd2sil zz`Q`V4z0sF0U|xFHWy3sBWn}{cAR%-o!hoeb*ZM27R)fIVAP3Q8#Z~u6l0O{kd+~mcDV38wc8LHxp0O}LtKCetAE1v z4PmQog}$&py{?!ctFcVLBtYbig5ht5{Oz|-IxXCw&7wQ~^xZhOyP+!k7>}Bq-s$;h zuhTcirlkGr-^MC0k?X3w<<0+g!p%-sg+~c23ctfFJU@SEKet5m8Yk*-ZVSmn+G@Vo7mHoAs%*!1g6ddiUU81}>ZvFRbnF=AFd9&ny&pB}M-of*oc2}!1 z|IG>TxxY^}o literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-mdpi/pagedown.png b/TMessagesProj/src/main/res/drawable-mdpi/pagedown.png old mode 100644 new mode 100755 index ce2e10bf05f3af0e2e4a92a6571a99a4a6fa30f3..98e51e823af1bd8108144f9fab9bb8fae7591a0f GIT binary patch delta 1524 zcmZ{kc`(}v6vq=sSnAknQ=)aGwQK82h+i;GToFW_m8OomB8{7sxQa#@_o6o1I=cvp zQfCmfMNwUKcPU!OI>L4}s;;VI?ac0nfJb*`TqCHC2A!VlWnX_#YA8tAP`8* z+ze-Ttm^-^FywfWI4bB&MP)ENlaQ{YrA9^|kqQ8yjznn#08&*!6M@v!KpYpKhD7OU z1A3b1x&uY9=ruq=ndBYd85l-mUgo>tj5F3U(nbI#nkKq*h(#GNOnkcj(8imjX z@JP5qxPOqPr5-Mj9O6$2pqt|`@Z*iTr?;oxe=PnamdWBn{2yR^k_oOrquWt#>3NQxtAgnK30Uy)Iye|?nKNFqIa{NlyY(^*~(U9Z4JxqIPrgW09ia@ZF7J zczdrhZIm}R_xGcIyiZVyfOcxF7=%Sce7*LrD{-AD={D5Y*Y~Zht&L0~krJI*9Z)RZ z)RdtgGZgg74$uQbFD5j6mm7Qf%*?GK#R*1e=W`wmF&5&zBwdHEy1 z>p`{291OPGjB zoo;std^XuB$g%`mIaR~h>0VkLP$*Pd?X>Q+!8xFos1j=n4)|H;JeEC7qtW~rjNb-^ zhO}T?=qUrC*4n$(j^UkNwlULwKt|=wrVoPz0%24GT*zXvs`kPm7i6GK>qS1ZyT96k z(+B=8t!$t0p{%W~RWa)8?e>oL_8j*F#3dwLM|v<<;=~1j+IR_VxHN|^KUAJu$qW^F zKR(`D`Q*vidTMH_DlDxmBq%LX6GeAtL1A#|vvU>USLDVUE#Wca%i6-9=5xk_oq9n+WFxSZF3%dG+F!-t6aF1q_pVX0YlUT8Q$tKo7$ZO^u9IgIIYhlFkIc=@{UjX(E7xh< z7<}_)GryqV;1&uQ)rIz0T#&TT>o5`vq|v@u{O)^*DE>j%wOHM2rIXmSI!YuGLn1>% zw-?MVZRA6f%TolE2!MVU6x)5?|Buns+b*ElKdVPa?YO(UyNMwoA!$oXOIum)&y8gv z<{5oj9$AwImSfX^5+N@-I@$^4+ifzFc`pnVFJS$+R))H}JyEdKUzkJP! Z03Cg#&{7`R`W-)VkhzH!?x~Sm>^}tesl)&P delta 1387 zcmdlWu!3K)Gr-TCmrII^fq{Y7)59eQNLzp~2OE%F(Pk|-QBi|Yd7@XJs;O?WfuV_l zxuu?=siBF5p^k!)fuXU!fw8`!v96(!m4T&|q3OoIii|wY3L3tN$(ea2`NbKNHJM~% z98FwI44n*&&5cc+4GmpQ94(F991WceEiBAjon2g9loTp*3w(X8T=J7kb5rw5JY8&+ zfFgP+nJHFKy(YNyPF}=h0JFdlmm(#F;*y}$G^-Tc78v2uJ6Vs}sb2j|rX>Rdi?pYU zV@SoVH?!Y+2M0_1&x+_G;Gb&GU}iUeC)eE`N9D?LF`KADiyKsVRF`dCu~D z<+9{IOPRwPvk#~rR6p?aL2^ocNY5VLe|-Pg;@In1|3A7bA!pQZzFGP}x{Jb2u7&o^ z(+|oY=uBdsb>QuTuwCba12^#7xPRy7ieTF3Q{bky{t?ShsXfsg2Z|r)6|icr*>*tx zpnS2c_0B!l`QDvL?s=;G=Nz|*2vbn=#4B<-%<=5uMyVPc-x`!{sss-I;oZ=xS?~U% z`FeBsLF=Cka_sjPRr+LVv6^2h*;Fjz(6Vb~fQ6&mtorGix4hVnJeN%{4&G<*BjRB9 zOD2D{1A4Rgt}HU@S~q_QL(=ryGk&$azAs>*Qx`P<*{pYIuR_aT$;`dgEc?JplvyfM zhV9bV1se*&cHcV6U0-{O+l`k&>Zt5e@w?mVjoz*ZO%P^yopR&6Xt0{%G>!&epXw|j z2E$47t|}-PNq#YLlrrV$@CoIJIJ;$a)?P2MEACTgT0gjOA>i|oBQEE%BY*fTzx_7v z8E?l;!@QHq(@$SDv9Wn`D8xfW=w|Ej%|UkNc6N13*RMa{d3ueDlA)EE*|XC8%COU`Kp(bhLifkH?LTjW=@&{{H=2-g-PcJ6k(!^;bsq0~UUQ;gw$(oPQo& zU?G#MHaYYEstq@DWW~Bu?>_#>vgAPP;fMNu%Rf)N|Nj27D=N*6WdboC&32s$cVN=Gf`lm|Iv_bZu8wR({8HVBt0a0poPjA|I!h3P&eN@@-w`e>U-h zh?xwdz!Hvv-|@=Y32G;p4$3m*o!A>Eq>#KZf+beL;!0be(u=uHZ#1f^s{9_Vz5be4 zl!4Fqu7AtrrP(@_GZxmXu5FmlZCz!5dETE#Q&^YnSuEAmEp(ajftzHG99Q}{U1aiR?dggS*m$qIJ^Izd)6r@V??fLwv~9nj{Y0*gscykqY;%;_?4=Kt zoR+>78?uTguW_A2Sp+}Vp{Cgrx*j-Pc~-yu%gnMvCjBNG9*O*Yq7-x4u0B;#OP43| zknS{JQ=_M8`wXM}Hl@rIKP@`p;`*iu5fcJd8SL==U_5P&d8#|Bh{8rbR&Uc&B23~c zJI?0hPnnf-=yd7Cs*N|&B`qda2QP1~VGOL;vEOTdoUdRLf8S>D2jx2Lo1Xt?-59A7 zlD6ffKug(+A{{T!;$5uF^Ew}IH0-xuRAPVjaP?0!AN9kks{f)HpCxQi**vFecZ-zl pa=Ble{~WaiBmaech;CqHSadu*SO4AmWMKKp;OXk;vd$@?2>?l8K`;OS diff --git a/TMessagesProj/src/main/res/drawable-mdpi/scroll_tip.png b/TMessagesProj/src/main/res/drawable-mdpi/scroll_tip.png new file mode 100755 index 0000000000000000000000000000000000000000..7023f70173ea6de5bc56b3b99505ca8a7e6e7298 GIT binary patch literal 1112 zcmaJ=TSyd97@o|mM7w|_rQ|e*6u5I)-O-(#Y;|^Pv0ZF;%LNl0XO24A%o%5n?J5=C z2%;CCqN1YL%0jA#A|ktCl8_JfP!WPfMMRgEsE4dG>RJzN!_1ru-}n9h`Ty@6uMh9c z&EAkr5JYZpmlVO{O8l)|k%j-O^Nzj6!+PY8qbO`e3E2XKPlYXj4C-eI z1hJITVsR7??G_ZM)3Sr1le&r71W{F;G-ag&AhHFtX@*FBzI~S>HC3bi(r)1i1(s(L$Fl@zdm4vdYSHN>;o1w@#2h5~|;NMQw9(^R3t&zAEw z9LJSacsb6``+OdErLU4@{hZtDkt$PM2`XJWFi?uC{^7dQavdw^CXOrtOFIPAfCY7O z?#P0cHWxOKR&Rl;rq6{<%VlsfjMLb^8lBq0?s2Azw#AD@;{yY`-NLr!k9L*gD=`t2 ze6i$<;h{U(_jjxrd^+&`f$v8B$B!A?kMa8gMSXEA0^E@M)5*YHhD&de*xFV4_ZQmDu+@b$Iw%quQQ z%u7!7bg@+eis)r#rdYWe8X7vfI$IbSo4FVox|%teyE(bIn7J7`ni?1w8ktW1#VDfy zRb+`%(E=uo6f_gfa4A+&C@u*~O|weLPcF?(%`1VqCI#Udb6k2S%P>3DpPqBaje&t- znx~6nNX4zBAOHW`JHNSPo$8Rol=S$pgXuDbtqNw!69P{jIB&h_oLLU99!eEvq#ansY3!^lc9!RcN#fOxXZmb8vkJ0WvaT_oWHyaGqQ)Z0YGI+ZBxvXJaB;PCG&XZHF?4cuH8gZJbu=?JwXiTV zGIlYwv~V*spRB_q15;#-Q_(L*b@uSgyp;U%;>k;xG(sF*jV&BaT+IzkEsV^JTrJGa z4J_OYTr3=&EzKNVoXnIIic5l0)2veRlS^|`^GZBjY?TPK7nI`C!@+nFLlPrcNmo>XEA0^E@M*W*6>YC&de*xFV4`MY{n!fXK3i?>g;S} zZ02HU=xXLql+U>H4B(DCVMe)MS+dNsZ2?s zxFjew%_=27ximL5uf)^ER;ePlKrbaT#mdzXtk(&r6*A0D^@jwEI~f=lnmk<`Ln>}1 z{rUgjp4m)b`IlL0!V7|G`C9_5A4)c*e0ehrpr>W`8Dswii zX-Qf$YvzZHqGk|XkWs|Sc1ly=*wUGXWiMrOX8A3yJjmnm)+vm6(?!wwY8g`&+2+NTP$9dl~rB~~2cSt53Xjl+Op&aKp+St-0zfX-p?boFyt=akR{0DBU8T>t<8 delta 311 zcmbQhae;k;C!@edFLlPrvlvq*XEAC{&S8?2H*|C|H8wCdbaHhyG;}q!G&M4CHgj<@ zu`qNrH8C)ptivQH2UcQ)Q^_wzb@uSgyp;U%;>k;xG(sF*jV&BaT+IzkEsV^JTrJGa z4J_OYTr3=&EzKNVoXnIIic5l0)2veRlS^|`^GZBjY?TPQbDT9du%vmynjVH) zeS3+EJH8!S(BPc-qcuWZdY{12kL;385#JZr1su8hUGo5gbZLWkuz^Y)&;|xiS3j3^ HP6Bf^_x$nP=e*82-{tjwzd!3A-*XZzEsVIq!e9UZz-@d6W4%Ap zem#c{?7w@^xPkpah;rJ2Vngzxgx~@R06jd(g8(u1#hoWu6L9#@fPR8D0Kg6f4s^Ax0}3JHb)fbt=8ES21_W>7 znXo{DO_&ANGwh(Iltucmw+uC30K)opxe@z92kdP4h5G8q1;CTh4hK9y34HOEok3f(^{U|sp!jCNV zTLD8Ldj=ByDMXSV}0|DC4&geg#Q{t8HW#a|))2AZ4y z|4?7wf6!!#HQ~Sh{-45RY^Xm$!J0rO1qFKU7tTxSmnnZugFpg~LJGu^NIt)_Xz5L& zkjUO7e~5vNDgqXXSnkS7xHnyTt33`$8Kh19@k zAd#n4^^}y<)sgB3swkA2ih-W$Z!U)98RSdwqx|OL|I1bSSMIM?@b%xfj3ERPg9&)U zK$0)yuaGr~|L%+8zv}(X#s9l6$baQ3>^r0ItFZs8(7(6#yXV*RpSIm!{L}aZzx{3x z+;3~aAeU4C;14cij2@Oc`RN?d$7M_a{idZ}$F(=O{YiL1YB`)}EeRWHh^JE|n`~{5 zgBMSp+NkbGtumq0EhnR9%5UzL(6IGQx}|YY9yYcnUBQz93+k>5GjAhX!XK%Hq^;w< z-W2A3XU?cSD*o~laQ02xY7y(X&iiTdi-A9j3d)o*LXDSOFHiLQ;Ovy-IJj~q7#{0C zgqX7AXbpLFhW5ZGk0N}LhsCWa452)l&BxQVuRSaD9uGCs{JZc`@VX2uk{Qn6aspak z(`IrrrgB3LMQd@w9$#d<&Rtc^Zenvt5oNy4{jRi12on&iDBgmyG?*Q^a!$ZB138uv z<6zD``9|zz#`&BmkD_5PkA$CxxR?0jKhJ$G^HFpop!*}7<#c_fpF||Jri}1OC`CPs zIGqV3$fB-0=So~nG`62lr#5FlX?#M!=cJI|ZRWTwKMm zDP%Lo=`Oo^fEp_5h0n=k%K|TS+65zIrXYZ`pdyDvoHxR>Jr&bF!IZrN@3dgL<%ZuY zps#7L+8BJx?!Y6+QzP9eT;Wd&!~z@H@W?2cHxVgW#Obw!)^L3Wy*i5qWxD0ez6@;Q z7pS`NS2|zPN_@92fSbpHnLL5uu4j`_3e$?yK*Cl?k z+foJ_L^U!iRCk~oDawO-8mJFUeb;I<&>1i-g}*VPxP}JvqrH~4?Ac&i23?=HZd=j{ zp)#eTZ5Mzsn2nYDg{FXJnIJzmbAHY!$O{XQ?&8%qV>ZW92j2!GfNItyIRtX03jHYVHX^KwnLY^;|^JMc;*;9q1qeKCDe9 zhQ`_1z4rF>?AZ(t4i;Qo#M|gy5*HABccVhFxS;_xQ_G>AeLtVAZ?X*Pyr6&ffz{@K zT(vTaSrM>zc&p<5;b>DWLm?sTf=v0w2(+r|i*32ck=L2LuR6QEdh_$En zGzbC^W2wg0&j%dc+$#3OR!b9aPsHx(ys>Vt2W_=x^wdY$*$LiRR|z<`$@`R#QLoFF zD4uFM-cm7m|55ic&+&xkd5b`CYpYG3XU9&4zZJZcSV60j8>qZlld};>E{7l9I3UGm zqulG_mTeP1Hm0)uWl3nVrDf2cSAYY*t;jlA4+rqyOmQrmTxttf7;3$=rF3-B^^Yq) zwpi-|YQJcj@y}zu-tN@yI^G6!S9ZtC+A2@=$37*xHjD%??NRvMD%A_I)Z)UW8{58q zeyS_pwVohY^QYZwriWAx60JwJeX3>4o4IejSSNQcV~n3QL@ z2N2`7$D=HHTqQpa@Xx7^8BUI>`3R?FHl!b%ui&+e$%?-)I{WoVQq={J-?ehUL0e%- z#_mwgso+n80XwzLy&3IW^Of%x0i^_ZO-=2{+|$^rV1Gpypnpx94gF~M4OIA#e35GI zyR$M)YPqZIPvN*DfQDKwTiSkt^c&ROI;I9i1p~N>3VwT2n)r35nkVW^zmkSnq;XYtK=uPs_>d)H^{4{s$0a? zyU(InZR4P>pGEUgal!C+0}o1+`z#78`j<`Lde^Y5$UhgaOI*dswm(r8Gp1CZ zPtRlf#N`C2T=mN|R8`TE(7G;zxa!c9_@Kjbp8d>wah(~F>=U|^8raRw&~Hmd<5ifL zyU<`=5+y~!7jC_V< z2{j%rpHR|>J*UT2!p|_d&te7Md_=UG)$gBa4@#$dM0)C6!18iAT;<242ZCw7_KlmT zw4P066-z{Rbm%tV`>gEV&3qRSDZuIy zmg_RVzj97oj9sBsofDAffZb%|G3@G*!zrUpYo7x~4hY;+;gk+^{~+}+agi=whSiFY z6a3y~EFq+ux?&zg`GQ@QF2xn5@E>p$w(-d9Kf_--1hVS;@J!gb-lgjH7h&A_V7siD zo409fo1Y$NO0SJCqkFXeNEy>RbRooOr*&uR3B7XhoYs_Z<y#3yGnt7Z@*ruH#w6BmHbxszO^ zcPt^(-8jap-c8w|xfLTWkmQ$J+p?GYOOozz!bCLl#^ABehYH+3fcY-nPm6jJbp6|^ zZwtAxnhi}uoRl~!|Mgo+-md%&#OQpz3|(*pnTTB-*LeWu=CTF!PDOR?Bi zWy7rG*EecyaoOAhqKtc?ltytu!DkxD>!KIW0*cJEW)HWqK*eIKH&7kHr6m8C<5K=g za~#jpOs5{rBf4wF7Fh9OFC)lmK-mS#!B1s zoRf?b`B0)aw`Q=m=Z@o0JSfMA&hL&3b z(#?8XDR|>yT_Z^^M)${Fa*5NW^3!;Q)MV^o36U)0oxX!k7}d6Bp=TN`60x$KZ5Fkm zV2kVeS3Rfbv)r~cpjURLeV<gqTnc|pheh_y&6k&CnTzSWHf|hQ%C^U*DxBC^ z;9q%L-NZ3xz>lh|ZKC^*o_4$<+xgLMmR?4G^|C!g`L(afPxm%evFzBj9YdRkFPg~2 zbLODXt3E`XA7RHq)GNy^8}jOEDcjp;j9TkXr6>V!gnc&B%6;nu%>z?TMlY_Pgh5w2wpQoNRv^d;gX zhuA_=!hmELSmJ&|k2>V?tv_LaIFjWc+0%1$IXkvS!3HoFS}o8y6f8*5t{@LZUZgUL zRlziN>0vbT^5PK%&K#(Ahp|S_?&blIFj(vA2QW!9qIm^+q57Gv5E%c%3l7pWT>*1lZM${s}Z1c5$P7Y%A}cscf~#tUQ4F`IfM294oPK zjrV#>lF_@m>N{Oy-9}T?xJU5#@x7!ZHb93p?y>iWq2|bZxwsVZ-oiUyWx1CV6D%r4 z&>~k@8|PpA(Q3HqdL;br^n~cW%w+b2?TPyiwclP~g>>?iFs88E2V&do-G-|s9m<}e zvOrIH?B--`i`a~sp4Xj7B_3ai?Kfh1xBt94-?6YVn_v&5Ip^+SR}zPsPpmm+$>ila zQ_O@?2l~3O>fvB8I$_GQf5;3->#&(KFfd`8IlcDql<1zxlau%x!v^9K>kk>{mqdF{ z!TMkHv1{eHSf*EF_fm$q!hI|LfiVm|TuBR5Jrg<>9t z6=iZ{Ewd+N(981FNi>tRRjZQvjCiLUzJ|o=Xe+4gF?F-;l5YnydM;;l>dj&X{9Ji( z@$tgCIlB?iJw;au2>%vZb`!=lO||91z1Pp%t^{>h#)uy%JOb<5tb zhd|aJWiInwSzD*UV4(%AHH&_XE``Sh?U^6)wQxZFVT)+qTT?BSYr{3mR!o_S19KFS z>!S>>^`RB}M2hYQa| pb(6am;8H1Pg zm*6ZKd25fPgy`LDMs5=gTe;SrTo+~_H;l}t0G0tvKMIscCsQfT6mmdBNGk;g0ALu+ z)syQ3ZZfj9xgMC1L{r&ODq&_7A3Tp?(x9jbRF#xN{!{r3IM6JPb$(5(`fW zqV0`jQ(Ph)T>T@1{V@SBb2BI|94iu_Q@CVkIQ=MtgAF%@{jG}?&DXZ!FzDY9Zm=or zKS_C#9HDq7n*ue^)7SM!Adyf*Lp>zgz|at>14SW_C^$lN8|os_SOY^W$_V<;1rtSM z2LxiB2{!-460J;OL0m2i3x|h=h3SRq>oM6>I1+=wtZAT7x*~)wCxXExhwCyp>c12S z6plZe#^TbL4CtC7*^e2@HHC>X{m&8Ttbb)0oPWwBDi}PR%z`8J5Nk*J1tgLFe<+>) zFPg)3ru;YG|5KRb8o{E#ohcku)@Xge5XKTr$I-LL`{NL<)K|S^(C_5M^X(jW}-=8q6jD*kDd1 zSh|M48TY0gJ@jg`*&L7tmrGz-QqFeW z)?%F+r7OLpJz`gM6yhz2>;B|I;xl*Z8q>D7Z0PuCT<>r4{v&1lBJ=%)o{;gWsEavE z-!EYDXNfvJcTFaihV@1+MlqbDegKcuruo`@?VJj}Tf1LiqaRj3E9cs3V>;&XcE4j< z_4;`^yZJKh8SUT0qnwB#zC7n)YEB;iYv!OeGO|aUlBnMPgC9$+PXf)tQX?QU4#VF2 zSmq%7I90`W^-qkBM55U+l1X|0{9RMKLG1<8c8?r5YJi-U41(beX5O|l5p6nOuRKHM zQ&!XZc1lbp&{J3&*<4jw7Y|9DIv2zgV zRt0aO?MG zzQ0InUHW%5s@)q;uy6D5{v%zx6U`7>cT)wa8lR-EpI+3fapop#dGT)|4w6t7x21f$ z6V1GM--bHc^)-|V&Is(4zRb^wyH>K%{Bv`BljP?)59(# z$FxU{DDU2zHrTX3Wx>YxPM1}%97`3Gl&n-MLG;k_N*5)Pfr9A5F-uxZL zrI9BNaHRWXL3ga9D;$f_Df{Gj{ylS_1sglalJC)3fM3T2G3TyTtYpi9LOp_z!4l zDo~>O4Wf0gxVkRY3}X^A>>l`7%B9_K zF_79d7hI=T?${N0?>uoZaN~f9d>jqaZ}xS7>^VB#SW-j#npsvUbKUOqY#Qg=7Fzg2 zZ#RC5X-q@j&9-YGM`wE8lP@yr#s1v#x{l_7^<1Nnysl&JVBB7Xi67FZmx}%UHHN zo7xNw%{^!_S)lLoG-}J>3&f7qTUy5R2Tz-n=?mS@8sV_vWwE0^sb~bZZ@%hA==2Ra zVfA-6*i?Il>Cs8SL}K1ov4T0jtG;-NTTTIu%5g&D3yiNN+4|p_V$<|RcC~r}Y_tBZ z9Xs{IF3v^<64XTgE`MRafBiUDc0Yq74=0w=OrMe584E=!f}yzWg82 zh5#(it}g8du32%a17#8Stkiv_^=IP3FjQ4SvwIF5(C&3BM)|pBcMo{5H~mSj@UDtQ z+s6efDswV1Q>Ymr1hlXhyGzaG4kJwBFKw!GUAlpbgB?Cw`i)<1zuLY|`bE_z%TJ)K zKV=B!(gnK|^Kx@xRc~?Yd;#4PoWJ51ryDw&q0%p|d?Fe{;0Rf7gvF_}-J5lup6#Wp zU~A`q6m|_o2~oR#fzXn1Cd}PUZNxzauPyze{41^0I^rEpg=R0Y#;pn z(@vzAyJs0Je6#JSla~(#cxSknt2+Qo+MVKQHRf4%T%y^kvnU)X3`Tzm?7TuOg!j#o zo+eM%;;1TB`^K_8UKrG6p`Wk5ACyYHq&%oQ9C5Jmk4m4`)-I3PLY9*({oy-k%bRhH zprvER-~w3J1pP0YKNllY^8DUcr;SLY*iF;XKMM16F!)t}ZrX8yGOJX?#903NkhJO&;D<*5Vb-Yss^tT0^?RgSa zCw8g3Dpu1=0$>C-;@Q`(ei5FWa38ibiXAVtkvE&%GJQO?1>T=LQMVpIoWPVcY>A5y zB4WUb_-YiuTM{>el7@!XRf5z)=F%_KT;ym$POZmT5S*v_bZ{0N<581dY#dxaR&U~+ z(s(JRY^H-EDG6!`J4|Mr|1qIGkDMsQWG|&W1ONx>-3kpAu@&K=amC@mL*`ItFkoCb z<#Dcp8d@ADcx$d+KCLJN28KTG-0qs9=f$fvJp@vnvOd*`0vw4BCD)y2leD~^B+b?l zJ~bE7fCBp*i-Z%e0h6ekXR&?3MIauG&6cd(a<3>k_Ds#qShZYL)TrX- zLEL~xYtp>+=IRE7n9Pd8f$YN!%}Lq&N*kv9`@r^(S3V2&p1lWV3awS-P(G)J@LVOp zvtt=N<5E5gmBa#_ zCtrVtb6nRc-d1ufI$w~i)A(Q$_#{h?W5kIONX2@F&N2C@t%ea8f8!JBQXC{uKxF7(98F-qf*ShvAW&+X?j$Yo(1h zZI7#zhir`{-_VudOAAQ1h}X7%hyA>HvD*5MPsgZJ9Ore}d0H{3%n!-Q_!vn^e4u1n zf&U||^<7jlm3};8%9rH`A{a)9(PNJ2CAt#3*2LJ$^#zy+qdn0B4 zVLQ0@Zq;(@*&9xf4IIffcu#V$8xafWdf+iwu(7|J7uFi<<`Ejuh1CFn17be5j$}u3 zGc|X-zr5Q%Mn1%!u*U`f4ebzuoBJg!8H~Yt`QS9g7N0eXfqgtQ#T?+~3g!eP*4xJ@ zjEJ=fv#@m!yX3CwA*QVb)(BDC6Y$59-M}IKemIg^h^E-zx@vp#{cVUC_-_dLlBU>y zoN_d`0weK6EEp~ilXF*qLcs`xJX8seKtN@|iV9Fgh{E2Dkb^3z!4YbT%HV%4u{~=< z4^K5~ebhg;_Ewr=-efXC4FU-c4wesw$>WJ$5U8rE>b{1eqTC)rjueU`yM@T%NaDX0 z^syv&q7Q-WgU5mQ72PoSK(eOTo~Qo_!JqK2EROU~n)VU~32`Gppz;d)A^irLoB#h% zfB%2cB(gR3zyAK8!X(>J0v2M8CE){!?t6vv6yG;RP(u>2Ze%>s7LWJ)okc5eJQ+{& z#uLCu8w6O&!3XDo4<t{;2m4*W=H=DE*NO*>eW6U)cXu=-*p=-LpUaw{7-<*B?oE-SHX0VNmm;5?|{Jbt%ON^!2^VNPyN4!EwNf=OcWbXb1 z%1S@68ucd_)ZR0fPSCbWE7ggvE4?SFR3K?yP->!Q!k_!rtJ}pZjt`sDxRchr~&eBMH|;oqr+AL&w~QuD121 zDtb}1JeysQ)Wn3|3cWtsB4k#}|H&8tRk&Ws4L!Mf+HHv#KlYU`dQ*8-n4wL5e*t)8 zaB!82fne;mOfEAs2PA}MZv`_lj#I*mV2hqwFUMPDZENo!>Do{iR}r^fZ;*aGijhTi z!PqzW6@A5M_3glGoDb1N7Cf{%#;lO zE*u1bj~%jn>*=ptMp^adr;7SuStyiO6K(e_ky=$!jq!Ayn?$BkujgG(#q%GfdSQzg zw3RUcZ;2Wxnm>9J+}IcwdY%Q8af8ZU7{5EhL^Z01ZBI*6{Gi_^?#=V?3-J%V(AQw5 zMEvZbzaQfYXn#Ow6Umvm(5*Vds zAdkc;gUdh0qYzL**&KI)g=1ANCnxN@c@Z}inI^LXVbs}GrSSu~rsGD8N9(01%7L-X zN@l-xvs!Tg(}({Y{KeN#5@ska1`-k%{v4og;W*mw%P~@%a)*0F*3aiMb4V6F{jA^u ztI627pCNG3=ta#p!E+!u{|62S;N{o%Wz%@5D%`wjw^d0+(PC!irV=c98ZBaCX<6Ku zD=S0qD@aP$TL~K~s#Aw+y4R^|pxlp+FC1=Jyn<6sA=bbe-aE-ax=QGWv3*ZPoP1>* zO#rE~H>jkBa4*l<(p`*l;0D*=_)g=ezWc@Ztpl;_@Hh6>CMM+Oj`4Ag$HD00%6!o6 zyqewyBXjeD@)eA4#RH`ucLgTJuEvzQtAchC=&gy}t{#5Tw=b&sw0JExN}ys>F8NBw zH}@MUbcmagdJJZg>tk{&%*JT*j9JgJ)6r-6$ASs~1sob2@;qJV%$TkGg25sP%kvXy=om zZ#dApURGyuJD>U(KA~J6g!0>=4F@FMnaa6sZwyq;H`!mIM98ewWzL45fN;CO7|Vo# zj`{2Fgw~PlPv)bEarzrqd4cMP_0qu}5ZZrZx%}eTNM~{vYDeD4Xs)LF+1a`G1_HL? zo_3tM|Kz=KCUaFp0YIp@oF~)N+Wmf!Mq1{(o!@FDP7AkH=X&tPD-2b}1JqS=9jDF9 ztXZrJg}<+n`$b>uo{-iH7jp#a%ZF>c-`5vRE{90Ho*tDJnQfdXla?-LpM2vAlCtZS z9GWZsuC(?1dxaSU8-nzzpX#glsbM`yrBb_D5)mxSz$o6f%u7|3nvo+uk$QcEVpl7# zezuk8m*%}V)$=FH*#Vy!5w^SeKO;la1#N&M?d@?ZsUpYu8OB>Do=Hh}i?c*oyhZ}b zr6cOGc1>SaFh4e*o;A(dAlcpGJ9n!sTkC41hRwNgZUIon0rYrzCf{KGTUwu9EKb#R zUi6tWRqxidFf0pD;dIV~uXx~c$)!!zpyw^+Ntl=cj6vv)8`<|14ugiv##>V^&6PG5 zfmh06IU6bq#(s{B)TVO^G_c9^aQL)-y@^o{ z*_8-oqdlBJuIL@oIizC=>e6EQY~Y#>VREkLJnSmcOQpng%mgSEhzk%n3j8gvy$i&g zOh}o7XR=Usbnf79pDyt_Y{+fF8uRiIjL2%hb*egH@>-NOcqj*_5>*X@Nr{WU-walX zx=T<{WWah4O@BO69j@W649{%*QiiGou^C!JeIeEmom^EWQ*|IIOLtH#Agpy)OP?O_4&XN%AVE?g*UYdP)RgJ$inqyY#%3b+#q;o^y%D zEyR)`+oZH2B>0*t!If!t;atdz-YIjc&IersACO&U)`*(!wF5<+6Zpg@6NHoE-HG1V zE4XnBZf#Cbc*?JtS*UZ^3U}nP$wRjg?0Rcs8Z6rAH5O$iTEU)SSh3NUo2vHdr0S8t zahy=>QXrcvEAhiWIm1QV8hQj&;xGZYs`pV*Q^wfvHI#_#aUC=>)S)*l| zWzNsV$(N*N7Vb1_SDnzB8kYIxw~ZyL_trXBR|rHatuV13CwRSkD6rTsn%kz$#H7M# zGdcC;qD5|9O8lLBH9cc4_Gww-P0qP8D2sbYF_q)8lzOaoQ+;H@!K)oo=lVO79lz}& zMU`8u$vrrDJSs&I#x*(b3UU%%mA{bK+KeA zy_ce+!j?*Hru|$QSzCAhieVvSE2XoaxwDp=TXxguwLs!gLi_{U+g}$v%7~&TyxAr zpaP`Nd_6R^lDNVvEt&gh1Mjk_jf}W&2@BRA5?ol&?LAo?I;9s_zV33kBSM@-XjIRB z0nJwQ{OLG>@*@7!>%}=`o4%$GpVVBD}#or#`WC74#c=N{!2DG|(QxEC1^3X1v<}fav z+xQ|wn~Dje-3k?%3W4-O?+h#s1#Ys(-S~L4mkn?gPm&!xy^D5^LM*Rsd$0pK&@SQKTne4ZNZL;f|-vzkNL}|ZXbO_y%b((o63<5G}8lqSEIoBvYKaMksY?uH-Gc8~+ zuGsd#;lu}#7Vox9Z?q{4(2yBvJdqAFqR%=~Z2_7oCa0j_V3m2QR30Q<8Vn38hf*6Z zHvf7uZO1r3Z*V*W&=OxNx3?h0*J&5N$$PfNvlj9J6y&#G;JC_sb6y%U`JHxQ`O0Y| zpu@Q;=UG^1YDTHmbON1P8@+s<39#~IBy84SC8=26FIjB77Tg?KvnfnUt;;z!sWCCz^?ltq&Zgu<|3F_F z6cK)_3AccE@M00`W; zW5?j9o^|pJIpz_TPfu5$FAuT~1Tj-ci5IEc*>^mS%ZynbbBmo2*$OS~fEWHq4on83 zqiWcL6Y^SjkNxV0>D9)!C5iB)+YF`@9Z(|(2eVpe9Fy@ zdtdU_QkY5}3~hP+pyI<$IUf2kapTF`sqD+2ruEI6!3;tXI`1f|6CI&2fF6!y5diK^ WJi`|T)A9R%E{qK<^egpTqyGo2Ago{j literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/book_logo.png b/TMessagesProj/src/main/res/drawable-xhdpi/book_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..3524d3067e3e9872e4548ada038a14550d5f197e GIT binary patch literal 1635 zcmaJ>Yfuws6phkSz!yjrL2S2+tuSq#hz(TUSBnt^tZDF7c z6%|It2cwZt6%h>-Wqc2gh>xN$T9k*5h-K=4h+wr+9ZNSTv_DFBcK3VCIp^N{-7{Zt zoHBBbho1+7!I%>r1;^9lN6yd9mHvlv998t-OUX2pib$a>hzVtc>xpC(h&CdrXgrGO zb26`?%NYz85L0U?jUpD(5k?l`#IUSJl4dg)%U4)QM3;e5Kr)(&;S%ua*(MOc^b&Bb zK*3RvQZx;V$~B>?T%}r0^U6!9nGXq zf#PBLyIAx}0;W+E39;E0i-lzgW)Y@THdibbJ2iMbCXHa4b8rf=GI4XjqymhZbta6Y zFaighibyh%MM*$9)3+rU$thXf{O*|O17lkelFemtoFz>H6^j3d8jVwEGZl~ilka~D zo7Fia%8p0PM3zZMH!dZ>8H$9YCKRCvlbRq5lcyM$Mo@$~jUWN3N(cn5#c(}gF$YcH z6$&UCH&Y0%L!)5{NGq@~Ob-cQUPuT(Le3S#;asjvD2BPBaJft<5by-T5V>@c3lq95 zBZ^a#T>TVR%9)nyw1SbOBg3c(%SQEb6JZ1w%(@f-N9hY?2LxP)z+R9yJDi$$cIh+*;XEdhpI=S!JB2GBm4a5u8+!*5x8-ZK$Z; z3eBs|gi^;`i$?FC?iJk=kF2{cNo^Ys?R{*k4egM8;GdYkH^o0Pz4X|1pOqb9FUH`#ptYiYaLeh8^?4h;x=t7N5dDC| z5~3QN!-Q*vzIMl|e2?SdL6^Q?->#0`)jYHI%d1O&oa=w~0DqYwrCwVy@MzQVR`RuR z!n@Ei zQ8u5Qx-(~7x%N;|0XDX;Dq;2eowc@+D>QaO@Cw8 z&Vjn`meedfP$X*RB))NLX^ojZl5YD()p+Zy#?k#aFxltUwyGPeB%OkOnHOA>XSmTh zbhFW%C$F{#3UVCx{P?^;{sM!w9~(b{s?*)aXK@(z2ZQr(9`^m(dF@Bbl&~WrDgSR` CMSO_> literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/book_user.png b/TMessagesProj/src/main/res/drawable-xhdpi/book_user.png new file mode 100755 index 0000000000000000000000000000000000000000..64df4bf317337ac62977c0dcbc937bfcbbd0445a GIT binary patch literal 3972 zcmaJ^c|26@+dnf#mcl4Pc``{QjAeu&LyVD~>`P%VW1pF3Ms_A5yQH#LmYy&vWy_L% zH;58hDtQcwD9X+|p5N2+d;fTQ&*z+TFX#LHUe|qH*L}|C#G090;Nd>P4FCX-zMhUb ztEKH-ocmb!77c6C-P-xuqSGsj`w0-Z(1E1?TQbK#4EbHi*MK-B98-O2+cWBs9*$ zQ!j{&vj{S=bPe)$Rdo~B)PSi6sIdfmaa1fUz}JUBQ42tc|E;UW>hDd_f(#5HuYiz~XPwG$g|li(%4!G|*guCj z%Np4YuV${J`;RTw2qo@8rIOU-Q&;1PT>PaK-8Cpu|}UvYwu9YRWpgXeFeos)DMH zs)7PW8L6nCtBS#(6}1qGx(HpRUtArctDi59K>fva`!84Px7@u_@FlS<>)^-#X_88fp_u0Oec%J(anA{^2mrEiA?n~7*Z&+{ywVkR=YqnhZVHP%m|typFlQ!``2 z9w4Xm)I|jfEfcUL6_D0Ew!U|ZCNcyr6??}ROXovWDW(OiXmF%IRdm9C#hG}4WO(Tr zA8=vuc63|Qp|+5ebq=N-oVK#W&NN|KkLQ1)+(s_LnPrUC3c!5Iktxg6{eGHPA)K*A zKioJy(!9k*ImFJ!k)i2Ue~1b?i@5Lp9^C|qj_$~Ck2nP&UGCvEni#pxt`Y0m!bjR8 zV667?HUf5;VTL69VX; zT^u_@3mJInVS4`v@#STr5SJ(F!(q*CFB01T;7{$BSv`p%{&P0 z<#w=D_>qBF{m){exlNOtJjqESyZ15?Iqr6L8Mm`ijZb3M8DJ@CIAUjkZOd0;_}>)Ce+C0>WeH5o#h>fKollnN^9yz*J|^S>D|#q5ZY4yL~;F((ewn^9R z$SdXoltZcY$APv(4f*yx_UD2gUhR2d?R_CRuBEXmDse^t$lf<3Rl(Vpv($EcZkT7) z&R-^wvvlFEC3qy)K32GD&dh7!Kw^;H~nQm3QyQ(qN^eebKjN%4#o< zY$$@yuMeFpu4tDf-uD`IgxuwH2-gMti{IXwvrWjmxLW+Ic}zI|%-o?D6lIW)6#?5* zc15Lq*KnCu};1_mn*3R*Vn?wnqVP4#IOTng@RCZG2 zEih-)rNr^33iNT@6ojC6nD%`7()f734N?ObNrwUwux_UE3 z7`RSi%^6Oq!CHVr{^N@_hp}M~79^K8ZHFJNg^v zJJsw!r1%Bm@=fX6Z;DeokvYf-6ZDCnKEvWp(AeV`_Q1assb~PX@ca!C-eNNO?x$%Y z&=*B`*W6nRF2braHw(v1+hGt|YFVhml4om-Ft3HvZt1koaq6ldu1%Nhk-PhTqvseO z1o{YYc5#W@xS0s zlYA{i6XLHL_R!s++p=WF1o*in=tpqu~|cXY5K%%7A52ha=; zVe)<|YR(Lu!wi|$P5&_=l%VpSZdLPY|9PN?o@Q=^DH<+|-f62lqfn+givcyhx{MMk z%Hh%coKh89zlj;(guc|i?8gpw4Wb4h-D0d)06vA?PY-Kb(CU8mJ+t4f_T|NznLl9! z>q|J{>Tg2<^Qd!mWOw+C+FU9jeErd9ue*bpcPbn|^N`+C+{w9mqQ+mn5&?*>sOE%8 zC|gVYN{w>`tZ^vYWbS)lcp$|C2NHMU*Uw8IpeP~dcga#Et{i%{t_pBx!l z)#a?|j!)FT+~^B}^)ETgzv&rDSG^k=M6jN!Ltr1bEy%g%BOUH)g=LSDBd%u_lWS^< z`tGbNWq1Mu87QFsTl9Qw(|1{qc8;20xf(0{R~`d3J1Sn{FX=%sofr2b;=VfCjD}?- zx^sX0L*am4WZty(g6HPVmCBaN#Xk=M#bUb`f6i+b3a2$3q4}P+-~!fA{Q8v2TD#c+ zU%mH(jYE#<>%H$Fw35jxw+X9gjd*+J=>hBPY6 z7QSl$G@AaPEb}nSaZ;eVzOBx=b6H7kU~fVFJMEeWD~%k0bR~_sl?lNO`dyGp>MqGrl#q zVzKAug1J%a*&m@Wy)3v4&?}Thw3x@UD!^3m#ZHCU4}_FY0dq0DVHp^EIWnhXM4b+53m) zwg$Dm@+ZN3sjYBV9>W0SbrHLtd!%PW4AfcN;g*7|6B@3tf5a(}<3Lttn(R-OCFC^v;fVTq;=* zXXhMi;a`e3nXNDc0E6CnK_{i`mOun5bY@{q{v12-hMRwOvQfmDGIJIG=(3nc6#$49 z-Vl!XQvTAXExdDlY_Vb)odX4EXun;?_>%?v{+s=)eu$T$mCZ#K2TA!($Nz97a4a2Y zn7{rlXC8Fx8!SWQOwb|>1-r7u&+IXA)QJXY^e!1~zr;%VNf>0&o8fBWr1w@!wWDji zDCpkGDRZgTHpNkimNi5$&8^XI-C=MNr+7{NNk=EO#4z$G7=XI2on7X_n>6wyi(Z+3 zyc2T?S>FK%Nxag*bH2U|QqqW1LiMRnzvX!Wr$H)jW;7fYnBmV>(}b0Y$J3YMfP*iR ztK+Mi9!2k(QGXDFaEN&&kOU37lEWw|2oqL2kQi~a-~7lxjh4`P@Q7pB?R8^(qD%Az ztv`jjWU47|=_+%6EPP$-x%6}CP7*?KcJ`1s`DSABn!4rG_mFnF6j*N``4E9$`bKTn zc8R;sz*m074L->t&U|jOi>%kJ`Kk&x0UA+AowdheNk+_{`>z&nEp5SQ?`Q>^O4XoPSJf6$P4LQ+M|Vt3|_DGda4hXaEtI}R$W-(o-? MW1{m!+d1NY0NkqFNB{r; literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/chat_badge.9.png b/TMessagesProj/src/main/res/drawable-xhdpi/chat_badge.9.png new file mode 100755 index 0000000000000000000000000000000000000000..51610d0c20d94d8f038ab949e8aa15889cd98001 GIT binary patch literal 1627 zcmaJ>Yfuws6x|p>h=!yTs?b5#02LAPG7l4pxFjG!f;>_|I)Wj)kPwm$yNgNCY6c%E z+R-?z1Ets!g()+ZKD3UF0$PfIwm@)D3$_FGEw$pSpbpdN1_Aq{bZ2+J$DDKS`R={* zRi>w{qtilZ007XH$%;&JJy-CIzJ$sG?>R}lx zTd3ly%yLAhPcFxhtnxIqw%nkVz^wQ+V4PJ-3K$VW16qwGCR}Qju_kq;VS$qn z!XRV4GL@rB2jwV+fI{}lSS^>w14SY>PaqVDcrhTK%j0vnyQx8I)&Sjv^G5{r@&B>Y5(%zB~-;wC8|ae_pcAmsB!{8iIj z1*$DGA|_&*3%}+{w=@EO=}w1XM+ZT|ko z(ch0OytW$c|3p06Et}Vx=UXct>ODLD+qQ@9gHeOeSL}-k)Nq{5Rk!beL=r)h(!wgD zZx6M~hw3(X@F;5cS_7l!+TJrzC9`9^?!Kc3DspZ1$-BW2&>LL-_K5>NK*zW%WH(!0 z78&ZFUuQ_Cf$`x~`wNrcwCm;@^!ukhOmJ~k5Q#eKW?R-ANG@eiZ)NL%>aU@v%wm^S zOBtcGV|@U3$X-Y*u7A=V*zfbfxDWOG_LfvjXJhkYO7Xn*yJ6Jx6FUUBM~{Vn1{v+o zHB|Mj7j1OM&mURc?qi1+Sj^gjMb0)++q14igHaBftMTJ>kmA3rQ!h5_(F+q;zW-)wqzDy#D2k_VgVV_Q4ZBD+TREOk)W1Ciska9VV} zHlSvSdDq%~-KNdq_f&c&j literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/inline_video_pause.png b/TMessagesProj/src/main/res/drawable-xhdpi/inline_video_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..5ac38bd6f78f9c710a7cdcdbfd8cb16a4775c421 GIT binary patch literal 1023 zcmaJ=O=#0l91qA;rUP%&G1xQP!LKGwyQWKMoNd#(Ix|aGTu~UylGiqD$;*a(-34#wFGle)vI$aV2q;n3>f(W zE-2?k4VjGU>oA^a6EsD|hCN%=W*`RBuw+;g{p-mp4GdkPCn8Evv6HZDjMN-BUK`76 zwHZy&>EY8L=81&BgjfZhIcvG1C((6Yk=%VTOM|)!o{{KnrwU3IB#{F_gb4?{+u>S+g=pdPenwD(15%h$l_?R|;2 z#j?a1)-Ua@Qa44UfBb8!aml0A`p_c%?vTd*5uMGFp_t3a@w~Ub`Q}mQV({RNPw5X^ zLT~St_?OM8uajpUe(ZaG@%9~lwCC>nt4r&j&#k?_eEt1_m4(UJ1^dtBz6U4X-Cw## zRSrFimA+jq`op?wt^MA3I`MLJ@o}xMV{xf_b?fL#$6@O2^2zE=v)dnWDw7(MmlIRh F{sLQoMg;%> literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/inline_video_play.png b/TMessagesProj/src/main/res/drawable-xhdpi/inline_video_play.png new file mode 100644 index 0000000000000000000000000000000000000000..99e0e6fb84cafb3e92ac15194d342053fbdb3c35 GIT binary patch literal 1322 zcmeAS@N?(olHy`uVBq!ia0vp^3P7yF!3HE>yta4*q$EpRBT9nv(@M${i&7aJQ}UBi z6+Ckj(^G>|6H_V+Po~;1FfglRhD4M^`1)8S=jZArg4F0$^BXQ!4Z zB&DWj=GiK}-@RW+Av48RDcsc8z_-9TH6zobswg$M$}c3jDm&RSMakYy!KT6rXh3di zNuokUZcbjYRfVk**jy_h8zii+qySb@l5ML5aa4qFfP!;=QL2Kep0RGSfuW&-nVFuU ziK&^Hp^k!)fuWJU0T7w#8k$&{npqi{D?ot~(6*wKG^-#NH>h1eo~=?wNlAf~zJ7Um zxn8-kUVc%!zM-Y1CCCgTBVC{h-Qvo;lEez#ykcdT2`;I{$wiq3C7Jno3Lp~`lk!VT zY?Xj6g?J&i0B&qvF*KNf0j6J(SfFpHX8`gNOrftYexnUy@&(kzb(T9Bihb;hUJ8nFkWk z1ncniwerj>E=kNwPW5!LRRWr!mzkMjZfNOZX=vzb=45VWYH8+bVr*<^ zVQFY?3e)S7pIn-onpXnTn}X15fm1IiG2|8iZFWg5$}CGwaVyHtRRDY1Die!aoZTEP zjg1T)aheC!n}XXdW;pfg10ACeidv+oh6w@F4~Pj*xIhj(>8Iua(|ZvxVMm#t-N?Ye zILFh)F{I+wks0|2%sB!*^?UpZ~q*{P&mT&qcXfXPiF5 zyncH^%+&*H8u({(d$6r@b=uQ%UE$6Fxh+|CO%Jw8Rezo8Qr&C591Euu|)#)s=+-UpWP_9=4LKI7Fln2_P8 zzTaTYUZ#IDj$e4_rNGWyutN6(+vTF7xZ{hYjJlqOC^l(MlRSuhzE2i>oHk$gTk$>modmHxexx;lMYNq2O+f|oWO>qdTn{dyq r){mX_+e0D0t!={Rzs8%?Gx0ENU|#dF^wimVpwi3J)z4*}Q$iB}@B80J literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/instant_camera.png b/TMessagesProj/src/main/res/drawable-xhdpi/instant_camera.png new file mode 100755 index 0000000000000000000000000000000000000000..767694370cc1aaada80c3190dbef9f23b34c959e GIT binary patch literal 2883 zcmaJ@c{r478=tI&N_L6fhH56v7|h5R%NQeM3Nu-oa4n5CJqtL$s`6i1=7Q0F*k zL1UYW#v#jxlrZR=IOjXEe^la(PIbONzP|f<-}hOr-|u(d_jBLZdp)TxM-FY3+bIVE zfwnq2*tv;EJMn9^Sw{TLz0;^F9yED)PactVk{9OBp@Oi1tN<$D$nZZ!b))(RMx1G< zVn85CFrDPd^CUQKU~4M?6OI-OFsM9#Ae<4*y69{NWCYR^Wq);90EWu(07@Z!7HZwnn!{ZP*go!y4fxx4% z=18P1(jIvbfybMhm}0+j?O2o$29?SC$_@O7YxiC5Mk_GbV$XI|4n33_XwP9WfX^wT z>EDk9`(3>++`#Y0V*6b#TpSF1qp<%~=&xJi?%9}rYg_T+TjNui;%?`N+xp*^9_Sm(&cl3xJyPY|B5<@nd8u<>x*DZMz?J?rnRTpomY+ zqGmQatL>?&PHvOZ7FD@pKf_QIxw)b?~n8zl~=?IT&G*FK#yHc_+%z#6vy_Is%@$u?CaAqB5Uch z7a|)Uifz!fq;95Q{{=*v#Jh?H9fpc(or&tLR_x}j4w5$*G+OvpnbZE071$H;r@D)g zrA>1h{e2hCAavHj2E-WGb4nWL21HF~`CX?UG|hzaEymN2){KjG0UfH@zt2|s@+Ye7 z@dR19ElJlaD(HI~hM&WFWNLhaf`fe$AMAxd6eeM{%#oe7*#f}kJ-@shQf%~#!cW>X zzX}7j4%y%Y)9fW&u=jOy!yLC#VE~09O1;?Wes_Rb){trE#2+dzFVD}BNb!N4An?gh z?P!ETSz5u6>=VdTDPgCcZ`RWv6mds29aEOlq=|ZY-C;ArHyK%3ML^K>)b#W;lTO#9 zF&I~Ey5;2M2eeSZp2dZQ?>S;Cwl8a!eNeoN;K0-qYY!)I&gf)4dm$37BkfWl{eCj{ zRUlg+J0P2g;HKnaLwzU|(PAO8O|gRY*JXElhITEJ7im zRuZhrJ`f2%(d6?(e*TiAj|HMWW_U`oGQEpry)+?uRJSa67(Mvvl~+U&Z6}aGKiaA!Bq{BsaB6C5l1x&DxR~yO%&rbrfut~xU&czx`fc)ybTT1n zYirxjqSm{>y3;`{mRUB&=;+yqi1$0K8nVV>h98C8-Km8&ggSV8Yu&l^$xJpTxu~d! z?xHyJ)FSL%wC-sen*raC|Cx^35ha7%Z{y|VH4$v8n{sP0##ZaL;Y; z`!O$rAHD^Kt|cA3VcZWl$6%rpqze0TQ&UsbdLQ{}@GL@8{r{rw4nslMW1&Se->*c4%8IjVWq6 zaNMRvcaf;S=+@}R`ZfK$n_F&<#=YjloI7_8 z{YiRH_=i0BS}jYo7XPB9p`k&cS0cB#xG8B~BA8D9eFnnXVjhx|5;EC!^)jUF`Hh=5 zf7)RPDC(=Y*msAGKbC$rHb(X>Z(TbUz9W$597d58epkW#d#*bY3G8lnz@dC~F9w zK%r11`}c7r%l5KH+I(lHb8CCv{(Axw&5KTZk4I)>qF30nZ5dh~Y3b?kO|)&Rl|{~K zz5JG_qa4n>Yw;x$S1&a;bkzjn^JDC z%x9Ydw(cWTefJ;B8>5z)sd{XjoCjg@z}&X9x{o??3g&e)meFx@o$l|*P3t6Y?~Zy( z%@36dHT!VZ#1eDvN!1f5oQi(fLHla2pI+Q~^DOHWX4+QuWr4c=p+n8TJ}JU*%g{J( zyv3)j7}vY40ri}@3H(AmcXAcFdW%pLAdsi(>FG7xalb3CDu<8lk)L0nlb%rEPcb_O zs)D;WMMwa`t~xM2{GztYQhrOEA^37fh@~4yP$RR;H&1DOMb}OUXn~~_XF1kR5?MbI zwAZlyrbE}up%92e2>NQH{8@FAy0swjRSJ4A-JPsZQ1Od$C?5j(*D((dt+B+>L;W U`nTN-*tpg@;*Z!>+LGh`0wTuK;s5{u literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/pagedown.png b/TMessagesProj/src/main/res/drawable-xhdpi/pagedown.png old mode 100644 new mode 100755 index c84cc4057e40ee0d795b3b15ee0b2778993ab260..685d8306a10f14608cd3264ee200a065f792c8f1 GIT binary patch delta 3271 zcmZ`+cQhMp7mpZKv#JA0ZDPbo5TT(cAqcIac4?>;qeeuFP?cKM*NR1*#%BWm=^ch2|y`QGz9_nz~d`#blK`~2?Mxx9BpEbfMxAvcE*2LJ%z#u({a zFs#FWFFPAU&u-FelEq}0#FLGaq*N4e$}pHH0-*>~RYM?PP*J!t46dZiI0yxps-_x3 z6Ruu)BF4md3n40V+tb(8FC;Kk4tT}kx*ozn9|1$a4K&a&m;pi;iG-n*8Jb~LRN-iG zQKFAO7OSc6hYR*`^9{o2Yl$;n6kR=CHUEcI|2J%EAyE1M0aX9ZNnA8A$inTOrt7~4 zQ2RG*suHsvsYhr;3;^Kt#OUi<635r_?D70#f}PJcF9K!w?R=v*+b6bj3WBy#bth$Bx10 zBBcpNVR7rrhrElD{O%ng&mi5$Malew^xVzkg`7>2!@UJ?v?#lNMm#O5RQ`jtd1e%c z$nl+SX>YPC?%vy3%x0e@ACbZYIvg1t_3-iX`dv{~6_DP4&)&DvMlKPJ)0pB|s*T;r zP1F~k-3?5OXfDc87vO3J3k#oa#_>a%i@IYxG;cPawQERIsbOw0cgOwka)nG0R=&P1 zQo2KGo?P5}@W-fCD-m7DB!>N2XTi9^7$<;+} zf1d1)xptl>vk5n3b{&RdofEz+qkt1M)^b78Ov=#Bq3LPTUzavOx`mH?D)?$&3wiDE zI9LeUeQa!OHdmUqZv*q;WBMz!#(LDsse<@lzxP(xt3yX}F(wjs9`#pzDkyN_<5Xe8 z0^NmO8o~~DyjEl#+rXE5BCB4QZr^?A6sa;=>puAWB$l8v*@oilp-oIobY&-_u>f~c z1t%LDTl0&I*;mSgkKqOQGCj2gexzT_g+_<>)1MJ8r*iS|mt~dj1ZpA!M-7CZEt5)!XROvX9(Dh0YAe zw>eYgX%0+=leJDQ5#jtkiieL`6_rT<&JlSvS5W1~{rnq$8z_tm%|3ji* z+TSu|KRG!$P;(&*$Y0@-m=fQO2@PRLxUKD&*a>3GG^%^*W5{ai{*EMSvdOKNQl%rh zWqIHp8pSOj75ogz#<*qe@;w6wRkuMb+L<*!EMWK+4;(#Ge}Mg9Rl+hRnr`xiEs*l&f| zq{0i=_GMx&7Pet3pm&-{$DNwZyZU3{9h))I%#%Sg)Tr|%Q<{ytEQEwwO|d04u% zVwCu})rmv-DV$%R3UrZ76js!x)Z)jgM3y45vjnaOLUFb;?d|RPOI>h&T&4hhd$v=a zlY^tVRHE0^nVqs7nGo_12n1TCHQp}vwvK*WsfwAULuah4tVklOWmOU#7JSH4go0BO zu_#II>Rl+-VxUsk_f=&v8e5oa25AUCLd2m9HF}vnC3xqG0>I@iiN&Z%Fvz0Jdex$a zD*y?e8_2sV{+E%Y9>VX3DT9?GE(g22e|;~%lR#DX8P_i)l{}VXBq)kO0Li&T)eR6jHk_(>%6% zoQ?Jz7gQ~6Y>WtNqfvGagg-+==ayGi#DDrD=c~6#pyNxPGSy(C@bFI_(Lz4k`Z(FG zr2A@63`;nXh;&;5BO*#!K%umeXgQd}xyLuec`v^iE&;grOAk-PCnUHXe4O&7jg4WX zArSu8F*tu{M4@u6*h0}Iy!9Oxex^>{5~e43M9xyMl$2D}ks%s=UT?ADep&8ch#eg# zuXxh!Y4dlHUBn;EHyRM^IXq*RJ8 zX0f7H4vz99d0*zSRD~ZA43_+UG>NJE{g5me3bOfon$B~S<}87ImB)LNlEnBcdLj(z z`aa4`rGnD4@r8wjw}v$EVZ~o}_3cQ9kGNr!;d)`S*_nhe9=Z+m(y)AQPmk-t!%t4M z;bAdfy1f})fu>`3cmem89eh=MF#j5fvUw8`IbQ1nl2ytOaoai!`$rmb%+}Jcj*qQO zRIc6B)U1!|`>h#yr<$-nUSjx6SgY02@DPz%SDK#vpt9u68&Uf@-*y4?4;RvG-brI) zV~zQ9lx|4KqwCMOW%(XNlJNMoYx>MUB+=W=&8-c0pDXDb5YNulni5(cz-4ICJ0B3t z_q59u9_Lj0)ogJc501vwhht!8a=lTigd|@ zNd>^dZpGzNTud!vkz<`045{%P5^0H6My^LB6l&F~#S} zY}pVqGlS{t`Z%Bs0BypObIIkpS@DM}Tv<;#H|Z57qCtBr-$>`UIU++gCS`r?9&b>wz_pIT7Aqow(V+;vF^DHT@NCWzW(Xq zK{M?Hp)u7$`lH0m%cRa@A92t$rb| z>())Ie+*i-NWi0LB(?dvw3{k6QCTlTAY1+$^YuULU$(UTiQnkl0fLn8mi@!KAPGp? z7vI2I38RSqppD^WqC1Bvl`T;~{<<`^y?|?H!(cGm`}_M7V?)#}XCOjyOygDxK+!Bf zM}6|(cSU*mW>wN*9u;UR0OIVXg~>Jrv~$PxJO!yo3@-j6DbPJLsQ{>r3?`xqCU%JF zAbPyj4w&mIj%Xoy6ttnoS^-cajxS0@9FtTpXH?GqX_kt6gt2^L9cM*K*u(ubThonG zZCvs(3r-f8(GeKrcn!%&1B2aQ8<~om)@g!n?p#pWRx@Vgvqfv`n`KDS>9NwXt%yC~ zxfZ#KRJBi#&3bN-KPQBI;yXLUsNkgYTx?UnE+Tr!aARlle#rYvTQ*A~V66v1gLFD^ zIxPArqfJ5+`yv(QT-pf2PRh<7@ZaA%+UJ=q_8$U4Zn{}F5Y}kLN$?j?8GiYkH9OL|66VXVZ>vY?+6c0g_q_9B3^;wW+kFk) zQZ9jkfhy9Aw(UJtv2|#cr|$ybHTp~&PsA6-*7x_WT)9%LS1|NuME5g#lq1KsNl^lB za*kcd0U6gB>_8^Icbnf}bVgLfF@3Z8%GH9_+8UACR`h5OUXhaU{gHJBTk5Vcr!A>J z2}vJr4nNW^iaC!4Ke?sO4tuR`Mw~4W_^^ozJR^f2wSI|u&_8Gwv>{Nb0VvJVp(Pwyt^R&Z%Pk;LK zNt*!CqHbugjRi`i9xuy?VFyjp^Sr3I#5r$~9Gj+UBSh((--G-)?iW7zN&J~JAWGO}DO)E#hu|4o$}Wpcc#6QxaWaqCK=D1{?V&#S vDqL7TL_8Mg|5B}@0Sq=7PJeW&LzKev2qaqF3}b{eL88r(hU&)=NR%-GWsF3@ zk;kzJ3>In9u&)7>v{Q$PgX5H#aPvLQ04ZkBN7}<1`OCMq%VI?7ytp|6}tiKnS7@8jnWWB23Is zCU!p;xDf9h9)S(}iNL??!Eirk^Nhf@bS%=91^__9o$xj%lg5_Moc1_*N_KK0!{Uk= z%s1*)g`(FpA$_LYpr0Vmqe|}dFIS;AkYqR3*oojQB=W;va0=OE`<0^FSfy!nMUAJ> zX;N*rZ$}#3QOUC`s5}z0R1{mvEk#{0N>&>wOi_vzc^8se*YasBnbjh5t;A7?J`eg8 z)RY;(kt~B2*|MC<2TlmPzhBatfvs!ALd;!RUgBcR2|aNih#_Deyy>WZENw=Z0p@h3 z?K1u_3|=l0CPGS~r7MFtNzfXIGT0fwfpHLRifs^cyT)W?V36on{cnv7jUrK^{iJTP zKTBu_Wc*>NSq_nA87-`%MK=KDv2M;5=M6boA04lSCS~oJ+F^(+^1!XA^t*j;0UH59XTB^t>P=}Dl!5nWD0Kea}G_T z@`bvOtb)^g0w<%>!8AAF;O9b(~YfR zr~B9mk}kKfE)_PdPe}K$*;p-Zey;Raqi{#bgQw&4}C4jmF)M%!=RlRl@=M`4NNV6V;5i zSyu1PokN!@-A)1v8y;5@0?@$NW78?@yyhakE_P&wL|f64RZZHMVX-rZ5TzyXs?$@q z`f74@MGF^kOeQkGcc2@i8+0MxZL#3zr2Uf|r zf%4)U<*zk9eE4u~xW?yfOG^v9`JX_G#8tzL$kfk#KEHy&z`%kFW@k+D3knMUNqLcl zOWui^dY-TZYJbuaJ)v}g2ufI z@7%fbJaKJu1rCQx)NNU=uljxCe_8YO^Lvs(eNONO4pqw4x3(r)ui>`3uZluf8$(^` zk{caSzYlC}Z+|ZIwdOS&q~oOMGhG{h^iuBLy_sL@!)cItT1Y^P!+Uw#pXBZA_3Z1d853{+X$| zVJY<)HchG(&Zisbm(cZSuQ#QWg2OGu&7CyRT#3&72YNK#K}-Hsc(jN}vRU%HbuHCn zt7w7D2^vpqsm~k`HceVqkk#{e&<%F%?zyE*m!9MfaF$9KidL3IGquYE*#gN^>+13JHVJz5oXEHXjR@V>ByxW1 zdh|!LQU=25h(O%D?@iX$oBqXfJ{o-5>AH30V)bs=>RObsVBMke z$FPia(}xgtF8+><#gR71g_=;wF)$U(r-(Ol(@ZsGdEV;*KuPk7zhSkRk zXr&uHkE$^AD2G$jbXe%NbvH#@n;Vdow?gOuSpgq}Cu6fwVabO*ARigO$qJ4-t8l|Z zsP;V9n~x8@M4{c)t=aN^5_^(+4-dCSbW1j`kQ-Yxic;%0k#EIin;u64SJ9}1f)qoK z`3gqTIUMUxG8{`74P>1GiGMxzrLt$N@)4?ll0{$O@3m`=3aIkqCyc(yc@)c2E?gbg zY=g5bzV~F@L$A6$*D1Rwvz*y*DTp`|pi?%VIf|wjErmkp-lJK`;NtxK5UOj6Ghd&@ zkXM2!zM}-PN6sf)RU@{4Rq)rAC9BR-bF(#uD09#j?rSZyk-T@!ONe_;DuwNs=wdBI ze>X}5SH(Tv3&lbI(;F3V=R#;*7d24t%4bBCOpm_Tv*-mvsk3Rlg7S_I{JyT(J2_{+ zDAr!$Sdo9kFs*HZV>Pv~`0;StoYZJthj`J_;V&6AXzDj57c;I>kg|nND3@O>{%fHE zf$A4N2-oTZEEo9OKW)s(g~?hyq&hj$*R>0ydb)gk)=T4wh=LV-bRA_Q7fftY&Y!Ca z`yR=1Bh6WU39{hX{MD$cK~{kJdebI|Sr_Vr@4g(1o&O@Fqh+pg1Erg4o3>wHI}$eb z?7AbdudgceiexRu2Tv`~FSg3Lca5qHac7^<%m%74gNGP^SX&djZ^j%z(vM%T_y69p SK7R0$0h|c#_$Ri(SN;nge{Jmm diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/scroll_tip.png b/TMessagesProj/src/main/res/drawable-xhdpi/scroll_tip.png new file mode 100755 index 0000000000000000000000000000000000000000..9d088d80ebefbaebb53b3f1badd16e1d67105ead GIT binary patch literal 1316 zcmeAS@N?(olHy`uVBq!ia0vp^LO`s8U}fi7AzZCsS=07?_nZLn2Bde0{8v^Kf6`()~Xj@TAnpKdC8`Lf!&sHg;q@=(~U%$M( zT(8_%FTW^V-_X+15@d#vkuFe$ZgFK^Nn(X=Ua>OF1ees}+T7#d8#0MoBXEYLU9GXQxBrqI_HztY@Xxa#7Ppj3o=u^L<)Qdy9y zACy|0Us{w5jJPyqkW~d%&PAz-CHX}m`T04pPz=b(FUc>?$S+WE4mMNJ2+zz*$uBR~ z1grP;werj>E=kNwPW5!LRRWrzmzkMjW#(q!?BeQZW@v0~X=vzbW@&2X>|*HTXl80; zH{644~kf%h=vIPQxAvNIhdYKVDCF>!VSd)@*Wpd2TuE71@d!Kgw`tyC^ zQz!nLucg0kIq!Szs#D&<1BKVwzO*FX^=z|IQegB_IQrRQiG}j!$=OSuDaN1m>|WqH z;alHK)e6ox|K=9%p5JJ=gR!pX_~GT{XKs21w6+FZKTzD_@!sq6=^R(7tq=C=DIc== z=5uw)PA)m!v)syOlFwe5oYJz)^um@ItD*v~Udk^DdwDvJUAOz^oEce#ZZjPdBdlkv z+a{O9Eo63}TlvXMIa6Ja8!WQk9__ZG<{l>+!r6K%=5&7YzkTrb174E@(@sl2f2x0f hui%f8nRToZ43k~?Uw&HsrUq1QdAj>g_3=CXdj17&9&0Gu(UCo@#-JD!p%-jqeO$`hTjZ7#1Vw9DK zs4>E+W&x8%3d}%DH)m%PBXehQ5?oYcMb{eDQQ~45_%azf{SR$-<%_?I(OB`q@y{I>R?(xScduu^ zFx*wnrY7KY>5K`p^XJS@9=EpK{Pc@e``QfsGkh(Dkv>het8eG`OqaY@Y?iz8>i)wv ziDolcRoIgAX9=IK7GKh)JpFXYl|x>6RuWSV@S4_JB(p7+Z*+r63$m=uuNH`mdKI;Vst0M6IGm;e9( delta 391 zcmZqToX0W2lTl!!mpbF*S&S)@vlule=P*giyO~>9I6FESI=Q+U8oHV~IlH+z8k)Ho z85x-vx;nW`)?t!`s&T}r<`<(ndw6DEN`86q_Ltz-)t?e3Ql}&H0R~b8LG8wfA)Nx z-1+W?`oewJv?f~KFqqZyP2y|D%pMayU8_S;NA|O7{0h&Ws`tg%;GT(CNK0Wrch^S2 zjzt3B*u1z`ZaZ*^W!2wpCG#9tEnb_>ZD(i9XHw#*5FYl2VeuBeD*t7S(_RelF{r5}E)C_KGb4 diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/search_up.png b/TMessagesProj/src/main/res/drawable-xhdpi/search_up.png old mode 100755 new mode 100644 index a212e27c0331a885eee05fc73116d1a265718157..6cded70aad4c9441eb60c6ba8b01f0e254658a8e GIT binary patch delta 468 zcmZ3)afxGsC!@+nFLlPrcNmo>XEA0^E@M)5*YHhD&de*xFV4_ZQmDu+@b$Iw%quQQ z%u7!7bg@+eis)r#rdT<<7#O&^7@Hayo4FVox|%teyE(bIn7J7`ni?1w8ktW1#VDfy zRb+-!(E=uo6f_f!aVb_(C@u*~O|weLPcF?(%`1VqCI#Ud6I^;H%P>3DAJSVc!oa|A z#M8wwq~g|=D~?=;3PmfZ6AW@??c?X8H7EVn)sOFy$`WMf#f XE_>&zIcd9r9%Arx^>bP0l+XkKVdJa4~dpG;?(^GIVlvH8gZJb+j-rb96N~ za5OV@c5|^bo2k;xG(sF*jV&BafX0|w7?~NlT9}&~ zShyLuSU5UcnmM{SnJFn0mjtDzS*7GBm*%GCm3X??DiP8%*?`%pzV%1^Zw3a26`n4R zAr-gQT#e^yP~c&I@M>yLE%+X*Z$e~FZQb-7~N(tDnH z-5y7S@~T4%zt(S16FF#h>i9BVsdE7g&T|$sT{Gagdgw@owwP8j8*BCrr5VXf6h0}Q zJk*tNdX?fAeYV9?5-nMYNhPii{@E7yb?&PxS~o}Thxm_(Vzpy=$)`+HOVTIJ-SvLq r&8ZtiUKL$Gq?MR|W#9c+CLV^!rq`0sN>twf9nIkB>gTe~DWM4fTJw;m diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/book_bot.png b/TMessagesProj/src/main/res/drawable-xxhdpi/book_bot.png new file mode 100755 index 0000000000000000000000000000000000000000..eb3cec4117d47ccb87617c540d7e3adb6312dd23 GIT binary patch literal 6717 zcmaJ`c|4Te+rMWtma#8Gl#Hz`S;m%qA4`qQ*oAB}wlFfbvW;xnN|r(rszJ!oB)dVf zB!o%`5gz-UpK~XmuNpE#`Jey*FdG}` zo1L_jKMy_a$=ht)o9?9H4M5rinB(07f}Q0o(jyuZ5)LPbU8kA|F_)Coe0 zaLYHqIYi2rApEz2K91n(@97udiT8#7QFL~}2L@;eokaTIQ}FTokE}1@-)TBYm`sSX zpA14;_Rp052AY`s|4<*F|DXv0X1M=}_x}_oSlsf%$(Z2?_&|TxlZA5={^QC|RnH&i z9Dw(?z~jCD&LY|aAAl!#;Qiow=1OoeYfoP+KA0f>FT9C~s4{ZUK+5YX8pz2ZRP*M@AgK$^_f4mR;UsG1~{O4F?|55MXT$rWm0HhS$gz|%;8zeb;?tWTvAO=;k&~ro~pnA z6BQ|=9dnI-#2fnFwO=!D{UUxlZ!&x`dVbw*b$g;|r(ymAf75!%MAKw&Gog7l%w7C+ zUan}AWHEO1Jvp}INl9!;Ldk;?9SpBIvw2KOYRTpB9l0U7A&DWbA@!l!LIepz5+o5& zzfncmZZWfc7_p0$$z~)C)c3?!mdCz<)ZUlRpPoTQ%nO;b4zZJ57dembb+cs~MwJ1! zQQ3#TQJ3Vuw~?QgbQSqHxZI9j;6cgA{WdE<3&sFLVpew|;tN-aJM&^ggv5NVB)NBm zz!K);CWaK6KhMX~ghI=UeAr0zlQltC3C6uH4zSU_f$ah$N_JSbk zK-~8}PZr&(K%Ue71rj@k=;X{f^e9V{x2-8$x(|BXvl)z<4_va>e|T;>TO5et38Kk-Uk}jb>yzpK!8esTfhMHhsEzl@8eqj*edR_A3%2hEobR z`niIK(`>b}N?QdL<7NwiFO|^?I8y)JvygMA+pV*M62V#R4vEg8$RIN4BC?{m3I;KX z78`n$-E6uJz@n}fTe%+K6u&eR9dmz3WNc0OK1fFc?lf6fGP1W)Mb@r=edl|T0XOeB;z`3J?Y-{) zTw(utCgt+Bx+QQ*>B@VhC~IXS7QzG7Iyxe49^BYOn#%3E=%oH|4K`IObO?Ux9ow9< zCN<#0RjDunE(h5jIyKj5`(4(>aT_~ifl5nn1l$VBbIWp@=kd_2F=9-*LarsFBqb%u zjn$WL+_+(ZMr+4@qDMZ%NJuA|8Q(-?W^$V&Z5b@BGB7Z65h0$X^M1+G$bm=YVcl25 ziw=;x6|`q9NizMPMt&-oT>Ut(*_&H%%C@ERawWOZ1?>%LsjF5}jA^N_&Rz79D5-jt z^Rl$3qhCZ3o*vn?`NsYgP|Or^dHZP1*-hGjC+>nr$Z{H@eV$LW14%BZdN`^yY>unr zYPXNSN%BEG@_zw1I`219vy-!rmiO4}6;>Q>e;8eaE{+U)moeQ5Uf_Cu8wU^@hJ1P{lR%-Pbq$e1w)k-?qG8sNa+!Tc-qDi@ot)|LW=Bl> zeDo8CGb!?VUF%{UNw*tFz~kWwt$@dUIL+KV_0TBkUz(9pz)A;*cRqTB-^aq{G;=BV zV)1Lro_D0j_PMG4cCo{e$>YV3>F))DrQkjWYIJo?9y`Oc-zp`5w2JWVau~qBmYkKI<1s7nkxh8aj>$@R@MRWu+46`@wc%Nl7u;u&F3nhU12U1yP-t?7j$j#!wLa4J;Da#D=jsS1au%S z+qqOmQmSAuO-N$e_H@gA1E>csFvZH++B3oW=|~&L$4>5KwYY`sTQfVp&l<~OUQIT9 zuEnr0Tz?kg@Ad&&d4HjDb=>y4ZNb-jxj|bljBTHX(RwL?mW^9tZ)bbCgGtf&PdGmQ zr~ZL+adPKsgU-MV^jyz>e|q(yyf|eB75QA=+RrF#Lth}}b!uJIe$M65;ZYCeS9Q$F zBTrhC4LA=MX!*QKJtkNCDsAGY;5+#+2*q+5GN|6!zd+z&CKS20R+K+~^-;>=0o^>= z_Ik-}x;T&NGy>9Nas)M|cC9@hFh`vcM> z(yg_Ju18!@zNGA;ycG+i&SrR842n}a1LtU2ay5$f`88;yyXorRt}i>z8VgXbHwvuy z-I*z(W!k{g`&E3CVzYN2-oN{fhe9?zca8_!0m4`9V(5WF7xA4G(JtFPV2|TGf4X#l!9mT8!lz<@|1W zNMYIwTFWnA)2*$qSjewmyzIEmyi&iNb9L-8_3EV4l!F#(D$g4V$kdMouKY$$Nc4_Y z?#v8&t=$Wn(f!b`eK2s9{8fU&Lp3CX-Tjie;LtCgQCcxvH6Is0Pt+)7Ma9xAE&A3rW@|KIfQz5E+<$B*HIU`FK~2|f3;Q+f6Sr9f#q?lPt7H^NzK6{f5r1h zB!G7V59C{%_NZfMYn;jwpV^r?8mmc~Nx{v|MduJDx+?Yv3!;A5S*G!?3nN1bSH3tr zv3uNWi=NUO8q?B$#yMm_y_~W0{bR}-19<&oapTk+2xwneUA}@ETG@hsSwWK4{40my zrha5ueONT!XrNRazLfRD<-zl4ovM2pZ*uC{z>uA}vezR0sq~0;>sy=XKf!u-erB?THi`?-j zpDEgBt{Ak?me`zOX)@uY5U7nAV}cP{M_P8=UYkW#6Qj-}{k&9r-M8CNqF?7;Eb_xc zPWkwE-l5oco2pxHI(OD{z1`2s$6J+Zq7ZXq{&{_h5olugwR6PzGJ&7Hwdzi4E~Tpg>y%Y#+%h@{tAEU&(8_fRttXSCN86brtsIp(F{zS9;s zTCvV8)LhSTvepGTfd=JdhZl8*lRwkA8NFt4-L;dqbQydi-74u+tkmjBhiEy0 zUT5w@KEPkUJ>XUBSe0O}Vnq-eF{yXj3Uxbb9&wS*7yYd#sz$Z~O5lK<6Sr^H$M{!jX*3inA{()`;rz;ttoNT-N-=Pf`1y%>0-M)mu>CK)u z;;rM^jq;4H+g-CqoyqayzfgLaHpXqOFUS=}>9Fy>x&HlUL%B)dLzZtry1GNq<3)+4 zPeYHlUV+n3pD$CfiVJUO5Sy;Bf6j~sCY76?E;cxIhjR8t9`wRjzBPZ#)R3vswsGz; z8T}v_HWK;xd$bxTqbI?I*H@5Rrbcdk?KuW9bchuA#o`=>=@>Nhct5<5RC!@}ub(x= z?&#b&2f9rGwmed0cwv(X`ZgV;#(zw~mr<;u*0*eHaS$#F5AJ;{xA&L zM>+HMGOeF|C5;4*glErLsDyPHpXPBCZgg;7cV7 z%VincFLS27l(}b13$fwQ#L}^JxbMm)(8x{4{a8htJ6wg~Cid&5a{U*6m0_;J9+ET9 z?j3j}Lc{>y2(K8PIbu1RZz<=cFI1%Nb4wLTaH3PY3Ft?>SX@dbIAXu{_uJQAQ`_8X z6|=Lr_VcXQY;5GpuGdz@Riw;0C#gLBnh(gg(YcSYV12tDHaTnLD_ifbmu2r5z-rx> zh~Q+XKqKaC)colvn1}r?oL>BW0l?Wtr(=X*e;}0#e|I_-%mfuUCD;My%D4XWy-yd^ z_s4*IDn^8FLWyV2YBZ6c*cgZ>DEp~tNOaynRr~|+%^=wGSiubjhFe!U&lw0;pHj}z z7P@8%$BRl**ri~UBHs_K7@+(@s&F^d&!Y2Oy=@61c|bjT^%EAN+(#Cp5u@)uRb7ld za)1y;|1#nAZ_VVqTLbm8={|%ZT4j1t8FlYCHiq^YZ(WJf=7RV8r750%VCk~n%}SXE z)49U7I_s8SSU_cSl~0F6Rhu2~^0CKDavvY&Yp;(<_kPj%APAq)E8zk{zLZ)i4u9+a zWM^$In3ip*{Nqx!XGpRnHmnJ(K5to?O6Xi2tgm~6SovZ!>Dl zwXx!fHV)6>uF;%k11n?WDx)eI?6iPH6Ye%-X}1n zvnLp5hS+#^ZUUC63pGC!?0I$=vml(;r`b#9w~=;SP#Tu!Aal4-qfH48;O!#aHV%~tlc#>q7+Y0{45OEP%+jRfwUDT8H*rM7^MR@l zX(XY<=+?-^0YdH>w8b?i=WX{0mAdSmg=MW9ND#3oU?F^dj#=pQkGZeu3r8n=i$+w= zJNl{AFLS?{vG9R5I{|s_8UQ4BD5-Xmr&o+THNj-QSjkfhprhr@qv{?XY{n07C{>EY z&R>hRurUe^CMQLr>!W2hAcx|EH_}d9%6Z#?oU|h?czjQPU2o)|H>*gUy6+aE-|ct1 z!}KO+<2k?IOYVbit|4>d$VD|RA+x8-o4%vkC;OU!CGJy^j6+%JFyb)Hk;#QVZy$VS zCfsxGD|$BykL8SbEeQaRJ`AC*-FlRA&-By!5KUY^dIY!>|9wnp>9!%b<>?BF=G)Og zS^MEF(uIH-4%%Z|Cx`&{&m;O)Z(aq;iYnpObtwKr1ekS8Qr>$T+J)ssvs{bkEb6J@BK(~eiZ zMLa`0ZHgH`l3@GEnOdjibZz4r$Zu)en3q*2Z(ZEKfYM-R9b=@j9+R8^Hzk(w`_JfH zEz@3#;`&&DSV>!F|H)RVKB4V7m>cp23=mK)dgoqP^TdGVFTb~c{6<)1pwCoIBa8l& zcqIwD6!Vz%W9;LpNQB148Bsr8R>^PvFyA~-S`@j>RQ|j8%y~bMY;{<2_twZ~dzHjz z1ucf_%l_af&c-sV_=Tvky5tN>!R|BEJeCQfgviHwNL$Nv1r`q!FH&NXUr>bL@&Xt+ zeH}|+N^D4nB+hFdWsDNfR>@i7nU%EJ%(R!mfqha(R9n*SKbn=l#;169jTDHJfZP$e zDEGT24K`>3A|4midRkb4b?UQ^FYJYt@a)MAv`#+ff*qVHfM9>z3uA1th60UUn}Qrm zsc>nUxATOt89E3srLqqqy%wS==&%)~skr?as4$P)l5Lv2LQAjSUh1_R+UOl4;6Epr zzo$fU*FUctt>@Jn-{^bSG0mV&c&3*;C#LuSC1Gc|_EU-uLc2Y|77I(E&f7blDH@sfw(n zup4|n%`b9tT$TW~XU=Gve5MJJQz|eg{xD<*>F*y*Tl{>hD#1rl*sEp zFdeIZzD888+wh*`ta*u7|3f?g}8c&@MVv^@~Rjpo?Kd&bOo%*cea%PAd z>EpHK;qub`B~AkK^m&A|wbl%fQ`_{OS#q5DJ*<^h>!sVT2xJlNc$MYIkq81l40&5# Uzq5Yf&kr4ADwx=wfg3%`jo2><{9 literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/book_channel.png b/TMessagesProj/src/main/res/drawable-xxhdpi/book_channel.png new file mode 100755 index 0000000000000000000000000000000000000000..dd04a6844d2d5ebc749f9a9c286a01892a330935 GIT binary patch literal 5326 zcmaJ_c|26@+dpG!Bx^in$ugEm*@wvtvM+;)EFt?WG@3EP$i78L*2orFik2ZlWQj(} zlBGqXu_Q|f*%DEfcY1!$^ZedF-se4^bI!edzTfY4-S>Un=W|~t(fW)j_d$_^003~K z%#3VUCH?nv0K$6Pj1t*dg>ZneV}LE$DZ2$mE z5$qfT9MP6YESaR_`5U7YOro&Z0HCcKO!35C#0NkzcyEHQj?{AVGbtzmrz7Q{fmT6N z4DlBTW}*Ig+t4$1*wBku1Wro#G*mkn$r2#p13aO@B%&`B8LT7qr!JCJ|J|)D1^p8e za8XC_w}38siuQB{S);YzCN8gRI(0#r>!RZUrib;A`^)sY%-q}nOy zUzZfinm^79X=7yamn~LHN9saA00pV69269!6a-Tu`+F;^A`pn*8ft2aEQBI8#5ce* zSkad%{kMVH!|BK?D=2H4>L>+k<5OtlN4;FWFgRPrT%ENkJsq<@>DAPxQTo&jWkJ2IL0 z_bggpAP11C7swQ-p)DMG+=1YWBL`9C|HPxwNR%%%z|$9tM;Ylzu@saD1RPRbU*8x8 zJEf+IFhZ!R8p9C=YKA7p>INp7Fcmcu1C77AMr7=cmpp5kGf``A}3L&~Vxbn^AlbGyiPI-QkceK>q8zr}tPz9y( zD3N<-TC@TuBc>x-m&3~G;XJ=nBUoV&sqRH7m zJEg;30~ReRSLLS2<7rRIm#jbfbgX&qTZ?z6?OLo;8T&<` zO%VE(?_xA9`(J&`)8-zPEBEK+Xt7?CXs}VVEA7XFG3ms^!WoVsKQGVgGuG7gG(T+| z*bK7^6I!rw3f5bfpoxpn72)?!mo#a`V8e80 z4t}!vcyGQK^!4uGzFiQcb=ESsNxs`M-aBC_zs=mLaC_a?!g00jhU=>cH<@KLo~gFJ>?o`p1LVA2TEe z0@d~6JrHY=DB^pXmnFiqj?nz;?Cp41|0~|qJl|p*Y75!NHS%y)) zIzQqs9H445E=fZ=ToNa{bD{)iq%zM)e>NTA9 z;ii8*NX)Vp9!F03wr-(lI&EW*gn&kkYMpGc2k<4mvMTuqMIb18ke&gzVevtNe&wj2Z?J&8o zrJQkm^$Od3h&Ru6=b_BIM(2w_!ROHP4U>l3Ai;2tknasChEPj8ajz8tiMBo+-VlWR z#yen7*3!;+`<~PKay|ED6%?YYUJ|j7+Gk2s0hT(Ad4%pNNjC254)HUkp6h<{RERAi zv(1XF_*J`GF%=LcdUe40|M+#R$E9?L>+(2#>iN@cnavXG+O50+@$n?CNKCi&m!!re zkZ*?32>}D*M7AYHO#tj+`;PPZ`?z+cVHrP;(DNTO@M`;z^l`Z(F}dN-KIs6y*lp>~ z-FBOw_UMd`#jLpvpa`=fkn})Zb>4zET08aL74{KB)Tb@ZcOSKm&nk2Ew10lw{_R^S z&c4L1QZe)A@Im<#E_1K$jEiFn48DFF2Hb5Tu9omCb;!z=Ew(!eH#G!{wqruxS-VT8 zRd~{q<~U?!1t~Oo~UXb7>AahT z-J7p}aJx{Z&+0xN?FmDGJ<8{h@s`24xdj;m-nB_Tc2lkIcV zRSwEI!}~*s9Pur^yO0Z6KvkvW*Z@OI$h|vJ=<0-Pp3pd8DC=TaBydmb_Rq~_s}BHk zX~j1lt(lvjpD8UjTzhj9$%AQ>5d)9+yU8{jLZk0hLxHnp zvX#Ohc)aPS&0uD}N7-y+y)D=7OMT*}hWC6|2|Qxcj6zVz(w7IvtJd$qgx%f)PpTtF zF(hqR;*Igx;k!AcCcAlgT7vI^b}ox!*=o3 zY9C2Pns0%307$*eO^>LL>?%9;Y-cPSy?RMuQ#)T>&qC-^ zF$}~jnC_;ctGoWa944}o;HLdzrI%Z=1O{Y-uh)PL6W&&j^ZGhu<)9WeOyY00KsMJz ziy6~$d2gTS@WWdwZcfb~K&1?BK`sPd>fNTbu3FUv`R#F{a%M_FkJ#*H&(B>ST<%hH zO5t!~YY$iDdNY2$n;oM3YC>R9tuMu0MoXjs2FNy$M7Fp2&9yx^2fx&7uFXh@us#b` zVSrHaSX>4M%)Blb(vcNxuES+b3X?zdt)U|!LFa0x-3#DweGY%)*T!MP@Z#s8U=NEI zbG5F-6O-&&Dtajl^4=0C^0QRrSups?qcddUS3<%Ih(Ik zxKw;ds|||@o7<^zpt}{fNc}c$eRS$8nL(jg% zf%=DBp06*Qz)c^lgdB3$WCs`2H(uGQ&Ls{tq@s>1y zcPN5L^i(qKPLi1sS?a|5*X>`%5ivD?yw1;!>O-$u(Ked4?Oqz6EF(}|T^cl%Z#grr z$A0HcaLedMiMOW#&n$Kgi6whNkT)mCUpUcDxHKAw%wH4|VeO5!@EC!4vz7M-kS}M> zaNRx_nu1=4ZC2ig0&eER&CgC8jK3cR?Pxxt1_Z0m*VR^6o_Lf-T(y6A>F{dz_Fz0R z^6Of{hxW}fNU*|uU1pI)61!)5`(wG`inZ$tKhzgq`}ppxWOwGM(f6*sbdvHP;{5Ey z7m!;(COIlsjkKU?Cte9P4aG8d7i}Q9);lNW>#kJwGLGyYrQJ3lkbG~rjyy5rc;<(D zQH4%c_5oI4gPFIyub=F3=xKEw!S&odFDC2fcmu@i3LkYDk3t?la?hgo@ZpONozJ+L zkQuP1R8!TZ{0hY>FS>fJeW{=*GI~7uFZ7EHa5BHWKcw$9iHI zMVt?WfbI14{a`ysvW+G4hg)sdR%Z6B9OuUUr0v&25ZC0;lnF)Wb3S{X;WB(9VSQOO zmk~FPfUCF7kP!N`i>_BoVnzA+DOPSF-#UMdy9T=;dnR2l@>g0Z7FaSAyhLQ4 zQwj@&J0ULfrn3u1_$jO=>_T~p4y_y{R=c^{e6`EFZpg6~!{5)@-oCPj#{erDgGC!` zsa&x$vEt*y*|M^KILpT8!012hbwnW#FV&1P&8x#Y58E92sJ_-B8xMm6sVNbNc?jJn zy}T!xIU0nfVzo0(_HAV(P9$>!OYTcKMwf@@{k?lqdGQZNj^2TbiomK*4v7?7t&>EG zwHL*1!xu5}9B%|Xu&(u*HN8K@1e%?W0;6&{UvDRgXqIIy@thqweS4z8dm*)>A}u1+ zQ>rLWMfIecaIxXK-up0+=o!?O;)ANa^@Xn^=9kAwL-mOAH6ESmYKP=h=^0Bl9dYb1v7e;+LU@zq3tm(V0X-7WY2iLw=mgnU;A%@1}XPek3IqGK8 zs;`&yXC>_@aiw*ynOgO{5Af-xdweA(Se=eN8TG^*WARKG0_@3b8x*L-K=!)6G+0`_ zHq+O?T%jaigU~!8uAFRTK4RgG|1vr07i}*g!+p_?>bx89FqWGb?E511)b3D|y;P!+RT3XF6iXN^oF#fup z-Rv7DBeEXM5#5RY+P9wogs-+mSZH5I`*RF#JInNFRkoNNk7XO}+khfwS|;1}FT3IG z3|=L&Gi7B=p@Tf3t4XY5;IX!`PkRm;wGmg-Jf>HXPBMKcYaKwyekVnx1-Y8$DS$0( zsl%RbSc@3yeZ}E)1tH+v*|J?0iL>9aVhA$b+Oe%3m>%1w1>j!x9A}-1rdx|9+CY-R zvQ1wDCi-)!P+0s%^^VyQsNqDPSE{^{Ffcy)5k~7Hnr47BKQx>2b5w@6E$1J$UU^au zVP?yq`C#NV*e2Bzh$itft5n4TcPV-*<=TT*Gxo_FQ5AOZV)a zo-zvis@Abdj{2?)OWN*m`?V{5z<88Qwfd{VXoZcL*K_+*^j(y%vWF)rP_Pn-3RmMs zWTHn^=hUAo6fAq6ZC}KTZ)X_`u@wNn z(CgfKT@jolu>m8>ev_QBzp`4cWV)EJR$0u+Ut4PMNMecyg)5#pH@~9hJ|5y{fCdikEtFF@6sJyE@vueGOt@nPIamiG?seVn&`GECL< z#2*~#W3-=It07UE-Pha(On$wLEwxvtPmd4(5}T3p95Lz#);_77;&8Y|1W1(H@)m0E z#I?PYc@fko=S!5oO6F)7`l$BAydiT_QbB+cHZ5l_VRsk?kJ$fXCDNGRJ%HUcD)*5P zl*6Vj%4Hp7FNtN?QZjf^gk>r|=BP1PX6Qol3U5Z2m600z#m!G=Bi#52<_5-&-0Ay% zmU84{c(Bv(USv z=!kI_xBrb1_jUI?X9IxJOs*MI)5?%w_aodY&3cXSCKs zEYkR)zKP>QS4RZ|-%S->C11sJ0e2+Mp4Zpi&BII4SDEiGUB&bH?`>MHH<>!I3_Z9Q- z;{RI#j`VWGqCIhFj0f*;MSBN~H%^)F+|z%B;O_Y!Sr4y&r|CRlP+xmbsDwD|cSwH& z_4NLKsJr`r&|WwrL;5&z{%{3G{wE4X`}TZSXC z=m$u|EiA^J_pgu@(f{ZRO!FW0{>?@Fqc5;q|Hy@&I|Kb)*#A}Nzqii2=lAqKZF|1> zPvav!&bu9Z-qtnqIb;C9s;doGGx43<%BG1!nELg$t*@uA`?aA^2NK8k*sW7ERSA-+ zI!PR|hJ>&@CP$G-KBI&v?SfkASNTTCY57_L1suf@adC%QwJbK}WOq!MLOE2s1L?Q? zSL?n7wLP@W-VNSGJBRPAw|@Nb$!oW1Vf1I#EV5{I-m7&tYd7mSdsmeEd6r;1>xRWa zHLhk0v5r8a(2i(Rqv44@CdbH{snVQmq$Sc4mF{GT;53))ksOfh!p0Y*5@`psaDMAu z1vHlv?ZgulA6mgUHboMh zQZ_9_$mV8PQ*x?L;)n_GD|&%mAuDHl-39 z`c&c>w-Sq>RZ2aiY7TsQjWWwwP)b8YD`w!Xg-S1}Td0h>kXw~<@V4a+|FM#-(B-$m zzDYzW2)_fcCv>`P=u7NUTYieu(cT$E{=AclID&;}>=y0%oJ!@@ff)Yy3i(c8?!bV^MZKJ{slUIM?|9yxL|{z|bxO_02K zXr~LUYZs7pUty32!o#Lat<2GxYEf~X@8}z*;Wh%zj+Lpqg7JVq`Q=m>^H$z6Kz{k8RVX+gFS-w*;C-#%03yf* zJSW~7kLlbb<7J=qFNP5*6HY#=5xa3`K9rI{OJa>}! z;1HChnQ7iR3dGxRT{2FaN#un=uISzPOMaFuviJwYgFbZZVv<>%o!g~SP=0E)vSf;S z{1$1>`(OZI*xKKPV3H+RbXLlBQU^K_K(N}iM!G-3z-tqBeqY;d;h}tERJSdWk{%&X z^DTlxw{Sa;Y(Sogn|7K<6wvFgDZQ2ll+(a#L%)QTM&mttlbxgH%uEM4y2V{>CR3_U zp{$EMAx^;i{CTJ&|6x~ne}M*@oNT)mKI}3qp&7o%%(AB%^F_DW9N&8`NLMTTadLxf z$)N_}#*Dikp);iUXB5o^nyqWvdg5z7j}J;KTPuJZHX=LOGIih_hJDS9+LFty-8uK^ z?%p84khCAS-^2hjAV7D*LAhTlR<>C}z&MhIEhfZ}bFcRBOkI@|H9IItoTlC@$~#fV zqfHez`l<0s&x$)m**`qyt$=upj=#xBWLH$beRSCRun0hJY{LAFVM z(PW+Rxu1%|!_e`=70B?A*jR^ALW;SWaNYAf3&^eHpdM4PdId zKA#MFP}+0dDFFR#WeW10{#nKb_@i+#NILO}OQ3CNVxsQL#MaZ+?&p&8s?;gr(9}aCN6-c6G$q7b=_7jPf<>gkHy{7EeOoEGPOuOuR7421^ z@}l?miXaWccg@~YgfOy9e!(a9eF1l44}aZ~=$2aVn#xL9P#)MB%nD^UMZd;IHwWmL z3Ndd{>y3)XJ(|xR1w1c!W}@%X5$!CeYnnQryx~BGa4-MZ%JH*+NhqW8KyMRelh!{v z148C@s0n6%@j{lH^K_mesDQP3@^rB0*py)TD}9U94sRoKt8J}x&)D2nGYlWS_2E1Bu&Z7$CNRF> zZL5BcWc!iJ;zH~eBgL-O`%`rBWaeWgaPbGaZ<9cU#F6%WOPhDtXR~duT4%nF%E(Qj zt(x}y;}5z$EBdAJr`K1TBF3wpf3(^1*|c9;|DXD4H`lJF}Je zX2zK)8^z#%5Ae?;)%M}*gLmmg-6KX2|OAL8mj)=!GCf%zlSc%FWxZrMk#|Yak9%s+`-Qbb8vzM-M%lcPk{q;v6 z>p$M4B=EvxY~4b*^LfG|67ZnD3?X>32nXIIm_1LY*7lgH1H91%glX}EdEw~_9E6bl zPzJ-9dw}S~C3U!V2T)jW&a1vao|YKIz&D_jr*`}0JbA^w9A&*l^Pe6Xy7YjW`_ znEui@ZTkm}#zA;QFF1w(^t{~5H&~`H!Z>RLfrQd(mGfiZenmAB@uS?2Xl4Y;B zS1&zdIi6-AGeNy}-6H;h`+~YM>KI!nbN3Y(RiB>erujB67;KVRH6YVE^F`4$S(m%X z{tm}yGW)T2Gw;wr_K~-GiqLM@TUO}&JDQUOyDOHCY1Yv$%BoRQH4omKNf>R3)q`F-+};hRssQ!SRh zyb2YsrL78kRl%C{s-`7f(W2?KWS32|PUJ+Hp#JwiMW27TH;RX_UU83^vF&)enWI8F zh5=J|9*W>r&CB-ph?Gxy%4s0>lybH89_XqH)8_l*V2Wn{k%Jo|!z(O^4;mE$TRym} zKoa4NJ)p(tBYCGt*1$LG!Q#(6^!s-Zt8!_9G<)5pfCX6gF3-W+DaL3vclxE68gdwr ze~UIi{z3p+C}?3Z?V!0W2p=PrL~RbOiWi?zR`M$xZHW5T*Aa8)fnp-#jeIZ|j{yr4 zj@0f)h4QN$)*F(~5_3(uuYc?K_=R7YJwVJaicV@m-GP+`mTFO7Sd{XdmkW@+-$MR! z$_G8vRYIhAH2LyXy5C$=`Nsj(rTu-1GX>eX7Ntjrrs8bOtaEQeV}PdAtfX)fahc36tmM<&=aCoV!&ZY?!NVBFq9 z{jr>+ih^r-w0^AI5_o;!G_opeiLK_LUtiT7a)Lzhfj70vM(HXg{D@|+YSjAeiYL^d z{Cflz`)~yN`D8A9B@>lA(&hxno1dhc5gSql(VH<9aXdv(VCn3hMw!%e${mLJIUtQN zsIwZpqrc~+XjMC+`_uCIv{BxN$KTmU^HJjRRup>)VigjM7oCF3tT(T=W{b!16t+bo zk9Eab#YUZ;_9MsLD0%lTRcOB8JrH7>%%lWiNP>l^ZlGE$Hsl-Uw07GyTfvKlSs;$h z)f!F$3>{6CVQ0-0Bkr&uz01484b2Z>n2i0Cmg~{8OGqhTueQRBVlPXcMLw|Kgbhzt zoM9Jzn)M7LjYr|?T=ahR2ql#%ZZ}jj)X&vHZpOiCU7h=D-k1Av!2kcVL zm}ozj&Ynr8f(UdO$tujWpym@XK``ETvq=`m8^?NH*7$viE}iyIyY%R2#K&j&N%Ih`H4)8IHJ=5O^kXF^DYtSUGRPrKHgw@ZCHBab9Xisc6xEI(&!*0AayDEcEwi*5>Tx zREm>~b7Q7npQIt%qGK1Y$@{&1gw0nNuOq`#q}T~+SBm@gW=%-EbJ|X$obkiF;$~Ls zQ`cEHi_PTO&x}@tWl#)qc>B_Uhz1ka z&rAd=l{w7FS8aVFgPmsw83l8TTIj?Y_815ys1#_>*q)MAdacBiP^QIIv0Nt0J~A!P zU@;>HdM)Uy?hK|;Q%IKMzNEct>#Kykfv32gnN4H1w29!fsqm6Qicdlo zJykvZ{FkB+%ooUkh|8y%DKvTH@x+jcN2xd&RUf@PW(a0-78D|DYvdTLj^Yn7NTOlS zz!X4`TNI88Wjz5}J{loL$@GkVJ~byY+Cp{x`BNNkxF+qWZ8bwvI(IWOLcN#y3zp;*u8Wh=1~m%&B3_w6LAlm&qs=t=`6`Oe@FBBo#8*ny>xOO-nnCS2v4PABsF!>2Yr#cZ;`X?uTakhJ$0)0JI5Df@;F-*T2idpC&(YRbP zWE`un5aK!WtDcNLi6HabHKQd!UYj>YZFw#XRaymj^uh-3r@Z8D74K(=1g*{8-a@XM zRhyPy-G+!k^dP1iqOW!*q3f?fdTlK|+-70i(W32~C&d~4{>fR4zdl|$v%`acfvy3a UPHOwj-@h8PHTB^Y>i0ta2VaAvumAu6 literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/book_logo.png b/TMessagesProj/src/main/res/drawable-xxhdpi/book_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..ebe816fdf5031626d12f59824b30cd7efa2265e1 GIT binary patch literal 2056 zcmaJ?X;f2Z8jc{KZ0dj`i-8y{o08-P63B&!fv^q4A_Q3!Y?ccNc$1i1Bw>jxgBF=u zTu#d_SX)2^%Oa}>7^O;JfJjv+)uGivQ&a?sC}3r7tl0h-=AL`+x18sB-uHXo=bSGu zbWebZp_L&9gE0vTWQL)mi}qWmkN!cchsEe%iTFh!;o^8Cg(HP948E8P;eudJ0u%;u z_=>|S$P0tf#S7U{NE9msJ|vQXaytH z9Rx|D<9|;oiWQ3U6-yx;h2ZMKBLM)8N+kep6e<+nm`0-G@4d}Om;yLE;5A(;mBP?GP@N8 zCM4rYg%U(47U8st9IiMSq2tj^e?I~&`5-Hjy)P45Fru6zAp!)F_DCy07VG~*VfX`D zhJ-TCL|Ref%yJXF^qe6WKg(zE^e#pz31{* z&xNuomxv}q)He1{jb7P8-J_j;ur0dyV0=h~x?PIen!2Sk9D`Y#9>ip@n{LG`H>9QeXR$;gm7W*f<%G zzddFLW7FQfT(g2|ydOlpzpj3vel^`XBGRzSEyhgRdX+{D zZ-&7dTXNcHK_O{NyU}y3{U^=l0$cW}$OEtH33K3@(g0O`?zQMOoiUpijWnlw*Q%Nj zjbXQYbZ(q(naxc}!TK-pnO9Eb4sN!war&unX7MA;5Rh{u-~2z7w>g&T=**5|+5bM* z!*WQ9-fs7Haqp^R4@|TsrM!4UeRf(JnX(wkl=@4#k6epk5auNkb`?1Z#PA6nSnJ0FR zw!5pi%*I3ydr2@EYT4Z5G~2j9$aLr$WL4E>4uk-Q@|J&OJ$Js*P-Z{!b<2_MNAb0V z51U;Yk$I+O-&d!rsw^A6cBtCvS*z1s8%uZgy=C3--R&zIerxs_ms_#1lRyt;{LaaU zMM1(s@a$rHQ`U`jpA9GUsToz8;6TNa{a>BaY%W~&__=wgzaqLQ@uOD{9-Gdl%vbv? z8Q;z+pnBxhJ-RFE91iUGNujc9Jr`RCf3MhUV*EV9QuTbB^1Fj^@CEGA&Wc3UqgIoW z7wxHDjdZTY6Lck|D||gO_K$^NfBy8BUq>c$?ht0~hvprx?0?p)Pt1OR3|f;irnt$M zrotwSV}+DDvma^v+C} zd3gNky~C5Re&Ua+r(9T>Yw=GZ~c&gUN$+qXY%nVoHZgt0*t|D$ZZv zWi79t$mjK%PMa++E-6Yr?jE1(!wjDC?&x82END)N%~@|IAi`K7UK@;2)H zP`7ir^<(Smmb4luU|H$wGFq74Hqk|SwhL)}v0ji|r z8}^8>L62S<>FYRT$M4t-=c?D~h;<~w`@t_a>IO5@mrfhszy4I`b=dVinbGS90KEsn ze<2-BI8fSqqd{HIZZ!tiUEN@|bEqGFJb|Is=3G_=ly0+sGJhwkgxm6IYM0%;ep9KV zGX8BrYDrdZL&g~o%MBrf=Ga@8F4NS8R&U@pSVtV&P83+W+^^Tv@Efrk YV-P9i1Z5qV*WS%RetVd8KA&X&7o{Id0{{R3 literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/book_user.png b/TMessagesProj/src/main/res/drawable-xxhdpi/book_user.png new file mode 100755 index 0000000000000000000000000000000000000000..2f54d6b5e7de5b8f0adaad72ba241fa18b95943f GIT binary patch literal 5470 zcmaJ_cT^MGx1JCUA|MKgbVF!@v;+uUsv4vk2n3{uP%b5u00AjM6ahsLq>B_aRB0;G z2@ntnMMZj5#1g6sQUrW>fA`+s`{Ug=Yi3TF@B8-NXYV!ZtP^i;dXfDo|4{${*bPwz z=!2H}`#AzRc%!GhnGYI1;(095g5X9BaSp%(x~>ElJk-#~*&UC@JG+MZ_uw@F01WfA z#1gS4#_BkNkDT*wj2y|w?|=;en%X2kXPg(F2z9}`d-`g@RvTMkP)}Dan6h@!AJ|{K_vRA%gcv^gvf;`$`JzG<>6{-YQHrQ2-yRKY+$G_(U~Oc z8z}m>f&o4d7vSkf^d$H~e=9n>5Q2zWumey36M~Q5KeE1oe=XC&g2|Jd{p8_t3co}8 z8)#zk|3iIz{y_&4(fI%R`+o`tT88@J<j&VSiG%=40>S(5 zRW$b?5D9@E1V5<01rmD3+SAvS5E3Z$C*H(F-Ox9X=|hVR&-64S~_RA z&oeArdIdwU#N1rB{bbpS-ZS2~#yCY;fGX+Cl*6MVGhUn0SKV6~YNDAg=@?_M4CV5M z>2PzYhDp@91gEJr@NiT~OPRVEm^KRVCeJ)%EExc%%RRl7KhYd+$9WXVn;!^_{}WT7t^IjmpGOv_D@+>Y7g za70H~DWkw+=ZhFzrJJ2qgw>bWce9pB#bFPF1LNbS&2EOe2w@^J+g4eWDKbe}HOu*+ z_KT^E%ftS|7x}hYsz&ZDvsjFnDCw+OW)*Qg*g9=73^%1C3f8L2t!fh>M`C9$@7ZpI z9`geibl%%@usEfp!^gT`R{E*a{`!S$3$_-gic_ulsg4R6QK%s8c{yUf>9K6}LXajz zkz~ zDsTyxmx+{>%EU6zO!-bcBcMyqyMBsB&6#%t9tmw1e8@YLV33h0!Q-2F3p7L7Y4M}n zw0rcLMw>iH!7L`?Vx4cE%whP~>42R4`;a=54Ilpyo)amot_wfYn4=KSwBugKQ@7EX;eF@9IuAZ-po$A05ZfK?#0`rKF9c+-g9puRMU#e zBN<9utGdQoB@@C{V5j_VxBVJYx0p>=LMQ0D)ftd2N#{c8_u3wyeKl_g6}q51!Y;*i znlnklrbz+dgle^^i{L*ReuZvZwLfHao)Jd-+8Qq%{sZ_B?b?Op^Cn@sfXar@-K7X>aMrlm!4U}i~Oa`dx%oljN8VjE)wQ_Dg zJ3(!h#eD@qa0m7E4fNDk`|~v_NTpgRBGj}*ODt{CUqWASN^_wx7^xs-ug4X^Ps$yw z<@*-}T>G=7c^vkk657Q(r?i>dRQcA<{M;{7RLMwb`B1zrdGW(8$)CAG1^KpzQp^74 z^@P|kMoC|ot*pW_aR*N}E89<5X!J>^YAn9dvF!~2|~9@byJ4FoIem04Q)wJjs^ z>pSIWk(JDCd9G`pAbUGnR~^&lwIW&$Yoiu3-(^O=e9(L`cU4TSM7f&b$@0~}#-+AAPxiHG z_nn(ICyaEzJvSo_GdXpAy60(&lGsvSb5NTRNbnkOd1Arb)2Eq=lX)dxCUdvD=rAd{ zt@YVtpqN-2hZF6Q1D6eg2OE@h36{jM&6U3J80@gVeM*mLQkM=6GZEyKe`jWWl~m~l zlvjJWt2H>42F~7f+Sv_FWVy56-QLc0mI3oA{b>XT2EX((eczqu8Y*aRVdb(2V0fpT z43gsD_98EeQy+S@$9|99T-W(r`Te?sXhm8R#APeig>4O>ufVV~bDxal#B}ib}qJI`7`NT5wEFol(Gj z<7_(MAzk-*ikxfL^aEv{S|)H0OZjESHrCbcmBhALaS(E0W9kC&L{7#jV`G!2O!5m) zYCdPqbV8{98|*SgGGOCLFLFtVjn&*^Xf5GN4`3Wvcf^%Yl~zy8KACw{p{X3S;<7D+ z&j%APX+p4glDV9bI{8_zm2{c3O>}+z4M_JWz{7kw zNSs{Q?_g=$%Rlnr%G}(gRvTzk*2~OY`Dm(_Kx^|Q#+_w= za`p-TpgJb@(VF>exsoUE=(|jwH$bY+b{-|lD!q#@f?_|0`RQDs$6l~9ey3#p8aQ8@ zyX;&2&MzS!oig9ky4Pzu-k0IfS#tv7fm7oqF!j|5hHu<^)Z6rnCt_d7I>gXOF>hD= zRjk}`YvZ&rp@!|9GP=6P^>N{F$(UAGhCfO;1@)~tG(KN|$XH6guR6Ut$%4p?m>3y; zXB-qz`;rUd3>>eN>;5Bop)0sw(@z7Ay z9m`J28Je45xj|<;vSRt`4S)W8!6)r@y@r;oqR?SjO@A9St4XVSh77#afOxmjZ1+EI zu%r8nnpa-|Vk2VMLt+E=P+qdyGebMW4nWf)m6pcG>vVfT$E8<^jT%Tq*=3PIq*6ru z*%$??0zH~`&zaZh`tQ$28g;}5#GJ{BGR&M715`i~drXW%#68ET(A%z1DDD_VgtFq< zp`D-WOZT4|hauBa>g%698uHA_b4=d8aoGK|ZegJwXBx{VRSKh2+Wo|ot%uL^x8?Tp z-oY>R>iPG2?12XXoVVUprNz@l5}YC<9R`xjN+cHAY4?ncP0QTGv{naWGYtyc)z%9O zD!Rs_Yb#nt#)YhGdc^I|qnW{T8Bg1H^UDw!?KK}+_}bol{LY1_0G)mtyT#^lg0blJ z$kV<#P#B<@D2Bc+nz%1j(xF5}AD=j~{-HaRkFR-7bD@Kjc<}SNQ^m@zR4jr-Dqo_< z2EBQU9b8VW*X=Xv0XPj7s>`u% zq{QZN$k?gPH`JkK!rJ|Hj@&><`DI3Ju8^Sr^|qj!{LX<_pDISisr{^BuhOnC`x#vT zSDVg|37f6JxV;)#BF-j1G=@$87?E)2k&ab`=kXny_g9dHt4VQ=^CO}a6a`k7kE4Db z6V-udn`ECJ8=PJSd~2dz*min|-%GX3_+JHeZtWvVjPKGWPAid~WE+v0Z@Zv|YJSA<)57RT+O_kRtYeqOX+fGZMDyQEli3t%DLjZnY#q>W-+?*0M* zm+D`x@0i!x8?eZ0bJXl6C5{00FOP^Pso&enchp>p8rg|nX}`9lH6<%~m6^9q*q`Bz z{vmYki!scAY~&JN4SMi&dqANwN+%E7b3&qFjbDa&{AX_ub@W^)b<-L_HS2sYbK+*w zrutswdybAza;iA#Te{xAT*bkC#gQKKxf}0-egQ93emV|(`e5ApJd``q^zXS|GxPciUSOqqLXc}XCuN_k0K!RunB^j&kk*F-8sxSA>T} zzvewYrs~RaBxRM`1t?G2G80%~vpI!QxNT-S9#CSIl{qj8%oz%-cpq$j*>0uZ)2^P1 z!u9}I#l6K>9M^{K@39;;>T-yC_e=I5x&Bs))%Q>0NXp=F6XkA%SbB;Q-0m4jp?6!5 zeiz$|lZd^A1h(4$IIV{|l7*-~`ofaqFU$lkjAR z;S6jAjq`X}QL_@T#eC;*oOr~qi{JM0Sm6MYGwPgo$gMs%F?N!MmMzP??V2Kzpdj!3#O+85RJO%n9|6j~4c2`w4$nmsH% zNQa5Z^H{50FEi<@#QYv-O$bkji2r36QXvdGbw@ zHQP~x->+cxgi52J$m6xbcyUKGuv7n-Ip%#xX&CnABa~?lZ}$2k>+Z}@x{?aDqY+gA zb&DLNC6E09R5?C3>YNuaY`LA4Ilj7iD$UFKyKrj`f|Czn|4D0Mt0usA@g*N^l~ZC2 zMvv(bj+~_>81w^kQM6osypc}&)o&RalwEA$hU&bLnbx-rGL!QQn{YC8Ga+a6u2IQD`n3|A+35PkHUPMldro{sQ^7s1 zkeRVQO}V2VSnI?4{mR2oTKWt9+gaV@h9;@o(^Cd0)+G$?nmi)7{F9@Jy)essr?{S- zA4zd5zm6^$%X@JI?%ME_0~I&BD=BjZMU0<;_UX8D4Q8pc?)5per!bvodK6N;j|z7< zZF&jj$?(V^B|l4mV2|EuZXMVaE6*9@gZ`8XZ1kg?Vaq?ZQuRE@6l=GpZM7H65ET?- zMt1MTmBf7VX^y|XkVY2u7q+&4yz^{La6Ui*-@7Ai@k1(yAaUaErM53=r>%V~v?Qj2 zJ~E#f8q7lM*GUB~8XAM zoUXr_eUxS|uDngiRjrbFU*c+V~!52+{o82o{7d`V`I|ClYz8!CtKLyP@s2L~b# z`zae980E5TORs)2cYVH!Sl2PGBwhM;8(d;qf6T)(%R3HYz z0D&m|z%Kv;g)+cOBBJ4F-lue-Oo|uiFnE;|Lf9x2!%qbXgvkJmNdUwWkcoZX+JVJL zL`-ayCy&U30)Qk*V1^tB&j^kXW+V$~BCMY;hM}S(0#X1LU{ul+P(fEQvF~*0$Xd5e zz+&D(;AAHDucV@Rp_l-f9Kd+uDegidiG-n2@gxsVDwX7hArnbt0ugzs?j#SoCzVe2 z!n}X5NHn=9ksik4ypM%=Ol%SiLv#W`sZ`>X6ue9>CXi?}nofgEc1IBIigXYbsN6w? z^Qr<1PzdD`2$sk|j80LIAWMarSR~WGjv$3T$byRZWkL!@PzfLc2~X4=X%)!h{eP%b z`T?zg!+^i@{hz{$h;#@bgaHa!s$7UPF40*R3Ze(d0Rb$NN62I;t5pn5lEE@Xk_^HG zgi|rNC#sFOi7o9Dgq=jYT1n*)$G`#HMMEBd6Q)W55MJeq{^pX_KqO&d@s!+b7lXM}2W zPP;eIhHrbTK)_E7ixI%C^*FLKt}1R@{*A0Tr`hv?*Fp(=rfAbi^<&P~rKFvMyO%<( z-S_nrpTiuY^x;VPbLEL5Nnz64JFi^&W5j7y)U`Q$lqi@wTymKej%>eA=6|cKK{W8^yU-U%FeOLpYbEgb$@z~qxNTC>M8@h z63+B?c$xZ|b{ruzTigs{$66IB^Lzh|)}Dg4zP9%@o2gdKRloQOdQ}wb7}6PY=TL`P zzx{8fF2|n^-gs$WN;0muiLADN;M`WL{=-3S>Wfm=y2(*46}6FstWynVpO#c?@J7{d z(9oMh{|Ly5ohahw7!Yf`8I$a@6@|So@jNs1;h5}hcU9p+E$6^WRI8bBd=hYQvxC!X z_iw+*i+Xabd+NH^qa%{hzQ?|gL?<1loMHaSpbXR>Cqdq#d+FHcQ zE8T#8mcKNTJFwj5v!}_8oYFaPs5nuTfttK}eNhkh+Lzz4uZfOcPppzh9Mr)xBZu{jQ^a8{c54Y0=Y!ccP8h!6gG&X=7G+Ws619PvpH$g|oEqR_HDNgyXRBOu|2( zcnI8v;|mWoM?c$R?y-^0CTsQk0tWT%`WcA%)GoMu`aCWqm%>>LEllouY`yTZuIxL-Y{2K*N;O`ore`bt|-s;R`=1#1v5Aa^ltF?5NWyXcP?PVP`;I_~qYrx+s) z8_ymH_j^kS#cut9x<9Qo;M|$3S6K!sb+NMs-ml=(dLU4O-=3_k3PmeyByGzWo<|d8{@*Q?Zgz}*Tr5gHv&^5laxxFA^B_Z|#$Os$)U5Xi2xlf|zL<uz6L7R$w zjz69Hl`Hr8Y56!Y&ACL+F{{GGF37ocpnwWsf!vf@TFXNsDioSFosvS#rCEEx9qdv*`rc zCKL7~2t!cBXI}&#)WMh&^kpFUrZBX^1Rq3jD55BWH(lHLU=26-=WxF7eCM8X7e_}< zv^94$Qxw&f8kW;!<;XAWX(IF7*^XOeX~)Sd9z!`?P#s7O=x7{*l&Ma@G*tEC#dR2^ zsD>^hlf_x(l%ye(QGFlAGi`#VsA$}?Rc#VtFb*dSD@K1_S)+lW$LP~xg;VSV%p1cq z4jh{q$!If^nyAz9;~?rugusMY1)e!&xsn&7%e)fV`(~B~Wfwddqqjw6l~IsD4g_H) z6wo-H2ZF%xysN?>~v%OwopH8oFrO(MaKPoqi}=N;pu($jKly zRlUX0Ji^G$BO4^f1aL%AHN)}^N6QFBky4h6RZD{@IYtu>#xQg#F*G2Cf_xt@$|BDv zg~5mr7KLPQD8%=Pd?Z}O%1E0wp@pkheFrPn#rn-)+9a|J9pe(zha6;ra?+AfUyD$e zuY%R5G^LI85YAau(k8ZzwN0pXVgN%On^7`{t zv-I@wmvcYn&KGyjE`M+Ay7^}Q?egn=4dn+{cj}>jZeH2@>wRhe*1baq;?9dzeY5mo dy}e(E?`oo6or$!pT@Oe;Je3@g?+;#>{{wO1Or-z- literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/inline_video_play.png b/TMessagesProj/src/main/res/drawable-xxhdpi/inline_video_play.png new file mode 100644 index 0000000000000000000000000000000000000000..ef8b9cf6b256b7783c19b3ace16ffe8978984aea GIT binary patch literal 1518 zcmeAS@N?(olHy`uVBq!ia0vp^20(1a!3HE7qorm6DajJoh?3y^w370~qErUQl>DSr z1<%~X^wgl##FWaylc_cg49qH-ArU1JzCKpT`MG+DAT@dwxdlMo3=B5*6$OdO*{LN8 zNvY|XdA3ULckfqH$V{%1*XSQL?vFu&J;D8jzb> zlBiITo0C^;Rbi_HHrEQs1_|pcDS(xfWZNo192Makpx~Tel&WB=XRMoSU}&gdW~OIo zVrph)sH0$HU}&Uo07PcGh9*{~W>!Y#3Q(W~w5=#5%__*n4QdyVXRDM^Qc_^0uU}qX zu2*iXmtT~wZ)j<02{OaTNEfI=x41H|B(Xv_uUHvof=g;~a#3bMNoIbY0?5R~r2Ntn zTP2`NAzsKWfE$}v3=Jk=fazBx7U&!58GyV5Q|Rl9UukYGTy=3tP%6T`SPd=?sVqp< z4@xc0FD*(2MqHXQ$f^P>=c3falKi5O{QMkPC z!8&|>tvvIJOA_;vQ$1a5m4IgGWoD*WIlDPp8XFm!7+Shm8XCHqIhmW8TAI0<7#kZ} zSQ?s}!t}c2Czs}?=9R$orXcj1;?xUD47mkBn_W_iGRsm^+=}vZ6~Lah%Eav!Bb??z z^`_ur_#tn;opuuA3CNQ;xc|)VTcA@C;m{qG@_G&CDsV=VrtN z5h2%oVo3{A5*@pY{%yD1elJPw^GCaPmS3}9nSQqV|8D31>T}hPxmcK_o?kc|Q(i1~ zfpcD?cuKLz1r8$z;T??03Z2oMCj~nFCrn`LV@l}=iemn~pkXnCr04@4ohFk$rX|j$ z0gldr1eO{!OhZw)jRCwy1d|-+nTE{+R!7kDVE>kxrW2DD_rj!>^+{S9mv?d`O4d{ zLXm_mcVjn*o+`8Cm=MUl?!cCdWtN;5GVFOS{>yj>v|BAgtX4Ff@sohgGpz@rTN;JU zHy_!5km;-c`&q|cDC!+@XTQDmqY^tK{{sG;V(SxjCyYKHQeFRwtHWGxb%Mm6Ki^ZY zvdA>Y{Yts&w||1}G}p;{xU>~=3+H6ny($Pg!1qFNdZOu-?w{MGt{j=Y=PuKcla>ou z?|=AlX4gtt)i2y(2QpN)t(e8|P$Sl4MYNeibZU3f1C6almD*trHy-iy*2 zr;rlaYOIwdl;x1#>HJRT_x|yo_x?Q3b1$Fk`d-(4-QVYWt|!&e-a?39mLC8B2w7R8 zoY=h|`}RD@!+yGA#Rb`&49y%(b0+!G!Z8#)zywFa;(=BKj6dE9kHJNT_TUWw08UV# z3z~+uvqk!n2vE#V8E6E7%+>|~42&Yk7~fz#4T#122NDfIt4+^9z(AZK$W_}8W=A%| z2LxJ1QSi=D_Ab6r!M=JpkkLt?K?ITwK)};5zz9MJk&28k1pS4JWRHI?tAl`liO_-# zLH}_IZRZFyBT?``ZKx*17p9>BL?ECVTG|MNh8hqK(}1hP*cSq#p@q~&AmKW|zZ-~c zje_$-I-ySeZHqlK1O?D&WTd)!cz8H8ToX#7_^WH^>FNE%fWskd2?#ZkNW(-xh*ZU2 z2q-+&ml8;(1(JxspNJSNDU4}dHxusu%c6R^2 zD1q>gG?nIr|F6IQCot6|l8je(!c$3M6kqni`6>Q1MMj!Y@E972;zA;Y{EDJu0EtGT z29U@=GiL-4>>5bKk;17ef63d~A+3m18iweLw?Y|$*a*%bxs0d_0lOb_$!; z<_mg70D$1A70SdV;@y&GY>4w)iDz+-XGX2k#euWIir%`8l9rBEXdecG;Z!Efvy4FL zaC41kKDbQZ6tPqdh_N(dJ`&>N)8e6mJmV)>SexGKwUV2XRuVU(gY@28b+$lRpub#wWiovEo2L`e=Yv$$X!Mf6I~rt|NZOykr^2QJ@fCfvuZoy)?S z8Ige1lD(hzVqSeY?)RywsXnyiNpi7pI+l_C?AA4b%U;!27gttJ`sN-?RBHsf>ULDB zM1oFLk4iDg*?kWgOOWz|<%!x%B*itUp`qdEW1Dp2-=B$*Z{^%ms4YhWBl{*ShlL&t z(fj!2$)~R8535;>y?ggm`^1S)4o>NaqlQsm18KCc(;wcNv^_HLFIE+JG%#+#k#5Q) zXx!R5b)#B0K_lPxXh&Dqk2AgbfbQ*3ESBXa2~&JQ8U+fTzUDp^ta2)Edk9V?T{n6g zMx1W>?iCdDNdv3rbbP$hqhaKJNr`6(V1{rgc)`k#TilIX1bohzAtol)%WmP!Z!r&s z0^rg~sS0tf$Kz^^ImY=Ton)RY?-dspS09wV_*(+kq^$Ous)|YvJbHr_F zRXC3;7g(Kgk;$z-jz*E|vxn>53!n}XGY)6V>p@`fB0Ew-g(baYf?hyUUPUsruhf*IB=tmRnc*_*9ywV{#f z-2SX^`+XCbMSXAN<$<(x$!j9+UmiVo->=c+uz|tBCy!q;6>D!0?m4Ko(5H0hq*k+L ztyi&CKTNd^p6aV1UmSJDq*e8hWZGQISO`K$P>?1pEWF8jrkdR=ngSLQT~jjV$mP0T zzH)}k;@gQy_wV1oYeOKA+_JK=%Q}R2wEJ~+RM8;;SK)X9Oq$V^am2>X!NL1Caq*2f zGWkn|D}nW4WG3{<9vY-jQrX=6y{*J1H)DQw_BCi=-oVR8^uAo&`fShJvlZyI-ISD+ z+h!=GRCm#CjVts%$(OIvRg+s2eCl)n7S@;=5j9rt{`cMZ%hz%51JtBoE7(+H^`Le1 zh!y=Cy`WlXHXhXN59-lo3CkO7*Hu+jnX1HCfN~QTPp0_X$t><*#teCRdF^*qR8&;! zhKs#*7iCq0Us=B++9|<0n>OZO>wa`~{`rS9Uru^@y87nIWRo06%4!L6z_KknF?uIzjn$#)vj=9ywWUV$AFB=Q>PAu^ zhK&e$#S{Aa$sP-N{J20ryhYfTrSA__j6i}^p6&1TK0-Vh`Z#YLMtW*j#sxiPaondb4XPXZu#%FJ&po%= zcrOQg1%!m`Nr0D=vobLC?w@64Wk<;Ks$OI!3&E`ggN?=W`F3PljYhggC)9?09$73b zEY#2`e6DRzoOlv?1E9+Tvaw~?0~lit&Z^F+_sm3Y6wIHqA%oLeS}BiDGOYwFcl)Bs zI6J-3TGmq3vXVuPzT}W09N@K$j*gb^yYx{H+K*B3cpQ={6fYtUZ}4oki?KVYKU>!C){9 zYieqOW~U(SE8Rz9{`~rJ!KuvWPX=-FX;^{sy>Q$tH#fH`!x~>(J?B{dMpHmQAQv={ zq%Wnbt*!l*^RE0w)vH-qStyG1g@u3s>$7C!wR}O#?)>(a_*KvM+L66**4`X{cb(lAMdU}E`cj#_^ zc%FPVMbJ)sD!UaI<^AB(6E>IfTb}Bd^7}}CzhjnsEi&qe9K|#uHolGX;@dlP>$cF) z&{S+6;~?E&_v1PEh3p1i4p_6n8H;4Bk7=ROW83#gp_FI(2Um55#YIn#!H!{u_&Bb8 z_=FTUI(N3zMZt9S=8Jh^4gP|LesY$cmS_UsLZkkX*Ao*l4k7t$e%KmF-UaU1Z_{~? z>+0&B26Mb?iH+u*B3YnxM(PRUkE1z=Hg|>s4k;VJo)E%5y)Lyz#g{STHsxmR1e?o7rbzEVuGmpkRw6D!I&c%<|E<0w1pb7S4q z#>qGCjI6BFqeDZXov!dl_4NhlCuEG+dWd8A0qVC;Z}fnlZR6+Qaiu=z!@@ArFQJT@ zrejAgmCg0#c5Qw8b|pGcrpm20VCe2q#R1a-k2^~{8w-i9?wy|Ijg5^PS;m1f5xw&- zZaN!h&CT%jCJ7yO8cZWfPrZV3fd$Cp9>$N=P0%sid|JMuZojK}JXZTKR_A}e3n_nY z#@o%!ZCI??v$s-)6R?LYI=sI!)x7JWG}uD-+Ke*yd=?kG%&rn9!BK)MUQOqha~JV- z$R^F=|58K(as9-6~o zR1ANlX`l|gBsxqb8FhQSTJxVH{*c;1No;@5zVC3mQy>q4TIGJf(qERIntFP5tWG&} zb7^d?D5mSV#rq4$;Gm!z=a)u9pOutcY94PFU7v_x#QL6iTkTzKM49b2PicRJvVBro zg6}a%N{o&2^P9H_Uzzy0a3FaXm)DikcfRcTL|Vh&v9 zUkwi%2@45%UQ()+X_&nSCv^wakI>Ga_X;2q_k}OeS6d~Q z-`K<7mFiupb#LG5p8_aeobh+0C$wHKKQod)z$u-kQ`Nt$1Wbu=$G_RpHK`kO7yZt4 zTnZ-dAMkB!%eA{~1Csw@p3+WLlQdfk%gCN!2Wg}BQ;ZF&>or8FaU7*5vKbnpC~}s1 z=H5`ZGIl%Rp~b79`c;Lkb$U6meQ;iCecs2!q*WP}EN!X?vGjz~qJEQ%I?c?#k(=8fZCmYh z_H$nJyAu>DRSL9K>CSbv4SVtVWX_}I@{tZOoz{zsKl>D`_{VL0Ec3cE;? z;B@lv*j;5;3#6X4)&YyfC7+cr91Ha%rKJusU3mNa4bw;wu=(Dk3LMtP z9DEvi_H!^LkLl&;Ncab9+>ksn3P~0k*w=^#A|> literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/pagedown.png b/TMessagesProj/src/main/res/drawable-xxhdpi/pagedown.png old mode 100644 new mode 100755 index 96affd8d8e90a7d1ec2db92cb54212102da19023..2d58a5f8036a44921cfd19e45b4a260baebde646 GIT binary patch delta 5386 zcmZ{ocQjnx*T)$qm_aalCt7ra(IUoZql+#kdU?$m4nL2k}jB(oPw$% zOckosbSXd%wt(@A*?9T81w0JO5(n_0w4ex0sEVcnR7XWuPEH4=tOeE4R8W!CQr3aW z!{niY{9%59#>T4J0j?o_?*760+UkO&5otFsH`V{MRs1j8tO|ha|2u&G*Gq!@LBZzk z9;$Bt?Lhv&Y_nu25SS%vR&FvfNTI&Are)atb`dzxVkSR2-EFj4YOdLRj-88|{joqn zrG8kPBza-x_Vb0%c_BjsDA6R8BQ$?tK|yMcF5hHK#8*V0t9ilLQ{AsrzVQR)$422D zys#G&ucqdPeR^cWI7TB#7m7SBKD;XVJM#Lp z*p(WKKqlg!2*Mc&H?ENlImsF&H|d7UoSqu@iDLV-0L(jD1HGKgf2|+ufOHbilwS9R z(2U1v-qG5G;^_MMKjP&6!U7*NWxjI4_34p=UMlw<&O5Y}zU_B*a|rOZZ}%_ibsBga zj>Tf-TUuH?F>W3nCi3}4IvgAvz=^$;=0N!s58+kLVPblLCobRcvWbOh^ zw?dr*4Z0RfapT4f&xH8+w9}&_?&`Wa_2}{Clq(51<|+5B>V1Vy0G3RUN{<~kHD!L- zwcFcO)!qF^plhj@DfT{<-s7i|G%kTAbhNZli}Um6E3WD4p_LUCgKWS;v(6rzG(3Y` zDAwya1r`0G+QVOqdUD*$gp~^IR24FMyCU|d8ZafLtKCdtL;7!+1NjNcDLUHr9npEE z-;c@*FqAl>#pb}4mE5bJH4WAssQQu>M?XWPD*zqeJIL-N>1Jf-h7)<}DXjGZ)H%&7 z#sq&)%Ma{&xZB&%(6EaMH2F&7g>s^m%~g*)k;FFXM~@gS|E{e(I#-E(h9e5w$CvHxQpPv(zXu9 zq~vxF>+=yH>%rrYAeP`aVQhwihF(@Nf)MWmUi8tVSBG|#UTME*Y0c*v8&lEOh6F^0 zH(UTe(X7O^xOuX(z!q_mRUgX*=m}r5die@EW5AAzspXTW3>K zQ*=lqf%Kq2^s{xzB- zMmo%Sp8AHH8L^cwUTm}z3P)Kc_QRa@!#AA?bVNhRCnp#gY3aYBj6#_*OG}ugzGA5R zJFpOK_>UgxCW4tk04fvCR4g&DAGG@Er3FUP=!xp<2jAbZ7E<-%mLrfKm>{eU3tW&e zw)xH4t=B;t#r^m{$S2aR09C3bL(c%Zq%dC3$0{JTRS*OYEW!Le6Tbs^aSBqHjeg(a zHu7}9m__VUN`H1UdZEqlG#N9h5Q5?Z*yA}CC4b(V0)p&U`dbU^-@NMXe10(?vNmt_ z$v|w*>N?TT=xBZ8%a<>XD%@!!I?qdae{5~lNJ&bfOf&6e^6}uNQNQLayPw8{(n96? z-{i=X&MAEXZJMqR&pb=_Z|^FkO=tdpK(H+~0lfg2%K;`Qks-Tw;s%|{ zTx@LY?yPTHL&|bP1aM(UB$(cO}W1cQp zL-j<2P~Dz$q{;D_uZc>;Y%h-lG#o~-+(=O3=s}b%+@}-A9f4VW-Iv} zF?60mYSXot+S+GAFQ1KH1Fr6iF`f?mTIzb36s?T`z?RwrmYJBtFGm<;#45%@zMxN+ zmKnW2zgP_4C|cHWHEiZw1-dI}+*jsiMg2&$vn>L(?I`sRHytp$;u^E|PZk1}!9K&_ zoSznreknfQ-mHeVl4E8naPdV80ezR6WhUq`o`;K=eFlnTIo@wp8pm6v+X5A)1c))-v2s(5u&vDf*^7m95!A6@0+aI{@!6@P^psA<2N zbNFM_*>zn_S+BW9WD^UNOxf^8yyW(MzrUq?4118BxYU*_<0xHSq%V?_lXEyp?CKDY zWJcMKGgKF#5WXMo9mi=TW)*ndW#?YA)l28qkc9R|;R zD~uuc9qwrhx5gvx*4EZ%-2s6n$`4-^Ql8VIZ6qX_(jRBPTb(?$ZT_sxPOVf_R3ryM z$<7X?uvtvYDCaQ=S(fg~UQ+}$z1B=iswxu|@W~R^X#VvPPc}WY84Z#I!TJ=wX@3oZ=OX?%SW==~%CVOgFDA|o zHz1*plv%kgOz}OZTB_$0>d*yCHH^l_z zqi!!-)x9y5)xOnOs2&Mh{FW2e>)B+h0HmRA3)dKfE11n*QD?|_>28Or-R+GEpM@u# z(C$YFf@qHP4GGZ?Adm-T7|}`{Gx)*50pZ#=ELLeIQM7&Z-8;}J`FXkGenijZgD={0 zN3?Wwtv*dS3W^v{@>t>W2$k>yp@*%N?nroiLc+aUWuk!Wm>6>7$AHX&0?d|Y9d$Wb3y7?LLkX1ir;S%vE^hee%H=@}LI~I&Pab%)JRprB0*UXuMl@*M?0Hc3mKUD0p^6 zaWlei#T}tJ{;2ONj`!D_zDJj;)eQ{^?1dAn0Pe4?VVagL_z}ggbHxG*ZMFYIN90tM zQE^(=uJ?d0KgVi9&M-AI5Fw!qb<>>>kO*5dz~$woXog8#e4_Rm<9@t?6m?Sw8~{qr zsfgn;Sx*z>(H9iY3X>48BaP#|P={j9>E;#|L6-gpI%9|p)nN*7(Zp&|#UwtiEnxYP z_GksNMBM{&_xI=;JssW7(0*9&7X^7J^zqN1KRY0(xBB8335nO9XvN%;!od}07=c|o zBl0ZpyV%5^rKP1~WCGwC)!?B}2R>_)g$Z9@UvD}&J4~~v0=i^iU{QYbCyZz6W%LSP z*jMr)N}FALw1R@d_KOn_xTZl_LVU@jpG_dD?O))OEg+ioh`-x7#12k;{rc57NdiuY zbj{Ws`GQ#`(P~%d&OBN2Vy^IWJ!gp$vfI(8?{XwA@M%Ce8UJib_?`o4>S(`#VVu&6 z!?#=I2jrdf+{$};l|khE*0r{bz3z;^{II7H36ycO(+!sU+F*5N{!UX<`n2)aHVqD! z5NWWdpg<>M`p8Gp`n}?>C!_i|4D#Y|=4ng~E`m9VKfus%Y&XvPSlG#;u@0xFxgU*i zj*0!<<~w(wwvCQL^qey^3ePg5TaS|+Z*GfWdA*J)vI1a*zyCCoBy|BnoX`94;e&=z zkt$1Da%|0zQn{5{(=CwwC1sV(90^hyGCn*%60OmiBe7-xdjM0A-f;)4B<>{;)4tHd zGP9I}n3>NZ(S@q-pyEHi?;arvOa=9e0mKyjKc*{IuXc>TMDF%D%9RZ_WWPCxUJpF3+szK}2c_CKxopQrkg7zcdqz09Zj*9Ie3 z)Aj+Eh5aijR!k#XcPbr(QRJ;{m^S~XiayiJG{-)A(hl7nJ>A_=Sw_zwYvg42BXY=A zt-47nboKr8;v(+6Rlupnvp9U+c(`D~J3(d{=Nba;ybfLLC%4HAcv(|(k%wn6rk(}) z>iT*Lx2-uKla!9)G{)li!N$J#U$zog0wp1RcE<*ej*e(&=M@cEnr&)KKr19*vuq{7 z*mnS%+I=)-s6j^wn2A&Qbkm8ekufHgsE@cWmCL+AF!)phHwU+42Vm_jw@Dhv2i~YR zAzz42O|0{M6ROK;oXHqb+4v-%!7b!9s!vQxibbuy2C1s5dXk-!;|F*2o7UE%7i!l0 zX&mOKQEVkJ7wL^&Tf-FV@rJK?Muaz;T?e$06_hrF9&$Rb8zWZjN&>pa5}Ln5->8qT z3?^JShJVk&47<;k%391Dt_;jLMD9>IK@{3{?4!Q#Q3f%4QJ1sPu6TTONyL&=#OLuq zsf`=e55@`g+2}Rlp*JvAmzu!pfi}x%!iQNxMNs2UO&b3e4b0`yN4B`psT;?~#~S2Z z4ED20e*L<-H-$z`Jf1mVBUnMSM*H^T#p)Loq?5e?62l)d_34wPdcc=%DWUKlO+uRE zO{JCAOKr^zf{TZ0jjC{`lu&2d-D`S2 zFU8Xt$lO=)oI(mQ2;q`3-q91S&B zH56Uni~`dx9!%=-p*=~pjjQVQdy5zgfkvPS8hz;_VN?G{J)dn~_V5N{x}8z5Vc)Bq zk-}o^Jp-P_e;ALYuWS6-LlcQaHIY@8f7s=5Ji+kpx54FHYLhHPJ&|pFOO6O-L`JIH z9=>v*=o<@{lK&rTPtT2n(!SAtY8=b3(Sr>8L}_kqB^t0IDL)?Qnpk{>bHkh-I$$o3 ztw_gxh$>JdaiooJnA%m?RoeAIMr9bIaSYQ?mo0KM&lDvsR>z&)5q`L?7pQW10p~A0 z!&ACOH}yZE=KtaRbxq<6RqQ$n86BB#GdwYQe(_}HX zwU(V}Y570CWd{p6i`_1w@_r4V7=X<@ZuXnJtvpbvl_}tk3 z<#b4(GtUeO6Vpkuby8>F{wr~2>zR`3#l&n+?e>A~QSUU3jY3jwgeA}*+kE~l*&s~b z&${OG!FoY}G^&|YN+Pws_e5e(PLu`@z+#6;%5D#1sjg4kSYfPqhKl2`v4{a!s7*>X zSvi=Lw6k+Q{YPHMr^dQYJmTle7r*MOsyb>qI)0lJ5JPK5-3;EvV;=QksQUxEmP8Al zJ~i2%ZK`mdslVsX!9L3hSCY9|Yz_|wCIe)3IReFQJ!sJVGbTOg>`i~7{E>%V@8*fB zu$`S9R@rT?%pK$AX1nM0{K5#iC-@1rokubsnI!gUT?0 z2h$la%ft#zAjvafDsWnmiZSxJeoTgsz-VN)|89o0quD2@L(+?kcyS6UeuL6C0Nrzk z&hUzLHoZ?^F@vwAVrIx5q@DS4GU_COswHAAW@QHvj+t delta 3361 zcmZ`+dpHw{`&Xeh*D{jK{W8mCGq{tW!?6D^fcdv1 z4YfE5F*pPj^lt^wcG!Qzatx2y&~OVP<{}~z{df%e!nLW@V&5qF*Qe56&rUxvlS)&} zL@LHvVO@OQpLf99@@syp_uHb=4rOE;RD3-tV&1(NoQ$okORX!@awFi~2|459#AB>x zDVtA%(eZ$&w(W~*^pqyji^>qMPv!uN`kej8| zOBdd_FKG6+FJze#eWpYmCc98+RLSLbBW+``=h4(|w0$}&zP5Zqv_Sj_YM1(U)Uicv zg^+Vi>)yx;8U&&w5T z)!`h`M;>?%x#l3G@FOY9Kit8#pqHu;y>BaT@ad%YovdCxiKBZFAP~apD0T1V?7*-b zSoI7!ZCJ@6Hz`+jsg_@GJ;9<1}6+lb2khugHE-COT-t zy2@=)%zuNXabiQ}VG>_p_vxhVMX4ppoWf2gg$2R9di;dW=f+r1 zmtcRwg}L?NJe?&GiC8lzxp|9{D;6P-EZiA!|BvAe)N(IFAK2+KWaP2i=98T;t};_q zi0uT~21?wc0cmD9^bfAECSClcPCob7jfV7t3{sC-sklI5mLA}I9E4+{r^pT!0+ z&L#05M!Q@s|JuIqh$-NyZ*uXb?_h781fgj2Tp}n-HEnM8o zmmbh#^#BK)UuBIE`t_Ml5xSpWGz{zit^Z(4%Pg;)31D;xoJUS(@SLyQeW)^MZ1gdM zq7~`Dl+R=WI#W9|0L_&Jd@1OF^f?$mU2;WOzVf$30n2Xu@@wtS(uez=! zZx2@R_DWNe;jfR{R;M3Db^OTeMaBpRz7{!jDl;)L5%EpZ8XjV9nVri)46&`~{4S3t-};zE5aelS_h`;mf2zbUX=!Oy;cP@T+l7C! zbjOrG!~(B0=~Ocng4Pb2y#Dpm%yf8Rk>R%CP?qZYC>k)e$Vv$*uuYdvg27D2}x5A{5I|2Wz*tYB+}* z`w9b~&xb$_ZdY{cXjc}Qy5Hn~LGJdWwJ!!b5QmqTyM8KyY;Fa2!djM+#gd z&Zjx0UB>HG!*$>|n%9uoJgJ1mVm)D=H0t?#cGiDWyHitB^J-UD7mZnZkQI4;ytD@T z`h}aIKpUJ(%!w^{4;9@nW0~hmAlSRNVm;>&t>{vi%>2|rcMw;#Ma_A2f4m;~`8hg2 zaKtu|O>_y!?D)W!8^Lb<&+q$-rgznJ;@IZ##sh%)SaAg(j&ndhme{?ybH|Q29A-EE zeIz~MfC1982}IQe79ioe2jhyNKFM^`NiY7foFB}r;37;Of0CI67h4OOvpN82XAKyo zdgRa_O&>}n#0CLZdr6V~_~qQX5j#lo`m05%gi<9=e|<&LZ3ZDhE8|hl;%3cirlrA` zl5zc#kTJW_)bGOYJXJOY`*A-jl|K9Gb0$&)@YzXO|I8w&K7k$m#mWOu*pS-9oDhnA zQ;=(Qjk$j=qqJJ+?}4s9c|%?O_(Zh=FVg60=Zxt*b6})=G)Inti@ARyL#Ewq(S976 zTduVvXkqg|89k>NU)3UoqyQPn7b_#G8Tl)9{X3T?St37)bK5t4Paxxj`9LT{>Hed9 z`xD2LCQ%eWx+?z6${C>E)(8o`Op6m6Vx1~7`REy$lVWDQSkt9gYp!s-nYbp{>>P~QT++dHILNqU|$c=6)Nf(^Ij*p8zdttM#0_VVtP zMpkg5RfqUlx-~KFm293Qt=$R?9zOL{cV`u}|5s4yKr25~mR28_zWeiW4E`%R;G+zx z$9%`$@p$6+sW9cJjKy`i?|o`}-b+l#w3t{vO?=QQiv3&LHf$s z>lne|(mkwk@XaC=LvHFblnB$l{c=pBi9N zPCWV|J{?PpFA_Y@Y()klp;j*5PjV3Xy>9lFrEySR_Uvx7UzC@_I|-q3Z1^7^E+x}C z`(y>G9-_8*GXM}45^sz2wJX?p>wf5p)rf48>bKLnlPVa^)Aps8+A<`ZR_7(=8t%9T z;e**=g>TE)aO_OqC?}`5EbBtQee{%?*p!K4lh?EOTGLD%ITW%aLvpO0E1CBjgIdQB zNF{mF5yCeXT`4L*rct?q94}rrlr~dW4~r;H9$4PnN5u;Yz0R*u@doX>9J(jDm}O3F z;M?B4DZN~6)@nKdh4k$u9UM!<2kebY`n!zSuMNIQu$Tf*MbqVL7l#Z7vAXCOqtNeu z9__oS^t8ZKZj~Q-+11Gj9^k2W)(Y`lrX7o!adJDuNN}v%yig*-Nj*wq?(Dq=nJ*{& z+4}MIFt5t#OJ$!oG-2Fsrm?83_76jwTbYW+yHJCOk#m6MeU}xCXM^SQUFF`c$v4 zq}24xJX@vryZ0+8WTx0Eg`4^s_!c;)W@LI)6{QAO`Gq7`WhYyvDB0U7*i={n4aiL` zNmQuF&B-gas<2f8n`;GRgM{^!6u?SKvTcwn`GuBNuFf>#!Gt)CP zF*P$Y)KM@pFf`IP03tJ8LlY}gGbUo-h6WQb!1OB;3-k^33_xCjDfIQluQWFouDZA+C>7yetOgf{R2HP_ z2c;J0mlh=hBQ8xDWL1Hcb5UwyNq$jCetr%t6azByOY(~|@(UE4gUu8)!ZY(y^2>`g z!Rmc|tvvIJOA_;vQ$1a5m4GJbWoD*WnHd{7JG!|U85)~g8XCHqIa(SyyOv@OxC}mUbZnXFy(u?IEGZ* zdNV!VTO?58SijQi`3}>L>MXp?5p>5#JLZ($n?tvLG2C;jJ(QunsGH+p{)JYZlEgQw zb}w4ZojUvS=VQeQQqOJE|K-G;<+i^z^Ygst=El`&$7DI14lG$?xq)Td!Ngge%9^Q4 zRb0m$_-okK{#V@1H!mnW*USYx(6r)~PW zCoi=(=BustQM)GGap=%4?-eJtY%Ue1#zcoco)hk=HTl@{1SifvjCP-l*Qr_Te>8b& zrDn{c?GL2wV-Hzk+Q&6$&ojRD{Z99~=q3F3PRzV2vnc+-UiTD>e|>)oRezdJG@aVU$b5_Q z{bt*5{8K(IJ5hFE`W&U`Tvv^s)@6+~tkH@W+fCN@d2F?~&$WF<{X@~>X78T|o~&P8 zv*=A)ZRN97fyJGnC%7UNwNqNp=tr|(RZI<^DgNua*m{XiTyvW3BDmk4KO|MG@_y^x zeD?UF1J^VwW(2By_H&ASe&)aSZIYvf`{x%bzuBJb^XzW#>C&|u zM9*_w4tv)%?d9%rxg`Q}8}Fw~PEqO465V|9>A!xz>1SYOh(A)k<#YIADNu3j>FVdQ&MBb@0M8jt Ai~s-t literal 0 HcmV?d00001 diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/search_down.png b/TMessagesProj/src/main/res/drawable-xxhdpi/search_down.png old mode 100755 new mode 100644 index 55352cb7d639b11e2fa9cd373f6c351a0ddd1597..3b6909c7e9105808a04c3b4df4450ac593329f03 GIT binary patch delta 564 zcmeysv5|9vC!@+nFLlPrcNmo>XEA0^E@M)5*YHhD&de*xFV4_ZQmDu+@b$Iw%quQQ z%u7!7bg@+eis)r#rdV0JIXjyenL8R8n>iX9x|%teyE(bIn7J7`ni?1w8ktW1#VDfy zRpf+I(E=uo6f_epa4A+&C@u*~O|weLPcF?(%`1VqCI#UdOI&&<%P>3DXSmjwFfcGG zd%8G=RNUHg<1OzY1p&4MuRBLHSIwUBLZI$X9(#uDAs2JO=@(*cgnY8RA6nci;$)PH zns?#e-;AyM_fJ*N%F+*2$lLw({+|y&u9W0%+Wzg%?EvE|eX9xtF1^cKk)IT>dqL^r zeJa-!tIczdN7y!psBTnV@+q;DA+nyux_M55w%0P=ul~o3{jaP`NawKZSzFdwoTlS8 zUq`%hp;p2dhCrd&O>d5!4B?vHvt{Eag`5(eH9|F;W1h^MP`l=Ve(9#9Co(0RIbudE zu1T9G8fq|cI~FaljF7lA{p&`Z=V8pPF3TL2-cT?~)O~%`+m2Zyh{-i!^2DGFzOWX@ zGmTywl2Y_cSZBAoI9T4uQQ?w0Ja5i(YbTc(DF?4cuH8gZJbuuw?ay2q` zF*0#7H+OY2nykYl15;#-Q_(L*b@uSgyp;U%;>k;xG(sF*jV&BaT+IzkEsV^JTrJGa z4J_OYTr3=&EzKNVoXnIIic5l0)2veRlS^|`^GZBjY?TPF!d#(ra9a7+7OZXD7X>#T_mG$pZ?1I4cIS_iU@ry!57VT3w{kAsNA)PI24! zgk00Py!__wtwA5>+I^Jj`I@Pw>MT5KqKU-s-X%iYT4h>w70YiZFEQQL`sd)WM>Bsh z^}cm{HY54)kqn1K+u~+4l(`SwH7eP2cEQ}{U2ECBRfM7rvt9Z3V*b{I#-7Tm?2U!q=h=%chc+-Wh%l_V W_Ik}F=}*8gVDNPHb6Mw<&;$VZRg diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/search_up.png b/TMessagesProj/src/main/res/drawable-xxhdpi/search_up.png old mode 100755 new mode 100644 index 35730561de78e3025c4dca1e5aaaa3d9bb5e60dc..fd54f5b41bb5dd14989bd951b5313371f6fdc108 GIT binary patch delta 514 zcmaFL@tXEA0^E@M)5*YHhD&de*xFV4_ZQmDu+@b$Iw%quQQ z%u7!7bg@+eis)r#rdV0JIXjyenVT6In>iX9x|%teyE(bIn7J7`ni?1w8ktW1#VDfy zRb-A+(E=uo6f_e}a4A+&C@u*~O|weLPcF?(%`1VqCI#UdQ(Sr{%P>3DPuHv9VPIhR z=;`7ZQgQ3d^@p5?3vcc++U3fYYUo0xm%t1>7!e2yh76 z5a1-1ys$&(kdx$232w$Gnk`+e^$eHVQWE*MFHdx=cReP&I8m`>NwQ+ck|f2RC5a0g zmLx1}(K+TM7}?TlRAjiV=ya@X7`tvtjQjLrxA@2Ew|19kD-^5W-s!tl-mccVd8!lN z>rXP*ly84Ympa^~v_IkgiH*-SkF8Qr-ynZtCHsAIlUXNNZZGkO*Jos7aQjrCdyN0P ROAyfe44$rjF6*2UngEY&xE=ri delta 465 zcmey*@swkNC!@edFLlPrvlvq*XEAC{&S8>Ja5gcvG<7mDHgs}zH8gZJb#ikua&xpa zHgj_`GBkEGn5@Gj15;##Q_(L*b@uSgyp;U%;>k;xG(sF*jV&BaT+IzkEsV^JTrJGa z4J_OYTr3=&EzKNVoXnIIic5l0)2veRlS^|`^GZBjY?TPF!N3J>Y9a0cr4S345w|B?;W5PE}u0~cX=U;eyMOZ<_#B7-sbNxP*$$DIq*%uny zwh8b(C_H;!+9bP)DVb}%p<;2g;=>4U6_M=Tmc;0Z$r|#H_b;$Jwy0s*PIa@8?mJ1g z+)>>*hpbX>el>h&xU22b-KX_3ahp#}oOm@Ny`kpGfuw`wMrW;`M@%Tc#~jL)mEG?_DRn+ zq3C9ZkhF^sX^x)KCgdJbW7~yl7u9Zva+Y@5=a17_&sytudEWQ^zVG+F>v`7U;6Pt} z-L<+f7)+nxM-7EW^75mj4ZTT=!xCs%Bcnyi!oXd!WR3)YdGWxV0D>Xp@PSZ(!%Ip0 z1%McbGY$<46zg73q%y;tEQVsgn&muvR#;1 zrq~CF6ZoY{fUwj+7B@AXOXMLvJrHC$2@((jG7ds6Ob|&)atiX3E(w}1Z)1>%PY_u= z1^H)Ek<4I(4=4c;E@&qd7mLFo2n00F*@ZyBZ9(9%I6MXm-2@cQndCws;aw4*FC-LA z!rMg(rP4pgf>sn{oJ=MrVKB+b$>?M!G$`R?a6}?;Sp$zpK?syIMI__MQ6lNO6$L6F zm@M7&3D0Db7$T{RBjN%KDg_BCpalXRiAEqe(_LwJ9Fc~@;b;UR&fA&le*djK9?0tykIN+t=uo(uM?dY`$x zujhjMDi;GKgIR9ue>Hk#3v$o$^ownw#TVlPBFOC$$kx$jr*vR2t#=Hn7fU`eTC{Jg zBFU=FbMzZ4E1g3>7-@y|^k3}UtBNy=%QLU`3;5Tw+}#mHISHNNMf=pb@m#H__$N=x zP<0N>;J%{@vQGRe=YY$wd6cJ_fq}u)hsLx^))wS8iu!|1a&x`fQ$4Ye9@9O!@a9eW znPW|cF30Rq-#e-Pm~oVAHR}*s6K6C>R~sr(VoJLHN9z_Hb=Pmmx|E6?;k!@A-sQ>N zoAvj?1}hc`R5%}#>DrL)s?BcoHLX8fc{VVZSfmOQn=INDzvK?5mL{1hT~)R!-&OEA z+}z*N&AZIytH#EqDtv=RSFygzbk1_lch2MWnT98Ysv3nzWuYS5Y*wM1(?;M!%I4RhjUtX4P)=>7r?GB?Y=3-cFu-YJ`Dxu^W%nU6sO&cyIY90ru^LR*Vs|=f;q$vPmcOs5EwH!95<+~-G%TP4xo%A&_lDV z@zO-i>}l_;F=fE1h?pFeCjGj}gN-QXDZ#dAXDiyQ_6>z7WY}X1&-J#lD&vIzj%}7c zjU7K_(fhXF7)-q0%QXw>_t)N02GnLuUz%6!1Gi*9f09W8M3yD9-`LSgapIEtrBvg~ zX@hO^!Y+@Y1mkelmB5a!xbTDvk7Ip}MqUrvh1_1V=D~BtbFD| z^DlVoUtYB3?eLSmAs_t1`6st{cW`5ww_-R<+uz;B=w(msP9A$u+&yNbOI9i=(MuV1 z{H)jA$-VD-32C>f2bT632@(wlTbkhrCcC3epR^Qo- z-y*gtSxI^w4R1Y$>T1K%FP`(>qZoMqI@9w!wO+5Mx>&ccj=e__V7aY=jC$T^5cqow%u}Oa z!8pI>cK6l!v#Cw_8Zr&X8SJ9bD+B^OQMZYdcd+-(}?qK8heA`LF?M8&W|P77NQd(??l%ReWdpuZTelq+arl-Txnsua2uvB&||Q+Zg|M|YxP`^U0#X6C#1p8NaV=Q-aA zQPfsPoCgkr!8i&ccroZ1VfpNB(SO|R*IUuU9pT3#v6@6AO`?Y|Y`I1X0RojI35tOv z^8LC#C=`RS@==Q8k$B;DhD@U(N-RD^qe_dSF_=)6Q7e%pLkJ*+l9XyD{%LzB9#G1e z_?=W?pis+&6v~KnJrtWBC6=Wp%jj}E>kmMvk%1DZAVdNfRViwiVPxXp@-k4_Vg~WR zTNfmmiGP<=yif#iHF^l35`zL{fg}<@qY+6IDvd_+2grdWG8l-yv;Y!?L8URsTY&cu z9*w4#Co*Dq;qPOi8WXQT5G?}))6&w2X+cDdJ_#hz>2wPRnH+$61i<^%h{PD6hP{^< zcn~bpE47GHqXsOD5~(H?VdBwD-<6=!exOyu@7shn3^Yo#Ac+`gDQU@3DE$AfD%A&X z7>R*?&G&y2!{Yr~2#kSXO{!jo4ldE#5=zV9>LCfD(Tg>jl%+0;6dFVWD>Pbw8%qN= z3MDe7+G5!B79kWe1Zo(OsAZ6V$Hb!?M5R*BpoeoQq;Ni+OrldrBtDhS;RbX0d=7^e z&LIVcQWy`XB@3Hdb za|vD+3!=$@meKxZ)JrCGe=OpM#YL45s}HHseb=LlJ^r=(0tREP6Y$t#~j!WGqGpgeHn-#K_O`q_yySE1PcP9*Srq~Vq(kGJA zys8-q(jgEjPMoQ}H~Wi@Rap;D4vrKD!GlKD&y`O`+Tb^3Ps^UZ9Hh&xKhm9P8A_Df z+`?6thxwX3{jqQuX(J||?e*t5tNF|Z0LNh4DeTsFZ@3#QT46ZkxVNVN#{g*rR#ZGOu4*HJI@Q5zFs#zGOG?p9u%9$)*gmyAH>D)EljJny{wSys>pO z<@Ayu>pJXbUZ=o4SH53y*P}ViHO%$Fg(v<8J{me?2CNC#`(4r-+~Uc-E1xXJ0xpl& zeo-&D`rJK5-dMe|*?Pn(ma_6>XXb)uN~Eh|g|RETAt(Iai+z5~z7t^-E^~*XMjzA@ z@TlUf1I0OXXm6N-tZl%sW+w!OuSmS{Q`^bHJqsoRh$Zq>n-#FP$g9XB@rc;u6?mTz zAefZ)a{P0~E`JLRXC`LeU$Nor!40j@SjRuSkodgd(xZ3c%#^|&Rq9YCZdbJXf@fy_ z2-j5j>FVLE2QsPQFQS&*Pi8#i|Em4wRSg#VSYN7IF#Y5St&K=`>zUA$hIK@&na^wj z%Y0YWPP3DJPUhcIA9^v?s*P^fDTjW@c*4UQoU%%I*B2E7$nczBj615A#v5Gch|z z9rW^ZBF%I@a1AeJVb1sTHuX&Bv*esU)*RKfHgA+Le($yISLvdL@NQ88Qf@bO{Mhr^ z7MG2*nOOjL zWo}6EXRN|Dw}+#-gHeVQ@pl8(`yB6<%ik$+yA~a~%ku^nn^y1E>Q=SJHtmjB>le4$ zrMu!>QfE>v;!yY3b4gb&@uioZbA0MD2*9Z* z%_#cOcHUg#^!)5={6OQT5Z|4{Hp8AAUCFHVjVM|k%jHC7t0eH&gQAt|*7cQp@;BQ4 z(JUUgQJFu40Dk)>rG6JY)|(k;f}N@^EWqok46HHa;W6C4WOhZ?_jo9 - LibreChat - LibreChat + تيليجرام + نسخة تيليجرام التجريبية العربية Arabic ar @@ -66,7 +66,11 @@ تعطيل الأوسمة حديث + المحادثات + هل ترغب في حذف %1$s من الاقتراحات؟ معاينة الرابط + مسودة + تم مسح السجل نوع المجموعة نوع القناة @@ -105,8 +109,6 @@ أعضاء القائمة السوداء هم أعضاء تم حذفهم من المجموعة ولا يمكنهم العودة لها إلى بدعوة من المشرف. روابط الدعوة لن تمكنهم من العودة للمجموعة. قناة جديدة اسم القناة - التعليقات - إذا قمت بتفعيل التعليقات، الأعضاء سيتمكنون من مناقشة ما تنشره في قناتك. إضافة جهات اتصال لقناتك يستطيع الناس مشاركة هذا الرابط مع غيرهم ويمكنهم إيجاد قناتك من خلال البحث في تيليجرام. يستطيع الناس مشاركة هذا الرابط مع غيرهم ويمكنهم إيجاد مجموعتك من خلال البحث في تيليجرام. @@ -131,11 +133,8 @@ غادر القناة الإعدادات اشترك - معلومات القناة رسالة جماعية رسالة جماعية صامتة - تعليق - إظهار التعليقات ما هي القنوات؟ القنوات هي أداة جديدة للرسائل الجماعية لمجموعة كبيرة من المستقبلين. أنشاء قناة @@ -206,6 +205,7 @@ %1$s أرسل رسالة صوتية للقناة %2$s %1$s قام بإرسال مقطع صوتي للقناة %2$s %1$s قام بإرسال ملصق للقناة %2$s + %1$s أرسل %3$s ملصق للقناة %2$s %1$s أرسل رسالة %1$s أرسل صورة %1$s أرسل مقطع مرئي @@ -216,6 +216,7 @@ %1$s أرسل رسالة صوتية %1$s أرسل مقطع صوتي %1$s أرسل ملصق + %1$s أرسل %2$s ملصق من يستطيع إضافة أعضاء؟ جميع الأعضاء فقط المشرفون @@ -301,7 +302,7 @@ عداد التدمير الذاتي إشعارات الخدمة جاري جلب معلومات الرابط... - فتح في المتصفح + فتح بواسطة... انسخ الرابط أرسل %1$s هل ترغب في فتح الرابط باستخدام %1$s ؟ @@ -333,6 +334,12 @@ أخرى الوصف رسالة مثبتة + معدلة + انتقل إلى الأسفل للبوتات + %1$s + المعذرة، انتهت مدة التعديل على الرسائل. + إضافة اختصار + تمت إضافة اختصار للشاشة الرئيسية %1$s قام بتعيين عداد التدمير الذاتي إلى to %2$s لقد قمت بتعيين التدمير الذاتي إلى %1$s @@ -350,6 +357,7 @@ %1$s قام بإرسال رسالة صوتية %1$s أرسل مقطع صوتي لك %1$s قام بإرسال ملصق + %1$s قام بإرسال %2$s ملصق %1$s @ %2$s: %3$s %1$s قام بإرسال رسالة للمجموعة %2$s %1$s قام بإرسال صورة للمجموعة %2$s @@ -361,6 +369,7 @@ %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 @@ -384,6 +393,7 @@ %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 @@ -395,6 +405,7 @@ %1$s ثبت فيديو %1$s ثبت ملف %1$s ثبت ملصق + %1$s قام بتثبيت %2$s ملصق %1$s ثبت رسالة صوتية %1$s ثبت جهة اتصال %1$s ثبت خريطة @@ -403,7 +414,7 @@ اختر جهة اتصال لا توجد جهات اتصال بعد - Hey, let\'s switch to https://messenger.b44t.com + https://telegram.org/dl مرحبا! هيا نستخدم تيليجرام: في أمس الساعة متصل @@ -455,7 +466,7 @@ تحذير هذا القرار لا يمكن الرجوع عنه. لن تتمكن من إرجاع المجموعات الخارقة لتصبح مجموعات عادية. ]]>تم الوصول للحد القصى.]]>\n\nلتتمكن من تخطي هذا الحد والحصول على مميزات إضافية، يمكنك الترقية لمجموعة خارقة:\n\n• المجموعات الخارقة يمكن أن تصل إلى %1$s عضو\n• الأعضاء الجدد يمكنهم رؤية سجل المحادثات بالكامل \n• الرسائل المحذوفة ستختفي عن كل الأعضاء\n• يستطيع الأعضاء التعديل على رسائلهم\n• يمكن لمنشئ المجموعة إنشاء رابط دعوة لها - ]]>في المجموعات الخارقة]]>\n\n• الأعضاء الجدد يمكنهم رؤية سجل المحادثات بالكامل \n• الرسائل المحذوفة ستختفي عن كل الأعضاء\n• يستطيع الأعضاء التعديل على رسائلهم\n• يمكن لمنشئ المجموعة إنشاء رابط دعوة لها + ]]>في المجموعات الخارقة:]]>\n\n• الأعضاء الجدد يمكنهم رؤية سجل المحادثات بالكامل \n• الرسائل المحذوفة ستختفي عن كل الأعضاء\n• يستطيع الأعضاء التعديل على رسائلهم\n• يمكن لمنشئ المجموعة إنشاء رابط دعوة لها ]]>ملاحظة:]]> لا يمكنك الرجوع عن هذا القرار. مشاركة @@ -547,7 +558,7 @@ نرجو الأخذ بالعلم أن الدعم الفني في تيليجرام يقوم به مجموعة من المتطوعين. نحن نحاول الرد بسرعة قدر المستطاع، لكن ربما نستغرق القليل من الوقت.
    ]]> صفحة الأسئلة الأكثر شيوعًا
    ]]>: يوجد بها حلول للمشاكل وإجابات لمعظم الأسئلة. اسأل أحد المتطوعين الأسئلة الشائعة عن تيليجرام - https://messenger.b44t.com/faq + https://telegram.org/faq/ar سياسة الخصوصية https://telegram.org/privacy حذف التعريب؟ @@ -978,7 +989,7 @@ تيليجرام يحتاج صلاحية الحصول على موقعك. الإعدادات - LibreChat + تيليجرام سريع مجاني آمن @@ -1024,12 +1035,6 @@ %1$d رسائل %1$d رسالة %1$d رسالة - %1$d تعليقات - %1$d تعليق - %1$d تعليقان - %1$d تعليقات - %1$d تعليق - %1$d تعليق لا يوجد ملفات %1$d ملف %1$d ملف diff --git a/TMessagesProj/src/main/res/values-de/strings.xml b/TMessagesProj/src/main/res/values-de/strings.xml index d38481560..062ec18be 100644 --- a/TMessagesProj/src/main/res/values-de/strings.xml +++ b/TMessagesProj/src/main/res/values-de/strings.xml @@ -66,7 +66,11 @@ Dauerhaft Stumm HASHTAGS LETZTE + CHATS + Lösche %1$s aus Vorschlägen? Linkvorschau + Entwurf + Verlauf wurde gelöscht Gruppenart Kanalart @@ -105,8 +109,6 @@ Blockierte Nutzer können nur durch Admins erneut hinzugefügt werden. Einladungslinks funktionieren nicht. Neuer Kanal Kanalname - Kommentare - Wenn du Kommentare aktivierst, können sich alle an der Diskussion beteiligen. Kontakte zum Kanal hinzufügen Jeder kann diesen Link teilen und deinen Kanal in der Telegramsuche finden. Jeder kann diesen Link teilen und deine Gruppe in der Telegramsuche finden. @@ -131,11 +133,8 @@ Kanal verlassen Einstellungen BEITRETEN - Info Broadcast Lautloser Broadcast - Kommentar - Kommentare zeigen Was ist ein Kanal? In einem Kanal kannst du deine Nachrichten an ein großes Publikum schicken. KANAL ERSTELLEN @@ -206,6 +205,7 @@ %1$s hat eine Sprachnachricht 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 %1$s hat eine Nachricht gesendet %1$s hat ein Bild gesendet %1$s hat ein Video gesendet @@ -216,6 +216,7 @@ %1$s hat eine Sprachnachricht gesendet %1$s hat ein Musikstück gesendet %1$s hat einen Sticker gesendet + %1$s hat einen %2$s Sticker gesendet Wer kann Mitglieder einladen? Alle Mitglieder Nur Admins @@ -301,7 +302,7 @@ Selbstzerstörungs-Timer setzen Servicemeldungen Lade Linkvorschau... - Im Browser öffnen + ÖFFNEN MIT... URL kopieren %1$s senden URL %1$s öffnen? @@ -333,6 +334,12 @@ Sonstiges Beschreibung Angeheftete Nachricht + bearbeitet + Runterscrollen für Bots + %1$s + Bearbeitungszeit leider abgelaufen. + Verknüpfung hinzufügen + Zum Startbildschirm hinzugefügt %1$s hat den Selbstzerstörungs-Timer auf %2$s gesetzt Du hast den Selbstzerstörungs-Timer auf %1$s gesetzt @@ -350,6 +357,7 @@ %1$s hat dir eine Sprachnachricht 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 %1$s @ %2$s: %3$s %1$s hat eine Nachricht an die Gruppe %2$s gesendet %1$s hat ein Bild an die Gruppe %2$s gesendet @@ -361,6 +369,7 @@ %1$s hat eine Sprachnachricht 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 @@ -384,6 +393,7 @@ %1$s hat ein Video in der Gruppe %2$s angeheftet %1$s hat eine Datei in der Gruppe %2$s angeheftet %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 einen Kontakt in der Gruppe %2$s angeheftet %1$s hat einen Standort in der Gruppe %2$s angeheftet @@ -395,6 +405,7 @@ %1$s hat ein Video angeheftet %1$s hat eine Datei angeheftet %1$s hat einen Sticker angeheftet + %1$s hat einen %2$s Sticker angeheftet %1$s hat eine Sprachnachricht angeheftet %1$s hat einen Kontakt angeheftet %1$s hat einen Stamdort angeheftet @@ -454,8 +465,8 @@ In Supergruppe ändern Warnung Du kannst die Supergruppe nicht mehr in eine normale Gruppe ändern. - ]]>Gruppenlimit erreicht.]]>\n\nFür weitere Funktionen und um das Limit aufzuheben in Supergruppe ändern:\n\n• Bis zu %1$s sind nun möglich\n• Neue Mitglieder sehen gesamten Verlauf\n• Mitglieder können eigene Nachrichten bearbeiten\n• Gruppenersteller kann die Gruppe öffentlich machen - ]]>In Supergruppen:]]>\n\n• Neue Mitglieder sehen gesamten Verlauf\n• Nachrichten werden bei allen gelöscht\n• Jeder kann eigene Nachrichten bearbeiten\n• Gründer kann Gruppe öffentlich machen + ]]>Gruppenlimit erreicht.]]>\n\nFür weitere Funktionen und um das Limit aufzuheben in Supergruppe ändern:\n\n• Bis zu %1$s sind nun möglich\n• Neue Mitglieder sehen gesamten Verlauf\n• Gelöschte Nachrichten werden bei allen Mitgliedern entfernt\n•Gruppenersteller kann die Gruppe öffentlich machen + ]]>In Supergruppen:]]>\n\n• Neue Mitglieder sehen gesamten Verlauf\n• Nachrichten werden bei allen gelöscht\n• Gründer kann Gruppe öffentlich machen ]]>Wichtig:]]> Die Änderung kann nicht rückgängig gemacht werden. Teilen @@ -1023,12 +1034,6 @@ %1$d Nachrichten %1$d Nachrichten %1$d Nachrichten - %1$d Kommentare - %1$d Kommentar - %1$d Kommentare - %1$d Kommentare - %1$d Kommentare - %1$d Kommentare keine Objekte %1$d Objekt %1$d Objekte @@ -1197,5 +1202,4 @@ HH:mm h:mm a %1$s um %2$s - Erreiche jeden diff --git a/TMessagesProj/src/main/res/values-es/strings.xml b/TMessagesProj/src/main/res/values-es/strings.xml index 4aac647b2..8c97db97e 100644 --- a/TMessagesProj/src/main/res/values-es/strings.xml +++ b/TMessagesProj/src/main/res/values-es/strings.xml @@ -39,7 +39,7 @@ ayer Sin resultados Aún sin chats... - Envía mensajes pulsando el botón para\nredactar, en la parte inferior derecha,\no pulsa el botón menú para más opciones. + Envía mensajes tocando el botón para\nredactar, en la parte inferior derecha,\no pulsa el botón menú para más opciones. Esperando red... Conectando... Actualizando... @@ -56,7 +56,7 @@ Cuenta eliminada Elige el chat Mantén pulsado para ver - %1$s usa una versión antigua de Telegram, así que las fotos secretas serán mostradas en un modo de compatibilidad.\n\nCuando %2$s actualice Telegram, las fotos con autodestrucción de 1 minuto o menos funcionarán con el modo \'Mantén pulsado para ver\', y te notificaremos siempre que la otra parte haga una captura de pantalla. + %1$s usa una versión antigua de Telegram, así que las fotos secretas serán mostradas en un modo de compatibilidad.\n\nCuando %2$s actualice Telegram, las fotos con autodestrucción de 1 minuto o menos funcionarán con el modo “Mantén pulsado para ver”, y te notificaremos siempre que la otra parte haga una captura de pantalla. MENSAJES Buscar Silenciar notificaciones @@ -66,7 +66,11 @@ Desactivar HASHTAGS RECIENTES + CHATS + ¿Eliminar a %1$s de las sugerencias? Vista previa del enlace + Borrador + Historial borrado Tipo de grupo Tipo de canal @@ -105,8 +109,6 @@ Los usuarios bloqueados son eliminados del grupo y sólo pueden volver si son invitados por un administrador. Los enlaces de invitación no funcionan para ellos. Nuevo canal Nombre del canal - Comentarios - Si activas los comentarios, las personas podrán hablar de tus mensajes en el canal. Añadir contactos a tu canal Las personas pueden compartir este enlace con los demás y encontrar tu canal usando la búsqueda de Telegram. Las personas pueden compartir este enlace con los demás y encontrar tu grupo usando la búsqueda de Telegram. @@ -130,12 +132,9 @@ Dejar canal Dejar el canal Ajustes - UNIRSE - Información + UNIRME Difundir Difusión en silencio - Comentario - mostrar comentarios ¿Qué es un canal? Los canales son una nueva herramienta para difundir tus mensajes a grandes audiencias. CREAR CANAL @@ -179,7 +178,7 @@ Este usuario aún no se ha unido al canal. ¿Quieres invitarlo? Cualquiera que tenga Telegram instalada podrá unirse a tu canal siguiendo este enlace. Puedes añadir administradores para que te ayuden en el canal. Mantén pulsado para eliminarlos. - ¿Quieres unirte al canal \'%1$s\'? + ¿Quieres unirte al canal “%1$s”? Lo sentimos, este chat ya no es accesible. Lamentablemente, fuiste suspendido de participar en grupos públicos. Lo sentimos, este chat ya no es accesible. @@ -206,6 +205,7 @@ %1$s envió un mensaje de voz 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 %1$s publicó un mensaje %1$s publicó una foto %1$s publicó un vídeo @@ -216,6 +216,7 @@ %1$s publicó un mensaje de voz %1$s publicó una pista %1$s publicó un sticker + %1$s publicó un %2$s sticker ¿Quién puede invitar? Todos Administradores @@ -301,7 +302,7 @@ Establecer autodestrucción Servicio de notificaciones Obteniendo información... - Abrir en el navegador + ABRIR EN... Copiar URL Enviar %1$s ¿Abrir %1$s? @@ -316,7 +317,7 @@ https://telegram.org/faq/es#no-puedo-enviar-mensajes-a-quienes-no-son-mis-contactos Más información Enviar a... - Pulsa y ve los GIF guardados + Toca y ve los GIF guardados Anclar Notificar a los miembros Desanclar @@ -333,6 +334,12 @@ Otros Descripción Mensaje anclado + editado + Ve abajo para los bots + %1$s + Terminó el tiempo de edición. + Añadir acceso directo + Acceso directo añadido al escritorio %1$s activó la autodestrucción en %2$s Activaste la autodestrucción en %1$s @@ -350,6 +357,7 @@ %1$s te envió un mensaje de voz %1$s te envió una pista %1$s te envió un sticker + %1$s te envió un %2$s sticker %1$s @ %2$s: %3$s %1$s envió un mensaje al grupo %2$s %1$s envió una foto al grupo %2$s @@ -361,6 +369,7 @@ %1$s envió un mensaje de voz 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 cambió la foto del grupo %2$s @@ -371,7 +380,7 @@ %1$s te eliminó del grupo %2$s %1$s dejó el grupo %2$s ¡%1$s se unió a Telegram! - %1$s,\nDetectamos un inicio de sesión en tu cuenta desde un nuevo dispositivo, el %2$s\n\nDispositivo: %3$s\nUbicación: %4$s\n\nSi no eras tú, puedes ir a Ajustes - Privacidad y seguridad - Sesiones activas y cerrar esa sesión.\n\nSi crees que alguien ha iniciado la sesión sin tu consentimiento, puedes activar la verificación en dos pasos, en los ajustes de privacidad y seguridad.\n\nAtentamente,\nEl equipo de Telegram + %1$s,\nDetectamos un inicio de sesión en tu cuenta desde un nuevo dispositivo, el %2$s\n\nDispositivo: %3$s\nUbicación: %4$s\n\nSi no eras tú, puedes ir a Ajustes > Privacidad y seguridad > Sesiones activas y cerrar esa sesión.\n\nSi crees que alguien ha iniciado la sesión sin tu consentimiento, puedes activar la verificación en dos pasos, en los ajustes de privacidad y seguridad.\n\nAtentamente,\nEl equipo de Telegram %1$s actualizó su foto de perfil %1$s se unió al grupo %2$s con un enlace de invitación Responder @@ -384,6 +393,7 @@ %1$s ancló un vídeo en el grupo %2$s %1$s ancló un archivo en el grupo %2$s %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 contacto en el grupo %2$s %1$s ancló un mapa en el grupo %2$s @@ -395,6 +405,7 @@ %1$s ancló un vídeo %1$s ancló un archivo %1$s ancló un sticker + %1$s ancló un %2$s sticker %1$s ancló un mensaje de voz %1$s ancló un contacto %1$s ancló un mapa @@ -403,7 +414,7 @@ Elegir contacto Aún sin contactos - ¡Oye! Cambiémonos a LibreChat: https://messenger.b44t.com + ¡Oye! Cambiémonos a Telegram: https://telegram.org/dl a las ayer a las en línea @@ -423,7 +434,7 @@ Nombre del grupo Nombre del grupo %1$d/%2$d miembros - ¿Quieres unirte al chat \'%1$s\'? + ¿Quieres unirte al chat “%1$s”? Lo sentimos. Este grupo está lleno. Lo sentimos, este chat no existe. Enlace copiado al portapapeles @@ -454,8 +465,8 @@ Convertir en supergrupo Advertencia Esta acción es irreversible. No puedes convertir un supergrupo en un grupo normal. - ]]>Límite de miembros alcanzado.]]>\n\nPara superar el límite y tener características adicionales, conviértelo en un supergrupo:\n\n• Permiten hasta %1$s\n• Nuevos miembros ven todo el historial\n• Un mensaje eliminado se borra para todos\n• Un miembro puede editar sus mensajes\n• El creador puede generar un enlace público - ]]>En los supergrupos:]]>\n\n• Los nuevos miembros ven todo el historial\n• Un mensaje eliminado se borra para todos\n• Un miembro puede editar sus mensajes\n• El creador puede generar un enlace público + ]]>Límite de miembros alcanzado.]]>\n\nPara superar el límite y tener características adicionales, conviértelo en un supergrupo:\n\n Permiten hasta %1$s\n• Nuevos miembros ven todo el historial\n• Un mensaje eliminado se borra para todos\n• El creador puede generar un enlace público + ]]>En los supergrupos:]]>\n\n• Los nuevos miembros ven todo el historial\n• Un mensaje eliminado se borra para todos\n• El creador puede generar un enlace público ]]>Importante:]]> Esta acción no se puede deshacer. Compartir @@ -494,7 +505,7 @@ Ocurrió un error. Stickers - Los artistas pueden añadir sus propios packs de stickers usando el bot @stickers.\n\nLos usuarios pueden añadir stickers pulsando y eligiendo \"Añadir stickers\". + Los artistas pueden añadir sus propios packs de stickers usando el bot @stickers.\n\nLos usuarios pueden añadir stickers tocando y eligiendo “Añadir stickers”. Añadir stickers Añadir a stickers Stickers no encontrados @@ -538,7 +549,7 @@ Sólo si está silenciado Fondo de chat Mensajes - Enviar con \'Intro\' + Enviar con “Intro” Cerrar todas las otras sesiones Eventos Un contacto se unió a Telegram @@ -547,7 +558,7 @@ 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]]>.
    Preguntar Preguntas frecuentes - https://messenger.b44t.com/faq + https://telegram.org/faq/es Política de privacidad https://telegram.org/privacy ¿Eliminar traducción? @@ -589,7 +600,7 @@ Máxima Nunca Repetir notificaciones - Puedes cambiar tu número de Telegram aquí. Tu cuenta y todos tus datos de la nube — mensajes, archivos, grupos, contactos, etc., se moverán al nuevo número.\n\nImportante:]]> Todos tus contactos de Telegram tendrán tu nuevo número]]> añadido a sus agendas de contactos, siempre que hayan tenido tu número viejo y no los hayas bloqueado en Telegram. + Puedes cambiar tu número de Telegram aquí. Tu cuenta y todos tus datos de la nube, mensajes, archivos, grupos, contactos, etc., se moverán al nuevo número.\n\nImportante:]]> Todos tus contactos de Telegram tendrán tu nuevo número]]> añadido a sus agendas de contactos, siempre que hayan tenido tu número viejo y no los hayas bloqueado en Telegram. Todos tus contactos de Telegram tendrán tu número nuevo añadido a sus agendas de contactos, siempre que hayan tenido tu número viejo y no los hayas bloqueado en Telegram. CAMBIAR NÚMERO Nuevo número @@ -641,7 +652,7 @@ Puedes iniciar sesión en Telegram desde otro móvil, tablet o computador, usando el mismo número de teléfono. Todos tus datos se sincronizarán al instante. Sesiones activas Controla tus sesiones en otros dispositivos. - Pulsa una sesión para cerrarla. + Toca una sesión para cerrarla. ¿Cerrar esta sesión? aplicación no oficial @@ -876,7 +887,7 @@ Desde la cámara Desde la galería Eliminar foto - Establecer + OK OK RECORTAR @@ -924,7 +935,7 @@ Cargando... No tienes reproductor de vídeo. Por favor, instala uno para continuar. Por favor, envía un correo electrónico a sms@stel.com y cuéntanos tu problema. - No tienes aplicaciones que puedan manejar el tipo de archivo \'%1$s\'. Por favor, instala una para continuar. + No tienes aplicaciones que puedan manejar el tipo de archivo “%1$s”. Por favor, instala una para continuar. Este usuario aún no tiene Telegram. ¿Enviarle una invitación? ¿Quieres hacerlo? ¿Añadir a %1$s al chat %2$s? @@ -964,7 +975,7 @@ Este bot no puede unirse a grupos. ¿Quieres permitir las vistas previas ampliadas en chats secretos? Ten en cuenta que son generadas en los servidores de Telegram. Ten en cuenta que los bots integrados son hechos por terceros. Para que funcione, los símbolos escritos después del alias del bot, son enviados al desarrollador respectivo. - ¿Quieres habilitar \"Elevar para hablar\" para mensajes de voz? + ¿Quieres habilitar “Elevar para hablar” para mensajes de voz? No puedes editar este mensaje. Por favor, permite a Telegram recibir SMS, para ingresar el código automáticamente. Por favor, permite a Telegram recibir llamadas, para ingresar el código automáticamente. @@ -978,7 +989,7 @@ Telegram necesita acceso a tu ubicación. AJUSTES - LibreChat + Telegram Rápida Gratis Segura @@ -1006,12 +1017,12 @@ %1$d miembros %1$d miembros %1$d miembros - y %1$d personas más están escribiendo + y %1$d más están escribiendo y %1$d más están escribiendo - y %1$d personas más están escribiendo - y %1$d personas más están escribiendo - y %1$d personas más están escribiendo - y %1$d personas más están escribiendo + y %1$d más están escribiendo + y %1$d más están escribiendo + y %1$d más están escribiendo + y %1$d más están escribiendo Sin mensajes nuevos %1$d nuevo mensaje %1$d nuevos mensajes @@ -1024,12 +1035,6 @@ %1$d mensajes %1$d mensajes %1$d mensajes - %1$d comentarios - %1$d comentario - %1$d comentarios - %1$d comentarios - %1$d comentarios - %1$d comentarios Sin ítems %1$d ítem %1$d ítems diff --git a/TMessagesProj/src/main/res/values-it/strings.xml b/TMessagesProj/src/main/res/values-it/strings.xml index c320702e3..b6d673cfc 100644 --- a/TMessagesProj/src/main/res/values-it/strings.xml +++ b/TMessagesProj/src/main/res/values-it/strings.xml @@ -66,7 +66,11 @@ Disabilita HASHTAG RECENTI + CHAT + Eliminare %1$s dai suggerimenti? Anteprima link + Bozza + Cronologia eliminata Tipo di gruppo Tipo di canale @@ -105,8 +109,6 @@ Gli utenti in lista nera sono rimossi dal gruppo e possono tornare solo se invitati da un amministratore. I link di invito non funzionano per loro. Nuovo canale Nome canale - Commenti - Se attivi i commenti, i membri potranno discutere quello che pubblichi nel canale. Aggiungi contatti al tuo canale Le persone possono condividere questo link con gli altri e trovare il tuo canale usando la ricerca di Telegram. Le persone possono condividere questo link con gli altri e trovare il tuo gruppo usando la ricerca di Telegram. @@ -131,11 +133,8 @@ Lascia il canale Impostazioni UNISCITI - Info canale Post Post silenzioso - Commento - mostra commenti Cos\'è un canale? I canali sono un nuovo strumento per diffondere i tuoi messaggi a un grande pubblico. CREA CANALE @@ -157,14 +156,14 @@ Sei sicuro di voler lasciare il canale? Perderai tutti i messaggi in questo canale. Modifica - Per favore ricorda che se scegli un link pubblico per il tuo gruppo, chiunque sarà in grado di trovarlo nella ricerca e unirsi.\n\nNon creare questo link se vuoi che il tuo supergruppo rimanga privato. - Per favore ricorda che se scegli un link pubblico per il tuo canale, chiunque sarà in grado di trovarlo nella ricerca e unirsi.\n\nNon creare questo link se vuoi che il tuo canale rimanga privato. + Per favore nota che se scegli un link pubblico per il tuo gruppo, chiunque sarà in grado di trovarlo nella ricerca e unirsi.\n\nNon creare questo link se vuoi che il tuo supergruppo rimanga privato. + Per favore nota che se scegli un link pubblico per il tuo canale, chiunque sarà in grado di trovarlo nella ricerca e unirsi.\n\nNon creare questo link se vuoi che il tuo canale rimanga privato. Per favore scegli un link per il tuo canale pubblico, in modo che possa essere trovato nella ricerca e condiviso con altri.\n\nSe non sei interessato, ti consigliamo di creare un canale privato. Canale creato Foto del canale cambiata Foto del canale rimossa Nome del canale cambiato in un2 - Spiacenti, hai creato troppi canali pubblici. Puoi creare un canale privato o eliminare un tuo canale pubblico. + Spiacenti, hai creato troppi canali pubblici. Puoi creare un canale privato o eliminare un tuo canale esistente. Spiacenti, hai creato troppi link pubblici. Prova a eliminarne o a rendere qualche gruppo o canale privato. Moderatore Creatore @@ -206,6 +205,7 @@ %1$s ha inviato un messaggio vocale 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 %1$s ha pubblicato un messaggio %1$s ha pubblicato una foto %1$s ha pubblicato un video @@ -216,6 +216,7 @@ %1$s ha pubblicato un messaggio vocale %1$s ha pubblicato una traccia %1$s ha pubblicato uno sticker + %1$s ha pubblicato uno %2$s sticker Chi può aggiungere nuovi membri? Tutti i membri Solo gli amministratori @@ -254,16 +255,16 @@ sta scrivendo… sta scrivendo… stanno scrivendo… - %1$s sta registrando un messaggio vocale... + %1$s sta registrando un audio... %1$s sta inviando un audio... %1$s sta inviando una foto... %1$s sta inviando un video... %1$s sta inviando un file... - registrando un messaggio vocale... - inviando un audio... - inviando una foto... - inviando un video... - inviando un file... + sta registrando un audio... + sta inviando un audio... + sta inviando una foto... + sta inviando un video... + sta inviando un file... Hai una domanda\nsu Telegram? Scatta foto Galleria @@ -301,7 +302,7 @@ Timer di autodistruzione Notifiche di servizio Recupero le info del link... - Apri nel Browser + APRI IN... Copia URL Invia %1$s Aprire url %1$s? @@ -314,7 +315,7 @@ Spiacenti, ma al momento puoi scrivere solo ai contatti reciproci. Spiacenti, ma al momento puoi aggiungere ai gruppi solo contatti reciproci. https://telegram.org/faq/it#non-posso-inviare-messaggi-a-chi-non-far-parte-dei-miei-contatti - Più info + Maggiori info Invia a... Premi qui per accedere alle GIF salvate Fissa @@ -333,6 +334,12 @@ Altro Descrizione Messaggio fissato + modificato + Scorri verso il basso per i bot + %1$s + Spiacenti, tempo di modifica scaduto. + Aggiungi scorciatoia + Scorciatoia aggiunta alla schermata home %1$s ha impostato il timer di autodistruzione a %2$s Hai impostato il timer di autodistruzione a %1$s @@ -350,6 +357,7 @@ %1$s ti ha inviato un messaggio vocale %1$s ti ha inviato una traccia %1$s ti ha inviato uno sticker + %1$s ti ha inviato uno %2$s sticker %1$s @ %2$s: %3$s %1$s ha inviato un messaggio al gruppo %2$s %1$s ha inviato una foto al gruppo %2$s @@ -361,6 +369,7 @@ %1$s ha inviato un messaggio vocale 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 modificato la foto del gruppo %2$s @@ -384,6 +393,7 @@ %1$s ha fissato un video nel gruppo %2$s %1$s ha fissato un file nel gruppo %2$s %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 contatto nel gruppo %2$s %1$s ha fissato una posizione nel gruppo %2$s @@ -395,6 +405,7 @@ %1$s ha fissato un video %1$s ha fissato un file %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 contatto %1$s ha fissato una posizione @@ -403,7 +414,7 @@ Seleziona contatto Ancora nessun contatto - Ehi, è il momento di passare a LibreChat: https://messenger.b44t.com + Ehi, è il momento di passare a Telegram: https://telegram.org/dl alle ieri alle in linea @@ -454,8 +465,8 @@ Converti in supergruppo Attenzione Questa azione è irreversibile. Non è possibile trasformare un supergruppo in un gruppo normale. - ]]>Limite membri raggiunto.]]>\n\nPer superare il limite e sbloccare nuove funzioni, aggiorna a supergruppo:\n\n• Il supergruppi hanno massimo %1$s\n• I nuovi membri vedono tutta la cronologia\n• I messaggi eliminati scompaiono per tutti\n• I membri possono modificare i loro messaggi\n• Il creatore può creare un link pubblico per il gruppo - ]]>Nei supergruppi:]]>\n\n• I nuovi membri vedono tutta la cronologia\n• I messaggi eliminati scompaiono per tutti\n• I membri possono modificare i loro messaggi\n• Il creatore può creare un link pubblico per il gruppo + ]]>Limite membri raggiunto.]]>\n\nPer superare il limite e sbloccare nuove funzioni, aggiorna a supergruppo:\n\n• Il supergruppi hanno massimo %1$s\n• I nuovi membri vedono tutta la cronologia\n• I messaggi eliminati scompaiono per tutti\n• Il creatore può creare un link pubblico per il gruppo + ]]>Nei supergruppi:]]>\n\n• I nuovi membri vedono tutta la cronologia\n• I messaggi eliminati scompaiono per tutti\n• Il creatore può creare un link pubblico per il gruppo ]]>Nota:]]> questa azione non può essere annullata. Condividi @@ -531,7 +542,7 @@ Annulla tutte le impostazioni di notifica personalizzate per tutti i tuoi contatti e gruppi Notifiche e suoni Utenti bloccati - Disconnetti + Esci Nessun suono Predefinite Supporto @@ -544,10 +555,10 @@ Un contatto si è unito a Telegram Messaggi fissati Lingua - Nota che il supporto di Telegram è fornito da volontari. Proviamo a rispondere quanto prima, ma potrebbe volerci del tempo.
    ]]>Dai un\'occhiata alle Domande frequenti]]>di Telegram: contengono risposte a quasi tutte le domande e suggerimenti importanti per risolvere i problemi]]>
    + Nota che il supporto di Telegram è fornito da volontari. Proviamo a rispondere quanto prima, ma potrebbe volerci del tempo.
    ]]>Dai un\'occhiata alle Domande frequenti di Telegram]]>: contengono risposte a quasi tutte le domande e suggerimenti importanti per risolvere i problemi]]>.
    Chiedi a un volontario Domande frequenti - https://messenger.b44t.com/faq + https://telegram.org/faq/it Informativa sulla privacy https://telegram.org/privacy Eliminare la traduzione? @@ -621,8 +632,8 @@ Database locale Eliminare i messaggi salvati nella cache? Eliminando il database locale, verranno cancellati i messaggi nella cache e il database verrà compresso per risparmiare spazio. Telegram ha bisogno di alcuni dati per funzionare, quindi il database non sarà azzerato.\n\nQuesta operazione può richiedere alcuni minuti. - Pulisci cache - Pulisci + Svuota cache + Svuota Calcolando... Documenti Foto @@ -632,7 +643,7 @@ Altri file 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 la spazio sul disco.\n\nTutti i media rimarranno nel cloud di Telegram e potranno essere riscaricati ogni volta che ne avrai bisogno. + 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 Sessioni attive @@ -736,7 +747,7 @@ Sei sicuro di voler eliminare questo video? Annullare le modifiche? Cancellare la cronologia di ricerca? - Pulisci + Cancella Foto Video Aggiungi una didascalia... @@ -754,7 +765,7 @@ Reinserisci la tua password E-mail di recupero La tua e-mail - Inserisci un\'e-mail valida. È l\'unico modo di recuperare una password dimenticata. + Inserisci un\'e-mail valida. È l\'unico modo per recuperare una password dimenticata. Salta Attenzione No, seriamente.\n\nSe dimentichi la tua password, perderai l\'accesso al tuo account Telegram. Non ci sarà modo di ripristinarlo. @@ -823,7 +834,7 @@ Non condividere con gli utenti... Aggiungi utenti Spiacenti, troppe richieste. Impossibile cambiare le impostazioni di privacy ora, attendi. - Disconnette tutti i dispositivi tranne questo. + Esce da tutti i dispositivi tranne questo. Tieni premuto sull\'utente per eliminarlo. Gruppi Chi può aggiungermi ai gruppi? @@ -841,7 +852,7 @@ Modifica video Video originale Video modificato - Inviando un video... + Invio video... Comprimi video bot @@ -934,7 +945,7 @@ Vuoi inoltrare i messaggi a %1$s? Inviare i messaggi a %1$s? Inviare contatto a %1$s? - Sei sicuro di volerti disconnettere?\n\nRicorda che puoi usare Telegram su tutti i tuoi dispositivi contemporaneamente.\n\nRicorda, quando ti disconnetti, elimini tutte le Chat Segrete. + Sei sicuro di voler uscire?\n\nNota che puoi usare Telegram su tutti i tuoi dispositivi contemporaneamente.\n\nRicorda, uscendo eliminerai tutte le Chat Segrete. Terminare tutte le altre sessioni? Sei sicuro di voler uscire ed eliminare il gruppo? Sei sicuro di voler eliminare questa chat? @@ -962,8 +973,8 @@ Spiacenti, questa funzione non è disponibile nel tuo paese. Non esiste alcun account Telegram con questo username. Questo bot non può unirsi ai gruppi. - Vuoi attivare le anteprime estese per i link nelle Chat Segrete? Ricorda che le anteprime dei link sono generate sui server di Telegram. - Per favore ricorda che i bot inline sono forniti da sviluppatori di terze parti. Per far funzionare il bot, i simboli che digiti dopo l\'username del bot sono inviati al rispettivo sviluppatore. + Vuoi attivare le anteprime estese per i link nelle Chat Segrete? Nota che le anteprime dei link sono generate sui server di Telegram. + Per favore nota che i bot inline sono forniti da sviluppatori di terze parti. Per far funzionare il bot, i simboli che digiti dopo l\'username del bot sono inviati al rispettivo sviluppatore. Vuoi attivare \"Alza per parlare\" per i messaggi vocali? Spiacenti, non puoi modificare questo messaggio. Per favore consenti a Telegram di ricevere SMS così potremo inserire in automatico il codice per te. @@ -978,7 +989,7 @@ Telegram deve accedere alla tua posizione. IMPOSTAZIONI - LibreChat + Telegram Veloce Gratuito Sicuro @@ -986,11 +997,11 @@ Basato sul cloud Privato L\'app di messaggi più veloce]]> al mondo.]]>È gratuita]]> e sicura]]>. - LibreChat]]> consegna i messaggi più]]>velocemente di qualsiasi altra app. - LibreChat]]> sarà sempre gratuito.]]>Nessuna pubblicità. Nessun abbonamento. - LibreChat]]> protegge i tuoi messaggi]]>dagli attacchi degli hacker. - LibreChat]]> non ha limiti di dimensione]]>per le tue chat e i file multimediali. - LibreChat]]> ti consente di accedere]]>ai tuoi messaggi da più dispositivi. + 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. Inizia a chattare @@ -1006,12 +1017,12 @@ %1$d membri %1$d membri %1$d membri - e altre %1$d persone stanno scrivendo - e %1$d altra persona stanno scrivendo - e altre %1$d persone stanno scrivendo - e altre %1$d persone stanno scrivendo - e altre %1$d persone stanno scrivendo - e altre %1$d persone stanno scrivendo + e altri %1$d stanno scrivendo + e %1$d altro stanno scrivendo + e altri %1$d stanno scrivendo + e altri %1$d stanno scrivendo + e altri %1$d stanno scrivendo + e altri %1$d stanno scrivendo nessun nuovo messaggio %1$d nuovo messaggio %1$d nuovi messaggi @@ -1024,12 +1035,6 @@ %1$d messaggi %1$d messaggi %1$d messaggi - %1$d commenti - %1$d commento - %1$d commenti - %1$d commenti - %1$d commenti - %1$d commenti nessun oggetto %1$d oggetto %1$d oggetti diff --git a/TMessagesProj/src/main/res/values-ko/strings.xml b/TMessagesProj/src/main/res/values-ko/strings.xml index 388793bc4..3c9671bea 100644 --- a/TMessagesProj/src/main/res/values-ko/strings.xml +++ b/TMessagesProj/src/main/res/values-ko/strings.xml @@ -3,8 +3,8 @@ - LibreChat - LibreChat + 텔레그램 + 텔레그램 베타 한국어 Korean ko @@ -66,7 +66,11 @@ 비활성화 해시태그 최신 + 대화 + %1$s를 추천에서 삭제하겠습니까? 링크 미리복 + 임시 + 내역이 초기화 됨 그룹 종류 채널 종류 @@ -105,8 +109,6 @@ 그룹방에서 차단되어 퇴장당한 사용자는 관리자가 초대해야지만 그룹방에 입장이 가능합니다. 초대링크로는 초대가 되지 않습니다. 새 채널 채널명 - 코멘트들 - 코멘트를 허용할 경우, 유저들이 회원님 글에 대하여 코멘트 등록이 가능합니다. 채널에 친구 추가 텔레그램 검색을 통하여 다른 유저들이 채널을 찾을 수 있습니다. 텔레그램 검색을 통하여 다른 유저들이 그룹을 찾을 수 있습니다. @@ -131,11 +133,8 @@ 채널 나가기 설정 입장 - 채널 정보 모두에게 메시지 전달 조용한 공지 - 코멘트 - 코멘트 보기 채널이 무엇인가요? 채널은 많은 유저들에게 메시지를 한번에 전송 할 수 있는 새로운 기능입니다. 채널 생성 @@ -206,6 +205,7 @@ %1$s님이 %2$s 채널에 음성메시지를 보냈습니다 %1$s님이 %2$s 채널에 트랙을 보냈습니다 %1$s님이 %2$s 채널에 스티커를 보냈습니다 + %1$s님이 %2$s 채널에 %3$s 스티커를 보냈습니다 %1$s 님이 메시지를 보냈습니다 %1$s 님이 사진을 보냈습니다 %1$s 님이 동영상을 보냈습니다 @@ -216,6 +216,7 @@ %1$s님이 음성메시지를 보냈습니다 %1$s님이 트랙을 보냈습니다 %1$s님이 스티커를 보냈습니다 + %1$s님이 %2$s스티커를 작성하였습니다 초대가 가능한 구성원 모든 구성원 관리자만 가능 @@ -301,7 +302,7 @@ 자동삭제 타이머 설정 서비스 알림 링크 정보를 가져오는 중... - 브라우져에서 열기 + 다음으로 열기.. URL 복사 %1$s 전송 %1$s 링크를 여시겠습니까? @@ -333,6 +334,12 @@ 기타 설명 메시지 고정 + 수정됨 + 봇을 보려면 스크롤 다운하세요 + %1$s + 죄송합니다, 수정가능시간이 만료되었습니다. + 바로 가기 추가 + 홈에 바로가기 추가 %1$s님이 자동삭제를 %2$s 후로 설정했습니다 자동삭제를 %1$s 후로 설정했습니다 @@ -350,6 +357,7 @@ %1$s님이 음성메시지를 보냈습니다 %1$s 님이 트랙을 보냈습니다 %1$s님이 스티커를 보냈습니다 + %1$s님이 %2$s스티커를 보냈습니다 %1$s @ %2$s: %3$s %1$s님이 %2$s 그룹에 메시지를 보냈습니다 %1$s님이 %2$s 그룹에 사진을 보냈습니다 @@ -361,6 +369,7 @@ %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 그룹 사진을 변경했습니다 @@ -384,6 +393,7 @@ %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 그룹방에 고정함 @@ -395,6 +405,7 @@ %1$s 님이 비디오를 고정함 %1$s 님이 파일을 고정함 %1$s 님이 스티커를 고정함 + %1$s 님이 %2$s 스티커를 고정함 %1$s 님이 음성메시지를 고정함 %1$s 님이 연락처를 고정함 %1$s 님이 지도를 고정함 @@ -403,7 +414,7 @@ 대화상대 선택 대화상대가 없습니다 - LibreChat을 사용해 보세요!: https://messenger.b44t.com + Telegram을 사용해 보세요!: https://telegram.org/dl 오늘 어제 온라인 @@ -455,7 +466,7 @@ 경고 이 작업은 되돌릴 수 없습니다. 슈퍼그룹에서 일반그룹방으로 다운 그레이드는 불가능 합니다. ]]>구성원이 최대치입니다.]]>\n\n추가 기능 및 더 많은 구성원을 추가하려면 슈퍼그룹방으로 업그레이드하세요:\n\n• 슈퍼그룹방은 %1$s명까지 초대가능합니다.\n• 새로운 구성원은 모든 대화내역을 볼 수 있습니다.\n• 메시지 삭제시 모두에게 삭제가 됩니다.\n• 방 생성자는 그룹방 공개링크 생성이 가능합니다. - ]]>슈퍼그룹:]]>\n\n• 새로운 구성원은 모든 대화내역을 볼 수 있습니다.\n• 메시지 삭제시 모두에게 삭제가 됩니다.\n• 메시지를 작성자가 수정 가능합니다.\n• 방 생성자는 그룹방 공개링크 생성이 가능합니다. + ]]>슈퍼그룹:]]>\n\n• 새로운 구성원은 모든 대화내역을 볼 수 있습니다.\n• 메시지 삭제시 모두에게 삭제가 됩니다.\n• 방 생성자는 그룹방 공개링크 생성이 가능합니다. ]]>주위:]]> 이 작업은 되돌릴 수 없습니다. 공유 @@ -547,7 +558,7 @@ 텔레그램에 관한 질문은 자원봉사자들이 답변해 드립니다. 신속한 답변을 위해 노력하지만 답변이 다소 늦을 수 있습니다.
    ]]>일반적인 문제와 해결방법]]>에 대해서는 \'자주 묻는 질문]]>\'을 확인해 보세요.
    질문하기 자주 묻는 질문 - https://messenger.b44t.com/faq + https://telegram.org/faq/ko 개인정보 정책 https://telegram.org/privacy 언어를 삭제할까요? @@ -978,7 +989,7 @@ 텔레그램이 위치에 대한 권한이 필요합니다. 설정 - LibreChat + 텔레그램 눈부신 속도 완전 무료 보안성 @@ -1024,12 +1035,6 @@ 메시지 %1$d건 메시지 %1$d건 메시지 %1$d건 - %1$d 코멘트 - %1$d 코멘트 - %1$d 코멘트 - %1$d 코멘트 - %1$d 코멘트 - %1$d 코멘트 아이템이 없습니다. %1$d개 아이템 %1$d개 아이템 diff --git a/TMessagesProj/src/main/res/values-nl/strings.xml b/TMessagesProj/src/main/res/values-nl/strings.xml index e3842e118..e3eed97f2 100644 --- a/TMessagesProj/src/main/res/values-nl/strings.xml +++ b/TMessagesProj/src/main/res/values-nl/strings.xml @@ -3,8 +3,8 @@ - LibreChat - LibreChat + Telegram + Telegram-bèta Nederlands Dutch nl @@ -66,7 +66,11 @@ Uitschakelen HASHTAGS RECENT + CHATS + %1$s uit suggesties verwijderen? Link-voorvertoning + Concept + Geschiedenis gewist Groepsvorm Kanaaltype @@ -105,8 +109,6 @@ Geblokkeerde gebruikers kunnen alleen worden uitgenodigd door beheerders, uitnodigingslinks werken niet voor hen. Nieuw kanaal Kanaalnaam - Reacties - Als je reacties inschakelt kunnen leden reageren op je bericht in het kanaal. Contacten aan je kanaal toevoegen Deze link kan gedeeld worden met anderen en je kanaal kan worden gevonden via de zoekfunctie. Deze link kan gedeeld worden met anderen en je groep kan worden gevonden via de zoekfunctie. @@ -131,11 +133,8 @@ Kanaal verlaten Instellingen LID WORDEN - Kanaalinformatie Massabericht Stil massabericht - Reactie - reacties weergeven Wat is een kanaal? Kanalen zijn een nieuwe functie om berichten naar een groot publiek te sturen. MAAK KANAAL @@ -206,6 +205,7 @@ %1$s heeft een spraakbericht 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 %1$s plaatste een bericht %1$s plaatste een foto %1$s plaatste een video @@ -216,6 +216,7 @@ %1$s plaatste een spraakbericht %1$s plaatste een muziekbestand %1$s plaatste een sticker + %1$s plaatste een %2$s sticker Wie kan leden toevoegen? Alle leden Alleen beheerders @@ -301,7 +302,7 @@ Zelfvernietigingstimer instellen Servicemeldingen Link-preview ophalen... - Openen in browser + OPENEN IN... Link kopiëren %1$s versturen URL %1$s openen? @@ -333,6 +334,12 @@ Overig Beschrijving Vastgezet bericht + bewerkt + Scrollen voor bots + %1$s + Bewerken kan niet meer. + Snelkoppeling toevoegen + Snelkoppeling aan startscherm toegevoegd %1$s heeft de zelfvernietigingstimer ingesteld op %2$s Je hebt de zelfvernietigingstimer ingesteld op %1$s @@ -350,6 +357,7 @@ %1$s heeft je een spraakbericht gestuurd %1$s heeft je een muziekbestand gestuurd %1$s heeft je een sticker gestuurd + %1$s heeft je een %2$s sticker gestuurd %1$s @ %2$s: %3$s %1$s heeft een bericht gestuurd naar de groep %2$s %1$s heeft een foto gestuurd naar de groep %2$s @@ -361,6 +369,7 @@ %1$s heeft een spraakbericht 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 @@ -384,6 +393,7 @@ %1$s heeft video vastgezet in de groep %2$s %1$s heeft bestand vastgezet in de groep %2$s %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 contact vastgezet in de groep %2$s %1$s heeft locatie vastgezet in de groep %2$s @@ -395,6 +405,7 @@ %1$s heeft video vastgezet %1$s heeft bestand vastgezet %1$s heeft sticker vastgezet + %1$s heeft een %2$s sticker vastgezet %1$s heeft spraakbericht vastgezet %1$s heeft contact vastgezet %1$s heeft locatie vastgezet @@ -403,7 +414,7 @@ Contact kiezen Nog geen contacten - Hey, zullen we overstappen op LibreChat: https://messenger.b44t.com + Hey, zullen we overstappen op Telegram: https://telegram.org/dl om gisteren om online @@ -454,8 +465,8 @@ Opwaarderen naar supergroep Waarschuwing Groep echt omzetten naar supergroep? Je kunt dit niet ongedaan maken. - ]]>Ledenlimiet bereikt.]]>\n\nWil je extra functies en een hogere limiet? Waardeer op naar een supergroep:\n\n• Supergroepen hebben tot %1$s\n• Nieuwe leden zien de hele geschiedenis\n•Gewiste berichten gelden voor iedereen\n• Leden kunnen eigen berichten bewerken\n• Maker kan een publieke groepslink instellen - ]]>Supergroepen:]]>\n\n• Nieuwe leden zien de hele geschiedenis\n• Gewiste berichten gelden voor iedereen\n• Leden kunnen eigen berichten bewerken\n• Maker kan een publieke groepslink instellen + ]]>Ledenlimiet bereikt.]]>\n\nWil je extra functies en een hogere limiet? Waardeer op naar een supergroep:\n\n• Supergroepen hebben tot %1$s\n• Nieuwe leden zien de hele geschiedenis\n•Gewiste berichten gelden voor iedereen\n• Maker kan een publieke groepslink instellen + ]]>Supergroepen:]]>\n\n• Nieuwe leden zien de hele geschiedenis\n• Gewiste berichten gelden voor iedereen\n• Maker kan een publieke groepslink instellen ]]>Let op:]]> Je kunt dit niet ongedaan maken. Delen @@ -542,12 +553,12 @@ Beëindig alle andere sessies Gebeurtenissen Contact lid van Telegram - Vastgezet berichten + Vastgezette berichten Taal De ondersteuning van Telegram wordt gedaan door vrijwilligers. We doen ons best om zo snel mogelijk te antwoorden.
    ]]>Bekijk ook de veelgestelde vragen]]>: Hier staan de antwoorden op de meeste vragen en belangrijke tips voor het oplossen van problemen]]>.
    Vraag een vrijwilliger Veelgestelde vragen - https://messenger.b44t.com/faq + https://telegram.org/faq Privacybeleid https://telegram.org/privacy Vertaling verwijderen? @@ -978,7 +989,7 @@ Telegram heeft toegang tot je locatie nodig. INSTELLINGEN - LibreChat + Telegram Snel Gratis Veilig @@ -986,12 +997,12 @@ In de cloud Privé \'s Werelds snelste]]> berichtendienst.]]>Het is gratis]]> en veilig]]>. - LibreChat]]> bezorgt berichten sneller dan]]>elke andere applicatie. - LibreChat]]> is altijd gratis. Geen advertenties.]]>Geen abonnementskosten. - LibreChat]]> beveiligd je berichten]]>tegen aanvallen van hackers. - LibreChat]]> beperkt je niet in de grootte van]]>je media of chats. - LibreChat]]> biedt toegang tot je berichten]]>vanaf meerdere apparaten. - LibreChat]]> berichten zijn sterk versleuteld]]>en kunnen zichzelf vernietigen. + 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. Begin met chatten %1$d online @@ -1006,12 +1017,12 @@ %1$d leden %1$d leden %1$d leden - en nog %1$d personen zijn aan het typen - en nog %1$d persoon is aan het typen - en nog %1$d personen zijn aan het typen - en nog %1$d personen zijn aan het typen - en nog %1$d personen zijn aan het typen - en nog %1$d personen zijn aan het typen + en %1$d meer zijn aan het typen + en %1$d meer is aan het typen + en %1$d meer zijn aan het typen + en %1$d meer zijn aan het typen + en %1$d meer zijn aan het typen + en %1$d meer zijn aan het typen geen nieuwe berichten %1$d nieuw bericht %1$d nieuwe berichten @@ -1024,12 +1035,6 @@ %1$d berichten %1$d berichten %1$d berichten - %1$d reacties - %1$d reactie - %1$d reacties - %1$d reacties - %1$d reacties - %1$d reacties geen items %1$d item %1$d items diff --git a/TMessagesProj/src/main/res/values-pt-rBR/strings.xml b/TMessagesProj/src/main/res/values-pt-rBR/strings.xml index a2122d88b..83deb22c4 100644 --- a/TMessagesProj/src/main/res/values-pt-rBR/strings.xml +++ b/TMessagesProj/src/main/res/values-pt-rBR/strings.xml @@ -66,7 +66,11 @@ Desativar HASHTAGS RECENTE + CONVERSAS + Apagar %1$s das sugestões? Prévia do link + Rascunho + O histórico foi apagado Tipo de Grupo Tipo de canal @@ -100,13 +104,11 @@ un1 fixou um mapa un1 fixou um GIF un1 fixou uma música - Este grupo foi atualizado para um supergrupo - %1$s foi atualizado para um supergrupo + Esse grupo foi convertido para um supergrupo + %1$s foi convertido a um supergrupo Usuários bloqueados são removidos do grupo e só podem voltar se convidados por um administrador. Convites por link não funcionam para eles. Novo Canal Nome do canal - Comentários - Se você habilitar comentários, pessoas poderão discutir seu post no canal. Adicionar contatos no canal Pessoas podem compartilhar esse link com outros e encontrar seu canal usando a busca do Telegram. Pessoas podem compartilhar esse link com outros e encontrar o seu grupo usando a busca do Telegram. @@ -131,11 +133,8 @@ Sair do Canal Configurações ENTRAR - Info do Canal Transmissão Mensagem Silenciosa - Comentário - mostrar comentários O que é um Canal? Canais são uma nova ferramenta para transmissão de suas mensagens para grandes audiências. CRIAR CANAL @@ -206,6 +205,7 @@ %1$s enviou uma mensagem 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 %1$s postou uma mensagem %1$s postou uma foto %1$s postou um vídeo @@ -216,6 +216,7 @@ %1$s postou uma mensagem de voz %1$s postou uma música %1$s postou um sticker + %1$s postou um %2$s sticker Quem pode adicionar novos membros? Todos os Membros Somente Administradores @@ -301,7 +302,7 @@ Definir timer de autodestruição Notificações de serviço Obtendo informações... - Abrir no Navegador + ABRIR EM... Copiar URL Enviar %1$s Abrir URL em %1$s? @@ -333,6 +334,12 @@ Outro Descrição Mensagem Fixada + editado + Desça para ver os bots + %1$s + Desculpe, o tempo para editar expirou. + Adicionar atalho + Atalho adicionado à tela de início %1$s estabeleceu o tempo de autodestruição para %2$s Você estabeleceu o tempo de autodestruição para %1$s @@ -350,6 +357,7 @@ %1$s enviou uma mensagem de voz %1$s enviou uma música %1$s lhe enviou um sticker + %1$s lhe enviou um %2$s sticker %1$s @ %2$s: %3$s %1$s enviou uma mensagem para o grupo %2$s %1$s enviou uma foto para o grupo %2$s @@ -361,6 +369,7 @@ %1$s enviou uma mensagem 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 @@ -384,6 +393,7 @@ %1$s fixou um vídeo no grupo %2$s %1$s fixou um arquivo no grupo %2$s %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 um contato no grupo %2$s %1$s fixou um mapa no grupo %2$s @@ -395,6 +405,7 @@ %1$s fixou um vídeo %1$s fixou um arquivo %1$s fixou um sticker + %1$s fixou um %2$s sticker %1$s fixou uma mensagem de voz %1$s fixou um contato %1$s fixou um mapa @@ -403,7 +414,7 @@ Selecionar Contato Ainda não há contatos - Ei, vamos mudar para o LibreChat: https://messenger.b44t.com + Ei, vamos mudar para o Telegram: https://telegram.org/dl às ontem às online @@ -449,13 +460,13 @@ Apagar e sair do grupo Notificações Remover do grupo - Atualizar para Supergrupo + Converter a Supergrupo Converter a Supergrupo Converter a supergrupo Atenção Essa ação é irreversível. Não é possível voltar de um supergrupo para um grupo normal. - ]]>Limite de membros alcançado.]]>\n\nPara ir além do limite e ter funções adicionais, atualize para um supergrupo:\n\n• Supergrupos podem ter até %1$s\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Membros podem editar as próprias mensagens\n• Criador pode definir um link público para o grupo - ]]>Em supergroupos:]]>\n\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Membros podem editar as próprias mensagens\n• Criador pode definir um link público para o grupo + ]]>Limite de membros atingido.]]>\n\nPara ir além do limite e ter funções adcionais, converta para um supergrupo:\n\n• Supergrupos podem ter até %1$s\n• Novos membros veêm todo o histórico de conversas\n• Administradores deletam mensagens para todos\n• O criador pode definir um link público para o grupo + ]]>Em supergrupos:]]>\n\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Criador pode definir um link público para o grupo ]]>Nota:]]> essa ação não pode ser desfeita. Compartilhar @@ -547,7 +558,7 @@ 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]]>.
    Pergunte a um voluntário Perguntas frequentes - https://messenger.b44t.com/faq + https://telegram.org/faq Política de Privacidade https://telegram.org/privacy Apagar localização? @@ -978,7 +989,7 @@ O Telegram precisa acessar sua localização CONFIGURAÇÕES - LibreChat + Telegram Rápido Gratuito Seguro @@ -986,12 +997,12 @@ Baseado na nuvem Privado O mais rápido]]> aplicativo de mensagem do mundo. ]]>É gratuito]]> e seguro]]>. - O LibreChat]]> envia mensagens mais rápido]]>que qualquer outro aplicativo. - O LibreChat]]> é grátis para sempre. ]]>Sem propagandas. Sem taxas. - O LibreChat]]> mantém suas mensagens]]>seguras de ataques de hackers. - O LibreChat]]> não possui limites no tamanho]]>de seus arquivos e conversas. - O LibreChat]]> permite você acessar suas]]> mensagens de múltiplos dispositivos. - O LibreChat]]> possui mensagens fortemente]]>encriptadas e podem se auto-destruir. + 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. Comece a conversar %1$d online @@ -1006,12 +1017,12 @@ %1$d membros %1$d membros %1$d membros - e mais %1$d pessoas estão escrevendo - e mais %1$d estão digitando - e mais %1$d pessoas estão escrevendo - e mais %1$d pessoas estão escrevendo - e mais %1$d pessoas estão escrevendo - e mais %1$d pessoas estão escrevendo + e mais %1$d estão escrevendo + e mais %1$d estão escrevendo + e mais %1$d estão escrevendo + e mais %1$d estão escrevendo + e mais %1$d estão escrevendo + e mais %1$d estão escrevendo sem novas mensagens %1$d nova mensagem %1$d novas mensagens @@ -1024,12 +1035,6 @@ %1$d mensagens %1$d mensagens %1$d mensagens - %1$d comentários - %1$d comentário - %1$d comentários - %1$d comentários - %1$d comentários - %1$d comentários nenhum item %1$d item %1$d itens diff --git a/TMessagesProj/src/main/res/values-pt-rPT/strings.xml b/TMessagesProj/src/main/res/values-pt-rPT/strings.xml index 61cf39640..c4009956d 100644 --- a/TMessagesProj/src/main/res/values-pt-rPT/strings.xml +++ b/TMessagesProj/src/main/res/values-pt-rPT/strings.xml @@ -66,7 +66,11 @@ Desativar HASHTAGS RECENTE + CONVERSAS + Apagar %1$s das sugestões? Prévia do link + Rascunho + O histórico foi apagado Tipo de Grupo Tipo de canal @@ -100,13 +104,11 @@ un1 fixou um mapa un1 fixou um GIF un1 fixou uma música - Este grupo foi atualizado para um supergrupo - %1$s foi atualizado para um supergrupo + Esse grupo foi convertido para um supergrupo + %1$s foi convertido a um supergrupo Usuários bloqueados são removidos do grupo e só podem voltar se convidados por um administrador. Convites por link não funcionam para eles. Novo Canal Nome do canal - Comentários - Se você habilitar comentários, pessoas poderão discutir seu post no canal. Adicionar contatos no canal Pessoas podem compartilhar esse link com outros e encontrar seu canal usando a busca do Telegram. Pessoas podem compartilhar esse link com outros e encontrar o seu grupo usando a busca do Telegram. @@ -131,11 +133,8 @@ Sair do Canal Configurações ENTRAR - Info do Canal Transmissão Mensagem Silenciosa - Comentário - mostrar comentários O que é um Canal? Canais são uma nova ferramenta para transmissão de suas mensagens para grandes audiências. CRIAR CANAL @@ -206,6 +205,7 @@ %1$s enviou uma mensagem 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 %1$s postou uma mensagem %1$s postou uma foto %1$s postou um vídeo @@ -216,6 +216,7 @@ %1$s postou uma mensagem de voz %1$s postou uma música %1$s postou um sticker + %1$s postou um %2$s sticker Quem pode adicionar novos membros? Todos os Membros Somente Administradores @@ -301,7 +302,7 @@ Definir timer de autodestruição Notificações de serviço Obtendo informações... - Abrir no Navegador + ABRIR EM... Copiar URL Enviar %1$s Abrir URL em %1$s? @@ -333,6 +334,12 @@ Outro Descrição Mensagem Fixada + editado + Desça para ver os bots + %1$s + Desculpe, o tempo para editar expirou. + Adicionar atalho + Atalho adicionado à tela de início %1$s estabeleceu o tempo de autodestruição para %2$s Você estabeleceu o tempo de autodestruição para %1$s @@ -350,6 +357,7 @@ %1$s enviou uma mensagem de voz %1$s enviou uma música %1$s lhe enviou um sticker + %1$s lhe enviou um %2$s sticker %1$s @ %2$s: %3$s %1$s enviou uma mensagem para o grupo %2$s %1$s enviou uma foto para o grupo %2$s @@ -361,6 +369,7 @@ %1$s enviou uma mensagem 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 @@ -384,6 +393,7 @@ %1$s fixou um vídeo no grupo %2$s %1$s fixou um arquivo no grupo %2$s %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 um contato no grupo %2$s %1$s fixou um mapa no grupo %2$s @@ -395,6 +405,7 @@ %1$s fixou um vídeo %1$s fixou um arquivo %1$s fixou um sticker + %1$s fixou um %2$s sticker %1$s fixou uma mensagem de voz %1$s fixou um contato %1$s fixou um mapa @@ -403,7 +414,7 @@ Selecionar Contato Ainda não há contatos - Ei, vamos mudar para o LibreChat: https://messenger.b44t.com + Ei, vamos mudar para o Telegram: https://telegram.org/dl às ontem às online @@ -449,13 +460,13 @@ Apagar e sair do grupo Notificações Remover do grupo - Atualizar para Supergrupo + Converter a Supergrupo Converter a Supergrupo Converter a supergrupo Atenção Essa ação é irreversível. Não é possível voltar de um supergrupo para um grupo normal. - ]]>Limite de membros alcançado.]]>\n\nPara ir além do limite e ter funções adicionais, atualize para um supergrupo:\n\n• Supergrupos podem ter até %1$s\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Membros podem editar as próprias mensagens\n• Criador pode definir um link público para o grupo - ]]>Em supergroupos:]]>\n\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Membros podem editar as próprias mensagens\n• Criador pode definir um link público para o grupo + ]]>Limite de membros atingido.]]>\n\nPara ir além do limite e ter funções adcionais, converta para um supergrupo:\n\n• Supergrupos podem ter até %1$s\n• Novos membros veêm todo o histórico de conversas\n• Administradores deletam mensagens para todos\n• O criador pode definir um link público para o grupo + ]]>Em supergrupos:]]>\n\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Criador pode definir um link público para o grupo ]]>Nota:]]> essa ação não pode ser desfeita. Compartilhar @@ -547,7 +558,7 @@ 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]]>.
    Pergunte a um voluntário Perguntas frequentes - https://messenger.b44t.com/faq + https://telegram.org/faq Política de Privacidade https://telegram.org/privacy Apagar localização? @@ -978,7 +989,7 @@ O Telegram precisa acessar sua localização CONFIGURAÇÕES - LibreChat + Telegram Rápido Gratuito Seguro @@ -986,12 +997,12 @@ Baseado na nuvem Privado O mais rápido]]> aplicativo de mensagem do mundo. ]]>É gratuito]]> e seguro]]>. - O LibreChat]]> envia mensagens mais rápido]]>que qualquer outro aplicativo. - O LibreChat]]> é grátis para sempre. ]]>Sem propagandas. Sem taxas. - O LibreChat]]> mantém suas mensagens]]>seguras de ataques de hackers. - O LibreChat]]> não possui limites no tamanho]]>de seus arquivos e conversas. - O LibreChat]]> permite você acessar suas]]> mensagens de múltiplos dispositivos. - O LibreChat]]> possui mensagens fortemente]]>encriptadas e podem se auto-destruir. + 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. Comece a conversar %1$d online @@ -1006,12 +1017,12 @@ %1$d membros %1$d membros %1$d membros - e mais %1$d pessoas estão escrevendo - e mais %1$d estão digitando - e mais %1$d pessoas estão escrevendo - e mais %1$d pessoas estão escrevendo - e mais %1$d pessoas estão escrevendo - e mais %1$d pessoas estão escrevendo + e mais %1$d estão escrevendo + e mais %1$d estão escrevendo + e mais %1$d estão escrevendo + e mais %1$d estão escrevendo + e mais %1$d estão escrevendo + e mais %1$d estão escrevendo sem novas mensagens %1$d nova mensagem %1$d novas mensagens @@ -1024,12 +1035,6 @@ %1$d mensagens %1$d mensagens %1$d mensagens - %1$d comentários - %1$d comentário - %1$d comentários - %1$d comentários - %1$d comentários - %1$d comentários nenhum item %1$d item %1$d itens diff --git a/TMessagesProj/src/main/res/values/strings.xml b/TMessagesProj/src/main/res/values/strings.xml index 47f940b33..72d31e62b 100644 --- a/TMessagesProj/src/main/res/values/strings.xml +++ b/TMessagesProj/src/main/res/values/strings.xml @@ -66,7 +66,11 @@ Disable HASHTAGS RECENT + CHATS + Delete %1$s from suggestions? Link preview + Draft + History was cleared Group Type Channel Type @@ -105,8 +109,6 @@ Blocked users are removed from the group and can only come back if invited by an admin. Invite links don\'t work for them. New Channel Channel name - Comments - If you enable comments, people will be able to discuss your posts in the channel. Add contacts to your channel People can share this link with others and find your channel using Telegram search. People can share this link with others and find your group using Telegram search. @@ -131,11 +133,8 @@ Leave channel Settings JOIN - Channel info Broadcast Silent Broadcast - Comment - show comments What is a Channel? Channels are a new tool for broadcasting your messages to large audiences. CREATE CHANNEL @@ -206,6 +205,7 @@ %1$s sent a voice message to the channel %2$s %1$s sent a track to the channel %2$s %1$s sent a sticker to the channel %2$s + %1$s sent a %3$s sticker to the channel %2$s %1$s posted a message %1$s posted a photo %1$s posted a video @@ -216,6 +216,7 @@ %1$s posted a voice message %1$s posted a track %1$s posted a sticker + %1$s posted a %2$s sticker Who can add new members? All Members Only Admins @@ -333,6 +334,12 @@ Other Description Pinned Message + edited + Scroll down for bots + %1$s + Sorry, editing time expired. + Add shortcut + Shortcut added to home screen %1$s set the self-destruct timer to %2$s You set the self-destruct timer to %1$s @@ -350,6 +357,7 @@ %1$s sent you a voice message %1$s sent you a track %1$s sent you a sticker + %1$s sent you a %2$s sticker %1$s @ %2$s: %3$s %1$s sent a message to the group %2$s %1$s sent a photo to the group %2$s @@ -361,6 +369,7 @@ %1$s sent a voice message to the group %2$s %1$s sent a track to the group %2$s %1$s sent a sticker to the group %2$s + %1$s sent a %3$s sticker to the group %2$s %1$s invited you to the group %2$s %1$s edited the group\'s %2$s name %1$s edited the group\'s %2$s photo @@ -384,6 +393,7 @@ %1$s pinned a video in the group %2$s %1$s pinned a file in the group %2$s %1$s pinned a sticker in the group %2$s + %1$s pinned a %3$s sticker in the group %2$s %1$s pinned a voice message in the group %2$s %1$s pinned a contact in the group %2$s %1$s pinned a map in the group %2$s @@ -395,6 +405,7 @@ %1$s pinned a video %1$s pinned a file %1$s pinned a sticker + %1$s pinned a %2$s sticker %1$s pinned a voice message %1$s pinned a contact %1$s pinned a map @@ -454,8 +465,8 @@ Convert to supergroup Warning This action is irreversible. It is not possible to downgrade a supergroup to a regular group. - ]]>Members limit reached.]]>\n\nTo go over the limit and get additional features, upgrade to a supergroup:\n\n• Supergroups can get up to %1$s\n• New members can see the full message history\n• Deleted messages will disappear for all members\n• Members can edit their own messages\n• Creator can set a public link for the group - ]]>In supergroups:]]>\n\n• New members can see the full message history\n• Deleted messages will disappear for all members\n• Members can edit their own messages\n• Creator can set a public link for the group + ]]>Members limit reached.]]>\n\nTo go over the limit and get additional features, upgrade to a supergroup:\n\n• Supergroups can get up to %1$s\n• New members can see the full message history\n• Deleted messages will disappear for all members\n• Admins can pin important messages\n• Creator can set a public link for the group + ]]>In supergroups:]]>\n\n• New members can see the full message history\n• Deleted messages will disappear for all members\n• Admins can pin important messages\n• Creator can set a public link for the group ]]>Note:]]> this action can\'t be undone. Share @@ -1006,12 +1017,12 @@ %1$d members %1$d members %1$d members - and %1$d more people are typing - and %1$d more person are typing - and %1$d more people are typing - and %1$d more people are typing - and %1$d more people are typing - and %1$d more people are typing + and %1$d more are typing + and %1$d more are typing + and %1$d more are typing + and %1$d more are typing + and %1$d more are typing + and %1$d more are typing no new messages %1$d new message %1$d new messages @@ -1024,12 +1035,6 @@ %1$d messages %1$d messages %1$d messages - %1$d comments - %1$d comment - %1$d comments - %1$d comments - %1$d comments - %1$d comments no items %1$d item %1$d items diff --git a/TMessagesProj/src/main/res/xml/automotive_app_desc.xml b/TMessagesProj/src/main/res/xml/automotive_app_desc.xml index b6df3b517..79ec6f5e2 100644 --- a/TMessagesProj/src/main/res/xml/automotive_app_desc.xml +++ b/TMessagesProj/src/main/res/xml/automotive_app_desc.xml @@ -1,4 +1,5 @@ + \ No newline at end of file